5道Java基础面试题带你从入门到精通,面试不再哑火
面试被问原理答不上来,这种尴尬谁懂?很多开发者背了无数八股文,结果面试官换个问法就卡壳。今天不聊虚的,直接拆解Java基础中最高频的5个痛点,带你从入门到精通,把原理吃透,面试稳拿offer。
一、概念速懂:为什么String是不可变的?
很多新手觉得String immutable(不可变)是个设计缺陷,其实这是Java为性能和线程安全做的权衡。
核心逻辑:
- 字符串常量池优化:JVM在编译阶段会将字面量放入字符串常量池。如果String可变,
a = "hello"; b = a; b += "world";会导致a也变成"helloworld",破坏引用语义。 - 线程安全:不可变对象天然线程安全,无需同步。
- hashCode缓存:String的hashCode被缓存,因为内容不变,计算一次即可,提升HashMap性能。
代码演示:
public class StringImmutableDemo {public static void main(String[] args) {String s1 = "Hello";String s2 = "Hello";String s3 = new String("Hello");// s1和s2指向常量池同一对象System.out.println(s1 == s2); // true// s3是新对象,地址不同System.out.println(s1 == s3); // false// 内容相同,equals为trueSystem.out.println(s1.equals(s3)); // true// 尝试修改,实际创建了新对象s1 = s1 + " World";System.out.println(s1); // Hello World}
}
面试加分点: 提到String常量池在JDK1.7后从永久代移到堆中,说明你关注JVM变化。
二、环境准备:JDK版本差异的坑
很多面试官喜欢问JDK版本差异,尤其是JDK8之后。
必知差异:
- JDK8:Lambda表达式、Stream API、默认方法
- JDK11:局部变量类型推断(var)、HTTP Client
- JDK17:Sealed类、Records、强封装
代码演示:
import java.util.List;
import java.util.stream.Collectors;public class JdkVersionDemo {public static void main(String[] args) {// JDK8 Stream APIList<String> names = List.of("Alice", "Bob", "Charlie");List<String> upperNames = names.stream().map(String::toUpperCase).collect(Collectors.toList());System.out.println(upperNames);// JDK11 局部变量类型推断var list = List.of(1, 2, 3);var map = List.of("a", "b");System.out.println(list);System.out.println(map);// JDK17 Records(简化POJO)record Person(String name, int age) {}var p = new Person("Alice", 25);System.out.println(p.name());System.out.println(p.age());}
}
避坑提示: 企业项目常用JDK8或11,面试前确认目标公司技术栈,避免用错版本特性。
三、核心语法:多线程与并发陷阱
并发是Java面试重灾区,尤其是ThreadLocal和线程池。
核心概念:
- ThreadLocal:线程隔离数据,避免共享变量竞争
- 线程池:复用线程,避免频繁创建销毁
- volatile:保证可见性,不保证原子性
代码演示:
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class ConcurrencyDemo {private static final ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));public static void main(String[] args) {// 线程池示例ExecutorService executor = Executors.newFixedThreadPool(3);AtomicInteger counter = new AtomicInteger(0);for (int i = 0; i < 10; i++) {executor.submit(() -> {// 使用ThreadLocal获取线程私有对象SimpleDateFormat sdf = threadLocal.get();try {Thread.sleep(100);counter.incrementAndGet();System.out.println(Thread.currentThread().getName() + " 处理: " + sdf.format(new java.util.Date()));} catch (InterruptedException e) {Thread.currentThread().interrupt();}});}executor.shutdown();try {executor.awaitTermination(5, TimeUnit.SECONDS);} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("总处理数: " + counter.get());}
}
面试高频问题:
- ThreadLocal内存泄漏如何避免?→ 手动remove()
- 线程池参数如何设置?→ CPU密集型:N+1;IO密集型:2N
- volatile能否替代synchronized?→ 不能,volatile不保证原子性
四、完整代码示例:JVM内存模型实战
JVM内存模型是基础中的基础,理解堆、栈、方法区的关系至关重要。
核心区域:
- 堆:对象实例,GC主要区域
- 栈:局部变量、方法调用栈
- 方法区:类元数据、常量池
- 程序计数器:字节码行号,线程私有
代码演示:
import java.util.ArrayList;
import java.util.List;public class JvmMemoryDemo {// 静态变量,存储在方法区(JDK8后元空间)private static List<String> staticList = new ArrayList<>();public static void main(String[] args) {// 局部变量,存储在栈帧int localVar = 100;String str = "Hello JVM";// 对象实例,存储在堆JvmMemoryDemo obj = new JvmMemoryDemo();obj.instanceList.add("instance");// 静态变量,存储在方法区staticList.add("static");// 触发GC观察内存变化for (int i = 0; i < 1000000; i++) {new Object(); // 快速创建对象,触发GC}System.gc(); // 建议GC(不保证立即执行)System.out.println("堆内存使用: " + Runtime.getRuntime().totalMemory() / 1024 / 1024 + "MB");System.out.println("可用内存: " + Runtime.getRuntime().freeMemory() / 1024 / 1024 + "MB");}// 实例变量,存储在对象头+实例数据中private List<String> instanceList = new ArrayList<>();
}
面试技巧: 结合JVisualVM或Arthas工具分析内存泄漏,展示实战能力。
五、常见报错:OOM与ClassNotFound
生产环境最常见的两个问题,面试必问。
OOM(OutOfMemoryError):
- 堆溢出:大量对象无法回收
- 排查:
jmap -dump+ MAT分析 - 解决:增加堆内存、修复内存泄漏
- 排查:
- 元空间溢出:类加载过多
- 排查:检查动态代理、反射
- 解决:增加Metaspace大小
ClassNotFound/NoClassDefFound:
- 依赖缺失:jar包未引入
- 类加载冲突:多个jar包含相同类
- 初始化失败:静态块抛出异常
代码演示:
import java.lang.reflect.*;public class ErrorDemo {public static void main(String[] args) {// 模拟ClassNotFoundtry {Class<?> clazz = Class.forName("com.example.MissingClass");} catch (ClassNotFoundException e) {System.out.println("类未找到: " + e.getMessage());}// 模拟NoClassDefFoundError(静态初始化失败)try {Class<?> clazz = Class.forName("com.example.BrokenInit");clazz.newInstance();} catch (Exception e) {System.out.println("初始化失败: " + e.getMessage());}// 模拟OOMtry {List<byte[]> list = new ArrayList<>();while (true) {list.add(new byte[1024 * 1024]); // 1MB}} catch (OutOfMemoryError e) {System.out.println("堆溢出: " + e.getMessage());}}
}// 模拟静态初始化失败
class BrokenInit {static {throw new RuntimeException("静态块异常");}
}
避坑建议: 使用Maven/Gradle严格管理依赖,CI/CD中集成内存监控。
六、小结:从入门到精通的路径
Java基础面试不是背八股,而是理解设计哲学。
学习建议:
- 源码阅读:从String、ArrayList、HashMap入手
- JVM调优:学习GC日志分析,理解不同收集器适用场景
- 并发编程:掌握JMM、线程池、锁机制
- 实战项目:结合GitHub开源仓库(如Spring Boot、Netty)深入理解
关键资源:
- GitHub仓库:
awesome-java汇总优质Java项目 - 官方文档:OpenJDK规范、Java SE API文档
- 工具:JVisualVM、Arthas、JConsole
面试被问原理答不上来,本质是只知其然不知其所以然。把每个知识点背后的"为什么"想清楚,从入门到精通只是时间问题。
这个知识点你面试被问过吗?留言说说