🔥 高频题(第1~25题)


第1题:Java作为一门编程语言,其核心特点有哪些?

【核心答案】

Java 的核心特点可以概括为以下六点:

特点 说明
跨平台性 通过 JVM + 字节码实现「一次编写,到处运行」
面向对象 封装、继承、多态,一切皆对象(除基本类型)
自动内存管理(GC) 垃圾回收器自动回收不再使用的对象,无需手动释放内存
强类型 + 健壮性 编译时类型检查、异常处理机制、无指针,减少运行时错误
多线程支持 内置 Thread/Runnable,JUC 并发包提供高级并发工具
丰富的生态 Spring、MyBatis、Netty 等成熟框架,适用于企业级开发

【通俗理解】

把 Java 想象成一家国际连锁餐厅:

  • 跨平台 = 同样的菜谱(代码)在不同国家的分店(操作系统)都能做出同样的菜
  • 面向对象 = 厨房分工明确——洗菜(类A)、切菜(类B)、炒菜(类C),各司其职
  • GC = 自动洗碗机,你不用手动洗碗(释放内存)
  • 强类型 = 菜刀就是菜刀,不能当锅铲用,类型严格限制

【常见误区】

误区:Java 是纯解释型语言,所以很慢
正解:Java 采用「解释 + JIT 编译」混合模式,热点代码被编译为本地机器码后性能接近 C/C++


第2题:Java是如何实现"一次编写,到处运行"的跨平台特性的?

【核心答案】

Java 通过两层抽象实现跨平台:

  1. 编译阶段.java 源文件 → javac 编译 → .class 字节码(与平台无关的中间码)
  2. 运行阶段:不同平台的 JVM 将同样的字节码翻译为对应平台的机器码
┌──────────────────────────────────────────────┐
│                  Java 源文件 (.java)           │
└──────────────────┬───────────────────────────┘
                   │ javac 编译
                   ▼
┌──────────────────────────────────────────────┐
│              字节码 (.class)   ← 一次编写       │
└──────┬──────────────┬──────────────┬─────────┘
       │              │              │
       ▼              ▼              ▼
┌──────────┐   ┌──────────┐   ┌──────────┐
│ Linux JVM│   │Windows JVM│  │ macOS JVM│  ← 到处运行
└──────────┘   └──────────┘   └──────────┘

【通俗理解】

  • 字节码就像「五线谱」——同样的乐谱,无论在中国、美国、法国,钢琴家(JVM)都能演奏
  • 你把字节码想象成一份「国际通用说明书」,各地工厂(JVM)拿到后各自用本地工艺(机器码)生产

【常见误区】

误区:Java 程序直接编译成机器码运行
正解:先编译成字节码(.class),再由 JVM 的 JIT 编译器或解释器转换为机器码


第3题:请解释JVM、JDK、JRE三者分别是什么,以及它们之间的关系?

【核心答案】

概念 全称 作用 包含内容
JVM Java Virtual Machine 执行字节码,提供运行时环境 类加载器、字节码执行引擎、GC
JRE Java Runtime Environment 运行 Java 程序的最小环境 JVM + 核心类库 (rt.jar)
JDK Java Development Kit 开发 Java 程序的工具包 JRE + 开发工具 (javac, jar, javadoc等)

【关系图解】

┌─────────────────────────────────────┐
│                JDK                   │
│  ┌─────────────────────────────┐    │
│  │  javac / javap / jar / ...  │ ← 开发工具    │
│  │  ┌───────────────────────┐  │    │
│  │  │        JRE             │  │    │
│  │  │  ┌──────────────┐     │  │    │
│  │  │  │     JVM       │     │  │    │
│  │  │  │  类库 (rt.jar)│     │  │    │
│  │  │  └──────────────┘     │  │    │
│  │  └───────────────────────┘  │    │
│  └─────────────────────────────┘    │
└─────────────────────────────────────┘

包含关系JDK ⊃ JRE ⊃ JVM

【通俗理解】

  • JVM = 汽车的发动机(核心动力)
  • JRE = 发动机 + 轮胎 + 方向盘(能开起来的最小配置)
  • JDK = 整车 + 工具箱(既能开车,也能修车/造车)

第4题:为什么说Java既是编译型语言又是解释型语言?JIT编译器在其中起什么作用?

【核心答案】

Java 的执行分为两个阶段:

源代码(.java) ──编译──▶ 字节码(.class) ──解释/JIT编译──▶ 机器码
       ↑                      ↑                    ↑
   编译型语言特征         中间产物          解释+编译混合执行
阶段 做了什么 性质
javac 编译 .java.class 字节码 编译型特征
JVM 解释执行 逐条解释字节码为机器码 解释型特征
JIT 即时编译 热点代码直接编译为机器码并缓存 编译型特征(运行时)

JIT(Just-In-Time)编译器的作用:

  • 监控方法调用频率,将热点代码(频繁执行的代码)直接编译为本地机器码
  • 下次调用时直接执行机器码,无需再次解释,性能大幅提升
  • 典型优化:方法内联、逃逸分析、锁消除等
解释执行速度:  ████████░░  慢(每次都翻译)
JIT编译后速度: ████████████████████  快(只翻译一次,后续直接用)

【通俗理解】

  • Java 像是一个同声传译 + 速记员的组合:
    • 平常内容逐句翻译(解释执行)
    • 听到重复/重要的话,直接记下来下次复读(JIT 编译)
  • 你第一次唱一首歌需要看歌词(解释),唱多了就直接会了(JIT 缓存)

第5题:值传递和引用传递有什么区别?Java中使用的是哪一种?请举例说明。

【核心答案】

Java 中只有值传递(Pass by Value),没有引用传递!

传递方式 含义 Java 中是否存在
值传递 将实参的值拷贝一份传给形参 ✅ 是
引用传递 将实参的地址直接传给形参(形参和实参指向同一内存地址) ❌ 否

对于基本类型:传递的是值的拷贝

public static void main(String[] args) {
    int a = 10;
    modify(a);
    System.out.println(a); // 输出 10,没有改变!
}

public static void modify(int x) {
    x = 20;  // 改变的是 x 这个副本
}

对于引用类型:传递的是引用的拷贝(不是对象本身!)

public static void main(String[] args) {
    User user = new User("张三");
    modify(user);
    System.out.println(user.name); // 输出 "李四",对象内容变了!
}

public static void modify(User u) {
    u.name = "李四";  // u 是 user 引用的拷贝,但指向同一个对象
    u = new User("王五");  // 改变的是 u 这个副本指向,原 user 不受影响
}

【图解】

基本类型(值传递):
  main: a = [10]
              │ 拷贝值
  modify: x = [10] → x = [20]   // 改的是拷贝
  main: a = [10]                // 不受影响

引用类型(传引用拷贝):
  main: user ──→ [User("张三")]   ← 堆中的真实对象
                   ▲
  modify: u ──────┘   // u 是 user 的引用拷贝,指向同一对象
           u.name = "李四"  → 修改了真实对象 ✅
           u = new User("王五") → u 指向新对象,user 不变 ✅

【常见误区】

误区:Java 基本类型是值传递,引用类型是引用传递
正解:Java 一切都是值传递。引用类型传递的是「引用地址的值」的拷贝,不是对象本身地址的直接传递


第6题:Java的八种基本数据类型分别是什么?每种各占用多少字节?

【核心答案】

数据类型 关键字 占用字节 占用位数 取值范围 默认值
字节型 byte 1 8 -128 ~ 127 0
短整型 short 2 16 -32768 ~ 32767 0
整型 int 4 32 -2³¹ ~ 2³¹-1(约±21亿) 0
长整型 long 8 64 -2⁶³ ~ 2⁶³-1 0L
单精度浮点 float 4 32 ±3.4E-38 ~ ±3.4E+38 0.0f
双精度浮点 double 8 64 ±1.7E-308 ~ ±1.7E+308 0.0d
字符型 char 2 16 0 ~ 65535 (Unicode) ‘\u0000’
布尔型 boolean 1(虚拟机相关) - true / false false

【记忆口诀】

byte → short → int → long → float → double(从小到大)
1-2-4-8-4-8 字节,char 占 2 字节,boolean 占 1 字节

【自动类型提升顺序】

byte → short → int → long → float → double
         ↑
        char

第7题:为什么涉及金额计算时推荐使用BigDecimal而不是double或float?

【核心答案】

因为 floatdouble 采用二进制浮点数表示,很多十进制小数无法精确表示,会产生精度丢失

// 经典翻车现场
System.out.println(0.1 + 0.2);      // 输出:0.30000000000000004  ❌
System.out.println(1.0 - 0.9);      // 输出:0.09999999999999998  ❌

// 使用 BigDecimal
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b));       // 输出:0.3  ✅

【为什么会精度丢失?】

10 进制的 0.1 转换为 2 进制是无限循环小数:0.0001100110011...,计算机只能用有限的位数近似存储。

【BigDecimal 使用关键点】

// ❌ 错误:用 double 构造 BigDecimal,精度已经丢失
BigDecimal bad = new BigDecimal(0.1);  // 0.100000000000000005551...

// ✅ 正确:用字符串构造
BigDecimal good = new BigDecimal("0.1");

// ✅ 也可以用 valueOf(内部用了字符串)
BigDecimal alsoGood = BigDecimal.valueOf(0.1);
对比维度 double/float BigDecimal
精度 不精确(二进制近似) 精确(十进制整数存储)
性能 快(硬件支持) 慢(软件计算)
使用场景 科学计算、图形渲染 金融、货币计算
API 复杂度 简单 复杂(加减乘除都用方法)

【常见误区】

误区new BigDecimal(0.1) 就能精确表示 0.1
正解:double 的 0.1 本身就不精确,构造 BigDecimal 时已经晚了,必须用 new BigDecimal("0.1")


第8题:什么是自动装箱和自动拆箱?使用过程中有哪些常见的坑?

【核心答案】

概念 方向 编译器做的事
自动装箱 (Autoboxing) 基本类型 → 包装类 Integer.valueOf(int)
自动拆箱 (Unboxing) 包装类 → 基本类型 Integer.intValue()
// 自动装箱:int → Integer
Integer i = 100;           // 编译器生成:Integer.valueOf(100)

// 自动拆箱:Integer → int
int j = i;                 // 编译器生成:i.intValue()

// 运算时自动拆箱
Integer a = 10;
Integer b = 20;
int c = a + b;             // a.intValue() + b.intValue()

【常见坑】

坑1:NullPointerException

