Java 基础概述与常见误区
本模块面试重点
- 语言特点与运行机制:面向对象、一次编写到处运行、JVM/字节码/JIT 的基本概念,会画“编译 -> 类加载 -> 执行”的大致流程图。
- 值比较 vs 引用比较:
==和equals()的区别、为什么重写equals一定要重写hashCode、常见坑(如Integer缓存)。 - 常见基础坑点:
String不可变、装箱/拆箱、基本类型默认值、final/static的常见考法。
❓ 面试官:Java 语言有哪些特点?它为什么能“一次编写,到处运行”?
频率:🔥🔥🔥
💡 一句话总结(先抛结论): Java 最核心的特点是面向对象和跨平台(平台无关性)。它能跨平台是因为 Java 代码被编译成了平台无关的字节码(.class 文件),然后由不同操作系统上不同版本的 JVM(Java 虚拟机) 负责将其解释或编译成机器码执行。
📝 详细原理解析:
- 编译与运行过程:
HelloWorld.java->javac编译器 ->HelloWorld.class(字节码)。- JVM 读取
.class文件,通过解释器逐行解释执行,或者通过 JIT(即时编译器)将热点代码编译成本地机器码执行。
- 核心特点归纳:
- 面向对象:封装、继承、多态。
- 自动内存管理:自带垃圾回收器(GC),开发者不需要像 C/C++ 那样手动
free/delete释放内存,极大减少了内存泄漏和指针越界的风险。 - 多线程支持:语言层面内置了多线程 API。
🌟 面试加分项(底层进阶): "其实现在的 JVM(如 HotSpot)并不是纯粹的『解释执行』。为了提高速度,它引入了 JIT(Just-In-Time)编译器。当某段代码(比如一个循环体或方法)被频繁调用时,JIT 会把它直接编译成操作系统底层的机器码并缓存起来。下次再运行这段代码,速度就和 C++ 差不多了。所以我们常说 Java 是一门半编译、半解释型的语言。"
❓ 面试官:能说说 == 和 equals() 的区别吗?为什么重写 equals() 一定要重写 hashCode()?
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论):
==比较的是物理内存地址(对于基本类型是比值,对于引用类型是比内存地址)。equals()默认行为和==一样,但在很多类(如String)中被重写了,用来比较业务上的内容(值)是否相等。- 重写 hashCode 规则:因为哈希表(如
HashMap)在存储对象时,先看hashCode判断桶位置,再用equals判断是否是同一个对象。如果不重写,两个值相等的对象可能会被放到不同的桶里,导致哈希表逻辑崩溃。
📝 详细原理解析与常见坑点:
==的坑:Integer 缓存池javaInteger a = 100, b = 100; System.out.println(a == b); // true (走的是 -128~127 的缓存池) Integer c = 200, d = 200; System.out.println(c == d); // false (超出了缓存池范围,创建了两个不同的对象)- 为什么重写 equals 必须重写 hashCode?
- 假设你有一个
User(id=1, name="张三")对象。你重写了equals:只要id相同就认为是同一个人。 - 你没有重写
hashCode(默认依然返回内存地址的哈希值)。 - 你把
User A存入HashMap。然后你拿着完全一样的User B(id=1, name="张三")去map.get(B)。 HashMap第一步先算B的hashCode,发现跟A不一样,直接去别的桶里找,结果返回 null!- 结论:逻辑上相等的对象,必须拥有相同的 hashCode。
- 假设你有一个
❓ 面试官:String、StringBuffer 和 StringBuilder 的区别是什么?【中级】
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论):
String是不可变的,每次修改都会产生新对象,极其浪费内存。StringBuffer是可变的,且方法都加了synchronized锁,线程安全但性能差。StringBuilder是可变的,没有加锁,线程不安全但在单线程下性能最高。
📝 详细原理解析:
- String 为什么不可变?
- 在 JDK 8 之前,String 内部是一个
final char[] value;JDK 9 之后为了省内存改成了final byte[] value。因为加了final关键字且类本身也不可被继承,所以一旦创建,内容永远无法修改。
- 在 JDK 8 之前,String 内部是一个
- 场景选择:
- 极少修改:直接用
String(自带字符串常量池优化)。 - 单线程大量拼接:毫无疑问选
StringBuilder(日常业务代码最常用)。 - 多线程并发拼接:选
StringBuffer(实际上极少遇到多线程共享一个字符串去拼接的业务场景)。
- 极少修改:直接用
🌟 面试加分项(实战底层): 面试官常追问:"我在代码里写了 String s = "a" + "b" + "c"; 性能会很差吗?" 绝杀回答: "不会。因为 Java 编译器非常聪明,在编译期(javac 阶段)就会进行常量折叠优化,直接把它编译成 String s = "abc";,根本不会在运行期产生中间对象。 甚至如果是 String s = s1 + s2;(变量拼接),在 JDK 8 编译器底层也会自动帮你转换成 StringBuilder.append() 来优化性能,但在循环体内的字符串拼接,绝不能依赖编译器优化,必须手动写 StringBuilder,否则会在循环里创建无数个 StringBuilder 对象导致内存爆炸。"
❓ 面试官:Java 里的异常体系是怎么样的?Error 和 Exception 有什么区别?
频率:🔥🔥🔥
💡 一句话总结(先抛结论): 它们的老祖宗都是 Throwable。
Error是系统级灾难错误(比如OutOfMemoryError内存溢出、StackOverflowError栈溢出),程序根本无法处理,只能等死。Exception是程序级异常,是我们代码可以通过try-catch捕获并抢救的。它又分为“受检异常(必须显式 catch,如IOException)”和“运行时异常(不强制 catch,如NullPointerException)”。
📝 详细原理解析:
- 受检异常(Checked Exception):比如读写文件时,编译器强制要求你写
try-catch,否则连编译都过不了。 - 运行时异常(RuntimeException):也就是我们常说的 Bug(如空指针、数组越界)。编译器不强求你 catch,通常应该通过优化代码逻辑(如提前判空)来避免,而不是用 catch 包起来当缩头乌龟。