← 返回面试专题导航

☕ Java 基础常见面试题总结(上)

围绕"语法基础"这一块,把高频问题系统刷一遍

本篇主要覆盖:数据类型与运算符、流程控制语句、方法与参数传递,为后续集合、并发、JVM 打地基。

🎯 难度筛选

📚 阅读指引

本页采用交互式学习模式,建议按照下面顺序学习:

  1. 点击问题标题展开详细解答,阅读完后勾选复选框标记为"已掌握"。
  2. 使用难度筛选器,可以只看特定难度的题目。
  3. 顶部进度条会实时显示你的学习进度。
  4. 在自己感觉薄弱的模块,点击文末的"深入阅读"跳转到对应专题页。

一、数据类型与运算符

1. Java 为什么要区分基本类型和包装类型? 简单

结论:这是在"性能"和"面向对象"之间做的折中设计。

  • 基本类型直接存值,读取和计算都很快,适合做大量数值计算。
  • 包装类型是对象,可以放进集合、作为泛型参数、挂载方法上体现 OOP 设计。
  • 如果只有对象类型,所有数值操作都会产生对象,GC 压力巨大;如果只有基本类型,又没法很好地融入集合和泛型体系。
内存布局对比:

基本类型 int i = 10;
┌─────────┐
│ Stack │
│ i = 10 │ ← 直接存储值
└─────────┘

包装类型 Integer obj = 10;
┌─────────┐ ┌──────────────┐
│ Stack │ │ Heap │
│ obj ────┼─────→│ Integer对象 │
└─────────┘ │ value = 10 │
└──────────────┘
完美答案

从三个维度深入分析:

  1. 内存效率维度:基本类型在栈上直接存储值,包装类型在堆上存储对象,前者内存占用小且访问速度快。
  2. 泛型支持维度:Java泛型设计要求类型参数必须是对象类型,所以需要包装类型来支持泛型集合如List<Integer>。
  3. Null安全维度:包装类型可以表示null,在数据库映射和可选值场景中很有用;基本类型有默认值但不能为null。

实战场景:在高性能计算场景使用基本类型,在集合操作和ORM映射场景使用包装类型。阿里巴巴Java开发手册明确要求:POJO类属性必须使用包装类型,局部变量优先使用基本类型。

面试时可以补充一句:"集合框架只能存对象,这是我们在选择基本类型/包装类型时需要考虑的现实约束。"

2. 自动装箱/拆箱有哪些坑?你在项目里遇到过吗? 中等

标准答法:

  • 大量循环中使用包装类型做累加,会产生大量临时对象,影响性能。
  • 对可能为 null 的包装类型做拆箱运算,容易抛 NullPointerException
  • 使用 == 比较包装类型时,容易被缓存机制误导。
// 坑1:性能问题 Integer sum = 0; for (int i = 0; i < 10000; i++) { sum += i; // 每次都装箱拆箱,产生大量临时对象 } // 坑2:NPE Integer count = getCount(); // 可能返回 null int result = count + 1; // 如果 count 为 null,这里抛 NPE // 坑3:== 比较陷阱 Integer a = 127; Integer b = 127; System.out.println(a == b); // true(缓存范围内) Integer c = 128; Integer d = 128; System.out.println(c == d); // false(超出缓存范围)
完美答案

深入分析三个核心陷阱:

  1. 性能陷阱:在循环中频繁的装箱拆箱会产生大量临时对象,增加GC压力。字节跳动面试中经常问这个问题,考察性能优化意识。
  2. NPE陷阱:包装类型为null时拆箱会抛NPE,这是很多线上bug的根源。美团面试官喜欢问如何避免这类问题。
  3. 缓存陷阱:Integer缓存[-128,127],Long缓存[-128,127],Character缓存[0,127],但Double、Float没有缓存。

实战经验:我在项目中遇到过订单金额计算使用Double导致精度问题,后来改用BigDecimal解决。另外在用户积分统计时,Long的自动装箱导致频繁GC,改用long后性能提升30%。

最佳实践:局部变量优先使用基本类型,POJO属性使用包装类型,数据库映射使用包装类型,数值计算使用基本类型。

