Java进阶

Stream流

Stream 是 Java 8 引入的一套对集合/数组数据进行声明式处理的方式。它本质上不是一个“装数据的容器”,而是一条“处理数据的流水线”。

如果用一句话概括 Stream 的思想:

先拿到数据源,再按步骤描述“我要怎么处理数据”,最后一次性得到结果。

1. Stream流的思想

传统写法里,我们经常会写很多 for 循环:

  • 先遍历一次筛选数据
  • 再遍历一次做转换
  • 再遍历一次排序
  • 最后再收集结果

Stream 的核心思想就是把这些步骤串成一条流水线

list.stream()
.filter(...)
.map(...)
.sorted(...)
.collect(...);

这样写的好处:

  • 代码更关注“做什么”,而不是“怎么一层层循环”
  • 业务逻辑更清晰,筛选、转换、排序、汇总一眼就能看出来
  • 很适合做集合数据处理

需要记住:Stream 不负责存储数据,它只是负责计算数据。

2. Stream流的几个核心特点

2.1 不存储数据

Stream 本身不保存元素,数据还是在数组、集合等容器中。

2.2 不改变原数据

大多数情况下,Stream 是从原集合中读取数据并生成新结果,不会直接修改原集合。

2.3 支持链式调用

中间操作可以一个接一个串起来,形成处理流水线。

2.4 惰性执行

只写中间操作时,代码通常不会立即执行,只有遇到终结操作才会真正开始处理数据。

list.stream()
.filter(x -> {
System.out.println("执行过滤");
return x > 10;
});

上面这段只有 filter(),没有终结操作,所以流水线不会真正跑起来。

2.5 一个Stream只能消费一次

Stream 一旦执行过终结操作,就不能再重复使用。

Stream<String> stream = list.stream();
stream.forEach(System.out::println);
// stream.forEach(System.out::println); // 会报错,流已经被消费

3. 创建Stream流

3.1 通过集合创建

List<String> list = Arrays.asList("java", "python", "go");
Stream<String> stream = list.stream();

如果想并行处理,还可以:

Stream<String> parallelStream = list.parallelStream();

但并行流不是越多越好,只有在数据量较大、任务适合并行时才考虑使用。

3.2 通过数组创建

String[] arr = {"java", "python", "go"};

Stream<String> stream1 = Arrays.stream(arr);
Stream<String> stream2 = Stream.of(arr);

3.3 直接通过若干元素创建

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

3.4 创建基础类型流

Java 还为基本类型准备了专门的流,避免频繁装箱拆箱:

  • IntStream
  • LongStream
  • DoubleStream
IntStream intStream = IntStream.of(1, 2, 3, 4);
IntStream range = IntStream.range(1, 5); // 1,2,3,4
IntStream range2 = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

4. Stream流的操作分类

Stream 的操作通常分成两大类:

  • 中间操作:返回新的流,可以继续链式调用
  • 终结操作:产生最终结果或者副作用,执行后流就结束了

也可以记成:

数据源 -> 中间操作 -> 终结操作

5. 常见中间操作

中间操作的特点是:返回的还是流

5.1 filter() 过滤

按照条件筛选元素。

List<Integer> list = Arrays.asList(10, 15, 20, 25, 30);

List<Integer> result = list.stream()
.filter(x -> x > 20)
.toList();

结果:[25, 30]

5.2 map() 映射/转换

把一种元素转换成另一种元素。

List<String> list = Arrays.asList("java", "python", "go");

List<Integer> lengths = list.stream()
.map(String::length)
.toList();

结果:[4, 6, 2]

5.3 flatMap() 打平

当元素本身还是一个集合时,可以把多层结构“摊平”。

List<List<String>> lists = Arrays.asList(
Arrays.asList("a", "b"),
Arrays.asList("c", "d")
);

List<String> result = lists.stream()
.flatMap(List::stream)
.toList();

结果:[a, b, c, d]

map() 是“一对一转换”,flatMap() 是“拆开再合并”。

5.4 sorted() 排序

List<Integer> list = Arrays.asList(5, 2, 8, 1);

List<Integer> result = list.stream()
.sorted()
.toList();

自定义排序:

List<String> list = Arrays.asList("java", "python", "go");

List<String> result = list.stream()
.sorted((a, b) -> b.length() - a.length())
.toList();

5.5 distinct() 去重

List<Integer> list = Arrays.asList(1, 2, 2, 3, 3, 3);

List<Integer> result = list.stream()
.distinct()
.toList();

结果:[1, 2, 3]

5.6 limit() 取前几个

List<Integer> result = Stream.of(1, 2, 3, 4, 5, 6)
.limit(3)
.toList();

结果:[1, 2, 3]

5.7 skip() 跳过前几个

List<Integer> result = Stream.of(1, 2, 3, 4, 5, 6)
.skip(2)
.toList();

结果:[3, 4, 5, 6]

5.8 peek() 查看流中的元素

peek() 常用于调试,观察中间过程,不建议把核心业务逻辑写在里面。

List<Integer> result = Stream.of(1, 2, 3, 4)
.peek(System.out::println)
.map(x -> x * 2)
.toList();

6. 常见终结操作(最终操作)

终结操作的特点是:一旦调用,流水线开始真正执行,并且流不能再复用。

6.1 forEach() 遍历

list.stream().forEach(System.out::println);

6.2 collect() 收集

这是最常见的终结操作之一,用来把流中的结果收集到集合或其他容器中。

List<String> result = list.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());

收集为 Set

Set<String> result = list.stream()
.collect(Collectors.toSet());

收集为 Map

Map<String, Integer> map = list.stream()
.collect(Collectors.toMap(
s -> s,
String::length
));

6.3 toList()

在较新的 Java 版本中,可以直接:

List<String> result = list.stream()
.filter(s -> s.length() > 3)
.toList();

它和 collect(Collectors.toList()) 的使用体验很像,写法更简洁。

6.4 count() 统计数量

long count = list.stream()
.filter(s -> s.length() > 3)
.count();

6.5 max() / min() 求最大最小值

Optional<Integer> max = Stream.of(3, 5, 2, 9)
.max(Integer::compareTo);

Optional<Integer> min = Stream.of(3, 5, 2, 9)
.min(Integer::compareTo);

因为有可能流为空,所以返回的是 Optional<T>

6.6 findFirst() / findAny()

Optional<String> first = list.stream()
.filter(s -> s.startsWith("j"))
.findFirst();

6.7 anyMatch() / allMatch() / noneMatch()