Integer x = null;
int y = x;  // 自动拆箱时调用 x.intValue() → NPE!

坑2:== 比较的是引用,不是值

Integer a = 127;
Integer b = 127;
System.out.println(a == b);  // true(缓存范围内)

Integer c = 128;
Integer d = 128;
System.out.println(c == d);  // false(超出缓存范围,new 了新对象)!

坑3:大量装箱影响性能

// ❌ 坏习惯:循环中大量装箱
Long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;  // 每次循环都会装箱/拆箱
}

// ✅ 好习惯:用基本类型
long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
}

第9题:Integer的缓存机制是什么?缓存范围是多少?为什么这样设计?

【核心答案】

Integer 内部维护了一个静态缓存数组,默认缓存 [-128, 127] 范围内的 Integer 对象。

// Integer.valueOf() 源码(简化版)
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high) {
        return IntegerCache.cache[i + (-IntegerCache.low)];
    }
    return new Integer(i);
}
Integer a = 100;  // Integer.valueOf(100) → 从缓存取
Integer b = 100;  // Integer.valueOf(100) → 从缓存取(同一个对象)
Integer c = 200;  // Integer.valueOf(200) → new Integer(200)
Integer d = 200;  // Integer.valueOf(200) → new Integer(200)(新对象)

System.out.println(a == b);  // true  ✅
System.out.println(c == d);  // false ❌ 容易踩坑

【为什么是 -128 ~ 127?】

  • 这个范围覆盖了最常用的小整数(循环计数、数组下标等)
  • 借鉴了「享元模式(Flyweight Pattern)」思想,避免频繁创建销毁小整数对象
  • 这个范围的数据在大多数应用中使用频率最高

【可通过 JVM 参数调整上限】

-XX:AutoBoxCacheMax=256   # 将缓存上限调整到 256

【图解】

┌─────────────────────────────────────────────┐
│           IntegerCache.cache[]               │
│  ┌────┬────┬────┬────┬────┬────┬────┬────┐  │
│  │-128│-127│ ...│ 0  │ 1  │ ...│ 126│ 127│  │
│  └────┴────┴────┴────┴────┴────┴────┴────┘  │
│         ↑ 缓存命中,返回同一对象               │
│                                 │
│ Integer.valueOf(128) ──────────▶ new Integer(128) │
│   超范围,每次 new 新对象                        │
└─────────────────────────────────────────────┘

【对比其他包装类】

包装类 缓存范围
Byte -128 ~ 127(全部缓存)
Short -128 ~ 127
Integer -128 ~ 127(可调上限)
Long -128 ~ 127
Character 0 ~ 127(ASCII)
Float, Double 无缓存

第10题:请结合代码示例,说明面向对象的三大特性:封装、继承、多态。

【核心答案】

面向对象三大特性
├── 封装(Encapsulation)
│       隐藏内部实现,只暴露必要的接口
├── 继承(Inheritance)
│       子类继承父类的属性和方法,实现代码复用
└── 多态(Polymorphism)
        同一个方法调用,不同对象产生不同行为

【1. 封装——安全的数据访问】

public class BankAccount {
    private double balance;  // 私有字段,外部不能直接访问

    // 只提供受控的访问方式
    public double getBalance() { return balance; }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("金额必须大于0");
        }
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount > balance) {
            throw new IllegalArgumentException("余额不足");
        }
        balance -= amount;
    }
}
// 外部无法 bankAccount.balance = -1000;  ← 被 private 保护

【2. 继承——代码复用】

// 父类
public class Animal {
    protected String name;

    public void eat() {
        System.out.println(name + " 在吃东西");
    }
}

// 子类继承父类
public class Dog extends Animal {
    public void bark() {
        System.out.println(name + " 汪汪叫");
    }
}

Dog dog = new Dog();
dog.name = "旺财";
dog.eat();   // 继承自 Animal
dog.bark();  // Dog 自己的方法

【3. 多态——同一接口,不同实现】

// 多态的核心:父类引用指向子类对象
Animal animal1 = new Dog();  // 向上转型
Animal animal2 = new Cat();

animal1.eat();  // 输出:"狗在吃骨头"(调用 Dog 的 eat)
animal2.eat();  // 输出:"猫在吃鱼"(调用 Cat 的 eat)

// 多态的三种形式:
// 1) 继承多态(如上)
// 2) 接口多态
List<String> list = new ArrayList<>(); // 接口引用指向实现类
// 3) 方法重载(编译时多态)
public void print(int x) { ... }
public void print(String s) { ... }

【三者关系图解】

        封装
      ┌───────┐
      │ 隐藏实现  │
      │ 保护数据  │
      └───┬───┘
          │ 提供接口
          ▼
        继承 ──────▶ 多态
      ┌───────┐   ┌───────────┐
      │ 复用代码  │──▶│ 灵活替换    │
      │ 建立层级  │   │ 扩展开放    │
      └───────┘   └───────────┘

第11题:方法重载(Overload)和方法重写(Override)有什么区别?各自的规则是什么?

【核心答案】

对比维度 重载 (Overload) 重写 (Override)
定义 同一个类中,方法名相同,参数列表不同 子类重新定义父类的同名方法
发生范围 同一个类(或父子类) 父子类之间
参数列表 必须不同(数量/类型/顺序) 必须相同
返回类型 可以不同 相同或是其子类(协变返回)
访问修饰符 可以任意 不能比父类更严格
异常 可以任意 不能比父类抛出更宽泛的异常
static方法 可以重载 不能重写(可以隐藏)
final方法 可以重载 不能重写
发生时机 编译时确定(编译时多态) 运行时确定(运行时多态)
注解 无需注解 @Override(推荐加上)

【代码示例】

// ========== 方法重载 ==========
public class Calculator {
    // 同一个方法名,不同参数
    public int add(int a, int b)           { return a + b; }
    public double add(double a, double b)  { return a + b; }   // 参数类型不同
    public int add(int a, int b, int c)    { return a + b + c; } // 参数个数不同
}
// 调用时编译期就能确定调哪个

// ========== 方法重写 ==========
public class Animal {
    public Animal makeSound() throws Exception {
        System.out.println("动物叫");
        return this;
    }
}

public class Dog extends Animal {
    @Override  // 编译期检查是否正确重写
    public Dog makeSound() {  // 返回类型可以是子类(协变)
        System.out.println("汪汪汪");
        return this;
    }
}

Animal a = new Dog();
a.makeSound();  // 运行时才确定调 Dog 的方法 → 输出"汪汪汪"

【常见误区】

误区1:重载可以通过不同的返回类型来区分
正解:重载只看方法名+参数列表,返回类型不同但参数相同不是重载,会编译报错

误区2:重写可以改变 static 方法的行为
正解:static 方法属于类,不能被子类重写,只能被「隐藏」(方法隐藏)


第12题:Java中抽象类和接口的区别是什么?在Java 8之后接口有哪些变化?

【核心答案】

对比维度 抽象类 (abstract class) 接口 (interface)
关键字 abstract class interface
继承/实现 单继承 (extends),只能继承一个 多实现 (implements),可实现多个
构造方法 ✅ 有 ❌ 无
成员变量 任意类型(可非 public) 只能 public static final 常量
普通方法 ✅ 可以有 Java 8+ 可以用 default
静态方法 ✅ 可以有 Java 8+ 可以有
私有方法 ✅ 可以有 Java 9+ 可以用 private
设计意图 “is-a” 关系,提取共性 “can-do” 能力,定义规范

【Java 8 之后接口的变化】

Java 7 及以前:
  接口中只能有:抽象方法 + 常量

Java 8:
  新增 default 方法(有默认实现)
  新增 static 方法

Java 9:
  新增 private 方法(供 default 方法内部复用)

代码示例:

// Java 8+ 的接口
public interface PaymentService {

    // 抽象方法(传统接口方法)
    void pay(BigDecimal amount);

    // Java 8: default 方法(有默认实现,子类可重写)
    default void logPayment(BigDecimal amount) {
        validate(amount);     // 调用私有方法
        System.out.println("支付金额:" + amount);
    }

    // Java 8: static 方法
    static PaymentService createAliPay() {
        return new AliPayServiceImpl();
    }

    // Java 9: private 方法
    private void validate(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("金额必须大于0");
        }
    }
}

【选择策略】

需要一个模板/骨架? → 抽象类
├── 多个类共享代码 → 抽象类
├── 需要非 public 成员 → 抽象类
├── 需要构造方法初始化 → 抽象类
└── "is-a" 关系 → 抽象类

只需要定义能力/契约? → 接口
├── 不相关的类需要共同行为 → 接口
├── 需要多重继承 → 接口
├── "can-do" 能力 → 接口
└── 解耦(面向接口编程) → 接口

第13题:final关键字在Java中可以修饰类、方法、变量,分别有什么作用?

【核心答案】

修饰对象 作用 效果
final class A {} 不能被继承(断子绝孙类),如 StringInteger
方法 final void m() {} 方法不能被子类重写
基本类型变量 final int x = 10; 不可修改(常量)
引用类型变量 final User u = new User(); 引用不可变(不能指向新对象),但对象内部状态可以变
// ===== 修饰类 =====
public final class StringUtils {
    // 不能被继承,工具类常用
}
// class Sub extends StringUtils {}  ← 编译错误!

// ===== 修饰方法 =====
public class Parent {
    public final void criticalLogic() {
        // 模板方法模式中保护核心算法
    }
}

// ===== 修饰变量 =====
final int MAX_SIZE = 100;        // 基本类型不可变
final List<String> list = new ArrayList<>();
list.add("hello");                // ✅ 对象内容可以变
// list = new ArrayList<>();      // ❌ 引用不可变,编译错误

【图解】

final int x = 10;
    x ──→ [10]  ← 10 这个值被锁死

final User u = new User("Tom");
    u ──→ [User(name="Tom")]  ← 这个箭头被锁死
                 │
            name 可以改!

【常见误区】

误区:final 修饰的引用变量指向的对象也不能修改
正解:引用不可变 ≠ 对象不可变。final List 不能 = new ArrayList(),但可以 list.add()


第14题:static关键字可以修饰哪些成员?分别产生什么效果?

【核心答案】

static 可以修饰:变量、方法、代码块、内部类。被 static 修饰的成员属于类级别,而非实例级别。

修饰对象 效果 加载时机 访问方式
静态变量 所有实例共享同一份数据 类加载时 类名.变量
静态方法 只能访问静态成员,不能使用 this 类加载时 类名.方法()
静态代码块 类加载时执行一次,用于初始化静态变量 类加载时 自动执行
静态内部类 不依赖外部类实例,可独立存在 使用时加载 new Outer.Inner()
public class StaticDemo {
    // 静态变量:所有实例共享
    private static int count = 0;

