Java基础常见面试题总结(上)
背诵重点
上篇只保留 Java 基础里真正高频、容易追问底层的问题。像注释类型、关键字清单、Java ME、标识符规则、break/continue/return 这类偏语法手册的问题,面试价值很低,不再展开。
建议按这条线背:
- Java 为什么跨平台:JVM、字节码、JIT。
- 包装类:默认值、缓存池、装箱拆箱、空指针。
- 数值计算:浮点精度和
BigDecimal。 - 参数传递和方法:值传递、重载、重写、
static、final。
JVM、JDK、JRE 有什么区别?
JVM 是 Java 虚拟机,负责运行字节码。不同平台有不同 JVM 实现,因此同一份 .class 字节码可以在不同操作系统上运行。
JRE 是 Java 运行环境,包含 JVM 和 Java 基础类库,用于运行 Java 程序。
JDK 是 Java 开发工具包,包含 JRE,还包含 javac、javadoc、jdb、jconsole、javap 等开发和调试工具。
面试补充:
- JDK 9 后引入模块化,可以用
jlink裁剪运行时。 - JDK 11 后 Oracle 不再单独提供传统 JRE 下载。
- 开发和编译 Java 程序需要 JDK,只运行程序通常只需要运行时环境。
什么是字节码?为什么 Java 能跨平台?
字节码是 JVM 能识别的中间指令,通常保存在 .class 文件中。它不面向某个具体 CPU,而是面向 JVM。
Java 跨平台靠的是“字节码 + 不同平台的 JVM 实现”。源码经过 javac 编译成字节码,字节码再交给不同系统上的 JVM 执行。
一句话背诵:Java 不是源码跨平台,而是字节码跨平台,JVM 屏蔽了操作系统差异。
为什么说 Java 是编译与解释并存?
Java 源码会先通过 javac 编译成字节码,这是编译阶段。
程序运行时,JVM 会解释执行字节码;对于频繁执行的热点代码,JIT 会把它编译成本地机器码并缓存起来,提高后续执行效率。
所以 Java 既有编译过程,也有运行时解释和 JIT 编译优化。
面试追问:
- JIT:运行时根据热点代码动态优化,适合长期运行服务。
- AOT:提前编译成本地代码,启动更快,但运行时动态优化空间更小。
Java 和 C++ 有什么区别?
Java 更偏托管语言,运行在 JVM 上,有 GC、反射、跨平台能力和成熟生态;C++ 更接近底层,直接管理内存,性能控制更细,但复杂度更高。
常见对比:
- Java 不支持指针直接操作,C++ 支持指针。
- Java 类是单继承、多实现,C++ 支持多继承。
- Java 有自动垃圾回收,C++ 需要开发者管理对象生命周期。
- Java 不支持运算符重载和头文件,C++ 支持。
- Java 程序通过 JVM 运行,C++ 通常编译成本地机器码运行。
基本类型和包装类型有什么区别?
基本类型保存具体值,包装类型是对象引用。
主要区别:
- 默认值不同:成员变量里
int默认0,Integer默认null。 - 存储方式不同:基本类型直接存值,包装类型存对象引用。
- 比较方式不同:基本类型用
==比值,包装类型应该用equals()比值。 - 泛型限制不同:泛型只能使用引用类型,所以只能写
List<Integer>,不能写List<int>。
为什么需要包装类?
- 集合和泛型需要对象。
- 可以表达
null。 - 可以调用对象方法,比如类型转换、比较、解析。
- 反射和框架处理类型时更方便。
自动装箱和拆箱是什么?
自动装箱是基本类型自动转包装类型;自动拆箱是包装类型自动转基本类型。
Integer a = 100; // 编译后类似 Integer.valueOf(100)
int b = a; // 编译后类似 a.intValue()必须背住两个坑:
- 包装对象为
null时自动拆箱会抛NullPointerException。 - 频繁装箱会创建对象或触发缓存查找,可能带来性能和内存开销。
实际开发中,集合里拿 Integer 做计算前要注意判空。
自动装箱和自动拆箱的底层原理是什么?
自动装箱、自动拆箱本质是 Java 编译器提供的语法糖,源码里看起来是自动转换,编译后的字节码仍然会调用包装类的方法。
常见转换如下:
int装箱成Integer:调用Integer.valueOf(int)。Integer拆箱成int:调用Integer.intValue()。long装箱成Long:调用Long.valueOf(long)。Long拆箱成long:调用Long.longValue()。
Integer a = 10;
int b = a;大致等价于:
Integer a = Integer.valueOf(10);
int b = a.intValue();面试追问点:
Integer.valueOf()会优先使用缓存,默认缓存-128到127,所以小整数装箱不一定创建新对象。new Integer(10)会强制创建新对象,不会走缓存,不推荐使用。- 包装类型参与算术运算时通常会先拆箱,再计算;如果包装对象是
null,拆箱时直接抛NullPointerException。 - 包装类型比较数值要用
equals(),不要用==,因为==比的是引用地址,缓存命中时容易造成误判。
Integer 缓存池机制是什么?
Integer.valueOf() 默认缓存 -128 到 127 之间的整数对象。
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false原因是 127 命中缓存,128 默认没有命中缓存,通常会创建新对象。
注意:
==比较包装类型时比较的是引用地址。- 比较包装类型数值应该使用
equals()。 Byte、Short、Long、Character也有类似缓存机制,只是范围不同。
浮点数为什么会有精度问题?
浮点数采用二进制表示,很多十进制小数无法被二进制精确表示,比如 0.1。计算时只能保存近似值,所以可能出现精度误差。
System.out.println(0.1 + 0.2); // 0.30000000000000004所以浮点数适合科学计算、统计估算,不适合金额、库存结算这类需要精确计算的场景。
金额计算为什么用 BigDecimal?
BigDecimal 可以表示任意精度的十进制数,适合金额计算。
使用原则:
- 推荐用字符串构造:
new BigDecimal("0.1")。 - 不推荐用
new BigDecimal(0.1),因为传入前double已经有误差。 - 除法要指定精度和舍入模式,避免无限循环小数抛异常。
- 比较数值是否相等用
compareTo(),不要直接用equals()。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.equals(b)); // false,值和精度都比较
System.out.println(a.compareTo(b) == 0); // true,只比较数值大小Java 是值传递还是引用传递?
Java 只有值传递。
基本类型传递的是值的副本。引用类型传递的是引用地址的副本,方法内部可以通过这个地址副本修改对象内容,但不能改变调用方引用变量的指向。
void change(User user) {
user.setName("new"); // 调用方对象内容会变
user = new User(); // 调用方引用不会改指向
}一句话背诵:Java 传对象时传的是对象地址的副本,不是引用本身。
static 关键字怎么理解?
static 表示属于类,而不是属于某个对象。
常见用法:
- 静态变量:类级共享数据。
- 静态方法:无需创建对象即可调用。
- 静态代码块:类加载时执行一次。
- 静态内部类:不依赖外部类实例。
静态方法不能直接访问非静态成员,因为静态方法调用时可能还没有具体对象。
方法重载和方法重写有什么区别?
重载发生在同一个类中,方法名相同,参数列表不同,和返回值无关,属于编译期多态。
重写发生在父子类之间,子类改写父类方法,方法名、参数列表要相同,返回值类型要兼容,访问权限不能更小,抛出的异常不能更宽,属于运行期多态。
记忆点:
- 重载看参数。
- 重写看继承和运行时对象。
private、static、final方法不能被真正重写。- 构造方法不能被重写。
可变长参数有什么注意点?
可变长参数本质是数组。
void log(String... messages) {
System.out.println(messages.length);
}注意:
- 一个方法最多只能有一个可变长参数。
- 可变长参数必须放在参数列表最后。
- 方法重载时,可变长参数可能导致调用匹配不直观,公共 API 中要谨慎使用。
final、finally、finalize 有什么区别?
final 是关键字:
- 修饰类:不能被继承。
- 修饰方法:不能被重写。
- 修饰变量:只能赋值一次。
finally 是异常处理结构中的代码块,通常用于释放资源。
finalize() 是 Object 的方法,GC 前可能调用,不可靠,已经不推荐使用。
上篇口述模板
回答基础题时按这个顺序:
- 先给结论。
- 再讲底层机制。
- 补一个常见坑。
- 最后说实际开发建议。
比如问“自动装箱拆箱”:
可以答:自动装箱是基本类型转包装类型,底层常用 valueOf();拆箱是包装类型转基本类型,底层调用 xxxValue()。坑点是包装对象为 null 时拆箱会抛空指针,Integer 在 -128 到 127 有缓存,所以包装类型数值比较不要用 ==,应该用 equals()。