用于条件判断:

boolean b1 = list.stream().anyMatch(s -> s.length() > 5);
boolean b2 = list.stream().allMatch(s -> s.length() > 1);
boolean b3 = list.stream().noneMatch(String::isEmpty);

6.8 reduce() 归约

reduce() 可以把一批数据反复合并成一个结果。

例如求和:

int sum = Stream.of(1, 2, 3, 4, 5)
.reduce(0, Integer::sum);

例如求乘积:

int result = Stream.of(1, 2, 3, 4)
.reduce(1, (a, b) -> a * b);

reduce() 的思想很重要,本质是“把多个值逐步折叠成一个值”。

7. 常见汇总方法(Collectors)

很多时候我们不是简单地拿一个 List,而是希望做分组、统计、拼接,这时最常用的是 Collectors

7.1 joining() 字符串拼接

List<String> list = Arrays.asList("Java", "Python", "Go");

String result = list.stream()
.collect(Collectors.joining(", "));

结果:Java, Python, Go

7.2 counting() 统计个数

long count = list.stream()
.collect(Collectors.counting());

7.3 summingInt() / averagingInt()

List<String> list = Arrays.asList("java", "python", "go");

int totalLength = list.stream()
.collect(Collectors.summingInt(String::length));

double avgLength = list.stream()
.collect(Collectors.averagingInt(String::length));

7.4 groupingBy() 分组

这是非常常用的方法。

List<String> list = Arrays.asList("java", "go", "python", "c");

Map<Integer, List<String>> map = list.stream()
.collect(Collectors.groupingBy(String::length));

结果大致是:

{
1=[c],
2=[go],
4=[java],
6=[python]
}

7.5 partitioningBy() 分区

分区本质上是按 true/false 分成两组。

Map<Boolean, List<Integer>> map = Stream.of(1, 2, 3, 4, 5, 6)
.collect(Collectors.partitioningBy(x -> x % 2 == 0));

8. 一个完整案例

假设我们有一组名字,希望:

  • 去重
  • 过滤掉长度小于等于 3 的名字
  • 全部转成大写
  • 按长度倒序排序
  • 最后收集成列表
List<String> names = Arrays.asList("java", "go", "python", "java", "rust");

List<String> result = names.stream()
.distinct()
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.sorted((a, b) -> b.length() - a.length())
.toList();

System.out.println(result);

执行流程理解为:

  1. 先从 names 创建流
  2. distinct() 去重
  3. filter() 过滤
  4. map() 转换
  5. sorted() 排序
  6. toList() 终结并收集结果

9. 使用Stream时的注意点

9.1 Stream不等于集合

集合关注的是“存数据”,Stream 关注的是“处理数据”。

9.2 不要滥用Stream

如果逻辑非常简单,一个普通 for 循环可能更直观。
如果链式操作太长,反而会让代码可读性变差。

9.3 注意空指针问题

如果集合本身可能是 null,要先判空,否则 list.stream() 会直接报错。

9.4 Optional要会处理

findFirst()max()min() 返回的通常是 Optional,因为结果可能不存在。

Optional<String> result = list.stream()
.filter(s -> s.startsWith("z"))
.findFirst();

result.ifPresent(System.out::println);

9.5 并行流慎用

parallelStream() 不是银弹:

  • 小数据量下不一定更快
  • 多线程下调试更复杂
  • 如果有共享变量,还可能出线程安全问题

10. Stream流总结

可以把 Stream 记成一句固定模板:

数据源 + 中间操作 + 终结操作 = 一次声明式的数据处理过程

最常见的记忆方式:

  • 创建流:stream()Arrays.stream()Stream.of()
  • 过滤:filter()
  • 转换:map()
  • 打平:flatMap()
  • 排序:sorted()
  • 去重:distinct()
  • 截取:limit()skip()
  • 收集:collect()toList()
  • 统计:count()
  • 归约:reduce()
  • 判断:anyMatch()allMatch()noneMatch()

如果以后忘了 Stream 到底是干什么的,就记住它最核心的用途:

把“遍历 + 判断 + 转换 + 汇总”这一类集合处理逻辑,写成一条清晰的流水线。

方法引用

方法引用(Method Reference)可以理解为:当 Lambda 表达式只是“调用一个已经存在的方法”时,可以进一步简写。

所以它和 Lambda 的关系是:

方法引用本质上不是新功能,而是 Lambda 表达式的语法糖。

先看一个最直观的例子:

list.forEach(x -> System.out.println(x));

可以写成:

list.forEach(System.out::println);

因为这个 Lambda 做的事情非常单一,就是“把参数传给一个现成的方法”。

1. 方法引用的核心思想

方法引用适用于这种场景:

  • 已经有一个现成的方法
  • 这个方法做的事情正好和函数式接口要求的一致
  • 那就没必要再手写一层 Lambda 转发

所以它的本质是:

把“参数传递给某个现成方法”这件事直接写出来。

方法引用并不会让功能变强,它只是让代码更简洁、更容易读。

2. 方法引用的前提

不是所有地方都能随便写方法引用,它必须满足一个前提:

函数式接口的抽象方法参数列表、返回值,要能和被引用的方法对上。

比如:

Consumer<String> c1 = s -> System.out.println(s);
Consumer<String> c2 = System.out::println;

这里 Consumer<T> 的抽象方法是:

void accept(T t)

println(String) 也正好是“接收一个参数,没有返回值”,所以可以匹配。

3. 方法引用的几种常见写法

方法引用最常见的有 4 大类:

  • 类名::静态方法
  • 对象名::实例方法
  • 类名::实例方法
  • 类名::new

下面分别记。

4. 类名::静态方法

格式:

类名::静态方法

例如:

Function<String, Integer> f1 = s -> Integer.parseInt(s);
Function<String, Integer> f2 = Integer::parseInt;

这里 Integer.parseInt(String s) 本身就是一个静态方法,所以可以直接用 Integer::parseInt

再看一个例子:

List<String> list = Arrays.asList("1", "2", "3");

List<Integer> result = list.stream()
.map(Integer::parseInt)
.toList();

这类写法通常比较容易理解,因为它本质上就是“拿这个类里的静态工具方法来用”。

5. 对象名::实例方法

格式:

对象名::实例方法

意思是:调用“某个具体对象”的实例方法。

例如:

PrintStream ps = System.out;
Consumer<String> c = ps::println;

更常见的写法就是:

list.forEach(System.out::println);