    // 静态代码块:类加载时执行一次
    static {
        System.out.println("类加载时执行");
        count = 100;
    }

    // 静态方法:只能访问静态成员
    public static int getCount() {
        // instanceMethod();  ← 编译错误,不能调用非静态方法
        return count;
    }

    // 静态内部类
    static class Builder {
        public StaticDemo build() { return new StaticDemo(); }
    }
}

【内存图解】

        方法区/元空间
    ┌───────────────────┐
    │  StaticDemo.class │
    │   static count    │ ← 类级别,只有一份
    │   static {}       │
    └───────────────────┘

        堆内存
    ┌───────────────────┐
    │  new StaticDemo() │ ← 实例1 (没有自己的 count)
    │  new StaticDemo() │ ← 实例2 (没有自己的 count)
    └───────────────────┘

第15题:深拷贝和浅拷贝的区别是什么?如何实现一个对象的深拷贝?

【核心答案】

维度 浅拷贝 (Shallow Copy) 深拷贝 (Deep Copy)
基本类型字段 复制值 复制值
引用类型字段 只复制引用地址(指向同一对象) 递归创建新对象(完全独立)
独立性 拷贝对象和原对象共享内部引用对象 拷贝对象和原对象完全独立
实现难度 简单 复杂

【图解】

原对象:                          浅拷贝后:
Person {                          Person {
  name: "Tom"        ──复制值──▶    name: "Tom"
  address ──────────┐              address ───────────┐
}                   │             }                    │
                    ▼                                  ▼
              Address {                        同一个 Address!
                city: "北京"                        city: "北京"
              }                                }

深拷贝后:
Person {                          Person {
  name: "Tom"        ──复制值──▶    name: "Tom"
  address ──────────┐              address ──────┐
}                   │             }               │
                    ▼                             ▼
              Address {                    Address {
                city: "北京"                  city: "北京"
              }                            }  ← 独立新对象

【三种实现方式】

// 方式1:实现 Cloneable + 递归 clone(侵入性强,不推荐)
public class Person implements Cloneable {
    private String name;
    private Address address;

    @Override
    public Person clone() {
        Person p = (Person) super.clone();
        p.address = this.address.clone();  // 递归拷贝引用对象
        return p;
    }
}

// 方式2:序列化/反序列化(推荐,但性能一般)
public static <T> T deepCopy(T obj) {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(obj);
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);
    return (T) ois.readObject();
}

// 方式3:第三方库(Jackson / Gson / Fastjson,最常用)
Person copy = objectMapper.readValue(
    objectMapper.writeValueAsString(original), Person.class);
方式 优点 缺点
Cloneable 原生支持 侵入性强,需要递归实现
序列化 通用性强 性能差,所有类需实现 Serializable
JSON 序列化 简单,非侵入 有序列化成本,特殊类型可能丢失

【常见误区】

误区Object.clone() 就是深拷贝
正解Object.clone() 默认是浅拷贝!必须手动递归拷贝内部引用对象才是深拷贝


第16题:== 运算符与 equals() 方法在比较对象时有什么区别?

【核心答案】

对比维度 == equals()
比较内容 比较引用地址(栈中的引用值) 比较对象内容(取决于类的重写实现)
基本类型 比较 不能用(基本类型没有方法)
Object 默认 比较引用地址 比较引用地址(等价于 ==
String 比较引用地址 比较字符串内容(已重写)
自定义类 比较引用地址 取决于是否重写(需手动重写)
// 基本类型:== 比较值
int a = 10, b = 10;
System.out.println(a == b);  // true

// 引用类型:== 比较地址
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2);       // false(不同对象)
System.out.println(s1.equals(s2));  // true(内容相同,String 重写了 equals)

// 字符串常量池
String s3 = "hello";
String s4 = "hello";
System.out.println(s3 == s4);       // true(常量池中同一个对象)

【图解】

String s1 = new String("hello");
String s2 = new String("hello");

栈:                          堆:
s1 ──────→ [String "hello"]  ← 地址 0x1000
s2 ──────→ [String "hello"]  ← 地址 0x2000

s1 == s2       → false  (0x1000 != 0x2000)
s1.equals(s2)  → true   (内容都是 "hello")

第17题:hashCode()和equals()方法之间有什么约束关系?为什么重写equals时必须重写hashCode?

【核心答案】

「黄金三定律」——hashCode 和 equals 的约束关系:

规则 说明
规则1 两个对象 equals 为 true → hashCode 必须相等
规则2 两个对象 equals 为 false → hashCode 可以相等也可以不等(等就是哈希冲突)
规则3 同一个对象多次调用 hashCode必须返回相同的 int(前提是 equals 用到的字段没变)

【为什么必须同时重写?——血的教训!】

public class User {
    private String name;

    // 只重写了 equals,没重写 hashCode
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        return Objects.equals(name, ((User) o).name);
    }
    // hashCode 没重写!使用的是 Object 的 native hashCode
}

// 出问题的场景
Map<User, String> map = new HashMap<>();
map.put(new User("张三"), "北京");
System.out.println(map.get(new User("张三")));  // 输出 null !!!

【为什么会返回 null?】

HashMap 查找流程:
  get(key) → 计算 key.hashCode() → 定位到桶
          → 在该桶内用 equals() 逐一比较

  put 时: new User("张三").hashCode() = 123456  → 桶#5
  get 时: new User("张三").hashCode() = 789012  → 桶#8
  桶不同!根本找不到!

【正确做法】

public class User {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return age == user.age && Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);  // 必须使用与 equals 相同的字段
    }
}

第18题:String、StringBuffer、StringBuilder三者的区别是什么?分别在什么场景下使用?

【核心答案】

对比维度 String StringBuffer StringBuilder
可变性 不可变(final char[]/byte[]) 可变 可变
线程安全 ✅ 安全(不可变天然安全) ✅ 安全(synchronized) ❌ 不安全
性能 拼接时创建新对象,最慢 中等(有同步开销) 最快
继承关系 - 继承 AbstractStringBuilder 继承 AbstractStringBuilder
使用场景 字符串常量、少量拼接 多线程环境拼接 单线程环境拼接

【代码示例】

// String:每次拼接都创建新对象
String s = "Hello";
s += " World";  // 创建了新 String 对象!原 "Hello" 等待 GC

// StringBuffer:线程安全,多线程用
StringBuffer sb1 = new StringBuffer();
sb1.append("Hello").append(" World");  // 同一个对象上操作

// StringBuilder:非线程安全,单线程首选
StringBuilder sb2 = new StringBuilder();
sb2.append("Hello").append(" World");  // 同一个对象上操作,最快

【底层原理】

String:
  "Hello"  [H][e][l][l][o]          ← final byte[],不可变
  "Hello World" [H][e][l][l][o][ ][W][o][r][l][d]  ← 全新对象!

StringBuilder:
  [H][e][l][l][o][ ][W][o][r][l][d][ ][ ][ ][ ][ ]  ← 内部可变数组
  直接在原数组上追加,容量不够时扩容(2倍+2)

【选择决策图】

需要频繁拼接字符串?
├── 是 → 多线程环境?
│        ├── 是 → StringBuffer
│        └── 否 → StringBuilder
└── 否 → String

第19题:Java 8引入了哪些重要的新特性?请列举并简要说明。

【核心答案】

新特性 说明 解决的问题
Lambda 表达式 (参数) -> { 函数体 } 简化匿名内部类,支持函数式编程
函数式接口 @FunctionalInterface,如 PredicateFunctionConsumerSupplier 配合 Lambda 使用,定义函数签名
Stream API 对集合进行声明式的流式操作 告别 for 循环,链式处理数据
Optional 容器类,优雅处理 null 避免 NullPointerException
方法引用 类名::方法名,如 System.out::println 进一步简化 Lambda
接口默认方法/静态方法 default / static 方法 接口可添加新方法而不破坏实现类
新的日期时间 API LocalDateLocalTimeLocalDateTime(java.time包) 替代难用的 Date/Calendar
Base64 java.util.Base64 官方 Base64 编解码

【Lambda & Stream 代码示例】

List<String> names = Arrays.asList("张三", "李四", "王五", "赵六");

// 以前:匿名内部类
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

// Java 8:Lambda
names.sort((a, b) -> a.compareTo(b));

// Stream:过滤 + 转换 + 收集
List<String> result = names.stream()
    .filter(name -> name.startsWith("张"))   // 中间操作:过滤
    .map(String::toUpperCase)                 // 中间操作:转换
    .collect(Collectors.toList());            // 终端操作:收集
// 结果:["张三"]

【Optional 示例】

// 以前
User user = getUser();
if (user != null) {
    String name = user.getName();
    if (name != null) {
        return name.toUpperCase();
    }
}
return "Unknown";

// Java 8
return Optional.ofNullable(getUser())
    .map(User::getName)
    .map(String::toUpperCase)
    .orElse("Unknown");

【新日期 API 示例】

LocalDate today = LocalDate.now();           // 2026-05-17
LocalTime time = LocalTime.of(14, 30);       // 14:30
LocalDateTime dt = LocalDateTime.now();       // 2026-05-17T14:30:00

// 日期计算(不可变,返回新对象)
LocalDate nextWeek = today.plusWeeks(1);
Period period = Period.between(today, nextWeek);  // P7D

第20题:什么是Java反射机制?反射有哪些典型应用场景?它有什么优缺点?

【核心答案】

反射(Reflection) 是 Java 在运行时动态获取类的信息(构造方法、字段、方法、注解等)并操作对象的能力。

编译时:不知道具体是什么类
运行时:通过 Class 对象 → 获取构造器 → 创建实例 → 调用方法 → 访问字段

【核心 API 演示】

// 1. 获取 Class 对象的三种方式
Class<?> clazz1 = User.class;
Class<?> clazz2 = user.getClass();
Class<?> clazz3 = Class.forName("com.example.User");

// 2. 创建实例
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
Object obj = constructor.newInstance("张三");

// 3. 调用私有方法
Method method = clazz.getDeclaredMethod("secretMethod");
method.setAccessible(true);  // 突破私有权限
method.invoke(obj);

// 4. 访问私有字段
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
String value = (String) field.get(obj);
field.set(obj, "李四");

【典型应用场景】

