一、Java 基础数据类型

核心结论

Java 有 8 种基本数据类型,直接存储值(存在栈上),不是对象。除此之外的所有类型(包括 String、数组)都是引用类型,存储的是对象的地址。


1. 八大基本类型

类型字节数默认值取值范围说明
byte10-128 ~ 127最小整数类型
short20-32768 ~ 32767短整型
int40-2^31 ~ 2^31-1最常用整数类型
long80L-2^63 ~ 2^63-1长整型,字面量加 L
float40.0f约 ±3.4e38单精度浮点,字面量加 f
double80.0约 ±1.8e308双精度浮点,默认浮点类型
char2‘\u0000’0 ~ 65535Unicode 字符
booleanfalsetrue / falseJVM 实现相关,通常 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 包装类型

基本类型包装类型常用方法
intIntegerparseInt(), valueOf(), intValue()
longLongparseLong(), valueOf()
doubleDoubleparseDouble()
floatFloatparseFloat()
booleanBooleanparseBoolean()
charCharacterisDigit(), isLetter(), toLowerCase()
byteByteparseByte()
shortShortparseShort()

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. 三者对比

特性StringStringBuilderStringBuffer
可变性不可变可变可变
线程安全安全(不可变)不安全安全(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不能被继承StringInteger
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 起支持 defaultstatic 方法。

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 或 throwsIOException, SQLException
非受检异常RuntimeException可选NullPointerException, IllegalArgumentException
错误Error通常不处理OutOfMemoryError, StackOverflowError

2. try-catch-finally 执行规则

catch 中子类异常放前面,父类放后面。finally 无论如何都会执行(除非 System.exit())。

try {
    // 可能抛出异常的代码
} catch (FileNotFoundException e) {  // 子类放前面
    // 处理更具体的异常
} catch (IOException e) {            // 父类放后面
    // 处理更宽泛的异常
} finally {
    // 无论是否异常都执行,通常用于关闭资源
}

执行顺序:

场景trycatchfinally返回值
无异常执行跳过执行try 的 return
有异常且被捕获部分执行执行执行catch 的 return
有异常未被捕获部分执行跳过执行异常向上传播
finally 有 return执行finally 的 return(覆盖前面)

关键结论:

  • finally 中的 return覆盖 try/catch 的 return,生产代码中避免在 finally 写 return
  • finally 中修改基本类型变量不影响已确定的返回值快照
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

特性throwthrows
位置方法体内方法签名上
作用实际抛出一个异常实例声明方法可能抛出的异常类型
数量每次只能抛出一个可声明多个,逗号分隔
// 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(键值对)。日常最常用的是 ArrayListHashMapHashSet。选择集合类型时主要考虑:是否需要有序、是否允许重复、是否需要线程安全。


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 对比

特性ArrayListLinkedList
底层结构动态数组双向链表
随机访问 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
原理特点
ConcurrentHashMapCAS + 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
高并发 MapConcurrentHashMap
高并发且有序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 适合读多写少。
  • 阻塞队列是生产者消费者模型的核心。
  • VectorHashtableStack 已基本退出历史舞台。

八、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)就能让程序使用 MySQLDriverOracleDrivernew 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 / OutputStreamReader / 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

特性synchronizedvolatile
原子性保证不保证
可见性保证保证
有序性保证保证
性能开销较大(加锁/解锁)较小
适用场景复合操作、临界区状态标志位、单次读写

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. 线程安全集合

非线程安全线程安全替代说明
ArrayListCopyOnWriteArrayList写时复制,适合读多写少
HashMapConcurrentHashMap分段锁,高并发首选
HashSetCopyOnWriteArraySet写时复制
LinkedListConcurrentLinkedQueue无锁队列
PriorityQueuePriorityBlockingQueue阻塞优先级队列

10. 常见面试问题

Q:sleep() 和 wait() 的区别?

特性Thread.sleep()Object.wait()
所属类ThreadObject
是否释放锁不释放释放
唤醒方式超时自动唤醒notify() / notifyAll() / 超时
使用前提必须在 synchronized 块中

Q:线程池核心参数?

  • corePoolSize:核心线程数,长期保留,不会因空闲被销毁
  • maximumPoolSize:最大线程数,队列满时才扩展到此上限
  • keepAliveTime:非核心线程空闲超时后销毁
  • workQueue:任务队列,常用 LinkedBlockingQueueArrayBlockingQueue
  • handler:拒绝策略,队列满且达到最大线程数时触发

十二、常用算法

核心结论

冒泡排序和二分查找是最基础的算法,面试必考。冒泡排序时间复杂度 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
    ));

更多推荐