这里的 System.out 是一个具体对象,println 是它的实例方法。

再举一个例子:

String prefix = "hello ";
Function<String, String> f = prefix::concat;

等价于:

Function<String, String> f = s -> prefix.concat(s);

6. 类名::实例方法

格式:

类名::实例方法

这是最容易绕的一种,但它在 Stream 中特别常见。

先记住一句话:

如果 Lambda 的第一个参数,正好是某个对象的调用者,那么就可以写成 类名::实例方法

例如:

Function<String, Integer> f1 = s -> s.length();
Function<String, Integer> f2 = String::length;

这里 s.length() 中,s 是调用者,所以可以写成 String::length

再比如:

Predicate<String> p1 = s -> s.isEmpty();
Predicate<String> p2 = String::isEmpty;

Stream 里最常见:

List<String> list = Arrays.asList("java", "", "python");

List<String> result = list.stream()
.filter(s -> !s.isEmpty())
.toList();

如果想保留“非空字符串”,这种场景直接写 Lambda 反而更直观:

List<String> result = list.stream()
.filter(s -> !s.isEmpty())
.toList();

如果是不需要取反、直接筛选空字符串,就可以自然写成方法引用:

List<String> emptyList = list.stream()
.filter(String::isEmpty)
.toList();

如果你的 Java 版本支持,也可以这样写非空:

List<String> result = list.stream()
.filter(Predicate.not(String::isEmpty))
.toList();

这里的 Predicate.not(...) 是对方法引用再做一次取反,写法很顺,但要注意对应的 Java 版本支持情况。

还有一个非常经典的例子:

List<String> list = Arrays.asList("java", "python", "go");

List<Integer> lengths = list.stream()
.map(String::length)
.toList();

这就是 类名::实例方法 的典型使用。

7. 构造方法引用

格式:

类名::new

意思是:把“创建对象”这件事也当成一个函数来传。

例如无参构造:

Supplier<Person> s1 = () -> new Person();
Supplier<Person> s2 = Person::new;

例如有参构造:

Function<String, Person> f1 = name -> new Person(name);
Function<String, Person> f2 = Person::new;

只要构造器参数能和函数式接口对上,就可以这么写。

8. 数组构造引用

格式:

类型[]::new

例如:

Function<Integer, String[]> f1 = len -> new String[len];
Function<Integer, String[]> f2 = String[]::new;

这个写法在把流转成数组时很常见:

String[] arr = list.stream().toArray(String[]::new);

9. 方法引用和Lambda的对应关系

后面如果记混了,就按下面这个思路判断:

9.1 只有转发,没有额外逻辑

如果 Lambda 只是把参数原封不动传给现成方法,那就很可能可以改成方法引用。

s -> Integer.parseInt(s)

可以改成:

Integer::parseInt

9.2 一旦有额外逻辑,就不能简单替换

比如:

s -> s.trim().toUpperCase()

这里做了两步操作,不是单纯调用一个现成方法,就不能直接改成一个简单的方法引用。

10. 在Stream中的常见写法

方法引用和 Stream 经常一起出现,因为很多流操作本身就是“把每个元素交给某个方法处理”。

10.1 遍历输出

list.forEach(System.out::println);

10.2 映射转换

List<Integer> result = list.stream()
.map(String::length)
.toList();

10.3 条件过滤

List<String> result = list.stream()
.filter(String::isEmpty)
.toList();

10.4 转换基本类型

List<String> list = Arrays.asList("1", "2", "3");

List<Integer> result = list.stream()
.map(Integer::parseInt)
.toList();

10.5 收集为数组

String[] arr = list.stream().toArray(String[]::new);

11. 使用方法引用时的注意点

11.1 可读性优先

方法引用的目的是让代码更清晰,不是为了“写得更短”。

如果改成方法引用后更绕,那就直接保留 Lambda。

11.2 类名::实例方法 最容易混淆

这个要单独记:

  • String::length
  • String::isEmpty
  • String::toUpperCase

它们本质上都等价于:

s -> s.length()
s -> s.isEmpty()
s -> s.toUpperCase()

也就是说:Lambda 的参数本身就是方法调用者。

11.3 方法引用不能替代所有Lambda

如果你在 Lambda 里还有判断、拼接、计算、多步处理,那一般就还是老老实实写 Lambda。

12. 方法引用总结

可以把方法引用记成一句话:

Lambda 只是“替别人调用一个现成方法”时,就可以考虑改成方法引用。

最常见的几种形式:

  • 静态方法引用:类名::静态方法
  • 对象实例方法引用:对象名::实例方法
  • 任意对象实例方法引用:类名::实例方法
  • 构造方法引用:类名::new
  • 数组构造引用:类型[]::new

最值得优先记住的例子:

  • System.out::println
  • String::length
  • String::isEmpty
  • Integer::parseInt
  • Person::new
  • String[]::new

如果以后忘了方法引用到底是干什么的,就记住它最核心的作用:

把“只调用现成方法的 Lambda”进一步简化。

文件与 IO 流

这一块在 Java 里很重要,因为很多程序最终都离不开:

  • 读文件
  • 写文件
  • 读网络数据
  • 写网络数据
  • 做数据持久化

而 Java 的这套 IO 体系刚开始看会比较乱,主要是因为类很多、名字很像。所以这里不按 API 文档顺序背,而是按最适合回顾的方式记。

可以先记一句总纲:

File 负责表示文件和目录;流负责传输数据。

1. 什么是 IO

IO = Input / Output,也就是输入和输出。

在 Java 里可以这样理解:

  • Input:把数据读进程序
  • Output:把程序里的数据写出去

最常见的数据来源和去向有:

  • 文件
  • 控制台
  • 网络
  • 内存

2. File 类

File 类主要用来表示文件或目录路径,它本身并不负责真正“读取文件内容”。

也就是说:

  • File 更像是“文件对象的描述”
  • 真正读写内容,还是要靠各种输入流和输出流

2.1 创建 File 对象

File file1 = new File("a.txt");
File file2 = new File("/Users/test/demo.txt");
File dir = new File("demo");

2.2 File 常用方法

  • exists():文件/目录是否存在
  • isFile():是否是文件
  • isDirectory():是否是目录
  • getName():获取文件名
  • getPath():获取路径
  • getAbsolutePath():获取绝对路径
  • length():文件大小(字节数)
  • mkdir():创建单级目录
  • mkdirs():创建多级目录
  • createNewFile():创建文件
  • delete():删除文件或空目录
  • list():获取目录下的文件名数组
  • listFiles():获取目录下的 File 数组

