Java 基础知识核心复习手册(面试·进阶·实战)

本文约 1.2 万字,涵盖数据类型、集合、多线程、IO、反射等 11 大模块。每个知识点均以“问-答”形式剖析,配合代码验证与图表对比,适合查漏补缺和面试冲刺。

目录

  1. 数据类型与运算符
  2. 流程控制与字符串
  3. 面向对象(OOP)
  4. 集合框架
  5. 异常处理
  6. 泛型
  7. 常用 API
  8. 输入输出(I/O)
  9. 多线程基础(API 层面)
  10. 反射与注解
  11. JVM 相关基础(快速回顾)

一、数据类型与运算符

1.1 8 种基本类型

类型 字节数 取值范围 默认值 包装类
byte 1 -128 ~ 127 0 Byte
short 2 -32768 ~ 32767 0 Short
int 4 -2^31 ~ 2^31-1 0 Integer
long 8 -2^63 ~ 2^63-1 0L Long
float 4 ±3.4E-38 ~ ±3.4E+38 0.0f Float
double 8 ±1.7E-308 ~ ±1.7E+308 0.0d Double
char 2 0 ~ 65535(Unicode) ‘\u0000’ Character
boolean 未明确定义(通常1字节) true / false false Boolean

问: 为什么 floatdouble 不能用于精确计算(如货币)?
答: 浮点数采用 IEEE 754 表示,存在舍入误差。例如 0.1 + 0.2 在二进制中是无限循环小数。货币计算应使用 BigDecimal

1.2 自动装箱与 Integer 缓存

问: 下面代码的输出是什么?为什么?

Integer a = 100, b = 100;
Integer c = 200, d = 200;
System.out.println(a == b);  // true
System.out.println(c == d);  // false

答:

  • Integer 缓存池范围 -128 ~ 127(可通过 -XX:AutoBoxCacheMax 调整)。
  • 自动装箱时,值在此范围内会返回缓存对象,否则 new Integer()
  • 所以 ab 引用同一对象,cd 指向不同对象。

正确比较包装类值:使用 Objects.equals()intValue()

1.3 位运算经典应用

问: 如何用位运算快速判断一个数是否为 2 的幂?
答: n > 0 && (n & (n - 1)) == 0
原理:2 的幂的二进制只有一个 1,减 1 后低位全 1,与原数按位与得 0。


二、流程控制与字符串

2.1 switch 支持的类型

  • int / byte / short / char
  • String(Java 7+):实际比较的是 hashCode + equals
  • enum(枚举)

问: switch 能否用于 longdouble
答: 不能。只支持可隐式转为 int 的类型以及 String/enum

2.2 String 不可变性与常量池

String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2);   // true,常量池同一对象
System.out.println(s1 == s3);   // false,堆中新对象
System.out.println(s1.equals(s3)); // true

问: String 为什么设计成不可变?
答:

  1. 字符串常量池安全复用。
  2. 哈希码可缓存(StringhashCode 只计算一次)。
  3. 作为 HashMap 的 key 时保证一致性。
  4. 线程安全。

2.3 StringBuilder 与 StringBuffer

线程安全 性能 适用场景
StringBuilder 单线程字符串拼接
StringBuffer 是(方法加 synchronized 多线程环境(极少使用)

问: 下面代码创建了几个对象?

String result = "a" + "b" + "c";

答: 仅 1 个。编译器会优化为 "abc" 直接放入常量池。
若是 String result = a + b + c(变量),编译后使用 StringBuilder 拼接。


三、面向对象(OOP)

3.1 多态与虚方法表

问: 下面代码输出什么?

class Father {
    void show() { System.out.println("Father"); }
}
class Son extends Father {
    void show() { System.out.println("Son"); }
}
Father f = new Son();
f.show();  // Son

答: 多态:编译时类型为 Father,运行时类型为 Son,调用实际对象的方法。JVM 通过虚方法表动态绑定。

3.2 抽象类 vs 接口(Java 8+)

特性 抽象类 接口(Java 8)
关键字 abstract interface
成员变量 可以是各种类型 默认为 public static final
构造器
实例方法 可以有 default / static 方法
多继承 单继承 可多实现

问: 什么时候用抽象类,什么时候用接口?
答:

  • 抽象类:表示“is-a”关系,且需共享代码(状态或通用行为)。
  • 接口:表示“can-do”能力,常用于回调、策略模式,且 Java 8 后可用默认方法提供通用实现。

3.3 内部类

class Outer {
    private int a = 10;
    class Inner {        // 成员内部类
        void print() { System.out.println(a); }
    }
    static class StaticNested { }  // 静态嵌套类
    void method() {
        class Local { }             // 局部内部类
        Runnable r = new Runnable() {  // 匿名内部类
            @Override public void run() {}
        };
    }
}

问: 为什么非静态内部类不能有静态成员?
答: 非静态内部类依赖于外部类实例,而静态成员属于类级别,违背依赖关系。除非该成员是编译时常量(static final 基本类型或字符串)。


四、集合框架

4.1 ArrayList 扩容机制

问: ArrayList 初始容量多少?如何扩容?
答:

  • 无参构造:初始容量为 10(懒加载,第一次 add 时分配)。
  • 扩容:newCapacity = oldCapacity + (oldCapacity >> 1) → 1.5 倍。
  • 若仍不足,则直接扩至所需大小。
  • 最大容量 Integer.MAX_VALUE - 8

代码验证:

ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 20; i++) list.add(i);
// 在 debug 中观察 elementData 数组长度变化:10 → 15 → 22