场景 说明 例子
框架开发 Spring IoC 通过反射实例化 Bean、注入依赖 @Autowired 注入
动态代理 JDK 动态代理依赖反射调用目标方法 AOP 拦截
注解处理 运行时读取注解并执行对应逻辑 @TableName@RequestMapping
序列化/反序列化 JSON 库通过反射读写对象字段 Jackson、Gson
插件化开发 动态加载类,实现热部署 OSGi、IDEA 插件

【优缺点】

维度 说明
优点 灵活性极高,运行时动态操作;框架的基石
缺点1 性能差(比直接调用慢 10~100 倍),需要安全检查、包装类型
缺点2 破坏封装性,可以绕过访问修饰符
缺点3 编译期无法检查,错误推迟到运行时

第21题:请描述Java的异常体系结构:Error和Exception有什么区别?受检异常与非受检异常分别是什么?

【核心答案】

                     Throwable
                    /         \
               Error          Exception
          (严重错误)         /          \
            /    \     RuntimeException   其他 Exception
     OOMError  SOError  (非受检异常)      (受检异常)
                        /        \
                  NullPointer  IllegalArgumentException
                  IndexOutOfBounds
类型 父类 特点 是否需要处理 示例
Error Throwable JVM 级别严重错误,程序无法处理 ❌ 不需要 OutOfMemoryErrorStackOverflowError
受检异常 (Checked) Exception 编译期强制处理 必须 try-catch 或 throws IOExceptionSQLException
非受检异常 (Unchecked) RuntimeException 编译期不检查 ❌ 不需要强制处理 NullPointerExceptionIllegalArgumentException
// 受检异常:必须处理
public void readFile(String path) throws IOException {
    FileReader fr = new FileReader(path);  // FileReader 构造方法声明了 throws
}

// 非受检异常:可处理可不处理
public int divide(int a, int b) {
    return a / b;  // 可能 ArithmeticException,但编译期不报错
}

【常见误区】

误区:RuntimeException 不需要捕获,所以可以忽略
正解:不需要强制捕获 ≠ 不会发生。应该通过代码逻辑预防(如判空),而不是靠 try-catch 掩盖


第22题:双重校验锁(DCL)实现单例模式时,为什么必须使用volatile关键字?

【核心答案】

DCL(Double-Checked Locking)单例中 volatile 防止指令重排导致的「半初始化对象」问题。

public class Singleton {
    // volatile 是必须的!
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                    // 第一重检查
            synchronized (Singleton.class) {
                if (instance == null) {            // 第二重检查
                    instance = new Singleton();    // 问题在这里!
                }
            }
        }
        return instance;
    }
}

【为什么 new Singleton() 不是原子操作?】

instance = new Singleton() 在 JVM 层面分为三步:

① 分配内存空间
② 调用构造方法,初始化对象
③ instance 引用指向内存地址

正常顺序:① → ② → ③
指令重排:① → ③ → ②   ← 危险!

【指令重排时的灾难场景】

线程A:                                             线程B:
① 分配内存
③ instance 指向内存(还没初始化!)
                                                    if (instance == null) → false
                                                    return instance;  ← 拿到半成品!
② 调用构造方法(晚了!)

【volatile 如何解决?】

volatile 提供内存屏障,禁止 instance = new Singleton() 的指令重排,保证 ① → ② → ③ 严格有序。

有了 volatile:
   ① 分配内存   →   ② 初始化   →   ③ instance 赋值
                     ↑ 内存屏障保证有序 ↑

第23题:BIO(阻塞IO)、NIO(非阻塞IO)、AIO(异步IO)三者的区别和适用场景分别是什么?

【核心答案】

对比维度 BIO NIO AIO
全称 Blocking IO Non-blocking IO Asynchronous IO
IO模型 同步阻塞 同步非阻塞 异步非阻塞
线程模型 一个连接一个线程 一个线程管理多个连接(Selector) 回调/系统通知
API 调用 read() 阻塞等待数据 read() 不阻塞,有数据才读 read() 立即返回,系统完成后回调
并发能力 极高
编程复杂度 简单 复杂 中等
适用场景 连接数少、稳定 高并发、长连接 超高并发、异步处理

【形象比喻】

BIO:去餐厅吃饭,你在窗口一直等着,直到菜做好(阻塞)

NIO:你在座位上等,时不时去看看菜好了没(轮询/Selector)

AIO:你坐下刷手机,菜好了服务员会端过来(异步回调)

【图解】

BIO: 一个连接一个线程
  Client1 ──▶ Thread1
  Client2 ──▶ Thread2
  Client3 ──▶ Thread3
  (1000个连接需要1000个线程)

NIO: 一个 Selector 管理多个 Channel
  Client1 ──┐
  Client2 ──┼──▶ Selector ──▶ ThreadPool (少量线程)
  Client3 ──┘
  (1000个连接可能只需几个线程)

AIO: 系统完成IO后主动通知
  Client1 ──▶ 发起读请求 → 系统处理 → 回调通知
  (线程完全解放)

第24题:Java如何利用NIO的Channel、Buffer、Selector三大组件实现网络高并发编程?

【核心答案】

NIO 的三大核心组件各司其职:

组件 角色 职责
Channel 通道 数据的双向传输管道(读+写),对应一个连接
Buffer 缓冲区 数据的临时存储区,所有数据都通过 Buffer 读写
Selector 多路复用器 一个线程监控多个 Channel,只处理就绪的 Channel

【工作流程】

Selector 线程                         工作线程
     │                                  │
     │ ① 注册 Channel 到 Selector        │
     │    channel.register(selector,     │
     │        SelectionKey.OP_READ)      │
     │                                  │
     │ ② 轮询就绪的 Channel              │
     │    while (true) {                │
     │      selector.select();          │
     │      Set<SelectionKey> keys       │
     │        = selector.selectedKeys();│
     │      for (key : keys) {          │
     │        if (key.isReadable()) {   │
     │          ③ 分发到工作线程 ────────▶ ④ 从 Channel 读数据到 Buffer
     │        }                          │    channel.read(buffer)
     │      }                            │ ⑤ 处理数据
     │    }                              │ ⑥ 写响应数据到 Buffer → Channel
     │                                   │    buffer.flip(); channel.write(buffer)

【代码示例——NIO 服务端】

// 1. 打开 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);  // 非阻塞模式

// 2. 创建 Selector
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

// 3. 轮询
while (true) {
    selector.select();  // 阻塞直到有就绪事件
    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    while (it.hasNext()) {
        SelectionKey key = it.next();
        if (key.isAcceptable()) {
            // 接受新连接
            SocketChannel client = serverChannel.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            // 读取数据
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int len = client.read(buffer);
            if (len > 0) {
                buffer.flip();
                System.out.println(new String(buffer.array(), 0, len));
            }
        }
        it.remove();  // 必须移除,否则下次还会处理
    }
}

【底层原理:epoll 多路复用】

NIO 在 Linux 上底层使用 epoll 系统调用,epoll 通过红黑树 + 就绪链表,将主动轮询变为事件通知,O(1) 获取就绪事件。


第25题:什么是Java泛型?什么是泛型擦除?泛型擦除会带来哪些问题?

【核心答案】

泛型(Generics) 允许类/方法在定义时使用类型参数编译期进行类型检查,确保类型安全。

// 不用泛型:需要在运行时强制转换,不安全
List list = new ArrayList();
list.add("hello");
list.add(123);         // 不会报错!
String s = (String) list.get(1);  // 运行时 ClassCastException!

// 用泛型:编译期检查
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123);      // 编译错误!类型安全
String s = list.get(0); // 不需要强制转换

【泛型擦除(Type Erasure)】

Java 的泛型是编译期概念。编译后泛型信息会被「擦除」,字节码中不存在泛型,这是为了兼容老版本 JVM。

编译前:                                 编译后(擦除后):
List<String> list = new ArrayList<>();   List list = new ArrayList();
list.add("hello");                        list.add("hello");
String s = list.get(0);                  String s = (String) list.get(0);  ← 编译器自动加转型

【泛型擦除带来的问题】

问题1:不能使用基本类型

// List<int> list = new ArrayList<>();  ← 编译错误!
List<Integer> list = new ArrayList<>(); // 必须用包装类

问题2:无法用 instanceof 检查泛型类型

List<String> list = new ArrayList<>();
// if (list instanceof List<String>)  ← 编译错误!擦除后只有 List
if (list instanceof List) { }  // 只能这样

问题3:无法创建泛型数组

// T[] arr = new T[10];  ← 编译错误!编译器不知道 T 是什么
T[] arr = (T[]) new Object[10];  // 只能这样绕过去(有警告)

问题4:方法签名冲突

// 这两个方法在擦除后签名完全一样,编译错误!
public void print(List<String> list) {}
public void print(List<Integer> list) {}
// 擦除后都变成:public void print(List list) {}

问题5:无法获取泛型的 Class 对象

// List<String>.class  ← 不存在!
// 运行时只有 List.class

【上下界通配符】

// ? extends T:上界通配符,只能读不能写
public void readOnly(List<? extends Number> list) {
    Number n = list.get(0);   // ✅ 读取安全
    // list.add(1);            // ❌ 不能写(不知道具体子类型)
}

// ? super T:下界通配符,只能写不能读(精确类型)
public void writeOnly(List<? super Integer> list) {
    list.add(1);              // ✅ 写入安全
    // Integer i = list.get(0); // ❌ 读出来是 Object
}

// PECS 原则:Producer Extends, Consumer Super

⭐ 中频题(第26~72题)


第26题:相比其他编程语言,Java的核心优势和劣势分别是什么?

【核心答案】

维度 优势 ✅ 劣势 ❌
跨平台 JVM 屏蔽操作系统差异 需要安装 JRE,启动慢
内存管理 自动 GC,无需手动释放 GC 暂停影响实时性(ZGC 在改善)
生态 企业级框架极其丰富(Spring、MyBatis) 框架过多,学习成本高
类型安全 强类型 + 编译检查,减少运行时错误 代码冗长,不如动态语言灵活
多线程 JUC 并发包功能强大 相对于 Go 协程较重(虚拟线程在改善)
性能 JIT 编译后接近 C++ 占用内存大,冷启动慢
Java 典型优势场景:大型企业后端、分布式系统、大数据(Hadoop/Spark)
Java 不太适合的场景:嵌入式设备、系统编程、前端、AI 模型训练

第27题:JVM(Java虚拟机)和Java语言本身有什么区别?

【核心答案】