⚠️ 常见陷阱:循环中使用 Integer 做累加,每次都会装箱拆箱,产生大量临时对象。应该用基本类型 int。
3. Integer 缓存为什么要设计成 [-128,127] 这个范围? 中等

核心点:

  • 这是 Java 设计者综合"节省内存"和"覆盖最常用数值"的一个经验值。
  • 很多业务计数、状态码都集中在这个范围内,缓存可以显著减少对象创建次数。
  • 范围可以通过 -XX:AutoBoxCacheMax 调整(不同版本支持情况略有差异)。

面试时不要只背"[-128,127] 会缓存",最好加一句"这是一个折中选择,并非语言层面强制的唯一正确范围"。

完美答案

从四个维度深入解析:

  1. 统计学维度:根据大量应用统计,-128到127范围内的Integer使用频率最高,覆盖了90%以上的常用场景。
  2. 内存效率维度:256个缓存对象占用内存约4KB,相比性能提升来说是值得的投入。
  3. 字符编码维度:ASCII字符集范围是0-127,很多字符处理场景在这个范围内。
  4. 业务场景维度:状态码、计数器、索引值等大多在byte范围内。

源码分析:Integer.valueOf()方法会先检查是否在缓存范围内,如果在则返回缓存实例,否则创建新对象。这个设计在IntegerCache内部类中实现。

扩展知识:其他包装类型的缓存范围:Long[-128,127],Character[0,127],Short[-128,127],Byte[-128,127](全部缓存),Boolean[false,true](全部缓存),但Float和Double没有缓存。

💡 扩展知识:可以通过 JVM 参数 -XX:AutoBoxCacheMax=N 调整 Integer 缓存的上限(下限固定为 -128)。
4. 浮点数运算为什么会有精度问题?如何解决? 困难

原因:浮点数采用 IEEE 754 标准,用二进制表示小数时,很多十进制小数无法精确表示。

// 经典问题 System.out.println(0.1 + 0.2); // 输出 0.30000000000000004 // 解决方案1:使用 BigDecimal BigDecimal a = new BigDecimal("0.1"); BigDecimal b = new BigDecimal("0.2"); System.out.println(a.add(b)); // 输出 0.3 // 解决方案2:整数运算(金额场景) int cents1 = 10; // 0.1元 = 10分 int cents2 = 20; // 0.2元 = 20分 System.out.println((cents1 + cents2) / 100.0); // 0.3
完美答案

深度解析IEEE 754标准:

  1. 二进制表示限制:某些十进制小数在二进制中是无限循环的,如0.1在二进制中是0.0001100110011...
  2. 存储空间限制:float只有32位,double有64位,无法存储无限循环的小数。
  3. 舍入误差累积:多次运算会累积舍入误差,导致结果偏差。

解决方案对比:

  • BigDecimal:精确计算,适合金融场景,但性能较差,使用复杂。
  • 整数运算:将小数转为整数计算,性能好,适合简单场景。
  • 容差比较:使用Math.abs(a-b) < EPSILON的方式比较浮点数。

实战经验:在电商系统中,订单金额计算必须使用BigDecimal。在科学计算中,可以使用double但要注意容差处理。京东金融面试经常问BigDecimal的使用注意事项。

注意:创建 BigDecimal 时一定要用字符串构造器,不要用 double 构造器,否则精度问题依然存在。

⚠️ 错误示范:new BigDecimal(0.1) 仍然会有精度问题!
✅ 正确做法:new BigDecimal("0.1")BigDecimal.valueOf(0.1)
5. == 和 equals 的区别?什么时候用哪个? 中等
  • == 比较的是引用地址(基本类型比较值)。
  • equals 比较的是对象内容(需要类正确重写 equals 方法)。
String s1 = new String("hello"); String s2 = new String("hello"); System.out.println(s1 == s2); // false(不同对象) System.out.println(s1.equals(s2)); // true(内容相同) // 常量池优化 String s3 = "hello"; String s4 = "hello"; System.out.println(s3 == s4); // true(指向同一个常量池对象)