4.2 HashMap(核心)

存储结构
  • 数组 + 链表 + 红黑树(树化阈值 8,退化为链表阈值 6)
  • 默认容量 16,负载因子 0.75 → 扩容阈值 12
put 流程
1. 计算 key.hashCode() 并扰动 (hash = h ^ (h>>>16))
2. (n-1) & hash 计算槽位
3. 若槽位空,直接放入
4. 否则判断链表头或树根是否相同 key → 覆盖
5. 否则插入链表尾部(JDK 8),若链表长度≥8 且数组≥64 → 树化
6. 若元素总数 > 阈值 → 扩容(resize)

问: HashMap 为何线程不安全?
答:

  • JDK 7:扩容时头插法,多线程下可能形成环形链表,导致死循环 CPU 100%。
  • JDK 8:改用尾插法,但仍存在数据覆盖(两个线程同时 put 同一槽位,一个覆盖另一个的值)以及 size 计数丢失。

解决方案:ConcurrentHashMap

扩容时的 rehash 优化
// 新 table 长度是旧 table 的 2 倍
if ((e.hash & oldCap) == 0) {
    // 低位索引不变
} else {
    // 高位索引 = 原索引 + oldCap
}

为什么高效? 因为 oldCap 是 2 的幂,其二进制只有一个 1。e.hash & oldCap 可快速判断高位是 0 还是 1,无需重新计算 hash。

4.3 集合常见陷阱

陷阱1:Arrays.asList() 返回的 List 不支持增删

List<String> list = Arrays.asList("a", "b");
list.add("c"); // UnsupportedOperationException

原因:返回的是 Arrays 内部类 ArrayList,未实现 add/remove

陷阱2:在 for-each 中删除元素

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3));
for (Integer i : list) {
    if (i == 2) list.remove(i); // ConcurrentModificationException
}

修复:使用 Iterator.remove()


五、异常处理

5.1 异常体系

Throwable
├── Error(不可恢复,如 OutOfMemoryError、StackOverflowError)
└── Exception
    ├── RuntimeException(非受检,如 NullPointerException)
    └── 非 RuntimeException(受检,如 IOException、SQLException)

问: finally 块一定会执行吗?
答: 不一定。以下情况不执行:

  • System.exit(0)
  • JVM 崩溃
  • 线程被中断或杀死

5.2 try-with-resources

try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
    // 自动调用 close()
} catch (IOException e) {
    e.printStackTrace();
}

要求:资源类必须实现 AutoCloseable 接口。


六、泛型

6.1 类型擦除

问: 泛型 List<String> 在运行时能通过反射添加 Integer 吗?
答: 可以。因为 Java 泛型是编译期语法,运行时 JVM 并不知道 String 类型参数,实际类型为 Object(若不指定边界)。

List<String> list = new ArrayList<>();
list.getClass().getMethod("add", Object.class).invoke(list, 123);
System.out.println(list.get(0)); // 123,运行时没有类型检查

6.2 PECS 原则(Producer Extends, Consumer Super)

  • ? extends T:只能读,不能写(生产者)。
  • ? super T:只能写,不能精确读(消费者)。
public static void copy(List<? extends Number> src, List<? super Number> dest) {
    for (Number n : src) dest.add(n);
}

src 提供 Number 子类对象(生产),dest 消费 Number 对象(存储)。


七、常用 API

7.1 日期时间(java.time)

说明
LocalDate 年月日
LocalTime 时分秒纳秒
LocalDateTime 日期+时间
ZonedDateTime 带时区
DateTimeFormatter 线程安全的格式化器