对比维度 Java 语言 JVM
是什么 一门编程语言(语法规范) 一个运行时环境(执行字节码)
规范载体 JLS(Java Language Specification) JVMS(JVM Specification)
关注点 语法、语义、类型系统 类加载、字节码执行、内存管理
依赖关系 需要 JVM 来运行 不依赖 Java 语言,可运行 Kotlin/Scala/Groovy 等
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  Java 源码   │    │  Kotlin 源码  │    │  Scala 源码  │
└──────┬──────┘    └──────┬──────┘    └──────┬──────┘
       │ 编译              │ 编译              │ 编译
       ▼                   ▼                   ▼
┌─────────────────────────────────────────────────┐
│                字节码 (.class)                    │
│                  ▲        ▲                      │
│                  │        │                      │
│              ┌───┴────────┴───┐                  │
│              │      JVM        │                  │
│              └────────────────┘                  │
└─────────────────────────────────────────────────┘

第28题:JVM是什么?它的核心职责有哪些?

【核心答案】

JVM 是一个虚拟计算机,核心职责有四大块:

职责 说明
类加载 .class 字节码加载到内存(类加载器 + 双亲委派)
字节码执行 解释执行 + JIT 编译,将字节码转为机器码
内存管理 分配内存 + 垃圾回收(GC)
运行时支持 提供 Native 接口、异常处理、线程调度等
             ┌─────────────────┐
             │   类加载子系统      │
             └────────┬────────┘
                      ▼
             ┌─────────────────┐
             │   运行时数据区      │
             │  堆 栈 方法区 ...  │
             └────────┬────────┘
                      ▼
             ┌─────────────────┐
             │   字节码执行引擎    │
             │  解释器 + JIT     │
             └─────────────────┘

第29题:编译型语言和解释型语言有什么区别?Java属于哪一种?

类型 工作方式 速度 跨平台 代表
编译型 源码一次编译成机器码,直接运行 C、C++、Go、Rust
解释型 逐行翻译成机器码并执行 Python、JavaScript
Java 源码→字节码(编译)→解释/JIT执行 较快 Java
编译型:   源码 ──一次性编译──▶ 机器码 ──直接运行──▶ 输出

解释型:   源码 ──逐行翻译──▶ 机器码 ──逐行运行──▶ 输出

Java:    源码 ──编译──▶ 字节码 ──解释+JIT──▶ 机器码 ──运行──▶ 输出

第30题:Python和Java在语言特性、运行机制、适用场景上有哪些区别?

对比维度 Java Python
类型系统 静态强类型 动态强类型
运行方式 编译 → JVM 执行字节码 解释执行(CPython)
性能 快(JIT 优化后接近 C++) 慢(解释执行,GIL限制多线程)
并发模型 原生多线程 + JUC GIL 限制,多进程/协程
语法 严谨、冗长 简洁、灵活
内存 JVM 自动管理 引用计数 + GC
典型场景 企业级后端、大数据、Android AI/ML、脚本、爬虫、Web快速开发

第31题:int和long类型各占多少位、多少字节?各自的取值范围是多少?

类型 字节 位数 范围
int 4 字节 32 位 -2,147,483,648 ~ 2,147,483,647(约 ±21 亿)
long 8 字节 64 位 -9,223,372,036,854,775,808 ~ …807(约 ±900 亿亿)
int max = Integer.MAX_VALUE;   // 2147483647
int min = Integer.MIN_VALUE;   // -2147483648
long maxL = Long.MAX_VALUE;    // 9223372036854775807

第32题:long类型和int类型之间可以互相转换吗?转换时需要注意什么?

可以相互转换,但需要注意数据溢出或精度丢失。

// int → long:自动转换(安全)
int i = 100;
long l = i;           // 安全,自动提升

// long → int:必须强制转换(可能溢出)
long big = 3000000000L;  // 超出 int 范围
int j = (int) big;       // 溢出!值会变成负数或其他错误值
System.out.println(j);   // 输出一个你意想不到的值

// 安全做法:先检查范围
if (big >= Integer.MIN_VALUE && big <= Integer.MAX_VALUE) {
    int safe = (int) big;
}

第33题:Java中的数据类型转换分为哪几种方式?各自可能引发什么问题?

转换类型 方向 是否自动 风险
自动类型提升 小范围 → 大范围 ✅ 自动 无风险
强制类型转换 大范围 → 小范围 ❌ 需手动 数据溢出、精度丢失
表达式类型提升 不同类型混合运算 ✅ 自动提升为最大类型 浮点数精度问题
// 自动提升
byte b = 10;
int i = b;            // byte → int 自动

// 强制转换:溢出
int big = 300;
byte small = (byte) big;  // 300 % 256 = 44

// 表达式提升
int a = 5;
double d = 2.0;
double result = a / d;    // int 提升为 double

// 浮点转整数:截断
int x = (int) 3.99;       // x = 3,直接截断,不是四舍五入!

第34题:Java中为什么既要有int基本类型,又要有Integer包装类?二者如何对比?

对比维度 int Integer
类型 基本类型 引用类型(对象)
内存占用 4 字节 16+ 字节(对象头 + 值)
默认值 0 null
性能 慢(有对象开销)
能否用于泛型
能否为 null ✅(表示"无值")
提供方法 ✅(parseInt、valueOf 等)
// Integer 的必要场景
// 1. 泛型
List<Integer> list = new ArrayList<>();  // 不能 List<int>

// 2. 表示"无值"
Integer score = null;  // 数据库可能为 NULL

// 3. 工具方法
int val = Integer.parseInt("123");
String hex = Integer.toHexString(255);  // "ff"

第35题:既然有了Integer包装类,为什么还要保留int基本类型?

核心原因:性能。

// 包装类开销对比
int a = 100;           // 4 字节
Integer b = 100;       // 16+ 字节(对象头 8-12 + int值 4 + 对齐填充)

// 性能差异
long start = System.currentTimeMillis();
Long sum = 0L;          // 包装类
for (int i = 0; i < 100_000_000; i++) {
    sum += i;           // 每次循环拆箱→加法→装箱,大量对象创建!
}
// 耗时:几秒(且产生大量 GC)

long sum2 = 0L;         // 基本类型
for (int i = 0; i < 100_000_000; i++) {
    sum2 += i;          // 纯 CPU 运算
}
// 耗时:十几毫秒
保留原因 说明
性能 无对象头开销,直接 CPU 运算
内存 占用极小(4 字节 vs 16+ 字节)
简单 算术运算符直接使用
兼容性 与 C/C++ 底层交互需要

第36题:Java多态体现在哪几个方面?请分别举例说明。

多态体现在三个方面:

多态形式 发生时机 核心
继承多态 运行时 父类引用指向子类对象
接口多态 运行时 接口引用指向实现类对象
方法重载 编译时 同一个方法名,不同参数列表
// 1. 继承多态
Animal a = new Dog();
a.makeSound();  // 调用 Dog 的 makeSound()
a = new Cat();
a.makeSound();  // 调用 Cat 的 makeSound()

// 2. 接口多态
List<String> list = new ArrayList<>();  // 接口引用 → ArrayList
list = new LinkedList<>();               // 随时切换实现

// 3. 方法重载
public void print(int x) { /* ... */ }
public void print(String x) { /* ... */ }

第37题:多态机制解决了编程中的什么问题?

解决问题 说明 例子
可扩展性 新增子类无需修改调用方代码 新增 Cat 类,Animal 引用代码不变
可替换性 运行时灵活替换实现 List list = new ArrayList()new LinkedList()
解耦 依赖抽象不依赖具体实现 Service 依赖接口而不是具体实现
// 没有多态:每新增一个动物类型都要改代码
if (type == "dog") dogSound();
else if (type == "cat") catSound();
else if (type == "bird") birdSound();  // 无限膨胀!

// 有多态:新增动物只需新建类,调用代码不用改
Animal animal = zoo.getAnimal();  // 返回什么动物不需要关心
animal.makeSound();               // 自动调用对应的方法

【核心思想——开闭原则】:对扩展开放,对修改关闭。


第38题:面向对象的六大设计原则(SOLID + 迪米特法则)分别是什么?

缩写 原则 核心
S 单一职责 (SRP) 一个类只做一件事
O 开闭原则 (OCP) 对扩展开放,对修改关闭
L 里氏替换 (LSP) 子类可以完全替换父类
I 接口隔离 (ISP) 接口应小而专一
D 依赖倒置 (DIP) 依赖抽象,不依赖具体实现
LoD 迪米特法则 最少知识原则,只和直接朋友通信
// 单一职责
class OrderService { void createOrder() {} }       // 只管订单
class OrderPrinter  { void printOrder() {} }       // 只管打印

// 开闭原则
interface Shape { double area(); }
class Circle implements Shape { /* ... */ }
// 新增 Rectangle 不需要修改现有 Shape 接口和使用方

// 依赖倒置
class Controller {
    private Service service;  // 依赖接口
    // 不是 private ServiceImpl service; // ❌ 依赖具体实现
}

第39题:抽象类和普通类有什么区别?什么时候应该使用抽象类?

维度 普通类 抽象类
能否实例化 new ❌ 不能
抽象方法 ❌ 不能有 ✅ 可以有
目的 可直接使用 提供模板,让子类补充细节
设计意图 具体实现 抽象模板

使用抽象类的时机

  • 多个类有共同的行为和状态,但某些方法无法给出通用实现
  • 需要模板方法模式
  • 需要构造方法初始化公共状态
// 抽象类充当模板
public abstract class PaymentProcessor {
    // 公共状态
    protected String merchantId;

    public PaymentProcessor(String merchantId) {
        this.merchantId = merchantId;
    }

    // 模板方法
    public final void process() {
        validate();
        doPay();        // 交给子类实现
        sendNotification();
    }

    private void validate() { /* 公共验证逻辑 */ }
    protected abstract void doPay();  // 子类必须实现
    private void sendNotification() { /* 公共通知逻辑 */ }
}

第40题:抽象类可以用final修饰吗?为什么?

不能。 abstractfinal 是互斥的。

  • abstract 的含义:必须被继承才能使用
  • final 的含义:不能被继承
// public abstract final class A {}  ← 编译错误!
// abstract 说"我的方法要子类来实现"
// final 说"你不能有子类"
// 矛盾的!

第41题:从Java 8到Java 9,接口中可以定义哪些类型的方法?

方法类型 关键字 引入版本 能否有方法体 能否被重写
抽象方法 (无) Java 1.0 ✅ 必须
default 方法 default Java 8 ✅ 可选
static 方法 static Java 8 ❌(不能被重写)
private 方法 private Java 9 ❌(接口内部用)
public interface ModernInterface {
    // 抽象方法(所有版本都有)
    void doSomething();