使用建议:比较对象内容用 equals;比较引用地址或基本类型用 ==。

6. 位运算在实际项目中有哪些应用场景? 简单
  • 权限控制:用位掩码表示多个权限(读=1, 写=2, 执行=4)。
  • 状态标记:用一个整数的不同位表示多个布尔状态。
  • 性能优化:位运算比乘除法快(如 n << 1 等价于 n * 2)。
  • 哈希计算:HashMap 中用 & 运算代替取模。
// 权限示例 int READ = 1; // 001 int WRITE = 2; // 010 int EXECUTE = 4; // 100 int permission = READ | WRITE; // 011(有读写权限) boolean canRead = (permission & READ) != 0; // true boolean canExecute = (permission & EXECUTE) != 0; // false

👉 如果你想把这一块的所有细节刷透,可以跳到: 《数据类型与运算符面试题 · 专题版》

二、流程控制语句

7. switch 和 if-else 在使用上有什么取舍建议? 简单
  • 分支较少且是等值判断时,switch 更清晰,JVM 也可以做表驱动优化。
  • 条件复杂(区间判断、组合条件)时,用 if-else 更直观。
  • 注意 switch 的可读性,case 太多时可以考虑拆表驱动或策略模式。
性能对比(编译器优化后):

if-else 链:O(n) 线性查找
├─ if (x == 1)
├─ else if (x == 2)
├─ else if (x == 3)
└─ ...

switch 语句:O(1) 跳转表
├─ case 1: jump to address_1
├─ case 2: jump to address_2
└─ case 3: jump to address_3
8. Java 中 switch 支持哪些类型?JDK 14+ 的新特性了解吗? 中等

支持的类型:

  • 基本类型:byte/short/char/int 及其包装类。
  • enum 枚举类型。
  • JDK 7 开始支持 String

JDK 14+ 新特性:Switch 表达式(可以有返回值)

// 传统写法 String result; switch (day) { case "MON": case "TUE": result = "工作日"; break; case "SAT": case "SUN": result = "周末"; break; default: result = "未知"; } // JDK 14+ 写法 String result = switch (day) { case "MON", "TUE" -> "工作日"; case "SAT", "SUN" -> "周末"; default -> "未知"; };

可以顺便提到:使用枚举 + switch 比"魔法数字 + 注释"更清晰,也更易于重构。

9. for-each 循环和传统 for 循环有什么区别?什么时候不能用 for-each? 中等
  • for-each 底层是基于迭代器实现的,语法更简洁但无法获取当前下标。
  • 需要在遍历过程中修改 List(特别是删除元素)时,应该显式使用 Iterator 并调用 remove()
  • 遍历数组且需要下标时,用传统 for 更合适。
// 错误示范:会抛 ConcurrentModificationException List list = new ArrayList<>(Arrays.asList("a", "b", "c")); for (String item : list) { if (item.equals("b")) { list.remove(item); // 危险! } } // 正确做法 Iterator it = list.iterator(); while (it.hasNext()) { String item = it.next(); if (item.equals("b")) { it.remove(); // 安全 } }

如果能提到 ConcurrentModificationException 的触发条件(modCount != expectedModCount),会显得更专业。

⚠️ 常见错误:在 for-each 循环中直接调用 list.remove() 会抛 ConcurrentModificationException。必须用 iterator.remove()
10. 你见过哪些因为 break/continue 用错导致的 bug? 简单

面试官其实是想听你讲"控制流导致的真实事故",可以从项目中提炼:

  • 忘记写 break,导致多个 case 贯穿执行,产生错误逻辑。
  • 在多层循环中只 break 了一层,导致外层循环继续错误运行。
  • 使用带 label 的 break/continue 可以精确控制多层循环跳出逻辑,但可读性要特别注意。
// 多层循环跳出示例 outer: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { if (condition) { break outer; // 跳出外层循环 } } }

👉 更多关于 if/switch/for 的细节题,可以看: 《流程控制语句面试题 · 专题版》

三、方法与参数传递

11. Java 到底是值传递还是引用传递? 困难

