Java 常用特性整理(测开)
一、Java 基础数据类型
核心结论
Java 有 8 种基本数据类型,直接存储值(存在栈上),不是对象。除此之外的所有类型(包括 String、数组)都是引用类型,存储的是对象的地址。
1. 八大基本类型
| 类型 | 字节数 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|---|
byte | 1 | 0 | -128 ~ 127 | 最小整数类型 |
short | 2 | 0 | -32768 ~ 32767 | 短整型 |
int | 4 | 0 | -2^31 ~ 2^31-1 | 最常用整数类型 |
long | 8 | 0L | -2^63 ~ 2^63-1 | 长整型,字面量加 L |
float | 4 | 0.0f | 约 ±3.4e38 | 单精度浮点,字面量加 f |
double | 8 | 0.0 | 约 ±1.8e308 | 双精度浮点,默认浮点类型 |
char | 2 | ‘\u0000’ | 0 ~ 65535 | Unicode 字符 |
boolean | — | false | true / false | JVM 实现相关,通常 1 字节 |
int a = 100;
long b = 100L;
float c = 3.14f;
double d = 3.14;
char e = 'A';
boolean f = true;
2. String 是引用类型
String 不是基本类型,是 java.lang.String 类的实例(引用类型)。字符串字面量存储在字符串常量池中,new String() 则在堆上创建新对象。
String s1 = "hello"; // 字符串常量池
String s2 = new String("hello"); // 堆上新对象
System.out.println(s1 == s2); // false(引用地址不同)
System.out.println(s1.equals(s2)); // true(内容相同)
关键特性:
String是不可变的(final类),每次"修改"都会创建新对象- 频繁拼接字符串应使用
StringBuilder(非线程安全)或StringBuffer(线程安全) - 比较字符串内容一律用
equals(),不要用==
3. 数组
数组是引用类型,长度固定,元素类型相同。
// 一维数组
int[] arr = new int[5];
int[] arr2 = {1, 2, 3, 4, 5};
// 二维数组
int[][] matrix = new int[3][4];
int[][] matrix2 = {{1,2}, {3,4}, {5,6}};
// 遍历
for (int i = 0; i < arr.length; i++) { }
for (int x : arr) { } // 增强 for 循环(推荐)
二、包装类型与自动拆装箱
核心结论
每个基本类型都有对应的包装类,位于 java.lang 包。包装类是对象,可以存入集合(如 List<Integer>),也提供了大量工具方法。Java 5 引入自动装箱/拆箱,让两者可以无缝转换,但要注意 Integer 缓存陷阱和拆箱 NPE 两个常见坑。
1. 基本类型 vs 包装类型
| 基本类型 | 包装类型 | 常用方法 |
|---|---|---|
int | Integer | parseInt(), valueOf(), intValue() |
long | Long | parseLong(), valueOf() |
double | Double | parseDouble() |
float | Float | parseFloat() |
boolean | Boolean | parseBoolean() |
char | Character | isDigit(), isLetter(), toLowerCase() |
byte | Byte | parseByte() |
short | Short | parseShort() |
2. 自动装箱与自动拆箱
编译器自动完成基本类型与包装类型之间的转换,本质是插入 valueOf() 和 xxxValue() 调用。
// 自动装箱:基本类型 → 包装类型(编译器插入 Integer.valueOf())
Integer a = 100; // 等价于 Integer a = Integer.valueOf(100);
list.add(1); // 等价于 list.add(Integer.valueOf(1));
// 自动拆箱:包装类型 → 基本类型(编译器插入 intValue())
int b = a; // 等价于 int b = a.intValue();
int sum = a + 10; // a 先拆箱再运算
3. 整数缓存陷阱(Integer Cache)
Integer.valueOf() 对 -128 ~ 127 范围内的整数使用缓存,超出范围每次创建新对象。这是高频面试考点。
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true(同一个缓存对象)
Integer m = 128;
Integer n = 128;
System.out.println(m == n); // false(不同对象)
System.out.println(m.equals(n)); // true(值相等)
结论:包装类型比较值一律用 equals(),不要用 ==。
4. 自动拆箱的 NullPointerException
当包装类型为 null 时,自动拆箱会触发 NPE,这是一个容易忽视的运行时错误。
Integer count = null;
int result = count + 1; // 拆箱调用 count.intValue() → NullPointerException
// 防御写法
if (count != null) { int result2 = count + 1; }
// Java 8+ 推荐
int result3 = Optional.ofNullable(count).orElse(0) + 1;
5. 包装类常用工具方法
// String → 基本类型
int i = Integer.parseInt("42");
double d = Double.parseDouble("3.14");
boolean b = Boolean.parseBoolean("true");
// 基本类型 → String
String s1 = String.valueOf(42);
String s2 = Integer.toString(42);
// 数值工具
Integer.MAX_VALUE; // 2147483647
Integer.toBinaryString(10); // "1010"
Integer.toHexString(255); // "ff"
// Character 工具
Character.isDigit('A'); // false
Character.isLetter('A'); // true
Character.toLowerCase('A'); // 'a'
三、StringBuilder 与 StringBuffer
核心结论
String 是不可变的,每次拼接都会创建新对象,在循环中大量拼接会产生严重的性能问题。StringBuilder(单线程)和 StringBuffer(多线程)是可变字符序列,专为高效字符串拼接设计。
1. 三者对比
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变) | 不安全 | 安全(synchronized) |
| 性能 | 拼接慢(每次创建新对象) | 快 | 稍慢(同步开销) |
| 适用场景 | 内容不变的字符串 | 单线程字符串拼接 | 多线程字符串拼接 |
2. 为什么不用 String 做循环拼接
String 的 + 拼接在循环中每次都会创建新对象,产生大量临时垃圾,性能极差。
// 错误示范:10000 次循环产生 10000 个临时 String 对象
String result = "";
for (int i = 0; i < 10000; i++) {
result += i;
}
// 正确做法:StringBuilder 内部维护可扩容的 char[],无临时对象
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result2 = sb.toString();
3. StringBuilder 常用方法
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(' ').append(42); // 链式追加
sb.insert(5, " World"); // 在索引 5 处插入
sb.delete(5, 11); // 删除索引 5~10(左闭右开)
sb.deleteCharAt(0); // 删除索引 0 的字符
sb.replace(0, 5, "Hi"); // 替换索引 0~4
sb.reverse(); // 反转
int idx = sb.indexOf("Hi"); // 查找子串位置
char c = sb.charAt(0); // 获取某位置字符
int len = sb.length(); // 长度
String s = sb.toString(); // 转为 String
// 链式调用示例
String result = new StringBuilder()
.append("Name: ").append("Alice")
.append(", Age: ").append(30)
.toString();
4. 如何选择
| 场景 | 推荐 |
|---|---|
| 单线程字符串拼接 | StringBuilder |
| 多线程共享字符串构建 | StringBuffer |
| 字符串内容固定不变 | String |
四、继承与多态
核心结论
继承让子类复用父类的属性和方法,多态让同一个方法调用在运行时表现出不同行为。Java 是单继承(一个类只能有一个父类),但所有类都隐式继承 Object。
1. 访问修饰符对继承的影响
| 修饰符 | 子类能否直接访问 | 子类能否重写 | 说明 |
|---|---|---|---|
private | 否(内存中存在,但不可见) | 否 | 只能通过父类提供的 getter/setter 访问 |
protected | 是 | 是 | 同包或子类可访问 |
public | 是 | 是 | 任何地方可访问 |
final 方法 | 是(可调用) | 否 | 防止子类修改行为 |
final 类 | — | 不能被继承 | 如 String、Integer |
static | 是(属于类) | 否(只能隐藏) | 不参与多态 |
2. 构造器与 super
子类构造器必须调用父类构造器,且 super() 必须是第一行。若父类没有无参构造器,子类必须显式调用 super(参数列表)。
public class Vehicle {
private String brand;
public Vehicle(String brand) { this.brand = brand; }
}
public class Car extends Vehicle {
private int doors;
public Car(String brand, int doors) {
super(brand); // 必须是第一行
this.doors = doors;
}
}
执行顺序: 父类构造器 → 子类构造器,始终如此。
3. 方法重写(Override)vs 重载(Overload)
重写发生在父子类之间,重载发生在同一个类中。
| 特性 | 重写 Override | 重载 Overload |
|---|---|---|
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须相同 | 必须不同 |
| 返回类型 | 相同或子类型(协变) | 可以不同 |
| 访问权限 | 不能比父类更严格 | 无限制 |
| 发生位置 | 父子类之间 | 同一个类中 |
public class Animal {
public void speak() { System.out.println("..."); }
}
public class Dog extends Animal {
@Override
public void speak() { System.out.println("汪汪"); } // 重写
public void speak(String msg) { System.out.println(msg); } // 重载
public void speak(int times) { for (int i = 0; i < times; i++) speak(); }
}
4. 多态
多态是指:针对某个类型的方法调用,其真正执行的方法取决于运行时的实际类型,而非声明类型。
// 向上转型(自动,安全)
Animal a = new Dog();
a.speak(); // 运行时调用 Dog 的 speak(),而非 Animal 的
// 向下转型(需要 instanceof 判断,否则可能 ClassCastException)
if (a instanceof Dog d) { // Java 16+ 模式匹配,更简洁
d.fetch();
}
多态的价值: 统一处理不同子类,扩展新子类无需修改已有代码。
Animal[] animals = {new Dog(), new Cat(), new Bird()};
for (Animal animal : animals) {
animal.speak(); // 各自调用自己的实现,调用方无需关心具体类型
}
五、抽象类与接口
核心结论
抽象类和接口都是面向抽象编程的工具,让调用者只关心"能做什么",不关心"怎么做"。两者的选择原则:有共同状态(字段)用抽象类,只定义能力契约用接口。一个类只能继承一个抽象类,但可以实现多个接口。
1. 抽象类
用 abstract 修饰,不能被实例化。可以有构造器、普通方法、抽象方法。子类必须实现所有抽象方法,否则子类也必须声明为 abstract。
public abstract class Shape {
private String color;
public Shape(String color) { this.color = color; } // 有构造器
public abstract double area(); // 抽象方法:只有定义,无实现
public abstract double perimeter();
public void describe() { // 普通方法:子类直接继承
System.out.println("颜色: " + color + ", 面积: " + area());
}
}
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) { super(color); this.radius = radius; }
@Override public double area() { return Math.PI * radius * radius; }
@Override public double perimeter() { return 2 * Math.PI * radius; }
}
2. 接口
用 interface 定义,字段隐式为 public static final,方法隐式为 public abstract。Java 8 起支持 default 和 static 方法。
public interface Flyable {
double MAX_ALTITUDE = 10000.0; // 隐式 public static final
void fly(); // 隐式 public abstract,实现类必须重写
void land();
default void hover() { // Java 8+:实现类可选择是否重写
System.out.println("悬停中...");
}
static boolean isSafe(double alt) { return alt <= MAX_ALTITUDE; } // 只能通过接口名调用
}
// 一个类可以实现多个接口
public class Duck extends Animal implements Flyable, Swimmable {
@Override public void fly() { System.out.println("鸭子飞"); }
@Override public void land() { System.out.println("鸭子降落"); }
@Override public void swim() { System.out.println("鸭子游泳"); }
}
3. 抽象类 vs 接口
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 继承/实现数量 | 单继承 | 多实现 |
| 构造器 | 有 | 无 |
| 字段 | 任意类型 | 只有常量(public static final) |
| 方法 | 任意 | 抽象 + default + static |
| 适用场景 | 有共同状态和行为的类族 | 定义能力/契约 |
4. 静态字段与静态方法
静态成员属于类本身,所有实例共享,通过类名访问。
public class Counter {
private static int count = 0; // 静态字段:所有实例共享
private int id; // 实例字段:每个实例独有
public Counter() { this.id = ++count; }
public static int getCount() { return count; } // 静态方法
public int getId() { return id; } // 实例方法
}
Counter.getCount(); // 推荐:通过类名访问静态成员
new Counter().getCount(); // 能编译,但不推荐,容易误解为实例方法
六、异常处理
核心结论
Java 异常分三类:Error(JVM 级严重错误,不处理)、受检异常(编译器强制处理)、非受检异常(运行时异常,可选处理)。finally 块无论是否发生异常都会执行,常用于资源释放。
1. 异常类层次结构
Throwable
├── Error(严重错误,通常不处理)
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception
├── RuntimeException(非受检,可选处理)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ClassCastException
│ └── IllegalArgumentException
└── 受检异常(必须 catch 或 throws)
├── IOException → FileNotFoundException
└── SQLException
| 类型 | 继承自 | 是否必须处理 | 示例 |
|---|---|---|---|
| 受检异常 | Exception | 必须 catch 或 throws | IOException, SQLException |
| 非受检异常 | RuntimeException | 可选 | NullPointerException, IllegalArgumentException |
| 错误 | Error | 通常不处理 | OutOfMemoryError, StackOverflowError |
2. try-catch-finally 执行规则
catch 中子类异常放前面,父类放后面。finally 无论如何都会执行(除非 System.exit())。
try {
// 可能抛出异常的代码
} catch (FileNotFoundException e) { // 子类放前面
// 处理更具体的异常
} catch (IOException e) { // 父类放后面
// 处理更宽泛的异常
} finally {
// 无论是否异常都执行,通常用于关闭资源
}
执行顺序:
| 场景 | try | catch | finally | 返回值 |
|---|---|---|---|---|
| 无异常 | 执行 | 跳过 | 执行 | try 的 return |
| 有异常且被捕获 | 部分执行 | 执行 | 执行 | catch 的 return |
| 有异常未被捕获 | 部分执行 | 跳过 | 执行 | 异常向上传播 |
| finally 有 return | — | — | 执行 | finally 的 return(覆盖前面) |
关键结论:
finally中的return会覆盖 try/catch 的return,生产代码中避免在 finally 写 returnfinally中修改基本类型变量不影响已确定的返回值快照
public int test() {
try { return 1; }
finally { return 2; } // 实际返回 2
}
public int test2() {
int x = 1;
try { return x; } // 返回值快照已确定为 1
finally { x = 2; } // 修改 x,但不影响快照,实际返回 1
}
3. throw 与 throws
| 特性 | throw | throws |
|---|---|---|
| 位置 | 方法体内 | 方法签名上 |
| 作用 | 实际抛出一个异常实例 | 声明方法可能抛出的异常类型 |
| 数量 | 每次只能抛出一个 | 可声明多个,逗号分隔 |
// throw:主动抛出
public void validateAge(int age) {
if (age < 0) throw new IllegalArgumentException("年龄不能为负数: " + age);
}
// throws:声明受检异常,调用方必须处理或继续声明
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
}
4. 自定义异常与异常链
业务异常推荐继承 RuntimeException,避免强制调用方处理。异常链通过 cause 参数保留原始异常信息,便于排查根因。
public class BusinessException extends RuntimeException {
private final int errorCode;
public BusinessException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
// 异常链构造器:包装底层异常,保留原始信息
public BusinessException(int errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public int getErrorCode() { return errorCode; }
}
// 使用:底层 SQLException 被包装为业务异常,调用方看到的是业务语义
try {
// 数据库操作
} catch (SQLException e) {
throw new BusinessException(500, "数据库操作失败", e); // e 作为 cause
}
5. try-with-resources(推荐写法)
Java 7 引入,实现了 AutoCloseable 的资源会在 try 块结束后自动关闭,无需手动在 finally 中关闭。
// 旧写法:需要手动在 finally 关闭,代码冗长
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// ...
} finally {
if (fis != null) fis.close();
}
// 新写法(推荐):自动关闭,即使发生异常也会关闭
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
6. 常见异常速查
运行时异常(不强制处理):
| 异常类 | 触发原因 | 典型场景 |
|---|---|---|
NullPointerException | 对 null 引用调用方法 | str.length() 而 str 为 null |
ArrayIndexOutOfBoundsException | 数组下标越界 | arr[arr.length] |
ClassCastException | 强制类型转换失败 | (Dog) new Cat() |
NumberFormatException | 字符串转数字格式错误 | Integer.parseInt("abc") |
ArithmeticException | 算术错误 | 整数除以 0 |
ConcurrentModificationException | 迭代时修改集合 | for-each 中 list.remove() |
StackOverflowError | 递归过深,栈溢出 | 无终止条件的递归 |
受检异常(必须处理):
| 异常类 | 触发原因 |
|---|---|
IOException | 文件读写、网络 IO 失败 |
FileNotFoundException | 文件不存在 |
SQLException | 数据库操作失败 |
InterruptedException | 线程被中断 |
ClassNotFoundException | 反射加载类失败 |
七、集合框架
核心结论
Java 集合框架分两大体系:Collection(单值)和 Map(键值对)。日常最常用的是 ArrayList、HashMap、HashSet。选择集合类型时主要考虑:是否需要有序、是否允许重复、是否需要线程安全。
1. 集合类层次结构
Collection
├── List(有序,可重复)
│ ├── ArrayList — 动态数组,随机访问 O(1),推荐首选
│ ├── LinkedList — 双向链表,头尾增删 O(1),也实现了 Deque
│ └── Vector — 线程安全版 ArrayList(已过时)
├── Set(无序,不可重复)
│ ├── HashSet — 基于 HashMap,增删查 O(1)
│ ├── LinkedHashSet — 保持插入顺序
│ └── TreeSet — 基于红黑树,自动排序
└── Queue(队列)
├── LinkedList — 普通队列 / 双端队列
├── ArrayDeque — 高效双端队列(推荐替代 Stack)
└── PriorityQueue — 优先级队列(小顶堆)
Map(键值对,Key 不可重复)
├── HashMap — 最常用,无序,Key/Value 可为 null
├── LinkedHashMap — 保持插入顺序
├── TreeMap — 按 Key 排序
├── Hashtable — 线程安全(已过时,用 ConcurrentHashMap)
└── ConcurrentHashMap — 线程安全的高性能 HashMap
2. List
List 是有序、可重复的集合,ArrayList 适合随机访问,LinkedList 适合频繁头尾增删。
// ArrayList:最常用
List<String> list = new ArrayList<>();
list.add("Apple");
list.add(1, "Cherry"); // 在索引 1 处插入
list.set(0, "Avocado"); // 修改索引 0 的元素
list.remove("Banana"); // 按值删除(第一个匹配)
list.remove(0); // 按索引删除
list.get(0); // 随机访问 O(1)
list.size();
list.contains("Apple");
// 遍历(三种方式)
for (String s : list) { System.out.println(s); }
list.forEach(System.out::println);
for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }
// LinkedList:适合头尾操作
LinkedList<String> linked = new LinkedList<>();
linked.addFirst("First");
linked.addLast("Last");
linked.removeFirst();
linked.peekFirst(); // 查看但不移除
// 不可变列表(Java 9+)
List<String> immutable = List.of("A", "B", "C"); // 不能增删改
3. Set
Set 不允许重复元素,HashSet 最快,TreeSet 自动排序,LinkedHashSet 保持插入顺序。
// HashSet:无序,O(1) 增删查
Set<String> set = new HashSet<>();
set.add("Apple");
set.add("Apple"); // 重复,不会加入,add() 返回 false
set.contains("Apple"); // true
set.remove("Apple");
// LinkedHashSet:按插入顺序迭代
Set<String> linked = new LinkedHashSet<>();
linked.add("C"); linked.add("A"); linked.add("B");
System.out.println(linked); // [C, A, B]
// TreeSet:自动排序(自然顺序或自定义 Comparator)
Set<Integer> tree = new TreeSet<>();
tree.add(5); tree.add(1); tree.add(3);
System.out.println(tree); // [1, 3, 5]
// 集合运算
Set<String> a = new HashSet<>(Arrays.asList("A", "B", "C"));
Set<String> b = new HashSet<>(Arrays.asList("B", "C", "D"));
a.retainAll(b); // 交集:a 变为 {B, C}
a.addAll(b); // 并集
a.removeAll(b); // 差集
4. Map
Map 存储键值对,Key 不可重复。HashMap 是最常用的实现。
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 90);
map.put("Alice", 95); // 覆盖已有 Key 的值,返回旧值 90
map.get("Alice"); // 95
map.getOrDefault("Eve", 0); // Key 不存在时返回默认值 0
map.containsKey("Alice"); // true
map.containsValue(95); // true
map.remove("Alice");
// 遍历(推荐 entrySet,只遍历一次)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
map.forEach((k, v) -> System.out.println(k + ": " + v));
// Java 8 工具方法
map.putIfAbsent("Bob", 88); // Key 不存在时才放入
map.computeIfAbsent("Dave", k -> 70); // Key 不存在时计算并放入
map.merge("Alice", 5, Integer::sum); // 合并:Alice 的值 +5
5. 如何选择集合
| 需求 | 推荐 |
|---|---|
| 有序、可重复、随机访问快 | ArrayList |
| 有序、可重复、头尾增删频繁 | LinkedList |
| 无序、不重复、查找快 | HashSet |
| 不重复、保持插入顺序 | LinkedHashSet |
| 不重复、自动排序 | TreeSet |
| 键值对、无序 | HashMap |
| 键值对、保持插入顺序 | LinkedHashMap |
| 键值对、按 Key 排序 | TreeMap |
| 线程安全的键值对 | ConcurrentHashMap |
6. ArrayList vs LinkedList 对比
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
随机访问 get(i) | O(1) | O(n) |
| 头部插入/删除 | O(n)(需移动元素) | O(1) |
| 尾部插入 | O(1) 均摊 | O(1) |
| 内存占用 | 较小 | 较大(存前后指针) |
7. Collections 工具类
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));
Collections.sort(list); // 升序
Collections.sort(list, Comparator.reverseOrder()); // 降序
Collections.reverse(list); // 反转
Collections.shuffle(list); // 随机打乱
Collections.swap(list, 0, 1); // 交换两个位置
Collections.max(list); // 最大值
Collections.min(list); // 最小值
Collections.frequency(list, 1); // 元素出现次数
List<Integer> syncList = Collections.synchronizedList(list); // 线程安全包装
List<Integer> unmodifiable = Collections.unmodifiableList(list); // 只读包装
8. 线程安全集合(高频面试点)
8.1 传统线程安全集合(已过时)
| 集合 | 线程安全方式 | 问题 | 推荐替代 |
|---|---|---|---|
Vector | 方法全部 synchronized | 锁粒度粗,性能差 | CopyOnWriteArrayList |
Hashtable | 方法全部 synchronized | 性能差 | ConcurrentHashMap |
Stack | 继承自 Vector | 已过时 | ArrayDeque |
这些类虽然线程安全,但新项目中通常不推荐使用。
8.2 Collections.synchronizedXXX 包装类
List<String> syncList =
Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap =
Collections.synchronizedMap(new HashMap<>());
特点:
- 可以快速把普通集合包装成线程安全集合
- 本质仍然是
synchronized - 并发性能一般
- 迭代时必须手动加锁
synchronized (syncList) {
for (String s : syncList) {
System.out.println(s);
}
}
8.3 JUC 高性能并发集合(推荐)
List
| 类 | 原理 | 场景 |
|---|---|---|
CopyOnWriteArrayList | 写时复制 | 读多写少 |
CopyOnWriteArrayList<String> list =
new CopyOnWriteArrayList<>();
Set
| 类 | 原理 | 场景 |
|---|---|---|
CopyOnWriteArraySet | 写时复制 | 读多写少 |
ConcurrentSkipListSet | 跳表 | 并发有序集合 |
Map
| 类 | 原理 | 特点 |
|---|---|---|
ConcurrentHashMap | CAS + synchronized | 高并发首选 |
ConcurrentSkipListMap | 跳表 | 并发且有序 |
ConcurrentHashMap<String,Integer> map =
new ConcurrentHashMap<>();
map.putIfAbsent("A", 1);
map.computeIfAbsent("B", k -> 100);
map.merge("A", 1, Integer::sum);
Queue
| 类 | 是否阻塞 | 场景 |
|---|---|---|
ConcurrentLinkedQueue | 否 | 高并发无锁队列 |
ConcurrentLinkedDeque | 否 | 双端队列 |
ArrayBlockingQueue | 是 | 有界生产者消费者 |
LinkedBlockingQueue | 是 | 线程池常用 |
PriorityBlockingQueue | 是 | 优先级任务 |
DelayQueue | 是 | 延迟任务 |
BlockingQueue<String> queue =
new ArrayBlockingQueue<>(10);
queue.put("task");
String task = queue.take();
8.4 线程安全集合选型
| 场景 | 推荐 |
|---|---|
| 读多写少 | CopyOnWriteArrayList / CopyOnWriteArraySet |
| 高并发 Map | ConcurrentHashMap |
| 高并发且有序 | ConcurrentSkipListMap / ConcurrentSkipListSet |
| 生产者消费者 | LinkedBlockingQueue / ArrayBlockingQueue |
| 无锁队列 | ConcurrentLinkedQueue |
| 临时包装线程安全 | Collections.synchronizedXXX() |
线程安全集合选择决策树
是否需要线程安全?
│
├── 否 → ArrayList / HashMap / HashSet
│
└── 是
│
├── 是 Map 结构?
│ ├── 需要排序? → ConcurrentSkipListMap
│ └── 不需要排序 → ConcurrentHashMap(首选)
│
├── 是 List 结构?
│ └── 读多写少? → CopyOnWriteArrayList
│ 否则 → Collections.synchronizedList()
│
├── 是 Set 结构?
│ ├── 需要排序? → ConcurrentSkipListSet
│ └── 不需要排序 → CopyOnWriteArraySet
│
└── 是 Queue 结构?
├── 需要阻塞? → LinkedBlockingQueue / ArrayBlockingQueue
└── 不需要阻塞 → ConcurrentLinkedQueue
8.5 面试一句话总结
ConcurrentHashMap是高并发场景下 Map 的首选。CopyOnWriteArrayList适合读多写少。- 阻塞队列是生产者消费者模型的核心。
Vector、Hashtable、Stack已基本退出历史舞台。
八、Java 泛型
核心结论
泛型在编译时提供类型检查,避免强制类型转换,提高代码复用性。泛型信息在编译后会被类型擦除,运行时不存在泛型信息。通配符 ? 用于灵活接收不同类型的泛型参数,PECS 原则指导何时用 extends、何时用 super。
1. 泛型基础
// 泛型类:T 是类型参数,使用时指定具体类型
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T getValue() { return value; }
}
Box<String> strBox = new Box<>("Hello");
Box<Integer> intBox = new Box<>(42);
String s = strBox.getValue(); // 不需要强转,编译器保证类型安全
// 泛型方法
public <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// 泛型接口
public interface Converter<F, T> {
T convert(F from);
}
2.1 List<T> 与 List<?> 的区别(高频面试点)
很多人会混淆 List<T> 和 List<?>,实际上它们分别用于定义泛型和使用泛型。
| 特性 | List<T>(类型参数) | List<?>(无界通配符) |
|---|---|---|
| 使用场景 | 定义泛型类/泛型方法 | 声明变量、方法参数 |
| 本质 | 类型占位符 | 未知类型 |
| 是否可读 | 可以读为 T | 只能读为 Object |
| 是否可写 | 可以写入 T 类型 | 除 null 外不能写入 |
| 类型关联 | 多处 T 保持一致 | 每个 ? 独立存在 |
(1)List<T>:定义时使用
// 泛型方法
public static <T> T getFirst(List<T> list) {
return list.get(0);
}
public static <T> void addItem(List<T> list, T item) {
list.add(item); // 可以写
T value = list.get(0); // 可以读
}
这里的 T 是一个类型参数,编译器能够保证整个方法中的 T 始终是同一种类型。
(2)List<?>:使用时使用
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
List<?> 表示“某种未知类型的 List”。
List<String> names = List.of("Tom", "Jerry");
List<Integer> nums = List.of(1, 2, 3);
printList(names);
printList(nums);
因此它特别适合“不关心元素具体类型”的场景。
(3)为什么 List<?> 不能写入
public static void test(List<?> list) {
// list.add("hello"); // 编译错误
// list.add(123); // 编译错误
list.add(null); // 唯一允许
}
因为编译器无法确定它到底是:
List<String>
List<Integer>
List<Double>
为了保证类型安全,禁止写入任何具体对象。
但读取是安全的:
Object obj = list.get(0);
(4)记忆口诀
- 写泛型类、泛型方法:使用
<T> - 接收任意泛型集合:使用
<?> - 既要读又要写:优先使用
T - 只读不关心具体类型:使用
?
2.2 为什么不直接全部使用 T?—— T 与 ? 的设计意图
很多初学者会有一个疑问:
既然
T既能读取又能写入,为什么还需要??全部使用T不行吗?
答案是:
- 用
T确实可行 - 用
?在只读场景下更合适、更安全
两者的区别不在于“能不能完成任务”,而在于:
T表示:我关心这个类型,并且可能会操作它?表示:我不关心这个类型,也不希望操作它
示例:打印 List 中的元素
使用 T:
public static <T> void printList(List<T> list) {
for (T item : list) {
System.out.println(item);
}
}
使用 ?:
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
两种写法都能正常工作。
但是对于一个“只读取数据”的方法来说,T 实际上是多余的。
? 的真正价值:编译期禁止写入
假设以后有人修改代码:
public static void printList(List<?> list) {
list.add("hello"); // 编译错误
}
编译器会立即报错。
因为:
List<?>
表示某种未知类型的 List,编译器无法确定它到底是:
List<String>
List<Integer>
List<Double>
因此为了保证类型安全,禁止写入任何具体对象(除了 null)。
这意味着:
?相当于给集合加上了一把“只读锁”,在编译期就阻止错误写入。
API 设计意图更清晰
当别人看到:
void printList(List<?> list)
立刻就知道:
- 方法不关心元素类型
- 方法不会修改集合
- 仅仅用于读取数据
而看到:
<T> void printList(List<T> list)
则会让人以为:
- 方法可能依赖 T
- 方法可能会向 List 写入 T
- 方法可能会返回 T
因此在 API 设计上:
List<?>
比
List<T>
更能准确表达“只读”的意图。
最终理解
很多人把 ? 理解成“弱化版 T”,其实不准确。
更准确的理解是:
| 写法 | 表达的含义 |
|---|---|
T | 我关心这个类型,需要使用这个类型 |
? | 我不关心这个类型,只负责读取 |
? extends T | 我只读某个类型体系的数据 |
? super T | 我要向某个类型体系写数据 |
面试一句话总结
?最重要的价值并不是接收任意泛型,而是在编译期禁止写入,明确表达“只读集合”的设计意图,让 API 更安全、更清晰。
2. 类型参数 T 与通配符 ?
| 特性 | T(类型参数) | ?(通配符) |
|---|---|---|
| 使用场景 | 定义泛型类/方法时 | 使用泛型类型时(方法参数) |
| 是否可引用 | 可以,T 代表具体类型 | 不可以,? 是未知类型 |
| 是否可写入 | 可以写入 T 类型的值 | 通常不能写入 |
// T:定义时使用,可操作类型
public <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// ?:只读场景,接受任意类型的 List
public void printList(List<?> list) {
for (Object item : list) { System.out.println(item); }
// list.add("x"); // 编译错误:不能写入
}
3. 上界通配符(? extends T)— 只读,生产者
? extends Number 表示 Number 及其子类(Integer、Double 等),可以读取,不能写入(因为不知道具体是哪个子类)。
public double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) { total += n.doubleValue(); } // 可以读
// list.add(1); // 编译错误:不能写入
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> 合法
sum(List.of(1.1, 2.2, 3.3)); // List<Double> 合法
4. 下界通配符(? super T)— 可写,消费者
? super Integer 表示 Integer 及其父类(Number、Object),可以写入 Integer,读取只能用 Object 接收。
public void addNumbers(List<? super Integer> list) {
list.add(1); // 可以写入 Integer
list.add(2);
Object obj = list.get(0); // 读取只能用 Object 接收
}
addNumbers(new ArrayList<Number>()); // 合法
addNumbers(new ArrayList<Object>()); // 合法
5. PECS 原则
Producer Extends, Consumer Super — 从集合读数据(生产者)用 extends,向集合写数据(消费者)用 super,既读又写直接用 T。
public <T> void copy(List<? extends T> src, List<T> dest) { // src 是生产者
for (T item : src) dest.add(item);
}
public <T> void fill(List<? super T> dest, T value, int n) { // dest 是消费者
for (int i = 0; i < n; i++) dest.add(value);
}
6. 类型擦除
泛型是编译时特性,编译后所有泛型信息被擦除,List<String> 和 List<Integer> 在运行时都是 List。
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // true
// 类型擦除导致的限制:
// new T(); // 不能实例化类型参数
// T[] arr = new T[10]; // 不能创建泛型数组
// obj instanceof T // 不能对类型参数用 instanceof
九、反射与动态代理
核心结论
反射允许在运行时检查和操作类、方法、字段,是 Spring、MyBatis 等框架的核心机制。动态代理在运行时动态生成代理类,实现对方法调用的透明拦截(AOP 的底层原理)。两者的关键区别:反射只能操作已存在的类,动态代理可以生成全新的类。
1. 为什么有反射机制?直接 new 不行吗?
这是一个触及编程语言设计核心的问题。直接 new 可以解决 80% 的常规编码问题,但剩下的 20% 复杂场景(如框架设计、工具开发)必须依赖反射。
直接 new 为什么不够?
使用 new 创建对象要求在编译时就知道要创建哪个类的实例。这在很多场景下是做不到或会导致代码僵化:
-
场景一:你写的代码,要处理未来才会出现的类
比如,你写一个ServiceLoader组件,它需要动态加载用户提供的插件。在写ServiceLoader代码时,你根本不知道用户将来会定义什么插件类名,new无法针对一个未知的类名写死。 -
场景二:配置文件决定逻辑
比如,你希望不修改代码、只改配置文件(如application.yml)就能让程序使用MySQLDriver或OracleDriver。new MySQLDriver()是硬编码,无法通过改配置切换。 -
场景三:需要探查或操作一个未知对象
比如你写一个通用的 JSON 序列化工具,它接收任意对象,输出其所有字段名和值。如果没有反射,你需要为每个类单独写序列化代码。
反射做了什么?
反射允许程序在运行时:
- 根据一个字符串类名,获取该类的信息(构造器、方法、字段)
- 动态创建对象、调用方法、修改字段(即使是私有成员)
// 直接 new:编译时固定
User user = new User();
// 反射:类名可以来自配置文件、网络、用户输入
String className = "com.demo.User"; // 运行时才知道
Class<?> clazz = Class.forName(className);
Object obj = clazz.getDeclaredConstructor().newInstance();
对比表格
| 特性 | new 对象 | 反射 |
|---|---|---|
| 何时确定类 | 编译时 | 运行时 |
| 性能 | 快(JVM 可优化) | 慢(需要动态解析,禁用部分优化) |
| 安全性 | 编译时类型检查,安全 | 可能抛出运行时异常(类找不到、方法不存在) |
| 代码可读性 | 清晰直接 | 晦涩,不易调试 |
| 能否突破私有 | 不能 | 可以(setAccessible(true)) |
| 典型用途 | 常规业务逻辑 | 框架(Spring、MyBatis)、序列化、IDE、测试工具 |
为什么 Spring 必须用反射
Spring 容器根据 XML 或注解中的字符串 "com.service.UserService" 来创建 Bean,它内部本质上做了:
// 简化版 Spring 内部逻辑
String beanClassName = 读取配置文件("userService.class");
Class<?> clazz = Class.forName(beanClassName);
Object bean = clazz.getDeclaredConstructor().newInstance();
// 然后再通过反射调用 setter 注入依赖...
如果只用 new,意味着 Spring 要提前知道所有未来工程里定义的类 —— 这不可能。
结论
直接
new是"静态绑定",反射是"动态发现"。
写具体的业务逻辑,你当然应该用 new。但当你需要写一个"通用的、不依赖具体类的"组件(比如 JSON 解析器、RPC 框架、依赖注入容器)时,你会发现没有反射,根本无从下手。
- 业务代码:优先用
new,清晰、高效、安全。 - 框架/工具代码:必须用反射,因为它需要处理未知的类型。
2. 反射基础
// 获取 Class 对象(三种方式)
Class<?> c1 = String.class; // 类字面量(推荐,编译时确定)
Class<?> c2 = "hello".getClass(); // 实例方法
Class<?> c3 = Class.forName("java.lang.String"); // 全限定名(运行时加载)
// 获取类信息
c1.getName(); // "java.lang.String"
c1.getSimpleName(); // "String"
c1.getSuperclass(); // class java.lang.Object
// 创建实例
Constructor<?> ctor = c1.getConstructor(String.class);
Object obj = ctor.newInstance("Hello");
// 调用方法
Method method = c1.getMethod("substring", int.class, int.class);
Object result = method.invoke(obj, 0, 3); // "Hel"
// 访问/修改字段(包括 private)
Field field = SomeClass.class.getDeclaredField("privateField");
field.setAccessible(true); // 绕过访问控制
field.set(someObj, "newValue");
3. new / 反射 / 动态代理 的区别
| 方式 | 类是否预先存在 | 何时生成 | 典型用途 |
|---|---|---|---|
new | 必须预先编译存在 | 编译时确定,运行时实例化 | 普通对象创建 |
| 反射 | 必须预先存在 | 运行时加载并操作 | 框架、通用工具 |
| 动态代理 | 代理类运行时动态生成 | 运行时生成字节码并实例化 | AOP、RPC 客户端存根 |
关键区别:
- 反射只能调用已有方法,无法在方法前后透明地插入逻辑
- 动态代理可以拦截方法调用,在前后插入任意逻辑,调用方无感知
4. 有了反射为什么还要动态代理?
反射能做到"我主动去调某个方法",但做不到"别人调我时,我能拦截"。这是两者最本质的区别。
反射的局限:调用方有感知
反射可以在运行时拿到方法并调用它,但你必须主动写反射代码去触发,调用方不会自动经过你的逻辑:
// 用反射调用方法
Method method = obj.getClass().getMethod("save", String.class);
method.invoke(obj, "Alice");
如果调用方(Controller 层)写的是 userService.save("Alice"),它根本不知道你在中间做了什么,也不会经过你的反射代码。你无法"透明"地插入日志、事务等逻辑。
动态代理的能力:调用方无感知
动态代理生成了一个"假的" UserService 对象注入给调用方。调用方以为在调真实对象,实际每次调用都先经过 InvocationHandler,可以在里面做任意前置/后置逻辑:
// 调用方代码,它不知道拿到的是代理对象
userService.save("Alice");
// ↓ 实际走的路径:
// InvocationHandler.invoke() → 打日志 → 反射调用真实方法 → 打日志
动态代理内部也用了反射来最终调用真实方法,两者是配合关系,不是替代关系。
对比总结
| 反射 | 动态代理 | |
|---|---|---|
| 能做什么 | 我主动去调某个方法 | 让别人调我时,我能拦截 |
| 适合场景 | 框架实例化 Bean、序列化工具 | AOP、日志、事务、权限校验 |
| 调用方感知 | 有感知(你要写反射代码) | 无感知(调用方代码不用改) |
如果只有反射,Spring 的 @Transactional 就没法实现——你不可能要求每个调用方都手动写反射代码来触发事务逻辑。动态代理解决的正是这个**“透明拦截”**的问题。
5. JDK 动态代理(基于接口)
目标类必须实现接口。代理类在运行时生成,实现相同接口,所有方法调用都经过 InvocationHandler。
public interface UserService {
void save(String user);
String findById(int id);
}
// InvocationHandler:拦截所有方法调用,插入日志逻辑
public class LoggingHandler implements InvocationHandler {
private final Object target;
public LoggingHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[LOG] 调用: " + method.getName());
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
System.out.println("[LOG] 耗时: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
// 创建代理
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LoggingHandler(target)
);
proxy.save("Alice"); // 自动触发日志
6. CGLIB 动态代理(基于继承,无需接口)
通过生成目标类的子类来实现代理,不需要接口,但不能代理 final 类/方法。
public class CglibProxy implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("[CGLIB] 前置处理");
Object result = proxy.invokeSuper(obj, args); // 调用父类(原始)方法
System.out.println("[CGLIB] 后置处理");
return result;
}
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new CglibProxy());
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
7. JDK 代理 vs CGLIB 代理
| 特性 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 要求 | 目标类必须实现接口 | 无需接口(通过继承) |
| 原理 | 实现相同接口,生成代理类 | 生成目标类的子类 |
| 限制 | 只能代理接口方法 | 不能代理 final 类/方法 |
| 性能 | JDK 8+ 性能相近 | 创建代理稍慢,调用稍快 |
| Spring 选择 | 有接口时默认使用 | 无接口时或强制指定时使用 |
十、IO 流
核心结论
IO 流分两个维度:方向(输入/输出)和数据类型(字节/字符)。处理文本文件用字符流,处理图片、视频等二进制文件用字节流。Java 7 引入的 NIO Files 工具类大幅简化了文件操作,日常开发优先使用。
1. IO 流分类
按方向:
输入流(InputStream / Reader) — 从外部读入程序
输出流(OutputStream / Writer) — 从程序写出到外部
按数据类型:
字节流(InputStream / OutputStream)— 处理二进制数据(图片、视频、任意文件)
字符流(Reader / Writer) — 处理文本数据(自动处理字符编码)
按功能:
节点流 — 直接连接数据源(FileInputStream、FileReader)
包装流 — 包装节点流,增加功能(BufferedReader、PrintWriter)
| 特性 | 字节流 | 字符流 |
|---|---|---|
| 基类 | InputStream / OutputStream | Reader / Writer |
| 处理单位 | 字节(8位) | 字符(16位) |
| 适用场景 | 二进制文件(图片、视频) | 文本文件 |
| 编码处理 | 手动处理 | 自动处理字符编码 |
2. 字节流
// 读文件(带缓冲,推荐)
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("in.txt"))) {
byte[] buf = new byte[8192];
int n;
while ((n = bis.read(buf)) != -1) {
System.out.write(buf, 0, n);
}
}
// 写文件(true = 追加模式)
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("out.txt", true))) {
bos.write("Hello".getBytes("UTF-8"));
}
3. 字符流
// 按行读写文本文件(推荐)
try (BufferedReader br = new BufferedReader(new FileReader("input.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
String line;
while ((line = br.readLine()) != null) { // readLine() 不含换行符
bw.write(line);
bw.newLine(); // 写入系统换行符
}
}
// 字节流 → 字符流桥接(指定编码,避免乱码)
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("file.txt"), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) { System.out.println(line); }
}
4. NIO Files 工具类(Java 7+,推荐)
日常文件操作优先使用 Files,代码更简洁。
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
// 读取
List<String> lines = Files.readAllLines(Paths.get("input.txt"), StandardCharsets.UTF_8);
String content = Files.readString(Paths.get("input.txt")); // Java 11+
// 写入
Files.writeString(Paths.get("output.txt"), "内容", StandardCharsets.UTF_8);
Files.write(Paths.get("output.txt"), lines, StandardCharsets.UTF_8);
// 文件操作
Files.copy(Paths.get("src.txt"), Paths.get("dst.txt"), StandardCopyOption.REPLACE_EXISTING);
Files.move(Paths.get("old.txt"), Paths.get("new.txt"));
Files.delete(Paths.get("file.txt"));
Files.exists(Paths.get("file.txt"));
Files.size(Paths.get("file.txt"));
// 遍历目录
Files.list(Paths.get(".")).forEach(System.out::println);
Files.walk(Paths.get("."), 2)
.filter(Files::isRegularFile)
.forEach(System.out::println);
十一、多线程
核心结论
多线程让程序可以并发执行多个任务,提高 CPU 利用率。线程安全是多线程最核心的问题:多个线程同时读写共享数据时,必须通过同步机制保证数据一致性。生产环境中推荐使用线程池管理线程,避免频繁创建销毁的开销。
1. 创建线程的三种方式
| 方式 | 特点 | 推荐程度 |
|---|---|---|
继承 Thread | 简单,但占用继承名额 | 不推荐 |
实现 Runnable | 无返回值,无异常声明 | 推荐 |
实现 Callable | 有返回值,可抛异常 | 推荐(需要返回值时) |
// 方式1:继承 Thread
public class MyThread extends Thread {
@Override
public void run() { System.out.println("线程: " + getName()); }
}
new MyThread().start();
// 方式2:实现 Runnable(推荐,避免单继承限制)
new Thread(() -> System.out.println("Runnable 线程")).start();
// 方式3:实现 Callable(有返回值,可抛异常)
Callable<Integer> task = () -> { Thread.sleep(1000); return 42; };
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();
Integer result = future.get(); // 阻塞等待结果
2. 线程生命周期
NEW(新建) — 创建了 Thread 对象,还未 start()
↓ start()
RUNNABLE(可运行) — 已提交给 JVM,等待 CPU 调度或正在运行
↓ 等待锁 / sleep / wait / join
BLOCKED — 等待 synchronized 锁
WAITING — 调用 wait() / join(),无限期等待
TIMED_WAITING — 调用 sleep(n) / wait(n),有超时等待
↓ 获得锁 / notify / 超时到期
RUNNABLE
↓ run() 结束
TERMINATED(终止)
3. synchronized 关键字
synchronized 保证同一时刻只有一个线程执行该代码块,解决多线程竞争问题。
public class Counter {
private int count = 0;
public synchronized void increment() { count++; } // 锁是 this
public static synchronized void staticMethod() { } // 锁是 Class 对象
public void add(int n) {
synchronized (this) { count += n; } // 同步代码块,锁粒度更细
}
public int getCount() { return count; }
}
// 验证线程安全
Counter c = new Counter();
Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) c.increment(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) c.increment(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(c.getCount()); // 稳定输出 2000
4. volatile 关键字
volatile 保证变量的可见性:一个线程修改后,其他线程立即可见。但不保证原子性,count++ 这种复合操作仍然不安全。
public class StopThread {
private volatile boolean running = true; // 不加 volatile 可能导致其他线程看不到修改
public void stop() { running = false; }
public void run() {
while (running) { /* 执行任务 */ }
System.out.println("线程停止");
}
}
5. synchronized vs volatile
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | 保证 | 不保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 性能开销 | 较大(加锁/解锁) | 较小 |
| 适用场景 | 复合操作、临界区 | 状态标志位、单次读写 |
6. 线程池(ExecutorService)
直接创建线程开销大,线程池可以复用线程、控制并发数、统一管理。生产环境推荐用 ThreadPoolExecutor 手动指定参数,不推荐 Executors 工厂方法(隐患多)。
// 推荐写法:手动指定所有参数,清楚控制行为
ExecutorService pool = new ThreadPoolExecutor(
2, // corePoolSize:核心线程数,长期保留
5, // maximumPoolSize:最大线程数
60L, TimeUnit.SECONDS, // 非核心线程空闲存活时间
new LinkedBlockingQueue<>(100), // 任务队列(队列满才扩展到 max)
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 提交无返回值任务
pool.execute(() -> System.out.println("任务执行"));
// 提交有返回值任务
Future<String> future = pool.submit(() -> { Thread.sleep(500); return "结果"; });
String result = future.get(); // 阻塞获取
// 关闭线程池
pool.shutdown(); // 等待已提交任务完成后关闭(推荐)
pool.shutdownNow(); // 立即中断,返回未执行任务列表
7. 四种拒绝策略
当线程池达到最大线程数且队列已满时,新任务会触发拒绝策略。
| 策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy(默认) | 抛出 RejectedExecutionException | 需要感知拒绝时 |
CallerRunsPolicy | 由提交任务的线程自己执行 | 需要保证任务不丢失 |
DiscardPolicy | 静默丢弃新任务 | 可以接受任务丢失 |
DiscardOldestPolicy | 丢弃队列中最旧的任务,再尝试提交 | 新任务优先级更高 |
8. 常用并发工具类
// AtomicInteger:基于 CAS 的原子操作,无锁,高性能
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子 +1,返回新值
count.getAndIncrement(); // 原子 +1,返回旧值
count.compareAndSet(5, 10); // CAS:期望值为 5 时才设为 10
// CountDownLatch:等待多个线程都完成后再继续
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("子线程完成");
latch.countDown(); // 计数 -1
}).start();
}
latch.await(); // 阻塞,直到计数归零
System.out.println("所有子线程完成,主线程继续");
// CyclicBarrier:让多个线程在某个点互相等待,全部到达后同时继续
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("全部到达,继续"));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try { barrier.await(); } catch (Exception e) { } // 等待其他线程
}).start();
}
9. 线程安全集合
| 非线程安全 | 线程安全替代 | 说明 |
|---|---|---|
ArrayList | CopyOnWriteArrayList | 写时复制,适合读多写少 |
HashMap | ConcurrentHashMap | 分段锁,高并发首选 |
HashSet | CopyOnWriteArraySet | 写时复制 |
LinkedList | ConcurrentLinkedQueue | 无锁队列 |
PriorityQueue | PriorityBlockingQueue | 阻塞优先级队列 |
10. 常见面试问题
Q:sleep() 和 wait() 的区别?
| 特性 | Thread.sleep() | Object.wait() |
|---|---|---|
| 所属类 | Thread | Object |
| 是否释放锁 | 不释放 | 释放 |
| 唤醒方式 | 超时自动唤醒 | notify() / notifyAll() / 超时 |
| 使用前提 | 无 | 必须在 synchronized 块中 |
Q:线程池核心参数?
corePoolSize:核心线程数,长期保留,不会因空闲被销毁maximumPoolSize:最大线程数,队列满时才扩展到此上限keepAliveTime:非核心线程空闲超时后销毁workQueue:任务队列,常用LinkedBlockingQueue、ArrayBlockingQueuehandler:拒绝策略,队列满且达到最大线程数时触发
十二、常用算法
核心结论
冒泡排序和二分查找是最基础的算法,面试必考。冒泡排序时间复杂度 O(n²),二分查找要求数组有序,时间复杂度 O(log n)。
1. 冒泡排序
每轮将最大值"冒泡"到末尾。加入 swapped 标志位可在数组已有序时提前退出,是一个常见的优化点。
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
boolean swapped = false;
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 改为 < 则从大到小排序
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
if (!swapped) break; // 本轮无交换,数组已有序,提前退出
}
}
复杂度: 最坏 O(n²),最好 O(n)(已有序时),空间 O(1)。
2. 二分查找
前提:数组已排序。每次将搜索范围缩小一半。mid 的计算用 left + (right - left) / 2 而非 (left + right) / 2,是为了防止两个大整数相加溢出。
public static int binarySearch(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止 (left+right) 溢出
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1; // 目标在右半部分
} else {
right = mid - 1; // 目标在左半部分
}
}
return -1; // 未找到
}
复杂度: O(log n),空间 O(1)。
十三、RESTful API
核心结论
REST 是一种基于 HTTP 的接口设计风格,核心思想是:用 URI 表示资源,用 HTTP 方法表示操作。设计好的 RESTful API 应该语义清晰、URI 简洁、状态码准确。
1. REST 核心原则
| 原则 | 说明 |
|---|---|
| 无状态 | 每个请求包含所有必要信息,服务器不保存客户端状态 |
| 统一接口 | 使用标准 HTTP 方法,资源通过 URI 标识 |
| 客户端-服务器分离 | 前后端解耦,各自独立演化 |
| 可缓存 | 响应可被缓存,提高性能 |
2. HTTP 方法语义
| 方法 | 语义 | 幂等性 | 安全性 | 示例 |
|---|---|---|---|---|
| GET | 获取资源 | 是 | 是 | GET /users/1 |
| POST | 创建资源 | 否 | 否 | POST /users |
| PUT | 全量更新 | 是 | 否 | PUT /users/1 |
| PATCH | 部分更新 | 否 | 否 | PATCH /users/1 |
| DELETE | 删除资源 | 是 | 否 | DELETE /users/1 |
- 幂等性:多次执行相同操作,结果相同
- 安全性:不修改服务器数据
3. URI 设计规范
# 推荐设计(名词复数,层次清晰)
GET /users # 获取用户列表
GET /users/123 # 获取特定用户
POST /users # 创建用户
PUT /users/123 # 全量更新用户
PATCH /users/123 # 部分更新用户
DELETE /users/123 # 删除用户
GET /users/123/orders # 获取用户的订单列表
# 避免的设计
GET /getUser # URI 中包含动词
POST /deleteUser/123 # 用 POST 做删除
GET /user_list # 用下划线(推荐连字符 -)
GET /Users # 大写字母(URI 推荐全小写)
4. HTTP 状态码
| 范围 | 含义 | 常用状态码 |
|---|---|---|
| 2xx | 成功 | 200 OK,201 Created,204 No Content |
| 3xx | 重定向 | 301 永久重定向,304 Not Modified |
| 4xx | 客户端错误 | 400 Bad Request,401 Unauthorized,403 Forbidden,404 Not Found |
| 5xx | 服务器错误 | 500 Internal Server Error,503 Service Unavailable |
5. 统一响应格式
// 成功响应(201 Created)
{
"id": 12345,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}
// 统一错误响应格式
{
"code": 400,
"message": "邮箱格式不正确",
"timestamp": "2024-01-15T10:30:00Z",
"path": "/users"
}
十四、Lambda 表达式与 Stream API
核心结论
Lambda 表达式是 Java 8 引入的匿名函数写法,让代码更简洁。Stream API 基于 Lambda,提供链式操作集合数据的能力,可以替代大量 for 循环。测试开发中常用于处理接口返回的列表数据、过滤测试用例、批量转换数据格式。
1. Lambda 表达式基础
Lambda 是对"只有一个方法的接口"(函数式接口)的简化写法。
// 传统匿名类写法
Runnable r1 = new Runnable() {
@Override
public void run() { System.out.println("running"); }
};
// Lambda 写法
Runnable r2 = () -> System.out.println("running");
// 有参数
Comparator<String> c1 = (a, b) -> a.compareTo(b);
// 多行语句用花括号
Comparator<String> c2 = (a, b) -> {
int result = a.compareTo(b);
return result;
};
2. 常用函数式接口
Java 8 内置了四种核心函数式接口,覆盖大多数场景:
| 接口 | 方法签名 | 用途 |
|---|---|---|
Predicate<T> | boolean test(T t) | 判断条件,返回 true/false |
Function<T, R> | R apply(T t) | 类型转换,输入 T 返回 R |
Consumer<T> | void accept(T t) | 消费数据,无返回值 |
Supplier<T> | T get() | 提供数据,无输入参数 |
Predicate<String> isEmpty = s -> s.isEmpty();
Function<String, Integer> toLen = s -> s.length();
Consumer<String> printer = s -> System.out.println(s);
Supplier<String> hello = () -> "Hello";
System.out.println(isEmpty.test("")); // true
System.out.println(toLen.apply("abc")); // 3
printer.accept("test"); // 打印 test
System.out.println(hello.get()); // Hello
3. 方法引用
方法引用是 Lambda 的进一步简化,当 Lambda 只是直接调用一个已有方法时可以使用。
// 静态方法引用:类名::方法名
Function<String, Integer> parse = Integer::parseInt;
// 实例方法引用:实例::方法名
String str = "hello";
Supplier<Integer> len = str::length;
// 任意实例方法引用:类名::方法名
Function<String, String> upper = String::toUpperCase;
// 构造器引用
Supplier<ArrayList<String>> newList = ArrayList::new;
// 在集合操作中常见
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(System.out::println); // 等价于 s -> System.out.println(s)
4. Stream API 基础
Stream 是数据的流水线,分三个阶段:获取流 → 中间操作(可链式多个)→ 终止操作(只能有一个)。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Amy");
// 典型链式操作
List<String> result = names.stream()
.filter(s -> s.startsWith("A")) // 过滤:保留以 A 开头的
.map(String::toUpperCase) // 转换:转大写
.sorted() // 排序
.collect(Collectors.toList()); // 收集为 List
System.out.println(result); // [ALICE, AMY]
5. 常用中间操作
中间操作返回新的 Stream,可以链式调用,不会立即执行(懒加载)。
| 操作 | 说明 | 示例 |
|---|---|---|
filter(Predicate) | 过滤,保留满足条件的元素 | .filter(s -> s.length() > 3) |
map(Function) | 转换每个元素 | .map(String::toUpperCase) |
flatMap(Function) | 将嵌套集合展平 | .flatMap(List::stream) |
sorted() | 自然顺序排序 | .sorted() |
sorted(Comparator) | 自定义排序 | .sorted(Comparator.reverseOrder()) |
distinct() | 去重 | .distinct() |
limit(n) | 截取前 n 个 | .limit(5) |
skip(n) | 跳过前 n 个 | .skip(2) |
peek(Consumer) | 查看元素(调试用,不改变流) | .peek(System.out::println) |
6. 常用终止操作
终止操作触发整个流水线的执行,返回具体结果。
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
// 收集
List<Integer> list = nums.stream().filter(n -> n > 3).collect(Collectors.toList());
Set<Integer> set = nums.stream().collect(Collectors.toSet());
String str = nums.stream().map(String::valueOf).collect(Collectors.joining(", "));
// str = "1, 2, 3, 4, 5, 6"
// 统计
long count = nums.stream().filter(n -> n % 2 == 0).count(); // 3
int sum = nums.stream().mapToInt(Integer::intValue).sum(); // 21
double avg = nums.stream().mapToInt(Integer::intValue).average().orElse(0); // 3.5
int max = nums.stream().mapToInt(Integer::intValue).max().getAsInt(); // 6
// 匹配
boolean anyEven = nums.stream().anyMatch(n -> n % 2 == 0); // true(有一个满足)
boolean allPos = nums.stream().allMatch(n -> n > 0); // true(全部满足)
boolean noneNeg = nums.stream().noneMatch(n -> n < 0); // true(全部不满足)
// 查找
Optional<Integer> first = nums.stream().filter(n -> n > 3).findFirst(); // Optional[4]
first.orElse(-1); // 4,若不存在则返回 -1
// forEach
nums.stream().forEach(System.out::println);
7. 分组与分区
Collectors.groupingBy 是测试开发中处理接口返回数据时非常实用的操作。
List<String> words = Arrays.asList("apple", "banana", "avocado", "blueberry", "cherry");
// 按首字母分组,得到 Map<Character, List<String>>
Map<Character, List<String>> grouped = words.stream()
.collect(Collectors.groupingBy(s -> s.charAt(0)));
// {a=[apple, avocado], b=[banana, blueberry], c=[cherry]}
// 按条件分区(只分两组:true/false)
Map<Boolean, List<Integer>> partitioned = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {false=[1, 3, 5], true=[2, 4, 6]}
// 统计每组数量
Map<Character, Long> countByFirst = words.stream()
.collect(Collectors.groupingBy(s -> s.charAt(0), Collectors.counting()));
// {a=2, b=2, c=1}
8. 测试开发中的典型用法
处理接口返回的 JSON 列表数据时,Stream 可以替代大量 for 循环:
// 假设接口返回用户列表,只需要提取所有 userId
List<Map<String, Object>> users = response.getJSONArray("data");
// 提取所有 userId
List<Integer> userIds = users.stream()
.map(u -> (Integer) u.get("userId"))
.collect(Collectors.toList());
// 过滤出 status=1 的用户名
List<String> activeNames = users.stream()
.filter(u -> Integer.valueOf(1).equals(u.get("status")))
.map(u -> (String) u.get("username"))
.collect(Collectors.toList());
// 验证响应中是否包含某个 userId
boolean hasUser = users.stream()
.anyMatch(u -> Integer.valueOf(1001).equals(u.get("userId")));
// 转为以 userId 为 key 的 Map(方便按 id 查找)
Map<Integer, Map<String, Object>> userMap = users.stream()
.collect(Collectors.toMap(
u -> (Integer) u.get("userId"),
u -> u
));
更多推荐
所有评论(0)