    // Java 8:default 方法
    default void log() {
        privateHelper();  // 调用 Java 9 私有方法
        System.out.println("logging...");
    }

    // Java 8:static 方法
    static ModernInterface create() {
        return new ModernInterface() {
            public void doSomething() {}
        };
    }

    // Java 9:private 方法
    private void privateHelper() {
        System.out.println("private helper");
    }
}

第42题:抽象类可以直接new实例化吗?为什么?

不能。 抽象类中可能包含未实现的抽象方法,直接 new 出来的对象如果调用这些方法,没有代码可执行。

abstract class Animal {
    abstract void makeSound();  // 没有方法体
}

// Animal a = new Animal();  ← 编译错误!
// 如果允许:a.makeSound() 执行什么??

// 但可以用匿名内部类实例化(本质是创建了子类)
Animal a = new Animal() {  // 创建了 Animal 的匿名子类
    @Override
    void makeSound() {
        System.out.println("汪汪");
    }
};  // 这不是"直接"new 抽象类,而是 new 匿名子类

第43题:接口中能否定义构造函数?为什么?

不能。

  • 构造函数的作用是初始化实例的状态
  • 接口没有实例状态(只有常量),不需要初始化
  • 接口是规范/契约,不是具体实现,不需要被实例化
public interface MyInterface {
    // public MyInterface() {}  ← 编译错误!
}

// 接口的"初始化"通过实现类构造函数完成
class MyImpl implements MyInterface {
    public MyImpl() {
        // 在这里初始化具体状态
    }
}

第44题:静态变量和静态方法在内存中是如何存储的?它们的加载时机是什么?

项目 存储位置 加载时机 生命周期
静态变量 方法区/元空间(JDK 8+) 类加载的准备阶段赋默认值,初始化阶段赋真实值 类卸载时释放
静态方法 方法区/元空间 类加载时加载 类卸载时释放
实例变量 堆内存 对象创建时 对象被 GC 回收时
内存布局:
┌───────────────┐
│   元空间        │
│  ├─ 类信息      │
│  ├─ 静态变量    │ ← count, MAX_VALUE 等
│  ├─ 静态方法    │ ← staticMethod()
│  └─ 常量池      │
├───────────────┤
│   堆内存        │
│  └─ 实例对象    │ ← new 出来的对象
│      └─ 实例变量 │ ← name, age 等
├───────────────┤
│   栈内存        │
│  └─ 栈帧        │ ← 局部变量
└───────────────┘

第45题:非静态内部类和静态内部类有什么区别?分别如何实例化?

维度 非静态内部类 静态内部类
依赖关系 必须有外部类实例才能存在 不依赖外部类实例
访问外部成员 可以访问外部类的所有成员 只能访问外部类的静态成员
持有外部引用 ✅ 持有 Outer.this ❌ 不持有
内存泄漏风险 ⚠️ 高(持有外部引用) ✅ 低
static 成员 ❌ 不能定义 static 成员 ✅ 可以
public class Outer {
    private String name = "Outer";
    private static int count = 0;

    // 非静态内部类
    class Inner {
        void print() {
            System.out.println(name);   // ✅ 直接访问外部非静态成员
            System.out.println(count);  // ✅ 访问外部静态成员
        }
    }

    // 静态内部类
    static class StaticInner {
        void print() {
            // System.out.println(name); // ❌ 不能访问外部非静态成员
            System.out.println(count);   // ✅ 只能访问静态成员
        }
    }
}

// 实例化区别
Outer outer = new Outer();
Inner inner = outer.new Inner();              // 先有外部,再有内部
StaticInner staticInner = new Outer.StaticInner();  // 独立实例化

第46题:非静态内部类为什么能直接访问外部类的成员?编译器背后做了什么?

编译器自动为非静态内部类添加了一个指向外部类的引用:Outer this$0

// 你写的代码:
public class Outer {
    private String name = "hello";

    class Inner {
        void print() {
            System.out.println(name);  // 直接访问
        }
    }
}

// 编译后等效代码:
class Outer {
    private String name;
}

class Outer$Inner {
    final Outer this$0;  // ← 编译器自动加的!

    Outer$Inner(Outer outer) {
        this.this$0 = outer;
    }

    void print() {
        System.out.println(this$0.name);
    }
}

印证方式javap -verbose Outer$Inner.class 可以看到 final Outer this$0 字段


第47题:实现对象深拷贝有哪几种方法?各自优缺点是什么?

(详见第15题——深拷贝与浅拷贝)补充三种方式的对比:

方式 实现 优点 缺点
Cloneable 接口 重写 clone() 递归拷贝 JDK 原生,不需第三方 侵入性强,多层嵌套麻烦
序列化 ObjectOutputStream/ObjectInputStream 通用,自动处理嵌套 所有类需 Serializable,性能差
JSON 序列化 Jackson / Gson / Fastjson 简单,非侵入 性能一般,特殊类型可能丢失
手动拷贝 构造方法 / Builder / 工厂 最可控,最清晰 代码量大
// 推荐:构造方法方式——最可控
public class User {
    private String name;
    private Address address;

    // 深拷贝构造方法
    public User(User source) {
        this.name = source.name;
        this.address = new Address(source.address);  // 递归拷贝
    }
}

第48题:Java中创建对象有哪几种方式?除了new关键字还有哪些途径?

方式 代码示例 适用场景
new 关键字 new User() 最常用
反射 Class.forName().newInstance()Constructor.newInstance() 框架开发
clone() obj.clone() 按原型复制
反序列化 ObjectInputStream.readObject() 从持久化数据恢复
Unsafe Unsafe.allocateInstance() 底层框架
// 1. new
User u1 = new User();

// 2. 反射
Class<?> clazz = Class.forName("com.example.User");
User u2 = (User) clazz.getDeclaredConstructor().newInstance();

// 3. clone(需实现 Cloneable)
User u3 = u1.clone();

// 4. 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.obj"));
User u4 = (User) ois.readObject();

// 5. Unsafe(不调用构造方法)
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
User u5 = (User) unsafe.allocateInstance(User.class);  // 不调构造器!

第49题:通过new创建的对象什么时候会被垃圾回收?判断依据是什么?

判断依据:可达性分析算法(GC Roots)。 当一个对象从 GC Roots 出发不可达时,会被标记为垃圾并回收。

GC Roots 包括:
├── 虚拟机栈中引用的对象
├── 方法区中静态变量引用的对象
├── 方法区中常量引用的对象
├── 本地方法栈中 JNI 引用的对象
└── 活跃线程对象

可达性分析:
  GC Roots
     │
     ├──▶ objA     ← 可达,不回收
     │     └──▶ objB  ← 可达,不回收
     │
     └──  objC      ← 从 GC Roots 不可达 → 可以回收!
// 对象不可达的典型场景
public void method() {
    User user = new User();  // user 是 GC Root(栈帧中的引用)
    user = null;              // user 不再指向对象 → 不可达 → 可回收
}  // 方法结束,栈帧销毁,user 引用消失 → 不可达 → 可回收

第50题:如何访问一个类的私有字段或私有方法?有哪些途径?

途径 原理 示例
反射 setAccessible(true) 关闭访问检查 框架开发
getter/setter 私有字段的公开访问方法 日常开发
内部类 编译器桥接方法 语言特性
public class User {
    private String secret = "密码123";

    private void secretMethod() {
        System.out.println("私有方法被调用");
    }

    // 途径1:getter(推荐)
    public String getSecret() { return secret; }
}

// 途径2:反射(慎用)
User user = new User();
Field field = User.class.getDeclaredField("secret");
field.setAccessible(true);  // 突破私有限制
String value = (String) field.get(user);

Method method = User.class.getDeclaredMethod("secretMethod");
method.setAccessible(true);
method.invoke(user);

⚠️ 注意:反射破坏封装性,JDK 17+ 默认有模块化限制,需要 --add-opens 参数


第51题:Java注解的底层实现原理是什么?

注解的本质是接口,运行时通过动态代理生成代理实例。

1. 定义注解
   @Retention(RUNTIME)
   @interface MyAnnotation { String value(); }

2. 编译后生成: interface MyAnnotation extends java.lang.annotation.Annotation { }

3. 运行时获取:动态代理生成 Proxy 类
   调用 getAnnotation(MyAnnotation.class) →
   JDK 动态代理 →
   返回 $Proxy 实例(实现 MyAnnotation)→
   调用代理方法 → AnnotationInvocationHandler.invoke() →
   返回注解属性值
// 运行时获取注解
MyAnnotation ann = clazz.getAnnotation(MyAnnotation.class);
// ann 的实际类型是:com.sun.proxy.$Proxy1(动态代理对象)
// 调用 ann.value() 时,实际执行的是
// AnnotationInvocationHandler.invoke(proxy, method, args)

第52题:@Retention注解的三种保留策略分别代表什么含义?

策略 含义 保留到何时 典型注解
RetentionPolicy.SOURCE 源码级别,编译时丢弃 .java 文件 @Override, @SuppressWarnings
RetentionPolicy.CLASS 类文件级别,JVM 不加载 .class 文件(默认值) @NonNull(Lombok)
RetentionPolicy.RUNTIME 运行时级别,可通过反射读取 JVM 内存 @Autowired, @RequestMapping
生命周期:
  .java 源码 → javac 编译 → .class 字节码 → 类加载 → JVM 运行时
   SOURCE ──✕──                CLASS ──────✕───          RUNTIME ──▶ 可用反射读

第53题:@Target注解可以指定哪些作用域(ElementType)?常用的有哪些?

ElementType 作用域 常用场景
TYPE 类、接口、枚举 @Component, @Entity
FIELD 字段 @Autowired, @Value
METHOD 方法 @GetMapping, @Transactional
PARAMETER 方法参数 @RequestParam, @PathVariable
CONSTRUCTOR 构造方法 @Autowired
LOCAL_VARIABLE 局部变量 较少使用
ANNOTATION_TYPE 注解类型 定义元注解时
PACKAGE 包级别注解
TYPE_PARAMETER 类型参数 <@NonNull T>
TYPE_USE 任何类型使用处 泛型、异常等

第54题:Java中异常处理有哪些方式?try-with-resources语法有什么好处?

方式 语法 说明
try-catch-finally try {} catch {} finally {} 传统方式
try-with-resources try (Resource r = ...) {} 自动关闭资源(Java 7+)
throws 方法签名声明 往上抛给调用者
多层 catch catch (A | B e) 多个异常统一处理(Java 7+)
// 传统方式:繁琐且容易遗漏关闭
FileInputStream fis = null;
try {
    fis = new FileInputStream("file.txt");
    // 读文件
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try { fis.close(); } catch (IOException e) { }
    }
}