2.3 一个简单例子

File file = new File("test.txt");

if (!file.exists()) {
file.createNewFile();
}

System.out.println(file.getName());
System.out.println(file.getAbsolutePath());
System.out.println(file.length());

2.4 mkdir() 和 mkdirs() 的区别

  • mkdir():只能创建一级目录
  • mkdirs():可以一次创建多级目录

助记:

多出来的 s 可以理解成支持多层。

3. IO 流的分类

Java 的 IO 流最常见的分类方式有两套。

3.1 按数据单位分

  • 字节流InputStream / OutputStream
  • 字符流Reader / Writer

3.2 按功能分

  • 节点流:直接连接数据源
  • 处理流:对节点流进行包装,增强功能

例如:

  • FileInputStream:节点流,直接读文件字节
  • BufferedInputStream:处理流,在外面加缓冲

4. 字节流

字节流适合处理:

  • 图片
  • 音频
  • 视频
  • 压缩包
  • 任何二进制文件

也可以处理文本,但如果是文本,通常更推荐字符流。

4.1 InputStream 和 OutputStream

这是字节流的两个父类:

  • InputStream:输入字节流,负责读
  • OutputStream:输出字节流,负责写

4.2 FileInputStream / FileOutputStream

这是最基础的文件字节流。

读取文件

FileInputStream fis = new FileInputStream("a.txt");
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
fis.close();

这里的 read()

  • 每次读取一个字节
  • 如果返回 -1,表示读完了

按字节数组读取

FileInputStream fis = new FileInputStream("a.txt");
byte[] buffer = new byte[1024];
int len;

while ((len = fis.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}

fis.close();

这比一个一个字节读更常用。

写文件

FileOutputStream fos = new FileOutputStream("a.txt");
fos.write("hello".getBytes());
fos.close();

如果想追加写入:

FileOutputStream fos = new FileOutputStream("a.txt", true);
fos.write("world".getBytes());
fos.close();

第二个参数是 true 时表示追加,不会覆盖原文件。

4.3 字节流常用方法

  • read():读一个字节
  • read(byte[] b):读到字节数组中
  • write(int b):写一个字节
  • write(byte[] b):写一个字节数组
  • write(byte[] b, int off, int len):写数组的一部分
  • close():关闭流

5. 字符流

字符流适合处理:

  • 纯文本
  • 中文内容
  • 配置文件
  • 日志文件

因为字符流是面向“字符”处理的,所以读文本时通常比字节流更自然。

5.1 Reader 和 Writer

  • Reader:输入字符流,负责读
  • Writer:输出字符流,负责写

5.2 FileReader / FileWriter

这是最基础的文件字符流。

读取文本

FileReader fr = new FileReader("a.txt");
char[] buffer = new char[1024];
int len;

while ((len = fr.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}

fr.close();

写文本

FileWriter fw = new FileWriter("a.txt");
fw.write("hello java");
fw.close();

追加写:

FileWriter fw = new FileWriter("a.txt", true);
fw.write("\nnew line");
fw.close();

5.3 字节流和字符流怎么选

可以直接这样记:

  • 文本文件:优先字符流
  • 二进制文件:优先字节流

一句话助记:

能按“字符”理解的内容,用字符流;不能按字符理解的内容,用字节流。

6. 缓冲流

缓冲流的作用是:

给原来的流加一个缓冲区,提高读写效率。

最常见的有:

  • BufferedInputStream
  • BufferedOutputStream
  • BufferedReader
  • BufferedWriter

6.1 字节缓冲流

BufferedInputStream bis = new BufferedInputStream(new FileInputStream("a.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt"));

6.2 字符缓冲流

BufferedReader br = new BufferedReader(new FileReader("a.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));

6.3 BufferedReader 的常用方法

最经典的方法是:

  • readLine():一次读一行
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
String line;

while ((line = br.readLine()) != null) {
System.out.println(line);
}

br.close();

6.4 BufferedWriter 的常用方法

  • write():写内容
  • newLine():换行
BufferedWriter bw = new BufferedWriter(new FileWriter("a.txt"));
bw.write("first line");
bw.newLine();
bw.write("second line");
bw.close();

7. 转换流

有时候字节流和字符流要互相配合,这时就需要转换流。

最常见的两个类:

  • InputStreamReader
  • OutputStreamWriter

它们的作用是:

在字节流和字符流之间做桥梁。

7.1 为什么要用转换流

最典型的原因是:指定字符编码。

比如:

InputStreamReader isr = new InputStreamReader(
new FileInputStream("a.txt"), "UTF-8");

这样就可以按指定编码把字节转换成字符。

7.2 常见写法

BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("a.txt"), "UTF-8"));
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("a.txt"), "UTF-8"));

助记:

  • InputStreamReader:字节输入流 -> 字符输入流
  • OutputStreamWriter:字节输出流 -> 字符输出流

8. 对象流

对象流是 Java IO 里比较有代表性的一类流,它可以把对象直接写到文件里,或者从文件里读回来。

最常见的两个类:

  • ObjectOutputStream
  • ObjectInputStream

8.1 序列化和反序列化

  • 序列化:把对象写出去
  • 反序列化:把对象读回来

8.2 使用前提

要被写入对象流的类,通常需要实现:

Serializable

例如:

class Person implements Serializable {
private String name;
private int age;
}

8.3 简单例子

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.dat"));
oos.writeObject(new Person("Tom", 18));
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.dat"));
Person p = (Person) ois.readObject();
ois.close();

9. 打印流

打印流最常见的是:

  • PrintStream
  • PrintWriter

它们的特点是:

  • 输出更方便
  • 可以直接打印各种类型

例如:

PrintWriter pw = new PrintWriter(new FileWriter("a.txt"));
pw.println("hello");
pw.println(123);
pw.close();

10. close() 和 flush()

这两个方法很容易混。

10.1 flush()

flush() 的作用:

把缓冲区里的数据立刻刷出去,但流本身还可以继续用。

10.2 close()

close() 的作用:

  • 先刷新
  • 再关闭流
  • 关闭后不能继续使用

助记:

  • flush:刷新但不关
  • close:刷新并关闭

11. try-with-resources

IO 流最大的问题之一就是:用完一定要关。

Java 提供了更推荐的写法:

try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}

这样写的好处:

  • 不容易忘记关闭流
  • 即使出现异常,也会自动关闭

平时写 IO,优先用这种写法。