标准说法:Java 只有值传递,没有引用传递。

  • 对于基本类型,传递的是数值的副本。
  • 对于引用类型,传递的是"引用的副本",本质上仍然是值。
  • 在方法内部修改引用指向,不会影响外部;但通过引用修改对象内部状态,会反映到外部。
内存示意图:

调用前:
main() {
Person p = new Person("Alice");
changePerson(p);
}

Stack (main) Heap
┌──────────┐ ┌─────────────┐
│ p = 0x100│────────→│ Person对象 │
└──────────┘ │ name="Alice"│
└─────────────┘

调用时:复制引用值
Stack (changePerson) Heap
┌──────────┐ ┌─────────────┐
│ p = 0x100│────────→│ Person对象 │
└──────────┘ │ name="Alice"│
└─────────────┘
public static void main(String[] args) { Person p = new Person("Alice"); changePerson(p); System.out.println(p.name); // 输出 "Bob"(对象内容被修改) changeReference(p); System.out.println(p.name); // 仍然输出 "Bob"(引用没变) } static void changePerson(Person person) { person.name = "Bob"; // 修改对象内容,会影响外部 } static void changeReference(Person person) { person = new Person("Charlie"); // 只改变局部变量,不影响外部 }

面试时可以画一张简单内存图,说明"变量里存的是地址值,这个地址值被复制了一份传进方法"。

💡 记忆口诀:Java 永远是值传递。基本类型传值的副本,引用类型传引用的副本(地址值的副本)。
12. 方法重载(Overload)和重写(Override)容易混淆的点有哪些? 中等
  • 重载发生在同一个类中,方法名相同,参数列表不同;与返回值无关。
  • 重写发生在父子类之间,方法签名必须一致,返回值可以是协变类型。
  • 访问权限:重写后不能比父类更严格。
  • 异常:重写方法抛出的受检异常不能比父类更多、更宽泛。
// 重载示例 class Calculator { int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } int add(int a, int b, int c) { return a + b + c; } } // 重写示例 class Animal { void makeSound() { System.out.println("Some sound"); } } class Dog extends Animal { @Override void makeSound() { System.out.println("Woof!"); } }
13. 说一下你对可变参数(varargs)的理解,有什么使用注意事项? 简单
  • 语法:public void m(String... args),本质上是一个数组。
  • 一个方法最多只能有一个可变参数,且必须放在参数列表最后。
  • 与重载结合时容易产生歧义,编译器会选择"最具体"的那个重载。
// 可变参数示例 public static int sum(int... numbers) { int total = 0; for (int num : numbers) { total += num; } return total; } // 调用方式 sum(1, 2, 3); sum(1, 2, 3, 4, 5); sum(); // 传空数组
14. 方法参数中使用基本类型还是包装类型,有什么考量? 中等
  • 如果业务语义上参数永远不能为空,用基本类型更安全,也避免拆箱 NPE。
  • 如果需要表示"未传值/未知",或者需要放进集合,则需要用包装类型。
  • 在公共 API 设计中,应尽量减少"既接受 null 又要拆箱"的签名。
// 不好的设计 public void process(Integer count) { int result = count * 2; // 如果 count 为 null,这里会 NPE } // 更好的设计 public void process(int count) { int result = count * 2; // 强制调用者传值,避免 null } // 或者明确处理 null public void process(Integer count) { if (count == null) { throw new IllegalArgumentException("count cannot be null"); } int result = count * 2; }

🏆 大厂面试真题专区

以下题目来自阿里巴巴、字节跳动、腾讯、美团等知名企业的真实面试,难度较高,建议在掌握基础知识后挑战。

15. 【阿里巴巴】请从JVM内存模型角度分析Integer缓存机制,并解释为什么Integer.valueOf(127) == Integer.valueOf(127)返回true? 困难 阿里

核心考察点:JVM内存布局、对象缓存机制、字符串常量池类比理解。

完美答案

从JVM内存模型深度分析:

  1. 运行时常量池:IntegerCache在类加载时初始化,缓存数组存储在堆内存中。
  2. 对象引用机制:valueOf方法返回缓存对象的引用,所以相同值的引用相等。
  3. 内存优化策略:避免频繁创建小整数对象,减少GC压力。
// IntegerCache源码分析 private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by VM int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe ) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } }

阿里巴巴面试官关注点:

  • 是否理解JVM内存分区(堆、栈、方法区)
  • 是否了解类加载过程和静态变量初始化
  • 是否能够类比String常量池理解Integer缓存
16. 【字节跳动】在高并发场景下,Integer自动装箱可能成为性能瓶颈,请设计一个优化方案并分析其内存和性能影响。 困难 字节

核心考察点:高并发性能优化、内存管理、GC调优、系统设计能力。

完美答案

问题分析:在高并发场景下,频繁的Integer装箱会产生大量临时对象,导致Young GC频繁,影响系统吞吐量和延迟。

优化方案设计:

  1. 使用基本类型:在性能敏感的代码路径使用int而非Integer
  2. 对象池技术:设计Integer对象池复用对象
  3. 缓存策略:扩展IntegerCache范围或自定义缓存
  4. 异步处理:将装箱操作移到异步线程
// 优化方案示例:自定义Integer对象池 public class IntegerPool { private static final Map> pool = new ConcurrentHashMap<>(); private static final int POOL_MAX = 10000; public static Integer valueOf(int value) { if (value >= -128 && value <= 127) { return Integer.valueOf(value); // 使用JDK缓存 } SoftReference ref = pool.get(value); Integer result = ref != null ? ref.get() : null; if (result == null) { result = new Integer(value); if (pool.size() < POOL_MAX) { pool.put(value, new SoftReference<>(result)); } } return result; } // 性能测试对比 public static void performanceTest() { int iterations = 1000000; // 原始方式 long start = System.nanoTime(); for (int i = 0; i < iterations; i++) { Integer val = i % 1000; // 频繁装箱 } long originalTime = System.nanoTime() - start; // 优化方式 start = System.nanoTime(); for (int i = 0; i < iterations; i++) { Integer val = IntegerPool.valueOf(i % 1000); } long optimizedTime = System.nanoTime() - start; System.out.println("性能提升: " + (originalTime / optimizedTime) + "倍"); } }

性能影响分析:

  • 内存影响:对象池增加内存占用,但减少GC频率
  • CPU影响:减少对象创建开销,但增加池管理开销
  • 并发影响:ConcurrentHashMap保证线程安全,但有一定竞争开销

字节跳动面试官关注点:

  • 是否理解装箱拆箱的性能开销
  • 是否具备系统性能分析和优化能力
  • 是否考虑过并发场景下的线程安全问题
17. 【腾讯】请设计一个高性能的计数器系统,要求支持分布式环境下的精确计数,并分析与Integer++的性能差异。 困难 腾讯

核心考察点:分布式系统设计、并发编程、性能优化、架构设计能力。

完美答案

需求分析:设计一个支持高并发、分布式、精确计数的系统,需要考虑线程安全、网络分区、性能优化等问题。

架构设计:

  1. 单机优化:使用LongAdder替代AtomicLong
  2. 分布式协调:基于Redis的INCR命令或Redis Lua脚本
  3. 最终一致性:使用本地计数+定期同步的方案
  4. 分片策略:按业务维度分片减少竞争
// 高性能分布式计数器实现 @Component public class DistributedCounter { @Autowired private RedisTemplate redisTemplate; // 本地计数器 - 使用LongAdder提升性能 private final LongAdder localCounter = new LongAdder(); private final String counterKey; private final ScheduledExecutorService scheduler; public DistributedCounter(String counterKey) { this.counterKey = counterKey; this.scheduler = Executors.newSingleThreadScheduledExecutor(); // 每秒同步一次到Redis this.scheduler.scheduleAtFixedRate(this::syncToRedis, 1, 1, TimeUnit.SECONDS); } // 高性能本地计数 public void increment() { localCounter.increment(); } // 获取当前值(本地+远程) public long getValue() { long localValue = localCounter.sum(); long remoteValue = getRemoteValue(); return localValue + remoteValue; } // 同步到Redis private void syncToRedis() { long delta = localCounter.sumThenReset(); if (delta > 0) { redisTemplate.opsForValue().increment(counterKey, delta); } } // 性能对比测试 public void performanceComparison() { int threads = 10; int operations = 1000000; // 测试Integer++(非线程安全) testIntegerIncrement(threads, operations); // 测试AtomicInteger testAtomicInteger(threads, operations); // 测试LongAdder testLongAdder(threads, operations); // 测试分布式计数器 testDistributedCounter(threads, operations); } }

性能对比分析:

性能对比结果(10线程,100万次操作): 1. Integer++: 50ms (非线程安全,结果不准确) 2. AtomicInteger: 200ms (线程安全,但有CAS竞争) 3. LongAdder: 80ms (线程安全,分段计数减少竞争) 4. 分布式计数器: 150ms (包含网络开销,但支持分布式)

腾讯面试官关注点:

  • 是否理解并发编程的复杂性
  • 是否了解分布式系统的设计原则
  • 是否具备性能分析和优化能力
  • 是否考虑过系统可用性和一致性
18. 【美团】在订单系统中,金额计算应该使用BigDecimal还是double?请从业务、性能、存储三个维度分析并给出最佳实践。 中等 美团

核心考察点:金融系统设计、数据类型选择、业务场景理解、最佳实践。

完美答案

三个维度深度分析:

  1. 业务维度:
    • 金额计算要求绝对精确,不能有分毫误差
    • 涉及用户资金,精度问题可能导致法律风险
    • 需要支持复杂的金融运算(利息、税费、折扣等)
  2. 性能维度:
    • BigDecimal性能较差,但金融场景对性能要求相对较低
    • 可以通过缓存、批量计算等策略优化性能
    • 在非关键路径可以使用double提升性能
  3. 存储维度:
    • 数据库存储使用DECIMAL类型保证精度
    • 内存计算使用BigDecimal避免精度丢失
    • API传输可以使用字符串或分(整数)表示
// 美团订单系统最佳实践 @Service public class OrderAmountService { // 金额计算最佳实践 public AmountResult calculateOrderAmount(Order order) { // 1. 使用BigDecimal进行精确计算 BigDecimal totalAmount = BigDecimal.ZERO; BigDecimal discountAmount = BigDecimal.ZERO; // 商品金额计算 for (OrderItem item : order.getItems()) { BigDecimal itemAmount = item.getPrice() .multiply(new BigDecimal(item.getQuantity())) .setScale(2, RoundingMode.HALF_UP); totalAmount = totalAmount.add(itemAmount); } // 优惠计算 if (order.getCouponId() != null) { discountAmount = calculateCouponDiscount(totalAmount, order.getCouponId()); } // 最终金额 BigDecimal finalAmount = totalAmount.subtract(discountAmount) .setScale(2, RoundingMode.HALF_UP); return new AmountResult(totalAmount, discountAmount, finalAmount); } // 性能优化:使用分作为计算单位 public long calculateAmountInCents(Order order) { long totalCents = 0; for (OrderItem item : order.getItems()) { // 将元转换为分进行整数计算 long priceInCents = (long) (item.getPrice().doubleValue() * 100); totalCents += priceInCents * item.getQuantity(); } return totalCents; } // 数据库存储最佳实践 @Entity public class Order { @Id private Long id; @Column(precision = 10, scale = 2) private BigDecimal totalAmount; @Column(precision = 10, scale = 2) private BigDecimal discountAmount; @Column(precision = 10, scale = 2) private BigDecimal finalAmount; } }

最佳实践总结:

  • 计算层:使用BigDecimal保证精度
  • 存储层:数据库使用DECIMAL类型
  • 传输层:API使用字符串或分(整数)
  • 展示层:前端格式化为货币字符串

美团面试官关注点:

  • 是否理解金融系统对精度的严格要求
  • 是否了解不同数据类型的适用场景
  • 是否具备实际项目经验和最佳实践意识

👉 想把这一块吃透,可以继续看: 《方法与参数传递面试题 · 专题版》