// try-with-resources:简洁且自动关闭
// 要求资源类实现 AutoCloseable 接口
try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
    String line = br.readLine();
} catch (IOException e) {
    e.printStackTrace();
}
// fis 和 br 自动关闭,且关闭顺序与打开顺序相反
好处:
├── 代码更简洁,减少样板代码
├── 自动关闭资源,不会遗漏
├── 关闭顺序:后打开的先关闭
└── 异常压制机制:try 块异常优先,close 异常作为 suppressed 附加

第55题:为什么有些方法抛出异常时不需要显式使用throws声明?

因为 RuntimeException 及其子类是「非受检异常」,编译期不检查。

// 不需要 throws(非受检异常)
public int divide(int a, int b) {
    return a / b;  // 可能 ArithmeticException,但不需声明
}

public String getFirst(List<String> list) {
    return list.get(0);  // 可能 IndexOutOfBoundsException,不需声明
}

// 必须 throws(受检异常)
public void readFile(String path) throws IOException {
    new FileReader(path);  // FileReader 声明了 throws IOException
}
异常类型 需要 throws? 原因
RuntimeException / Error 非受检,编译期不强制
其他 Exception 受检异常,编译期强制处理

第56题:如果try块中有return “a”,finally块中也有return “b”,最终返回什么?为什么?

返回 “b”。finally 中的 return 会覆盖 try 中的 return。

public static String test() {
    try {
        return "a";
    } finally {
        return "b";
    }
}
// 输出:b
// 更微妙的例子
public static int test() {
    int x = 1;
    try {
        return x;        // 先把 x 的值(1)缓存在返回地址
    } finally {
        x = 2;           // 改了 x,但返回的是缓存的值
    }
}
// 输出:1  ← 不是 2!

// 但如果返回的是引用类型:
public static User test() {
    User u = new User("Tom");
    try {
        return u;
    } finally {
        u.setName("Jerry");  // 改了对象内容
    }
}
// 返回的 User 的 name 是 "Jerry"!
// 原因是:return 缓存的是引用值,指向对象没变
情况 结果 说明
finally 有 return 返回 finally 的值 覆盖 try 的 return
try return 基本类型 返回 try 瞬时的值 finally 修改无效
try return 引用类型 返回引用(对象可能被 finally 修改) 对象内容可变
finally 抛异常 异常传播,try 的 return 被丢弃 异常覆盖返回

⚠️ 避坑指南:永远不要在 finally 中写 return 或抛异常!


第57题:Object类中有哪些常用方法?各自的作用是什么?

方法 作用
getClass() 获取对象的 Class 对象
hashCode() 返回对象的哈希码(用于 HashMap/HashSet)
equals(Object) 判断两个对象是否「相等」(默认比较地址)
clone() 创建并返回对象的拷贝(需实现 Cloneable,浅拷贝)
toString() 返回对象的字符串表示(默认:类名@十六进制哈希)
notify() 唤醒一个在此对象监视器上等待的线程
notifyAll() 唤醒所有在此对象监视器上等待的线程
wait() / wait(long) / wait(long, int) 使当前线程等待,直到被唤醒
finalize() 对象被 GC 回收前调用(已废弃 JDK 9,JDK 18 彻底移除)
public class User {
    @Override
    public String toString() {
        return "User{name='" + name + "'}";
    }

    @Override
    public boolean equals(Object o) { /* ... */ }

    @Override
    public int hashCode() { /* ... */ }

    @Override
    protected Object clone() throws CloneNotSupportedException { /* ... */ }
}

第58题:String类有哪些常用的方法?请列举并说明用途。

方法 用途 示例
length() 字符串长度 "abc".length() → 3
charAt(int) 获取指定位置字符 "abc".charAt(0) → ‘a’
substring(int, int) 截取子串 "hello".substring(0,2) → “he”
contains(CharSequence) 是否包含 "abc".contains("b") → true
indexOf(String) 第一次出现位置 "abca".indexOf("a") → 0
startsWith/endsWith 前/后缀判断 "abc".startsWith("a") → true
replace/ replaceAll 替换 "a1b2".replaceAll("\\d","") → “ab”
split(String) 分割 "a,b".split(",") → [“a”,“b”]
trim() 去除首尾空白 " a ".trim() → “a”
toUpperCase/toLowerCase 大小写转换 "Abc".toUpperCase() → “ABC”
equals/equalsIgnoreCase 比较 "a".equals("a") → true
intern() 放入字符串常量池 s.intern()
format(String, Object...) 格式化 String.format("Hi %s", "Tom")
join(CharSeq, CharSeq...) 拼接 String.join("-", "a","b") → “a-b”
isBlank() 是否空白(Java 11+) " ".isBlank() → true
isEmpty() 是否空串 "".isEmpty() → true

第59题:Lambda表达式的基本语法是什么?它解决了什么问题?

Lambda 本质上是「可传递的匿名函数」,用于简化函数式接口的匿名内部类写法。

语法(参数列表) -> { 函数体 }参数 -> 表达式

形式 示例
无参 () -> System.out.println("hello")
单参 x -> x * 2
多参 (a, b) -> a + b
多行 (a, b) -> { int c = a + b; return c; }
// 以前:匿名内部类
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
    }
};

// Lambda:简洁明了
Runnable r2 = () -> System.out.println("hello");

// 以前:7 行
list.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

// Lambda:1 行
list.sort((a, b) -> a.length() - b.length());

// 方法引用:更简洁
list.sort(Comparator.comparingInt(String::length));

解决的问题

  • 消灭样板代码(匿名内部类的冗长语法)
  • 支持函数式编程风格
  • 便于并行处理(配合 Stream)

第60题:Stream API有哪些常用操作?中间操作和终端操作有什么区别?

类型 特点 常见操作
中间操作 返回 Stream,惰性执行,可链式调用 filtermapflatMappeekdistinctsortedlimitskip
终端操作 触发计算,消费 Stream,返回结果 collectforEachreducecountanyMatchfindFirstmaxmin
List<String> names = Arrays.asList("张三", "李四", "王五", "赵六", "张伟");

List<String> result = names.stream()
    .filter(n -> n.startsWith("张"))    // 中间:过滤
    .map(String::toUpperCase)            // 中间:转换
    .sorted()                             // 中间:排序
    .limit(2)                             // 中间:限制数量
    .collect(Collectors.toList());        // 终端:收集 → ["张三", "张伟"]

// 常用终端操作
boolean any = names.stream().anyMatch(n -> n.length() > 2);  // true
long count = names.stream().count();                          // 5
Optional<String> first = names.stream().findFirst();          // 张三

// 常用收集器
Map<Integer, List<String>> byLen = names.stream()
    .collect(Collectors.groupingBy(String::length));
String joined = names.stream()
    .collect(Collectors.joining(", "));  // "张三, 李四, ..."

第61题:ParallelStream是什么?适用于什么场景?使用时需要注意哪些问题?

ParallelStream 是并行流,使用 ForkJoinPool 将任务拆分到多核 CPU 并行执行。

// 顺序流
list.stream().filter(...).collect(...);   // 单线程

// 并行流
list.parallelStream().filter(...).collect(...);  // ForkJoinPool
维度 说明
适用 大数据量、独立计算、CPU 密集型(无 IO 等待)
不适用 小数据量、有状态操作、IO 密集型、结果依赖顺序

注意问题:

// ⚠️ 问题1:线程安全问题
List<Integer> list = new ArrayList<>();
IntStream.range(0, 1000)
    .parallel()
    .forEach(list::add);  // ❌ ArrayList 线程不安全!结果不可预测

// ✅ 解决:用线程安全集合或 collect
List<Integer> safe = IntStream.range(0, 1000)
    .parallel()
    .boxed()
    .collect(Collectors.toList());  // ✅ 安全

// ⚠️ 问题2:不要用 parallelStream 做 IO 操作
// parallelStream 使用公共 ForkJoinPool,IO 阻塞会影响其他并行任务

第62题:CompletableFuture是什么?它解决了传统Future的哪些痛点?

CompletableFuture 是 Java 8 引入的异步编程工具,实现了 Future + CompletionStage 接口。

痛点 传统 Future CompletableFuture
结果获取 get() 阻塞等待 thenAccept() 回调,不阻塞
链式处理 ❌ 不支持 thenApply/thenCompose
组合多任务 ❌ 需手动等待 allOf/anyOf
异常处理 仅在 get() 时暴露 exceptionally/handle
手动完成 complete()
// 传统 Future:阻塞等待
Future<String> future = executorService.submit(() -> {
    Thread.sleep(1000);
    return "结果";
});
String result = future.get();  // 阻塞 1 秒!

// CompletableFuture:异步编排
CompletableFuture.supplyAsync(() -> fetchUser(1L))
    .thenApply(user -> user.getName())        // 转换
    .thenAccept(name -> System.out.println(name))  // 消费
    .exceptionally(e -> {                      // 异常处理
        e.printStackTrace();
        return null;
    });

// 多任务组合
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
all.thenRun(() -> {
    String r1 = f1.join();  // 不抛受检异常
    String r2 = f2.join();
    System.out.println(r1 + " " + r2);
});

第63题:Java 21引入了哪些重要的新特性?虚拟线程(Virtual Thread)的原理是什么?

【主要新特性】

特性 说明
虚拟线程 (Virtual Thread) 轻量级线程,几百万个也不卡
模式匹配 (Record Pattern) 简化 instanceof + 解构
Switch 模式匹配 switch 支持类型+条件匹配
字符串模板 STR."Hello \{name}"
序列化集合 SequencedCollection(可逆序操作)

【虚拟线程核心原理】

传统平台线程:              虚拟线程:
  Java Thread                 Virtual Thread
      │                            │
      ▼                            ▼
  OS 线程 (重量级)            Carrier Thread (平台线程)
  创建开销大,切换慢               │
  每个约占用 1MB 栈           ┌────┼────┐
                              ▼    ▼    ▼
                           VT1  VT2  VT3 ... (轻量级)
                           无数虚拟线程共享少量平台线程
对比 平台线程 虚拟线程
成本 高(~1MB 栈 + OS 资源) 极低(按需分配)
数量 几千个 数百万个
阻塞 阻塞 OS 线程 阻塞时自动让出 Carrier
创建 new Thread() Thread.startVirtualThread()
// 虚拟线程使用
Thread vThread = Thread.startVirtualThread(() -> {
    System.out.println("虚拟线程运行中");
});