12. 常见应用场景怎么选流

这里最适合做回顾记忆。

12.1 读写文本文件

优先:

  • FileReader / FileWriter
  • BufferedReader / BufferedWriter

如果要按行读,优先:

  • BufferedReader

12.2 读写图片、音频、压缩包

优先:

  • FileInputStream
  • FileOutputStream
  • 或它们的缓冲流版本

12.3 需要指定编码

优先:

  • InputStreamReader
  • OutputStreamWriter

12.4 需要把对象保存到文件

优先:

  • ObjectOutputStream
  • ObjectInputStream

13. 一个最常用的文件复制模板

文件复制本质上就是:

一边读,一边写。

最常见写法:

try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("a.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.jpg"))
) {
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}

这个模板非常常用,值得直接记住。

14. 文件与 IO 流总结

如果以后回头看 IO 忘了从哪里入手,就按下面这套顺序记:

14.1 先分清 File 和 流

  • File:表示文件和目录
  • 流:负责读写数据

14.2 再分清字节流和字符流

  • 字节流:处理二进制,也能处理文本
  • 字符流:更适合纯文本

14.3 再分清节点流和处理流

  • 节点流:直接连数据源
  • 处理流:套在外面增强功能

14.4 常用类优先记这些

  • 文件字节流:FileInputStreamFileOutputStream
  • 文件字符流:FileReaderFileWriter
  • 缓冲字符流:BufferedReaderBufferedWriter
  • 缓冲字节流:BufferedInputStreamBufferedOutputStream
  • 转换流:InputStreamReaderOutputStreamWriter
  • 对象流:ObjectInputStreamObjectOutputStream

14.5 最实用的一句话

文本优先字符流,二进制优先字节流;平时优先加缓冲;写完记得关流,最好直接用 try-with-resources。

Java 并发编程

并发编程是 Java 进阶里非常核心的一块,因为只要程序开始同时处理多件事,就会碰到:

  • 多线程
  • 线程安全
  • 共享资源
  • 线程池

这一章最重要的不是死记 API,而是先把几个核心问题想清楚:

  • 为什么要并发
  • 并发为什么会出错
  • 怎么控制多个线程安全地协作

可以先记一句总纲:

并发编程的核心,就是“让多个线程同时工作”,同时想办法保证共享数据不出问题。

1. 进程和线程

1.1 什么是进程

进程可以理解为:

一个正在运行的程序。

例如你打开:

  • 一个浏览器
  • 一个 IDE
  • 一个终端

它们都可以看成不同的进程。

1.2 什么是线程

线程可以理解为:

进程内部的一条执行路径。

一个进程里可以有多个线程同时工作。

例如:

  • 主线程负责主逻辑
  • 子线程负责下载
  • 另一个线程负责日志输出

1.3 进程和线程的关系

可以这样记:

  • 进程是资源分配的基本单位
  • 线程是 CPU 调度的基本单位

一句话助记:

进程像一个工厂,线程像工厂里的工人。

2. 为什么需要并发

使用并发通常有几个常见原因:

2.1 提高程序响应性

例如 GUI 程序中,主线程负责界面,如果耗时任务也放在主线程做,界面就会卡住。

2.2 提高资源利用率

有些任务在等待 IO 时,CPU 是空着的,这时可以切换去做别的任务。

2.3 同时处理多个任务

例如:

  • Web 服务器同时处理多个请求
  • 下载器同时下载多个文件
  • 消息系统同时消费多个消息

3. 线程的创建方式

Java 中最经典的线程创建方式有 3 种:

  • 继承 Thread
  • 实现 Runnable
  • 实现 Callable

3.1 继承 Thread

class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行中");
}
}

MyThread t = new MyThread();
t.start();

注意:

  • 重写的是 run()
  • 真正启动线程要调用 start(),不是直接调 run()

3.2 实现 Runnable

这是更常见、更推荐的基础写法。

class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务执行中");
}
}

Thread t = new Thread(new MyTask());
t.start();

也可以写成 Lambda:

Thread t = new Thread(() -> System.out.println("任务执行中"));
t.start();

3.3 实现 Callable

Runnable 不能返回结果,也不能直接抛出受检异常。

如果任务需要返回结果,更常用的是 Callable

Callable<Integer> task = () -> 100;
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();

Integer result = futureTask.get();

助记:

  • Runnable:不返回结果
  • Callable:可以返回结果

4. 线程的常用方法

  • start():启动线程
  • run():线程执行逻辑
  • sleep(ms):让线程休眠
  • join():等待某个线程执行完
  • currentThread():获取当前线程
  • yield():让出 CPU
  • interrupt():中断线程

4.1 start() 和 run() 的区别

这是初学线程时最容易混的点。

  • run():只是普通方法调用
  • start():才会真正启动新线程

4.2 sleep() 和 join()

  • sleep():让当前线程暂停一段时间
  • join():让当前线程等待另一个线程结束

例如:

Thread t = new Thread(() -> {
System.out.println("子线程执行");
});

t.start();
t.join();
System.out.println("主线程继续");

5. 线程安全问题

并发最核心的问题就是线程安全。

5.1 什么叫线程安全

如果多个线程同时访问同一份共享数据,程序依然能得到正确结果,就可以认为它是线程安全的。

5.2 为什么会不安全

最常见原因:

  • 多个线程同时读写同一个变量
  • 操作不是原子的
  • 线程执行顺序不可控

例如下面这个 count++ 看起来只有一行,但它本质上不是一步完成的:

count++;

它通常可以拆成:

  1. 读取 count
  2. count + 1
  3. 写回去

如果两个线程同时执行,就可能丢失更新。

6. synchronized 关键字

synchronized 是 Java 最基础的同步手段。

它的作用是:

同一时刻只允许一个线程进入被保护的临界区。

6.1 修饰普通方法

public synchronized void method() {
// 同步方法
}

锁的是当前对象 this

6.2 修饰代码块

public void method() {
synchronized (this) {
// 同步代码块
}
}

这种方式更灵活,因为可以只锁关键部分。

6.3 修饰静态方法

public static synchronized void method() {
// 同步静态方法
}

锁的是类对象 Class

6.4 synchronized 的使用场景

适合:

  • 多线程修改共享变量
  • 多线程访问同一临界资源
  • 先追求正确,再考虑性能

助记:

  • 普通同步方法锁 this
  • 静态同步方法锁 类对象
  • 同步代码块锁括号里的对象

7. Lock 接口

