Java 进阶特性
参考指导书 2.4 Java 进阶特性
学习清单
- 异常处理(try-catch-finally, throws, 自定义异常)
- IO 流(File, InputStream, OutputStream, Reader, Writer)
- 多线程(Thread, Runnable, 线程池 ExecutorService)
- Lambda 表达式(Java 8+)
- Stream API(Java 8+)
- 注解(Annotation)
- 反射(Reflection)
1. 异常处理
1.1 为什么需要异常
没有异常机制的语言(比如 C)靠返回值来判断错误:
int result = doSomething();
if (result == -1) { /* 错误 1 */ }
if (result == -2) { /* 错误 2 */ }
// 正常逻辑和错误处理混在一起,层层 if 嵌套,可读性极差
Java 的异常机制把正常逻辑和错误处理分开——代码沿着"快乐路径"写,错误交给 catch 块处理。
1.2 异常体系
Throwable
├── Error 严重错误,程序无法处理(OutOfMemoryError、StackOverflowError)
└── Exception 可处理的异常
├── RuntimeException(运行期异常) 不强制处理,如 NullPointerException、ArrayIndexOutOfBoundsException
└── 其他(编译期异常) 必须显式处理,如 IOException、SQLException
编译期异常 vs 运行期异常:
| 编译期异常 (Checked) | 运行期异常 (Unchecked) | |
|---|---|---|
| 父类 | Exception(非 RuntimeException) | RuntimeException |
| 编译器 | 强制你处理(try-catch 或 throws) | 不强制 |
| 典型例子 | IOException, SQLException | NullPointerException, ArrayIndexOutOfBounds |
| 设计意图 | 外部因素导致,可以预见和恢复 | 程序 bug,应该修复代码而非捕获 |
1.3 基本语法
try {
// 可能出异常的代码
int result = 10 / 0;
} catch (ArithmeticException e) {
// 捕获特定异常
System.out.println("除数不能为 0!");
e.printStackTrace(); // 打印调用栈,调试用
} finally {
// 无论是否异常,都会执行(常用于关闭资源)
System.out.println("finally 一定会执行");
}
执行流程:try 出异常 → 跳过 try 剩余代码 → 匹配 catch → 执行 finally。try 没出异常 → 跳过 catch → 执行 finally。
1.4 finally 的一个特例
// 唯一不执行 finally 的情况:JVM 退出
try {
System.exit(0); // JVM 直接退出
} finally {
System.out.println("这行不会执行");
}
1.5 throws:把异常往上抛
// 自己不处理,声明"我可能抛这个异常",让调用方处理
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
// ...
}
选择原则:当前方法能处理就用 try-catch;处理不了(不知道该怎么恢复)就用 throws 往上抛。
1.6 自定义异常
class AgeException extends Exception {
public AgeException(String msg) { super(msg); }
}
public void setAge(int age) throws AgeException {
if (age < 0 || age > 150) {
throw new AgeException("年龄不合法:" + age);
}
this.age = age;
}
自定义异常的目的:让异常名本身就表达语义。AgeException 比 IllegalArgumentException 更有表达力。
1.7 try-with-resources(Java 7+)
// 旧写法:手动关闭,嵌套冗长
FileReader reader = null;
try {
reader = new FileReader("file.txt");
// ...
} finally {
if (reader != null) reader.close(); // 关闭也很啰嗦
}
// 新写法:自动关闭
try (FileReader reader = new FileReader("file.txt")) {
// ...
} // 自动调用 reader.close(),不用写 finally
任何实现了 AutoCloseable 接口的类都可以用在 try-with-resources 中。
2. IO 流
2.1 流的基本概念
Java 把数据的输入/输出抽象为"流(Stream)":
程序 ←──InputStream/Reader── 【数据源:文件、网络、键盘...】
程序 ──→OutputStream/Writer── 【数据目标:文件、网络、屏幕...】
| 字节流(二进制) | 字符流(文本) | |
|---|---|---|
| 输入 | InputStream |
Reader |
| 输出 | OutputStream |
Writer |
| 文件实现 | FileInputStream / FileOutputStream |
FileReader / FileWriter |
为什么分字节流和字符流? 字节流按 byte 读写,字符流按 char 读写 + 自动处理字符编码。读文本文件用字符流,读图片/视频用字节流。
2.2 File 类
File file = new File("test.txt");
file.exists() // 文件是否存在
file.getName() // "test.txt"
file.getAbsolutePath() // 绝对路径
file.length() // 文件大小(字节)
file.isFile() // 是文件还是目录
file.isDirectory()
file.createNewFile(); // 创建文件
file.delete(); // 删除
file.mkdir(); // 创建单层目录
file.mkdirs(); // 创建多层目录
2.3 文件读写(字符流)
// 写文件
try (FileWriter writer = new FileWriter("output.txt")) {
writer.write("Hello, Java IO!");
}
// 读文件(逐字符)
try (FileReader reader = new FileReader("output.txt")) {
int ch;
while ((ch = reader.read()) != -1) {
System.out.print((char) ch);
}
}
2.4 缓冲流:提高效率
// 不加缓冲:每读一个字符就访问一次磁盘 → 极慢
// 加缓冲:一次读一大块到内存缓冲区 → 快得多
try (BufferedReader reader = new BufferedReader(new FileReader("large.txt"))) {
String line;
while ((line = reader.readLine()) != null) { // 逐行读
System.out.println(line);
}
}
BufferedReader 和 BufferedWriter 就是给普通 Reader/Writer 套了一层缓冲区,减少磁盘访问次数。
3. 多线程
3.1 为什么需要多线程
单线程程序同一时间只能做一件事。如果主线程在等待网络响应(500ms),整个程序就卡住 500ms。
多线程让程序"同时"做多件事——一个线程等网络,另一个线程继续响应用户操作。这就是为什么你的手机不会因为下载文件而卡死。
3.2 创建线程的方式
方式一:继承 Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("新线程运行中: " + Thread.currentThread().getName());
}
}
MyThread t = new MyThread();
t.start(); // 启动线程(JVM 会调用 run())
// t.run(); 错误!直接调用 run() 只是普通方法调用,不会启动新线程
方式二:实现 Runnable(推荐)
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务执行中...");
}
}
Thread t = new Thread(new MyTask());
t.start();
用 Runnable 而不是 Thread 的原因:Java 单继承——继承 Thread 就不能再继承别的类。实现 Runnable 更灵活,还能把任务提交给线程池。
3.3 start() vs run()
t.start(); // 启动一个新线程,在新线程中执行 run()
t.run(); // 在当前线程中调用 run(),没有新线程!
这是初学者最常犯的错误。start() 是新线程并行执行;run() 是同步调用。
3.4 线程常用方法
Thread.sleep(1000); // 让当前线程休眠 1 秒(可能抛 InterruptedException)
t.join(); // 等待线程 t 执行完毕
Thread.currentThread() // 获取当前线程对象
t.setPriority(Thread.MAX_PRIORITY); // 设置优先级(1~10,只是建议,不保证)
3.5 线程安全问题
两个线程同时操作同一个变量时,结果不可预测:
class Counter {
private int count = 0;
public void increment() {
count++; // 这行代码实际分三步:读 → 加 1 → 写回
} // 线程 A 读完后线程 B 插进来改 → A 的写入覆盖了 B 的结果
}
// 两个线程各执行 10000 次 increment,最终 count 可能小于 20000
用 synchronized 加锁:
class Counter {
private int count = 0;
public synchronized void increment() { // 同一时刻只能一个线程进入
count++;
}
}
synchronized 保证了原子性——方法内的操作作为一个整体,不会被其他线程打断。
3.6 线程池(ExecutorService)
每次都 new Thread().start() 的问题:创建/销毁线程开销大、线程数不可控(来 10000 个请求就建 10000 个线程 → 系统崩)。
线程池 = 固定数量的"常驻"线程,任务来了排队等:
ExecutorService pool = Executors.newFixedThreadPool(3); // 创建 3 个常驻线程
for (int i = 0; i < 10; i++) {
final int taskId = i;
pool.submit(() -> {
System.out.println("任务 " + taskId + " 由 " + Thread.currentThread().getName() + " 执行");
Thread.sleep(1000);
return null;
});
}
pool.shutdown(); // 不再接收新任务,已有任务执行完就关闭
10 个任务,3 个线程,每个线程排队轮流执行——线程数可控,不会无限膨胀。
4. Lambda 表达式
4.1 Lambda 解决什么问题
回顾匿名内部类的痛点——为了实现一个简单逻辑,被迫写一堆样板代码:
// 匿名内部类:就为了排序逻辑,写了 5 行
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
// Lambda:同样的事,1 行
list.sort((a, b) -> a.compareTo(b));
Lambda 就是"把方法当参数传"——让代码更简洁,只保留核心逻辑,去掉样板。
4.2 语法
(参数) -> { 方法体 }
演变过程:
// 完整版
(String a, String b) -> { return a.compareTo(b); }
// 去掉参数类型(编译器能推断)
(a, b) -> { return a.compareTo(b); }
// 方法体只有一行时,去掉 {} 和 return
(a, b) -> a.compareTo(b);
// 如果只有一个参数,连 () 都可以省略
name -> System.out.println(name);
// 无参数
() -> System.out.println("Hello");
4.3 Lambda 的条件
Lambda 只能用在函数式接口上—— 有且仅有一个抽象方法的接口:
@FunctionalInterface // 注解:编译器检查是否只有一个抽象方法
interface Calculator {
int calc(int a, int b); // 只有一个抽象方法 → 可以用 Lambda
}
Calculator add = (a, b) -> a + b;
Calculator mul = (a, b) -> a * b;
System.out.println(add.calc(3, 5)); // 8
System.out.println(mul.calc(3, 5)); // 15
JDK 内置的常见函数式接口:Runnable、Comparator、ActionListener。这些都是单抽象方法接口,都可以用 Lambda 简化。
5. Stream API
5.1 为什么需要 Stream
对集合做"过滤 + 转换 + 汇总"这种操作,传统的 for 循环又长又啰嗦:
// 任务:从成绩表中筛选出及格(≥60)的学生姓名,按字母排序
List<Student> students = ...;
// 传统方式:6 步,中间变量一堆
List<String> passed = new ArrayList<>();
for (Student s : students) {
if (s.getScore() >= 60) {
passed.add(s.getName());
}
}
Collections.sort(passed);
// Stream 方式:一条流水线
List<String> passed = students.stream()
.filter(s -> s.getScore() >= 60) // 筛选
.map(Student::getName) // 提取姓名
.sorted() // 排序
.collect(Collectors.toList()); // 收集结果
Stream 把"对集合的多次操作"变成一条流水线——数据从上游流到下游,每经过一个操作就变换一次。
5.2 三种操作
数据源 ──▶ 中间操作(懒执行) ──▶ 终端操作(触发执行) ──▶ 结果
.stream() filter/map/sorted... collect/count/forEach...
中间操作不触发计算——它们只是"记下要做什么"。只有终端操作出现时,数据才真正开始流动。这叫惰性求值,避免中间结果的浪费。
5.3 常用操作速查
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
// 中间操作
nums.stream()
.filter(n -> n % 2 == 0) // 过滤出偶数 → [2, 4, 6]
.map(n -> n * 10) // 每个×10 → [20, 40, 60]
.sorted((a, b) -> b - a) // 降序排列 → [60, 40, 20]
.limit(2) // 只要前2个 → [60, 40]
.forEach(System.out::println); // 终端操作:打印每个元素
| 操作 | 类型 | 作用 | 示例 |
|---|---|---|---|
filter |
中间 | 保留满足条件的 | .filter(n -> n > 0) |
map |
中间 | 转换每个元素 | .map(s -> s.length()) |
sorted |
中间 | 排序 | .sorted() |
distinct |
中间 | 去重 | .distinct() |
limit |
中间 | 截断前 N 个 | .limit(10) |
collect |
终端 | 收集为集合 | .collect(Collectors.toList()) |
count |
终端 | 计数 | .count() |
forEach |
终端 | 遍历每个元素 | .forEach(System.out::println) |
6. 注解
6.1 什么是注解
注解 = 给代码贴标签。它本身不改变程序逻辑,但编译器和框架会读取这些标签做额外处理。
@Override // 告诉编译器:我在重写父类方法,请帮我检查
@Deprecated // 标记这个方法已过时,调用会出编译警告
@FunctionalInterface // 标记这是函数式接口,有多个抽象方法就报错
6.2 自定义注解
// 定义一个注解
@Retention(RetentionPolicy.RUNTIME) // 保留到运行时(否则编译后就丢了)
@Target(ElementType.METHOD) // 只能用在方法上
@interface MyTest {
String value() default ""; // 注解属性,default 表示可选
}
// 使用
@MyTest("这是一个测试方法")
public void testMethod() { ... }
为什么需要注解:注解提供了"在代码旁边放元数据"的能力。没有注解,JUnit 不知道哪些方法是测试;没有 @Override,你以为重写了其实没重写(方法签名不一样),编译器也不告诉你。注解把隐式的约定变成了显式的声明。
7. 反射
7.1 什么是反射
正常情况下,你写 obj.getName()——你知道类型是什么,编译器也知道。反射是反过来:运行时拿到一个未知的对象,动态查看它的类结构,动态调用它的方法。
// 正常方式:编译时就知道类型
Student s = new Student();
s.study(); // 编译器知道 Student 有 study 方法
// 反射方式:运行时才知道类型
Object obj = ...; // 不知道具体是什么类
Class<?> clazz = obj.getClass(); // 运行时获取类的信息
Method method = clazz.getMethod("study"); // 按名字获取方法
method.invoke(obj); // 动态调用
7.2 基本用法
Class<?> clazz = Student.class; // 方式 1:类名.class
Class<?> clazz2 = obj.getClass(); // 方式 2:对象.getClass()
Class<?> clazz3 = Class.forName("com.example.Student"); // 方式 3:全限定名
// 获取类信息
clazz.getName(); // 类全名
clazz.getSimpleName(); // 类简称
clazz.getDeclaredFields(); // 所有属性
clazz.getDeclaredMethods(); // 所有方法
// 动态创建对象
Object obj = clazz.getDeclaredConstructor().newInstance();
// 动态调用方法
Method method = clazz.getMethod("setName", String.class);
method.invoke(obj, "Zhang San");
// 动态访问私有字段(连 private 都拦不住)
Field field = clazz.getDeclaredField("name");
field.setAccessible(true); // 暴力破解 private
String name = (String) field.get(obj);
7.3 为什么需要反射
你写的代码能调用你写的类,这不需要反射。但框架不知道你会写什么类:
- JUnit 不知道你写了哪些测试方法 —— 靠反射扫描所有 @Test 方法
- Spring 不知道你写了哪些 Bean —— 靠反射扫描 @Component 并创建实例
- Gson 不知道你的 User 类有什么字段 —— 靠反射把 JSON 字段映射到 Java 对象
- Android 你在 XML 里写的
android:onClick="doSomething"—— 运行时靠反射找到doSomething方法
反射是框架的基石。没有反射,框架就需要你在配置文件中手动列出每个类——那是 Java 远古时代的做法。
7.4 反射的代价
- 性能低:反射调用比直接调用慢几十倍(需要安全检查、包装参数)
- 破坏封装:
setAccessible(true)可以绕过 private - 编译期安全丢失:方法名拼错了,编译期不报错,运行时才崩
使用原则:业务代码不要用反射。写框架/库/工具类时才用它。日常开发中你更多的是"受益于反射"而不是"使用反射"。
关键记忆点
异常
- try-catch 把正常逻辑和错误处理分开;finally 一定会执行(除了 System.exit)
- 编译期异常(IOException)必须处理,运行期异常(NPE)不强制
- try-with-resources 自动关流,替代 finally 中手动 close
IO 流
- 字节流(InputStream/OutputStream)处理二进制,字符流(Reader/Writer)处理文本
- BufferedReader/Writer 是缓冲包装——大幅减少磁盘 IO 次数
多线程
- 创建线程用
Runnable而非Thread——灵活、可复用、能被线程池管理 start()启动新线程,run()只是同步调用——不要搞混- 多线程操作共享变量 → 竞态条件 →
synchronized保证原子性 - 线程池避免频繁创建销毁线程,控制并发数——
Executors.newFixedThreadPool(n)
Lambda & Stream
- Lambda 只能在函数式接口(单抽象方法)上使用,本质是匿名内部类的语法糖
- Stream 中间操作是懒执行的,终端操作才触发计算
注解 & 反射
- 注解 = 给代码贴标签;反射 = 运行时读取这些标签并动态执行
- 反射是框架的基石(JUnit、Spring、Gson 都靠它),业务代码不要直接用