// 用 ExecutorService 管理虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) {
        executor.submit(() -> {
            Thread.sleep(1000);  // 阻塞不占 OS 线程!
            return "done";
        });
    }
}

第64题:在进行Java对象序列化时,有哪些推荐的最佳实践?

实践 说明
显式声明 serialVersionUID 避免反序列化时版本不匹配
transient 标记敏感字段 密码等敏感数据不参与序列化
使用 JSON/Protobuf 替代 跨语言、更安全、性能更好
谨慎序列化复杂对象 注意内部对象的序列化
考虑反序列化安全性 防止反序列化漏洞攻击
public class User implements Serializable {
    // 1. 必须声明 serialVersionUID(用 IDE 生成)
    private static final long serialVersionUID = 1L;

    private String name;

    // 2. 敏感字段用 transient
    private transient String password;

    // 3. 静态变量不会被序列化
    private static int count;

    // 4. 自定义序列化逻辑
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();    // 先写非 transient
        oos.writeObject(encrypt(password));  // 加密后写入
    }

    private void readObject(ObjectInputStream ois)
            throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        this.password = decrypt((String) ois.readObject());
    }
}

💡 更推荐:使用 JSON(Jackson/Gson)或 Protobuf 做序列化,不依赖 Java 原生序列化


第65题:Java原生序列化如何实现?serialVersionUID和transient关键字的作用是什么?

// 实现 Serializable 接口即可
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient String password;  // 不参与序列化
}

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"));
oos.writeObject(user);

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));
User user = (User) ois.readObject();
关键字 作用 典型场景
serialVersionUID 版本控制,反序列化时校验类是否匹配 所有 Serializable 类
transient 标记字段不参与序列化 密码、连接、缓存等
serialVersionUID 场景 结果
不声明 编译器自动生成(类变化后值不同 → 反序列化失败)
显式声明固定值 类有小改动仍可反序列化(需你自己保证兼容)

第66题:代理模式和适配器模式有什么区别?各自的适用场景是什么?

对比维度 代理模式 (Proxy) 适配器模式 (Adapter)
目的 控制对原对象的访问 转换接口以适配客户端
接口 代理与被代理实现相同接口 适配器改变接口
关注点 访问控制、增强功能 兼容性、接口转换
关系 代理持有被代理对象 适配器包装被适配对象
// ===== 代理模式 =====
// 接口相同,控制访问
interface UserService {
    void save();
}
class UserServiceImpl implements UserService {
    public void save() { /* 实际业务 */ }
}
class UserServiceProxy implements UserService {  // 实现同一接口
    private UserService target;
    public void save() {
        System.out.println("开始事务");  // 增强
        target.save();
        System.out.println("提交事务");  // 增强
    }
}

// ===== 适配器模式 =====
// 接口不同,转换适配
interface TypeC { void chargeTypeC(); }            // 新标准
class LightningPhone { void chargeLightning() {} }  // 旧接口
class LightningAdapter implements TypeC {           // 适配器
    private LightningPhone phone;
    public void chargeTypeC() {
        phone.chargeLightning();  // 转换为旧接口调用
    }
}
选择策略:
├── 需要新增功能/控制访问,接口不变 → 代理
└── 需要兼容不同接口 → 适配器

第67题:责任链模式和策略模式有什么区别?分别适用于什么业务场景?

对比维度 责任链模式 (Chain of Responsibility) 策略模式 (Strategy)
目的 多个处理器依次尝试处理请求 从多个算法中选择一个执行
关系 处理器之间形成链条 策略之间互斥独立
结果 可能被多个处理器处理(或截断) 只有一个策略被执行
// ===== 责任链:审批流程 =====
abstract class Approver {
    protected Approver next;
    public void setNext(Approver next) { this.next = next; }
    public abstract void approve(int amount);
}

class Manager extends Approver {
    public void approve(int amount) {
        if (amount <= 1000) System.out.println("经理审批");
        else if (next != null) next.approve(amount);
    }
}

// ===== 策略:支付方式 =====
interface PayStrategy {
    void pay(int amount);
}
class AliPay implements PayStrategy {
    public void pay(int amount) { System.out.println("支付宝支付" + amount); }
}
class WechatPay implements PayStrategy {
    public void pay(int amount) { System.out.println("微信支付" + amount); }
}

class PaymentContext {
    private PayStrategy strategy;
    public void setStrategy(PayStrategy s) { strategy = s; }
    public void pay(int amount) { strategy.pay(amount); }
}
场景 推荐模式 例子
审批流、拦截器链、过滤器链 责任链 Spring Interceptor、Servlet Filter
支付方式、排序算法、折扣规则 策略 多种支付、多种排序

第68题:NIO的三大核心组件Channel、Buffer、Selector分别起什么作用?

(详见第24题——NIO 实现高并发),在此做对比总结:

组件 比喻 核心职责
Channel 铁路/水管 数据的双向传输通道,连接文件或网络
Buffer 货车/水桶 数据的临时存储容器,所有 I/O 数据必经 Buffer
Selector 调度中心 单线程监控多个 Channel,仅处理就绪的
Channel:
├── FileChannel       文件读写
├── SocketChannel     客户端 TCP
├── ServerSocketChannel  服务端 TCP
└── DatagramChannel   UDP

Buffer:
├── ByteBuffer        最常用,字节
├── CharBuffer、IntBuffer、... 
└── MappedByteBuffer  内存映射

第69题:哪些主流框架底层使用了Java NIO技术?

框架 使用方式 说明
Netty 封装 NIO 最流行的网络框架,Dubbo/RocketMQ/Elasticsearch 底层
Tomcat 8+ NIO Connector(默认) 高并发 HTTP 处理
Jetty NIO 嵌入式容器
Undertow NIO (XNIO) WildFly 默认,Spring Boot 可替代 Tomcat
ZooKeeper NIO 分布式协调服务
Kafka NIO + 零拷贝 高吞吐消息队列

💡 Netty 几乎统治了 Java 网络编程:Dubbo、RocketMQ、Elasticsearch、gRPC 都基于 Netty


第70题:如何用Comparable接口对学生列表按分数降序、学号升序排序?

public class Student implements Comparable<Student> {
    private int id;      // 学号
    private int score;   // 分数

    @Override
    public int compareTo(Student other) {
        // 分数降序
        if (this.score != other.score) {
            return Integer.compare(other.score, this.score);
            // 注意:other 在前 = 降序
        }
        // 分数相同,学号升序
        return Integer.compare(this.id, other.id);
    }
}

// 使用
List<Student> students = new ArrayList<>();
Collections.sort(students);  // 直接排序

// ===== 或用 Comparator(不侵入类) =====
students.sort(Comparator
    .comparingInt(Student::getScore).reversed()   // 分数降序
    .thenComparingInt(Student::getId));            // 学号升序

第71题:Native方法是什么?Java中为什么要使用Native方法?

native 方法是用其他语言(C/C++/汇编)实现的,通过 JNI(Java Native Interface)调用。

// native 方法声明
public class Thread {
    private native void start0();  // 用 C/C++ 实现,只有声明没有方法体
}

// Object 中的 native 方法
public class Object {
    public native int hashCode();
    protected native Object clone() throws CloneNotSupportedException;
    public final native void notify();
    public final native void wait(long timeout) throws InterruptedException;
}
使用原因 说明
与 OS 交互 线程创建、文件 IO 等必须调用 OS 系统调用
性能关键代码 数学运算、加密算法等
复用 C/C++ 库 已有成熟库不必用 Java 重写
操作硬件 直接访问内存、寄存器等
Java ──JNI──▶ C/C++ .so/.dll ──系统调用──▶ OS Kernel

第72题:Java进程如何与操作系统进行交互?从用户态到内核态的调用链路是怎样的?

完整的调用链路:

┌─────────────────────────────────────────────┐
│                 用户态 (User Space)            │
│                                              │
│   Java 代码                                   │
│      │  new Thread().start()                 │
│      ▼                                       │
│   JVM (C++ 实现)                              │
│      │  调用 native start0()                  │
│      ▼                                       │
│   JNI 层 (C 代码)                              │
│      │  jvm.dll / libjvm.so                   │
│      ▼                                       │
│   libc / glibc                               │
│      │  pthread_create() / clone()            │
│      ▼                                       │
│   ═══════════════════════════════════════    │
│              系统调用 (syscall)                │
│   ═══════════════════════════════════════    │
│      ▼                                       │
└─────────────────────────────────────────────┘
                   │ clone 系统调用
                   ▼
┌─────────────────────────────────────────────┐
│               内核态 (Kernel Space)           │
│                                              │
│   中断/陷阱门                                  │
│      ▼                                       │
│   系统调用处理程序                              │
│      ▼                                       │
│   do_fork() → 创建内核线程                     │
│      ▼                                       │
│   调度器 分配 CPU 时间片                       │
│      ▼                                       │
│   返回用户态                                   │
└─────────────────────────────────────────────┘

关键步骤

步骤 发生了什么
① Java 调用 new Thread().start() 调用到 JVM 的 Thread 类
② JVM 调用 native start0() 进入 JNI 层 C/C++ 代码
③ C 代码调用 pthread_create()clone() 准备系统调用
用户态 → 内核态切换 触发软中断,CPU 切换到内核模式
⑤ 内核创建线程、分配资源 进程控制块(PCB)、栈、调度
内核态 → 用户态切换 返回用户态,线程进入就绪队列
// 你写的代码:
new Thread(() -> System.out.println("Hello")).start();

// 底层发生了什么:
// Java Thread.start()
//   → native start0()          [JVM C++ 代码]
//     → JVM_StartThread()      [HotSpot 源码]
//       → os::create_thread()  [OS 相关实现]
//         → pthread_create()   [Linux glibc]
//           → clone() 系统调用  [内核]

📝 总结

以上是对「一、Java 基础」全部 72 道题的详细解答,涵盖:

段落 题号 核心主题
高频 1-12 第1~12题 语言特性、JVM体系、面向对象、抽象类/接口
高频 13-25 第13~25题 final/static、拷贝、比较、字符串、Java8/反射/异常/DCL、IO/NIO、泛型
中频 26-47 第26~47题 语言对比、类型转换、多态/设计原则、抽象类、内部类、深拷贝
中频 48-72 第48~72题 创建对象/GC、注解、异常、Stream/Optional、Java21、序列化、设计模式、NIO、Native
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