除了 synchronized,Java 还提供了更灵活的显式锁,例如:

  • ReentrantLock

7.1 基本写法

Lock lock = new ReentrantLock();

lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}

这里最重要的一点是:

加锁后一定要在 finally 里解锁。

7.2 ReentrantLock 的特点

  • 可以手动加锁和解锁
  • synchronized 更灵活
  • 支持尝试加锁
  • 支持可中断锁
  • 支持公平锁

7.3 synchronized vs Lock

可以这样记:

  • synchronized:语法简单,基础同步够用
  • Lock:功能更丰富,控制更灵活

如果只是基础同步,先会 synchronized 更重要。

8. volatile 关键字

volatile 是并发里非常容易被误解的一个关键字。

它的核心作用不是“保证线程安全”,而是:

保证可见性。

8.1 什么是可见性

如果一个线程修改了变量,另一个线程能够立刻看到修改后的值,这就叫可见性。

private volatile boolean flag = true;

如果一个线程把 flag 改成 false,其他线程能尽快读到新值。

8.2 volatile 能做什么

  • 保证可见性
  • 一定程度上禁止指令重排序

8.3 volatile 不能做什么

不能保证复合操作的原子性

例如:

volatile int count = 0;
count++;

这里即使加了 volatilecount++ 依然不是线程安全的。

助记:

volatile 保证“看得见”,不保证“改得安全”。

9. 原子类 Atomic

如果只是想对单个变量做线程安全的自增、自减、CAS 操作,常用的是原子类。

最常见:

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReference

9.1 示例

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
count.getAndIncrement();
int value = count.get();

9.2 为什么会有 Atomic

因为有些场景只是对一个变量做简单并发操作,用锁太重,原子类更轻量。

10. 线程通信:wait()、notify()、notifyAll()

多个线程不只是要“抢资源”,有时还要“配合工作”。

这就涉及线程通信。

最经典的方法:

  • wait()
  • notify()
  • notifyAll()

10.1 基本理解

  • wait():当前线程进入等待状态,并释放锁
  • notify():随机唤醒一个等待线程
  • notifyAll():唤醒所有等待线程

10.2 使用前提

这几个方法必须在同步代码块或同步方法中使用,并且是针对同一个锁对象调用。

synchronized (lock) {
lock.wait();
}
synchronized (lock) {
lock.notifyAll();
}

11. 线程池

并发编程里非常重要的一条原则是:

不要频繁手动创建和销毁线程。

因为线程本身也是资源,频繁创建销毁开销很大。

所以更推荐使用线程池。

11.1 为什么要用线程池

  • 复用线程
  • 降低创建线程的开销
  • 统一管理线程
  • 更容易控制并发数量

11.2 ExecutorService

线程池最常见的使用接口是:

ExecutorService

例如:

ExecutorService pool = Executors.newFixedThreadPool(3);

pool.submit(() -> {
System.out.println(Thread.currentThread().getName());
});

pool.shutdown();

11.3 常见线程池

  • newFixedThreadPool(n):固定线程数
  • newCachedThreadPool():按需创建线程
  • newSingleThreadExecutor():单线程线程池
  • newScheduledThreadPool(n):定时任务线程池

11.4 submit() 和 execute()

  • execute():执行 Runnable,不关心返回值
  • submit():可以提交 RunnableCallable,并返回 Future

11.5 shutdown() 和 shutdownNow()

  • shutdown():平缓关闭,不再接收新任务,但会等已提交任务执行完
  • shutdownNow():尝试立刻停止

12. Future 和 Callable

如果任务提交后,后面还想拿结果,可以配合:

  • Callable
  • Future

示例:

ExecutorService pool = Executors.newFixedThreadPool(2);

Future<Integer> future = pool.submit(() -> 1 + 2);
Integer result = future.get();

pool.shutdown();

这里:

  • submit() 提交任务
  • Future 代表未来结果
  • get() 用来拿返回值

13. 并发工具类

Java 并发包 java.util.concurrent 里有很多非常常用的工具类。

先记几个最常见的:

  • CountDownLatch
  • CyclicBarrier
  • Semaphore

13.1 CountDownLatch

作用:

一个线程等待其他多个线程完成。

例如“主线程等 3 个子线程都执行完”。

核心方法:

  • countDown()
  • await()

13.2 CyclicBarrier

作用:

多个线程互相等待,等到都到齐后再一起继续。

13.3 Semaphore

作用:

控制同时访问某资源的线程数量。

可以把它理解成“许可证”。

14. 并发集合

普通集合在多线程环境下通常不安全,所以 Java 也提供了并发集合。

例如:

  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • BlockingQueue

14.1 ConcurrentHashMap

线程安全版的 Map,常用于并发环境下的键值存储。

14.2 CopyOnWriteArrayList

适合:

  • 读多写少

14.3 BlockingQueue

非常适合生产者-消费者模型。

常见实现:

  • ArrayBlockingQueue
  • LinkedBlockingQueue

15. 死锁

死锁是并发里非常经典的问题。

15.1 什么是死锁

多个线程互相等待对方释放资源,结果谁都走不下去。

15.2 经典场景

线程 A 拿了锁 1,等锁 2;
线程 B 拿了锁 2,等锁 1。

这样就卡住了。

15.3 怎么避免死锁

  • 加锁顺序保持一致
  • 尽量减少锁嵌套
  • 缩小同步代码块范围
  • 必要时使用尝试加锁

16. 线程状态

Java 线程常见状态可以先记这几个:

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING
  • TIMED_WAITING
  • TERMINATED

助记:

  • NEW:刚创建
  • RUNNABLE:可运行
  • BLOCKED:等锁
  • WAITING:无限等待
  • TIMED_WAITING:限时等待
  • TERMINATED:结束

17. 并发编程常见面试/易错点

17.1 start() 不是 run()

直接调用 run() 不会启动新线程。

17.2 volatile 不保证原子性

它只解决可见性,不解决 count++ 这种复合操作的线程安全。

17.3 共享变量才需要重点考虑线程安全

如果变量是线程私有的,一般不会有竞争问题。

17.4 锁不是越大越好

锁范围太大,会影响并发性能。

17.5 线程池比手动 new Thread 更常用

实际开发里,大量任务通常优先交给线程池管理。

18. Java 并发编程总结

如果以后回头看并发一时理不清,就按下面这套顺序记:

18.1 先分清几个核心概念

  • 进程:运行中的程序
  • 线程:进程中的执行路径
  • 并发:多个任务交替或同时执行