示例:

LocalDate today = LocalDate.now();
LocalDate parsed = LocalDate.parse("2025-03-15");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String formatted = today.format(formatter);

7.2 BigDecimal 精度计算

问: new BigDecimal(0.1)BigDecimal.valueOf(0.1) 有什么区别?
答:

  • new BigDecimal(0.1) 会精确表示浮点数的二进制不精确值,结果为 0.100000000000000005551...
  • BigDecimal.valueOf(0.1) 内部调用 Double.toString,更符合人预期。

推荐使用 BigDecimal.valueOf()new BigDecimal("0.1")


八、输入输出(I/O)

8.1 字节流 vs 字符流

  • 字节流InputStream / OutputStream,处理二进制数据(图片、视频)。
  • 字符流Reader / Writer,处理文本(自动处理字符编码)。

问: 为何不总是用字符流?
答: 字节流处理任何类型,但字符流需要编码转换,效率稍低,且不能用于非文本数据。

8.2 序列化

问: transient 关键字的作用?
答: 修饰的字段不会被序列化。例如密码、缓存数据等无需持久化的字段。

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient String password; // 序列化后为 null
}

九、多线程基础(API 层面)

9.1 线程创建方式

方式 特点
extends Thread 单继承限制,不推荐
implements Runnable 可多实现,无返回值
Callable + FutureTask 有返回值,可抛异常
FutureTask<Integer> task = new FutureTask<>(() -> {
    Thread.sleep(1000);
    return 100;
});
new Thread(task).start();
Integer result = task.get(); // 阻塞直到返回

9.2 线程状态迁移

NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED

问: sleep()wait() 的区别?
答:

  • sleep() 属于 Thread,不释放锁,时间到自动唤醒。
  • wait() 属于 Object,必须在同步块内调用,释放锁,需要 notify/notifyAll 唤醒。

9.3 同步工具类速览

工具 作用
CountDownLatch 等待多个线程完成
CyclicBarrier 多个线程互相等待,达到屏障后继续
Semaphore 控制并发访问数量
Exchanger 两个线程交换数据

示例: 使用 CountDownLatch 等待所有子线程结束

CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        // do work
        latch.countDown();
    }).start();
}
latch.await();

9.4 线程池(ThreadPoolExecutor)

核心参数:

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:空闲线程存活时间
  • workQueue:任务队列(LinkedBlockingQueueSynchronousQueue 等)
  • handler:拒绝策略(AbortPolicyCallerRunsPolicy 等)

问: Executors.newFixedThreadPool(10) 有什么风险?
答: 它使用无界队列 LinkedBlockingQueue,若任务生产速度大于消费速度,会导致 OOM。推荐手动创建 ThreadPoolExecutor


十、反射与注解

10.1 反射常用操作

// 获取 Class 对象
Class<?> clazz = Class.forName("com.example.User");
// 实例化(无参构造)
Object obj = clazz.getDeclaredConstructor().newInstance();
// 调用私有方法
Method m = clazz.getDeclaredMethod("secret");
m.setAccessible(true);
m.invoke(obj);

问: 反射的缺点?
答:

  • 性能损耗(需检查访问权限、动态解析)。
  • 破坏封装性(可访问私有成员)。
  • 安全限制(SecurityManager 可能阻止)。

10.2 注解保留策略

@Retention 取值 说明
SOURCE 仅存在源码中,编译后丢弃(如 @Override
CLASS 保留在 .class 文件中,但 JVM 不加载(默认)
RUNTIME 运行时可用,通过反射读取(常用)

十一、JVM 相关基础(快速回顾)

问: JVM 内存区域哪些是线程共享的?
答: 堆和方法区(JDK 8 后元空间)共享;虚拟机栈、本地方法栈、程序计数器私有。

问: 双亲委派模型如何防止核心类库被篡改?
答: 类加载请求向上委托,java.lang.String 始终由 BootstrapClassLoader 加载,自定义类加载器无法覆盖。

问: volatile 解决了什么?
答: 保证可见性(写后立即刷新到主内存)和有序性(禁止指令重排序),但不保证原子性。

问: 你说不保证原子性,那 volatile long 的赋值是原子的吗?
答: 在 32 位 JVM 中,64 位的 long/double 读写可能分两次操作,使用 volatile 可保证原子性(JVM 会插入 lock 指令)。但对于 i++ 仍非原子。

问: CMS 和 G1 的核心区别?
答: CMS 是标记-清除(碎片化),全堆并发,停顿不可控;G1 基于 Region,增量回收,可设定最大停顿时间。


更多推荐