Skip to content

Java 基础概述与常见误区 ​

本模块面试重点 ​

  • 语言特点与运行机制:面向对象、一次编写到处运行、JVM/字节码/JIT 的基本概念,会画“编译 -> 类加载 -> 执行”的大致流程图。
  • 值比较 vs 引用比较:== 和 equals() 的区别、为什么重写 equals 一定要重写 hashCode、常见坑(如 Integer 缓存)。
  • 常见基础坑点:String 不可变、装箱/拆箱、基本类型默认值、final/static 的常见考法。

❓ 面试官:Java 语言有哪些特点?它为什么能“一次编写,到处运行”? ​

频率:🔥🔥🔥

💡 一句话总结(先抛结论): Java 最核心的特点是面向对象和跨平台(平台无关性)。它能跨平台是因为 Java 代码被编译成了平台无关的字节码(.class 文件),然后由不同操作系统上不同版本的 JVM(Java 虚拟机) 负责将其解释或编译成机器码执行。

📝 详细原理解析:

  1. 编译与运行过程:
    • HelloWorld.java -> javac 编译器 -> HelloWorld.class(字节码)。
    • JVM 读取 .class 文件,通过解释器逐行解释执行,或者通过 JIT(即时编译器)将热点代码编译成本地机器码执行。
  2. 核心特点归纳:
    • 面向对象:封装、继承、多态。
    • 自动内存管理:自带垃圾回收器(GC),开发者不需要像 C/C++ 那样手动 free/delete 释放内存,极大减少了内存泄漏和指针越界的风险。
    • 多线程支持:语言层面内置了多线程 API。

🌟 面试加分项(底层进阶): "其实现在的 JVM(如 HotSpot)并不是纯粹的『解释执行』。为了提高速度,它引入了 JIT(Just-In-Time)编译器。当某段代码(比如一个循环体或方法)被频繁调用时,JIT 会把它直接编译成操作系统底层的机器码并缓存起来。下次再运行这段代码,速度就和 C++ 差不多了。所以我们常说 Java 是一门半编译、半解释型的语言。"


❓ 面试官:能说说 == 和 equals() 的区别吗?为什么重写 equals() 一定要重写 hashCode()? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论):

  • == 比较的是物理内存地址(对于基本类型是比值,对于引用类型是比内存地址)。
  • equals() 默认行为和 == 一样,但在很多类(如 String)中被重写了,用来比较业务上的内容(值)是否相等。
  • 重写 hashCode 规则:因为哈希表(如 HashMap)在存储对象时,先看 hashCode 判断桶位置,再用 equals 判断是否是同一个对象。如果不重写,两个值相等的对象可能会被放到不同的桶里,导致哈希表逻辑崩溃。

📝 详细原理解析与常见坑点:

  1. == 的坑:Integer 缓存池
    java
    Integer a = 100, b = 100;
    System.out.println(a == b); // true (走的是 -128~127 的缓存池)
    
    Integer c = 200, d = 200;
    System.out.println(c == d); // false (超出了缓存池范围,创建了两个不同的对象)
  2. 为什么重写 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 是可变的,没有加锁,线程不安全但在单线程下性能最高。

📝 详细原理解析:

  1. String 为什么不可变?
    • 在 JDK 8 之前,String 内部是一个 final char[] value;JDK 9 之后为了省内存改成了 final byte[] value。因为加了 final 关键字且类本身也不可被继承,所以一旦创建,内容永远无法修改。
  2. 场景选择:
    • 极少修改:直接用 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 包起来当缩头乌龟。