18.2 再记住几个核心问题

  • 为什么要并发:提高响应、提高吞吐、同时处理多个任务
  • 并发为什么危险:共享数据会有线程安全问题
  • 怎么保证安全:同步、锁、原子类、并发工具类

18.3 最常用的知识点优先记这些

  • 创建线程:ThreadRunnableCallable
  • 同步:synchronized
  • 显式锁:LockReentrantLock
  • 可见性:volatile
  • 原子操作:AtomicInteger
  • 线程池:ExecutorService
  • 异步结果:Future
  • 并发集合:ConcurrentHashMap

18.4 最实用的一句话

并发不是为了“开更多线程”,而是为了更高效地处理多个任务;而并发编程真正难的地方,是共享数据的安全和线程之间的协作。

Java Socket 网络编程

网络编程这一块,本质上就是让两台机器上的程序能够互相通信。

在 Java 里,最常见的入门方式就是 Socket 编程。

可以先记一句总纲:

Socket 是程序和网络之间的通信接口。

也可以把它理解成:

  • 客户端和服务端之间的一条通信连接
  • 程序读写网络数据时使用的“插座”

1. 网络编程的基本思想

Java Socket 编程最核心的模型就是:

  • 服务端先启动,监听某个端口
  • 客户端主动连接这个端口
  • 双方建立连接后,通过输入流和输出流通信

所以本质上就是:

连接 + 读写数据

2. Socket 编程里的几个核心类

最常用的类主要有:

  • ServerSocket
  • Socket
  • InputStream
  • OutputStream
  • BufferedReader
  • BufferedWriter
  • PrintWriter

2.1 ServerSocket

ServerSocket 只在服务端使用。

它的作用:

  • 绑定端口
  • 监听客户端连接

2.2 Socket

Socket 表示一次网络连接。

它既可以在客户端使用,也可以在服务端使用。

作用:

  • 连接远程主机
  • 获取输入流和输出流
  • 进行数据通信

3. 服务端和客户端的基本流程

3.1 服务端流程

  1. 创建 ServerSocket
  2. 绑定端口
  3. 调用 accept() 等待客户端连接
  4. 拿到 Socket
  5. 通过输入输出流收发数据
  6. 关闭资源

3.2 客户端流程

  1. 创建 Socket
  2. 指定服务器 IP 和端口
  3. 获取输入输出流
  4. 发送或接收数据
  5. 关闭资源

4. 最基础的 TCP 通信

Java 里最常见的 Socket 入门默认就是基于 TCP。

TCP 的特点:

  • 面向连接
  • 可靠传输
  • 有顺序

所以大多数“先连接再传数据”的场景,先想到 TCP。

4.1 服务端示例

ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();

InputStream is = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = is.read(buffer);
System.out.println(new String(buffer, 0, len));

socket.close();
serverSocket.close();

4.2 客户端示例

Socket socket = new Socket("127.0.0.1", 8888);
OutputStream os = socket.getOutputStream();
os.write("hello server".getBytes());
socket.close();

这就是最基础的 TCP 通信骨架。

5. 为什么 Socket 通信和 IO 流连在一起

这也是最容易理解的一点:

网络通信本质上就是数据输入输出。

所以 Socket 连接建立后,Java 给我们的操作方式就是:

  • getInputStream():读对方发来的数据
  • getOutputStream():向对方发送数据

也就是说:

  • 文件 IO 是对文件读写
  • Socket IO 是对网络连接读写

6. 常见读写方式

6.1 用字节流读写

适合:

  • 通用数据传输
  • 二进制内容
  • 简单字符串测试
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();

6.2 用字符流按行读写

如果传输的是文本,常常会再套一层字符流和缓冲流。

BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);

这里:

  • BufferedReader 适合按行读取
  • PrintWriter 适合方便地写文本

6.3 一个按行通信的例子

服务端读:

BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String msg = br.readLine();
System.out.println(msg);

客户端写:

PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
pw.println("hello server");

这里要特别注意:

如果服务端用 readLine(),那客户端通常就要发带换行的数据,比如 println()

7. accept() 的意义

accept() 是服务端编程里最核心的方法之一。

它的作用:

阻塞等待客户端连接。

也就是说,如果没有客户端连过来,程序会停在这里等。

Socket socket = serverSocket.accept();

一旦有客户端连接,就会返回一个 Socket 对象,表示这次连接建立成功。

8. 单次通信和循环通信

很多初学时的例子只收发一次数据,但真实场景通常不是“一次就结束”。

8.1 单次通信

适合入门理解。

例如:

  • 客户端发一句话
  • 服务端收一句话
  • 然后连接关闭

8.2 循环通信

更真实的场景通常是:

  • 不断读取对方消息
  • 直到对方关闭连接或收到结束标记

例如:

BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}

9. 多客户端连接问题

服务端如果只写成:

Socket socket = serverSocket.accept();

那通常只处理一个连接。

如果希望服务端能同时服务多个客户端,常见做法就是:

  • 每来一个客户端连接,就交给一个新线程处理
  • 或者交给线程池处理

例如:

ServerSocket serverSocket = new ServerSocket(8888);
ExecutorService pool = Executors.newCachedThreadPool();

while (true) {
Socket socket = serverSocket.accept();
pool.submit(() -> {
// 处理这个客户端
});
}

所以 Socket 编程和并发编程经常会一起出现。

10. UDP 简单理解

除了 TCP,网络编程里还有 UDP。

10.1 UDP 的特点

  • 无连接
  • 不保证可靠送达
  • 速度通常更快
  • 适合对实时性要求高、允许少量丢包的场景

10.2 Java 里的相关类

  • DatagramSocket
  • DatagramPacket

初学阶段先把 TCP Socket 搞清楚最重要,UDP 先理解“无连接数据报通信”这个概念即可。

11. Socket 编程常见问题

11.1 服务端先启动

客户端连接前,服务端必须先监听端口。

11.2 端口不能乱冲突

如果某个端口已经被占用,再绑定就会报错。

11.3 readLine() 可能一直阻塞

如果对方没有发换行,或者连接没关闭,而你这里在等一整行,就可能一直卡住。

11.4 一定要关闭资源

Socket、流、ServerSocket 都属于资源,用完最好关闭。

11.5 网络编程经常伴随异常处理

比如:

  • 连接失败
  • 对方断开
  • 端口占用
  • 读写超时

12. Java Socket 编程总结

如果以后回头看 Socket 忘了从哪里开始,就按下面这套顺序记:

