Java基础阶段
Java进阶
Stream流
Stream 是 Java 8 引入的一套对集合/数组数据进行声明式处理的方式。它本质上不是一个“装数据的容器”,而是一条“处理数据的流水线”。
如果用一句话概括 Stream 的思想:
先拿到数据源,再按步骤描述“我要怎么处理数据”,最后一次性得到结果。
1. Stream流的思想
传统写法里,我们经常会写很多 for 循环:
- 先遍历一次筛选数据
- 再遍历一次做转换
- 再遍历一次排序
- 最后再收集结果
而 Stream 的核心思想就是把这些步骤串成一条流水线:
list.stream() |
这样写的好处:
- 代码更关注“做什么”,而不是“怎么一层层循环”
- 业务逻辑更清晰,筛选、转换、排序、汇总一眼就能看出来
- 很适合做集合数据处理
需要记住:Stream 不负责存储数据,它只是负责计算数据。
2. Stream流的几个核心特点
2.1 不存储数据
Stream 本身不保存元素,数据还是在数组、集合等容器中。
2.2 不改变原数据
大多数情况下,Stream 是从原集合中读取数据并生成新结果,不会直接修改原集合。
2.3 支持链式调用
中间操作可以一个接一个串起来,形成处理流水线。
2.4 惰性执行
只写中间操作时,代码通常不会立即执行,只有遇到终结操作才会真正开始处理数据。
list.stream() |
上面这段只有 filter(),没有终结操作,所以流水线不会真正跑起来。
2.5 一个Stream只能消费一次
Stream 一旦执行过终结操作,就不能再重复使用。
Stream<String> stream = list.stream(); |
3. 创建Stream流
3.1 通过集合创建
List<String> list = Arrays.asList("java", "python", "go"); |
如果想并行处理,还可以:
Stream<String> parallelStream = list.parallelStream(); |
但并行流不是越多越好,只有在数据量较大、任务适合并行时才考虑使用。
3.2 通过数组创建
String[] arr = {"java", "python", "go"}; |
3.3 直接通过若干元素创建
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5); |
3.4 创建基础类型流
Java 还为基本类型准备了专门的流,避免频繁装箱拆箱:
IntStreamLongStreamDoubleStream
IntStream intStream = IntStream.of(1, 2, 3, 4); |
4. Stream流的操作分类
Stream 的操作通常分成两大类:
- 中间操作:返回新的流,可以继续链式调用
- 终结操作:产生最终结果或者副作用,执行后流就结束了
也可以记成:
数据源 -> 中间操作 -> 终结操作
5. 常见中间操作
中间操作的特点是:返回的还是流。
5.1 filter() 过滤
按照条件筛选元素。
List<Integer> list = Arrays.asList(10, 15, 20, 25, 30); |
结果:[25, 30]
5.2 map() 映射/转换
把一种元素转换成另一种元素。
List<String> list = Arrays.asList("java", "python", "go"); |
结果:[4, 6, 2]
5.3 flatMap() 打平
当元素本身还是一个集合时,可以把多层结构“摊平”。
List<List<String>> lists = Arrays.asList( |
结果:[a, b, c, d]
map() 是“一对一转换”,flatMap() 是“拆开再合并”。
5.4 sorted() 排序
List<Integer> list = Arrays.asList(5, 2, 8, 1); |
自定义排序:
List<String> list = Arrays.asList("java", "python", "go"); |
5.5 distinct() 去重
List<Integer> list = Arrays.asList(1, 2, 2, 3, 3, 3); |
结果:[1, 2, 3]
5.6 limit() 取前几个
List<Integer> result = Stream.of(1, 2, 3, 4, 5, 6) |
结果:[1, 2, 3]
5.7 skip() 跳过前几个
List<Integer> result = Stream.of(1, 2, 3, 4, 5, 6) |
结果:[3, 4, 5, 6]
5.8 peek() 查看流中的元素
peek() 常用于调试,观察中间过程,不建议把核心业务逻辑写在里面。
List<Integer> result = Stream.of(1, 2, 3, 4) |
6. 常见终结操作(最终操作)
终结操作的特点是:一旦调用,流水线开始真正执行,并且流不能再复用。
6.1 forEach() 遍历
list.stream().forEach(System.out::println); |
6.2 collect() 收集
这是最常见的终结操作之一,用来把流中的结果收集到集合或其他容器中。
List<String> result = list.stream() |
收集为 Set:
Set<String> result = list.stream() |
收集为 Map:
Map<String, Integer> map = list.stream() |
6.3 toList()
在较新的 Java 版本中,可以直接:
List<String> result = list.stream() |
它和 collect(Collectors.toList()) 的使用体验很像,写法更简洁。
6.4 count() 统计数量
long count = list.stream() |
6.5 max() / min() 求最大最小值
Optional<Integer> max = Stream.of(3, 5, 2, 9) |
因为有可能流为空,所以返回的是 Optional<T>。
6.6 findFirst() / findAny()
Optional<String> first = list.stream() |
6.7 anyMatch() / allMatch() / noneMatch()
用于条件判断:
boolean b1 = list.stream().anyMatch(s -> s.length() > 5); |
6.8 reduce() 归约
reduce() 可以把一批数据反复合并成一个结果。
例如求和:
int sum = Stream.of(1, 2, 3, 4, 5) |
例如求乘积:
int result = Stream.of(1, 2, 3, 4) |
reduce() 的思想很重要,本质是“把多个值逐步折叠成一个值”。
7. 常见汇总方法(Collectors)
很多时候我们不是简单地拿一个 List,而是希望做分组、统计、拼接,这时最常用的是 Collectors。
7.1 joining() 字符串拼接
List<String> list = Arrays.asList("Java", "Python", "Go"); |
结果:Java, Python, Go
7.2 counting() 统计个数
long count = list.stream() |
7.3 summingInt() / averagingInt()
List<String> list = Arrays.asList("java", "python", "go"); |
7.4 groupingBy() 分组
这是非常常用的方法。
List<String> list = Arrays.asList("java", "go", "python", "c"); |
结果大致是:
{ |
7.5 partitioningBy() 分区
分区本质上是按 true/false 分成两组。
Map<Boolean, List<Integer>> map = Stream.of(1, 2, 3, 4, 5, 6) |
8. 一个完整案例
假设我们有一组名字,希望:
- 去重
- 过滤掉长度小于等于 3 的名字
- 全部转成大写
- 按长度倒序排序
- 最后收集成列表
List<String> names = Arrays.asList("java", "go", "python", "java", "rust"); |
执行流程理解为:
- 先从
names创建流 distinct()去重filter()过滤map()转换sorted()排序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() |
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<T> 的抽象方法是:
void accept(T t) |
而 println(String) 也正好是“接收一个参数,没有返回值”,所以可以匹配。
3. 方法引用的几种常见写法
方法引用最常见的有 4 大类:
类名::静态方法对象名::实例方法类名::实例方法类名::new
下面分别记。
4. 类名::静态方法
格式:
类名::静态方法 |
例如:
Function<String, Integer> f1 = s -> Integer.parseInt(s); |
这里 Integer.parseInt(String s) 本身就是一个静态方法,所以可以直接用 Integer::parseInt。
再看一个例子:
List<String> list = Arrays.asList("1", "2", "3"); |
这类写法通常比较容易理解,因为它本质上就是“拿这个类里的静态工具方法来用”。
5. 对象名::实例方法
格式:
对象名::实例方法 |
意思是:调用“某个具体对象”的实例方法。
例如:
PrintStream ps = System.out; |
更常见的写法就是:
list.forEach(System.out::println); |
这里的 System.out 是一个具体对象,println 是它的实例方法。
再举一个例子:
String prefix = "hello "; |
等价于:
Function<String, String> f = s -> prefix.concat(s); |
6. 类名::实例方法
格式:
类名::实例方法 |
这是最容易绕的一种,但它在 Stream 中特别常见。
先记住一句话:
如果 Lambda 的第一个参数,正好是某个对象的调用者,那么就可以写成 类名::实例方法。
例如:
Function<String, Integer> f1 = s -> s.length(); |
这里 s.length() 中,s 是调用者,所以可以写成 String::length。
再比如:
Predicate<String> p1 = s -> s.isEmpty(); |
在 Stream 里最常见:
List<String> list = Arrays.asList("java", "", "python"); |
如果想保留“非空字符串”,这种场景直接写 Lambda 反而更直观:
List<String> result = list.stream() |
如果是不需要取反、直接筛选空字符串,就可以自然写成方法引用:
List<String> emptyList = list.stream() |
如果你的 Java 版本支持,也可以这样写非空:
List<String> result = list.stream() |
这里的 Predicate.not(...) 是对方法引用再做一次取反,写法很顺,但要注意对应的 Java 版本支持情况。
还有一个非常经典的例子:
List<String> list = Arrays.asList("java", "python", "go"); |
这就是 类名::实例方法 的典型使用。
7. 构造方法引用
格式:
类名::new |
意思是:把“创建对象”这件事也当成一个函数来传。
例如无参构造:
Supplier<Person> s1 = () -> new Person(); |
例如有参构造:
Function<String, Person> f1 = name -> new Person(name); |
只要构造器参数能和函数式接口对上,就可以这么写。
8. 数组构造引用
格式:
类型[]::new |
例如:
Function<Integer, String[]> f1 = len -> new String[len]; |
这个写法在把流转成数组时很常见:
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() |
10.3 条件过滤
List<String> result = list.stream() |
10.4 转换基本类型
List<String> list = Arrays.asList("1", "2", "3"); |
10.5 收集为数组
String[] arr = list.stream().toArray(String[]::new); |
11. 使用方法引用时的注意点
11.1 可读性优先
方法引用的目的是让代码更清晰,不是为了“写得更短”。
如果改成方法引用后更绕,那就直接保留 Lambda。
11.2 类名::实例方法 最容易混淆
这个要单独记:
String::lengthString::isEmptyString::toUpperCase
它们本质上都等价于:
s -> s.length() |
也就是说:Lambda 的参数本身就是方法调用者。
11.3 方法引用不能替代所有Lambda
如果你在 Lambda 里还有判断、拼接、计算、多步处理,那一般就还是老老实实写 Lambda。
12. 方法引用总结
可以把方法引用记成一句话:
Lambda 只是“替别人调用一个现成方法”时,就可以考虑改成方法引用。
最常见的几种形式:
- 静态方法引用:
类名::静态方法 - 对象实例方法引用:
对象名::实例方法 - 任意对象实例方法引用:
类名::实例方法 - 构造方法引用:
类名::new - 数组构造引用:
类型[]::new
最值得优先记住的例子:
System.out::printlnString::lengthString::isEmptyInteger::parseIntPerson::newString[]::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"); |
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"); |
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"); |
这里的 read():
- 每次读取一个字节
- 如果返回
-1,表示读完了
按字节数组读取
FileInputStream fis = new FileInputStream("a.txt"); |
这比一个一个字节读更常用。
写文件
FileOutputStream fos = new FileOutputStream("a.txt"); |
如果想追加写入:
FileOutputStream fos = new FileOutputStream("a.txt", true); |
第二个参数是 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"); |
写文本
FileWriter fw = new FileWriter("a.txt"); |
追加写:
FileWriter fw = new FileWriter("a.txt", true); |
5.3 字节流和字符流怎么选
可以直接这样记:
- 文本文件:优先字符流
- 二进制文件:优先字节流
一句话助记:
能按“字符”理解的内容,用字符流;不能按字符理解的内容,用字节流。
6. 缓冲流
缓冲流的作用是:
给原来的流加一个缓冲区,提高读写效率。
最常见的有:
BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
6.1 字节缓冲流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("a.txt")); |
6.2 字符缓冲流
BufferedReader br = new BufferedReader(new FileReader("a.txt")); |
6.3 BufferedReader 的常用方法
最经典的方法是:
readLine():一次读一行
BufferedReader br = new BufferedReader(new FileReader("a.txt")); |
6.4 BufferedWriter 的常用方法
write():写内容newLine():换行
BufferedWriter bw = new BufferedWriter(new FileWriter("a.txt")); |
7. 转换流
有时候字节流和字符流要互相配合,这时就需要转换流。
最常见的两个类:
InputStreamReaderOutputStreamWriter
它们的作用是:
在字节流和字符流之间做桥梁。
7.1 为什么要用转换流
最典型的原因是:指定字符编码。
比如:
InputStreamReader isr = new InputStreamReader( |
这样就可以按指定编码把字节转换成字符。
7.2 常见写法
BufferedReader br = new BufferedReader( |
BufferedWriter bw = new BufferedWriter( |
助记:
InputStreamReader:字节输入流 -> 字符输入流OutputStreamWriter:字节输出流 -> 字符输出流
8. 对象流
对象流是 Java IO 里比较有代表性的一类流,它可以把对象直接写到文件里,或者从文件里读回来。
最常见的两个类:
ObjectOutputStreamObjectInputStream
8.1 序列化和反序列化
- 序列化:把对象写出去
- 反序列化:把对象读回来
8.2 使用前提
要被写入对象流的类,通常需要实现:
Serializable |
例如:
class Person implements Serializable { |
8.3 简单例子
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.dat")); |
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.dat")); |
9. 打印流
打印流最常见的是:
PrintStreamPrintWriter
它们的特点是:
- 输出更方便
- 可以直接打印各种类型
例如:
PrintWriter pw = new PrintWriter(new FileWriter("a.txt")); |
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"))) { |
这样写的好处:
- 不容易忘记关闭流
- 即使出现异常,也会自动关闭
平时写 IO,优先用这种写法。
12. 常见应用场景怎么选流
这里最适合做回顾记忆。
12.1 读写文本文件
优先:
FileReader/FileWriter- 或
BufferedReader/BufferedWriter
如果要按行读,优先:
BufferedReader
12.2 读写图片、音频、压缩包
优先:
FileInputStreamFileOutputStream- 或它们的缓冲流版本
12.3 需要指定编码
优先:
InputStreamReaderOutputStreamWriter
12.4 需要把对象保存到文件
优先:
ObjectOutputStreamObjectInputStream
13. 一个最常用的文件复制模板
文件复制本质上就是:
一边读,一边写。
最常见写法:
try ( |
这个模板非常常用,值得直接记住。
14. 文件与 IO 流总结
如果以后回头看 IO 忘了从哪里入手,就按下面这套顺序记:
14.1 先分清 File 和 流
File:表示文件和目录- 流:负责读写数据
14.2 再分清字节流和字符流
- 字节流:处理二进制,也能处理文本
- 字符流:更适合纯文本
14.3 再分清节点流和处理流
- 节点流:直接连数据源
- 处理流:套在外面增强功能
14.4 常用类优先记这些
- 文件字节流:
FileInputStream、FileOutputStream - 文件字符流:
FileReader、FileWriter - 缓冲字符流:
BufferedReader、BufferedWriter - 缓冲字节流:
BufferedInputStream、BufferedOutputStream - 转换流:
InputStreamReader、OutputStreamWriter - 对象流:
ObjectInputStream、ObjectOutputStream
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 { |
注意:
- 重写的是
run() - 真正启动线程要调用
start(),不是直接调run()
3.2 实现 Runnable
这是更常见、更推荐的基础写法。
class MyTask implements Runnable { |
也可以写成 Lambda:
Thread t = new Thread(() -> System.out.println("任务执行中")); |
3.3 实现 Callable
Runnable 不能返回结果,也不能直接抛出受检异常。
如果任务需要返回结果,更常用的是 Callable。
Callable<Integer> task = () -> 100; |
助记:
Runnable:不返回结果Callable:可以返回结果
4. 线程的常用方法
start():启动线程run():线程执行逻辑sleep(ms):让线程休眠join():等待某个线程执行完currentThread():获取当前线程yield():让出 CPUinterrupt():中断线程
4.1 start() 和 run() 的区别
这是初学线程时最容易混的点。
run():只是普通方法调用start():才会真正启动新线程
4.2 sleep() 和 join()
sleep():让当前线程暂停一段时间join():让当前线程等待另一个线程结束
例如:
Thread t = new Thread(() -> { |
5. 线程安全问题
并发最核心的问题就是线程安全。
5.1 什么叫线程安全
如果多个线程同时访问同一份共享数据,程序依然能得到正确结果,就可以认为它是线程安全的。
5.2 为什么会不安全
最常见原因:
- 多个线程同时读写同一个变量
- 操作不是原子的
- 线程执行顺序不可控
例如下面这个 count++ 看起来只有一行,但它本质上不是一步完成的:
count++; |
它通常可以拆成:
- 读取
count count + 1- 写回去
如果两个线程同时执行,就可能丢失更新。
6. synchronized 关键字
synchronized 是 Java 最基础的同步手段。
它的作用是:
同一时刻只允许一个线程进入被保护的临界区。
6.1 修饰普通方法
public synchronized void method() { |
锁的是当前对象 this。
6.2 修饰代码块
public void method() { |
这种方式更灵活,因为可以只锁关键部分。
6.3 修饰静态方法
public static synchronized void method() { |
锁的是类对象 Class。
6.4 synchronized 的使用场景
适合:
- 多线程修改共享变量
- 多线程访问同一临界资源
- 先追求正确,再考虑性能
助记:
- 普通同步方法锁
this - 静态同步方法锁
类对象 - 同步代码块锁括号里的对象
7. Lock 接口
除了 synchronized,Java 还提供了更灵活的显式锁,例如:
ReentrantLock
7.1 基本写法
Lock lock = new ReentrantLock(); |
这里最重要的一点是:
加锁后一定要在 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; |
这里即使加了 volatile,count++ 依然不是线程安全的。
助记:
volatile 保证“看得见”,不保证“改得安全”。
9. 原子类 Atomic
如果只是想对单个变量做线程安全的自增、自减、CAS 操作,常用的是原子类。
最常见:
AtomicIntegerAtomicLongAtomicBooleanAtomicReference
9.1 示例
AtomicInteger count = new AtomicInteger(0); |
9.2 为什么会有 Atomic
因为有些场景只是对一个变量做简单并发操作,用锁太重,原子类更轻量。
10. 线程通信:wait()、notify()、notifyAll()
多个线程不只是要“抢资源”,有时还要“配合工作”。
这就涉及线程通信。
最经典的方法:
wait()notify()notifyAll()
10.1 基本理解
wait():当前线程进入等待状态,并释放锁notify():随机唤醒一个等待线程notifyAll():唤醒所有等待线程
10.2 使用前提
这几个方法必须在同步代码块或同步方法中使用,并且是针对同一个锁对象调用。
synchronized (lock) { |
synchronized (lock) { |
11. 线程池
并发编程里非常重要的一条原则是:
不要频繁手动创建和销毁线程。
因为线程本身也是资源,频繁创建销毁开销很大。
所以更推荐使用线程池。
11.1 为什么要用线程池
- 复用线程
- 降低创建线程的开销
- 统一管理线程
- 更容易控制并发数量
11.2 ExecutorService
线程池最常见的使用接口是:
ExecutorService |
例如:
ExecutorService pool = Executors.newFixedThreadPool(3); |
11.3 常见线程池
newFixedThreadPool(n):固定线程数newCachedThreadPool():按需创建线程newSingleThreadExecutor():单线程线程池newScheduledThreadPool(n):定时任务线程池
11.4 submit() 和 execute()
execute():执行Runnable,不关心返回值submit():可以提交Runnable或Callable,并返回Future
11.5 shutdown() 和 shutdownNow()
shutdown():平缓关闭,不再接收新任务,但会等已提交任务执行完shutdownNow():尝试立刻停止
12. Future 和 Callable
如果任务提交后,后面还想拿结果,可以配合:
CallableFuture
示例:
ExecutorService pool = Executors.newFixedThreadPool(2); |
这里:
submit()提交任务Future代表未来结果get()用来拿返回值
13. 并发工具类
Java 并发包 java.util.concurrent 里有很多非常常用的工具类。
先记几个最常见的:
CountDownLatchCyclicBarrierSemaphore
13.1 CountDownLatch
作用:
一个线程等待其他多个线程完成。
例如“主线程等 3 个子线程都执行完”。
核心方法:
countDown()await()
13.2 CyclicBarrier
作用:
多个线程互相等待,等到都到齐后再一起继续。
13.3 Semaphore
作用:
控制同时访问某资源的线程数量。
可以把它理解成“许可证”。
14. 并发集合
普通集合在多线程环境下通常不安全,所以 Java 也提供了并发集合。
例如:
ConcurrentHashMapCopyOnWriteArrayListBlockingQueue
14.1 ConcurrentHashMap
线程安全版的 Map,常用于并发环境下的键值存储。
14.2 CopyOnWriteArrayList
适合:
- 读多写少
14.3 BlockingQueue
非常适合生产者-消费者模型。
常见实现:
ArrayBlockingQueueLinkedBlockingQueue
15. 死锁
死锁是并发里非常经典的问题。
15.1 什么是死锁
多个线程互相等待对方释放资源,结果谁都走不下去。
15.2 经典场景
线程 A 拿了锁 1,等锁 2;
线程 B 拿了锁 2,等锁 1。
这样就卡住了。
15.3 怎么避免死锁
- 加锁顺序保持一致
- 尽量减少锁嵌套
- 缩小同步代码块范围
- 必要时使用尝试加锁
16. 线程状态
Java 线程常见状态可以先记这几个:
NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED
助记:
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 最常用的知识点优先记这些
- 创建线程:
Thread、Runnable、Callable - 同步:
synchronized - 显式锁:
Lock、ReentrantLock - 可见性:
volatile - 原子操作:
AtomicInteger - 线程池:
ExecutorService - 异步结果:
Future - 并发集合:
ConcurrentHashMap
18.4 最实用的一句话
并发不是为了“开更多线程”,而是为了更高效地处理多个任务;而并发编程真正难的地方,是共享数据的安全和线程之间的协作。
Java Socket 网络编程
网络编程这一块,本质上就是让两台机器上的程序能够互相通信。
在 Java 里,最常见的入门方式就是 Socket 编程。
可以先记一句总纲:
Socket 是程序和网络之间的通信接口。
也可以把它理解成:
- 客户端和服务端之间的一条通信连接
- 程序读写网络数据时使用的“插座”
1. 网络编程的基本思想
Java Socket 编程最核心的模型就是:
- 服务端先启动,监听某个端口
- 客户端主动连接这个端口
- 双方建立连接后,通过输入流和输出流通信
所以本质上就是:
连接 + 读写数据
2. Socket 编程里的几个核心类
最常用的类主要有:
ServerSocketSocketInputStreamOutputStreamBufferedReaderBufferedWriterPrintWriter
2.1 ServerSocket
ServerSocket 只在服务端使用。
它的作用:
- 绑定端口
- 监听客户端连接
2.2 Socket
Socket 表示一次网络连接。
它既可以在客户端使用,也可以在服务端使用。
作用:
- 连接远程主机
- 获取输入流和输出流
- 进行数据通信
3. 服务端和客户端的基本流程
3.1 服务端流程
- 创建
ServerSocket - 绑定端口
- 调用
accept()等待客户端连接 - 拿到
Socket - 通过输入输出流收发数据
- 关闭资源
3.2 客户端流程
- 创建
Socket - 指定服务器 IP 和端口
- 获取输入输出流
- 发送或接收数据
- 关闭资源
4. 最基础的 TCP 通信
Java 里最常见的 Socket 入门默认就是基于 TCP。
TCP 的特点:
- 面向连接
- 可靠传输
- 有顺序
所以大多数“先连接再传数据”的场景,先想到 TCP。
4.1 服务端示例
ServerSocket serverSocket = new ServerSocket(8888); |
4.2 客户端示例
Socket socket = new Socket("127.0.0.1", 8888); |
这就是最基础的 TCP 通信骨架。
5. 为什么 Socket 通信和 IO 流连在一起
这也是最容易理解的一点:
网络通信本质上就是数据输入输出。
所以 Socket 连接建立后,Java 给我们的操作方式就是:
getInputStream():读对方发来的数据getOutputStream():向对方发送数据
也就是说:
- 文件 IO 是对文件读写
- Socket IO 是对网络连接读写
6. 常见读写方式
6.1 用字节流读写
适合:
- 通用数据传输
- 二进制内容
- 简单字符串测试
InputStream is = socket.getInputStream(); |
6.2 用字符流按行读写
如果传输的是文本,常常会再套一层字符流和缓冲流。
BufferedReader br = new BufferedReader( |
这里:
BufferedReader适合按行读取PrintWriter适合方便地写文本
6.3 一个按行通信的例子
服务端读:
BufferedReader br = new BufferedReader( |
客户端写:
PrintWriter pw = new PrintWriter(socket.getOutputStream(), true); |
这里要特别注意:
如果服务端用 readLine(),那客户端通常就要发带换行的数据,比如 println()。
7. accept() 的意义
accept() 是服务端编程里最核心的方法之一。
它的作用:
阻塞等待客户端连接。
也就是说,如果没有客户端连过来,程序会停在这里等。
Socket socket = serverSocket.accept(); |
一旦有客户端连接,就会返回一个 Socket 对象,表示这次连接建立成功。
8. 单次通信和循环通信
很多初学时的例子只收发一次数据,但真实场景通常不是“一次就结束”。
8.1 单次通信
适合入门理解。
例如:
- 客户端发一句话
- 服务端收一句话
- 然后连接关闭
8.2 循环通信
更真实的场景通常是:
- 不断读取对方消息
- 直到对方关闭连接或收到结束标记
例如:
BufferedReader br = new BufferedReader( |
9. 多客户端连接问题
服务端如果只写成:
Socket socket = serverSocket.accept(); |
那通常只处理一个连接。
如果希望服务端能同时服务多个客户端,常见做法就是:
- 每来一个客户端连接,就交给一个新线程处理
- 或者交给线程池处理
例如:
ServerSocket serverSocket = new ServerSocket(8888); |
所以 Socket 编程和并发编程经常会一起出现。
10. UDP 简单理解
除了 TCP,网络编程里还有 UDP。
10.1 UDP 的特点
- 无连接
- 不保证可靠送达
- 速度通常更快
- 适合对实时性要求高、允许少量丢包的场景
10.2 Java 里的相关类
DatagramSocketDatagramPacket
初学阶段先把 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(); |
而反射的思路是:
- 类名可能是运行时才知道
- 要调的方法可能是配置里写的
- 要访问的属性名可能是字符串传进来的
所以反射解决的是:
运行时动态操作类。
2. 反射的核心类
最核心的类是:
ClassFieldMethodConstructor
它们分别对应:
- 类本身
- 成员变量
- 成员方法
- 构造方法
2.1 Class 类
反射的入口通常就是 Class 对象。
可以理解为:
每个类在 JVM 里都有一个对应的 Class 对象,里面记录了这个类的结构信息。
3. 获取 Class 对象的三种常见方式
3.1 类名.class
Class<Person> c1 = Person.class; |
3.2 对象.getClass()
Person p = new Person(); |
3.3 Class.forName()
Class<?> c3 = Class.forName("com.demo.Person"); |
其中最值得记住的是:
Class.forName(...) |
因为它最有“运行时动态加载”的味道。
4. 通过反射创建对象
4.1 调用无参构造
Class<?> clazz = Class.forName("com.demo.Person"); |
4.2 调用有参构造
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class); |
这里:
getDeclaredConstructor(...):获取构造器newInstance(...):创建对象
5. 通过反射获取字段
字段就是成员变量,对应 Field。
5.1 获取字段对象
Field field = clazz.getDeclaredField("name"); |
5.2 给字段赋值
field.setAccessible(true); |
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"); |
7. 获取类的结构信息
反射不仅能“调用”,还能“查看类的结构”。
例如:
- 获取所有字段
- 获取所有方法
- 获取所有构造器
- 获取包名
- 获取父类
- 获取接口
7.1 常见方法
getDeclaredFields()getDeclaredMethods()getDeclaredConstructors()getSuperclass()getInterfaces()
例如:
Field[] fields = clazz.getDeclaredFields(); |
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 { |
使用反射:
Class<?> clazz = Person.class; |
13. 反射的使用场景
最常见的几个场景:
- 框架底层
- 根据配置文件动态加载类
- 动态创建对象
- 通用工具类封装
- 单元测试或调试工具
14. Java 反射总结
如果以后回头看反射忘了从哪里入手,就按下面这套顺序记:
14.1 先记住反射在干什么
- 运行时获取类信息
- 运行时操作对象、字段、方法、构造器
14.2 再记住反射入口
Class对象
14.3 再记住最常用的四类对象
ClassConstructorFieldMethod
14.4 最常见动作
- 获取类:
Class.forName() - 创建对象:
getDeclaredConstructor().newInstance() - 改字段:
Field.set(...) - 调方法:
Method.invoke(...)
14.5 最实用的一句话
反射的本质,就是让程序在运行时“动态认识并操作类”;框架大量使用它,但日常业务代码不要为了炫技滥用。