12.1 先记住两个角色

  • 服务端:ServerSocket
  • 客户端:Socket

12.2 再记住通信本质

  • 建立连接
  • 获取流
  • 读写数据

12.3 再记住最常见模式

  • 文本通信:BufferedReader + PrintWriter
  • 通用/二进制通信:InputStream + OutputStream

12.4 最实用的一句话

Socket 编程就是“先连上,再通过 IO 流通信”;服务端负责监听,客户端负责连接。

Java 反射

反射(Reflection)是 Java 里一个非常重要、也非常容易一开始觉得抽象的知识点。

可以先记一句总纲:

反射就是程序在运行期间,动态获取类的信息,并操作类、对象、属性、方法、构造器。

也就是说,正常写代码时我们是“先知道类,再写代码操作它”;
而反射则是“运行时再去获取类的信息,并动态操作它”。

1. 反射到底在解决什么问题

普通写法里,我们在编译时就已经明确知道:

  • 类名是什么
  • 调哪个方法
  • 访问哪个字段

例如:

Person p = new Person();
p.setName("Tom");

而反射的思路是:

  • 类名可能是运行时才知道
  • 要调的方法可能是配置里写的
  • 要访问的属性名可能是字符串传进来的

所以反射解决的是:

运行时动态操作类。

2. 反射的核心类

最核心的类是:

  • Class
  • Field
  • Method
  • Constructor

它们分别对应:

  • 类本身
  • 成员变量
  • 成员方法
  • 构造方法

2.1 Class 类

反射的入口通常就是 Class 对象。

可以理解为:

每个类在 JVM 里都有一个对应的 Class 对象,里面记录了这个类的结构信息。

3. 获取 Class 对象的三种常见方式

3.1 类名.class

Class<Person> c1 = Person.class;

3.2 对象.getClass()

Person p = new Person();
Class<? extends Person> c2 = p.getClass();

3.3 Class.forName()

Class<?> c3 = Class.forName("com.demo.Person");

其中最值得记住的是:

Class.forName(...)

因为它最有“运行时动态加载”的味道。

4. 通过反射创建对象

4.1 调用无参构造

Class<?> clazz = Class.forName("com.demo.Person");
Object obj = clazz.getDeclaredConstructor().newInstance();

4.2 调用有参构造

Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
Object obj = constructor.newInstance("Tom", 18);

这里:

  • getDeclaredConstructor(...):获取构造器
  • newInstance(...):创建对象

5. 通过反射获取字段

字段就是成员变量,对应 Field

5.1 获取字段对象

Field field = clazz.getDeclaredField("name");

5.2 给字段赋值

field.setAccessible(true);
field.set(obj, "Tom");

5.3 读取字段值

Object value = field.get(obj);

这里的 setAccessible(true) 很重要,它表示:

允许访问 private 成员。

6. 通过反射获取方法

方法对应 Method

6.1 获取方法对象

Method method = clazz.getDeclaredMethod("setName", String.class);

6.2 调用方法

method.invoke(obj, "Tom");

如果是无参方法:

Method method = clazz.getDeclaredMethod("getName");
Object result = method.invoke(obj);

7. 获取类的结构信息

反射不仅能“调用”,还能“查看类的结构”。

例如:

  • 获取所有字段
  • 获取所有方法
  • 获取所有构造器
  • 获取包名
  • 获取父类
  • 获取接口

7.1 常见方法

  • getDeclaredFields()
  • getDeclaredMethods()
  • getDeclaredConstructors()
  • getSuperclass()
  • getInterfaces()

例如:

Field[] fields = clazz.getDeclaredFields();
Method[] methods = clazz.getDeclaredMethods();
Constructor<?>[] constructors = clazz.getDeclaredConstructors();

8. 反射为什么重要

很多框架底层都会大量使用反射。

例如:

  • Spring
  • MyBatis
  • JUnit
  • 各种 ORM 框架

原因很简单:

框架要做到“通用”,就不能把代码写死在某个具体类上,而是要在运行时动态处理用户提供的类。

9. 反射的优缺点

9.1 优点

  • 灵活
  • 动态性强
  • 适合做通用框架

9.2 缺点

  • 性能通常不如直接调用
  • 破坏封装性
  • 代码可读性和维护性会下降
  • 容易在运行时才暴露错误

所以反射很重要,但并不是说平时业务代码要大量主动使用它。

10. 反射中的常见概念区分

10.1 getField() 和 getDeclaredField()

  • getField():只能拿到 public 字段
  • getDeclaredField():可以拿到本类声明的字段,包括 private

10.2 getMethod() 和 getDeclaredMethod()

  • getMethod():获取 public 方法,包括父类继承来的
  • getDeclaredMethod():获取本类声明的方法,包括 private

10.3 setAccessible(true)

它的作用是:

暴力访问私有成员。

这个在反射里很常见,但也意味着你绕开了正常封装。

11. 注解、反射、框架的关系

这三者经常一起出现。

可以这样理解:

  • 注解:给类、方法、字段打标签
  • 反射:运行时读取这些标签和结构信息
  • 框架:根据这些信息自动完成配置和调用

所以很多框架本质上就是:

注解提供元信息,反射负责读取和执行。

12. 一个完整的反射小例子

class Person {
private String name;

public Person() {}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

使用反射:

Class<?> clazz = Person.class;
Object obj = clazz.getDeclaredConstructor().newInstance();

Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(obj, "Tom");

Method method = clazz.getDeclaredMethod("getName");
Object result = method.invoke(obj);

System.out.println(result);

13. 反射的使用场景

最常见的几个场景:

  • 框架底层
  • 根据配置文件动态加载类
  • 动态创建对象
  • 通用工具类封装
  • 单元测试或调试工具

14. Java 反射总结

如果以后回头看反射忘了从哪里入手,就按下面这套顺序记:

14.1 先记住反射在干什么

  • 运行时获取类信息
  • 运行时操作对象、字段、方法、构造器

14.2 再记住反射入口

  • Class 对象

14.3 再记住最常用的四类对象

  • Class
  • Constructor
  • Field
  • Method

14.4 最常见动作

  • 获取类:Class.forName()
  • 创建对象:getDeclaredConstructor().newInstance()
  • 改字段:Field.set(...)
  • 调方法:Method.invoke(...)

14.5 最实用的一句话

反射的本质,就是让程序在运行时“动态认识并操作类”;框架大量使用它,但日常业务代码不要为了炫技滥用。