文章目录
哈喽各位Javaer!今天咱们来聊聊让无数新人"闻风丧胆"的Java基础面试题(别慌,看完这篇你就能横着走了)。作为面过200+候选人的面试官,我发现很多同学明明技术不错,却总在基础题上翻车,简直亏到姥姥家!(相信我,这篇绝对能帮你省下至少3个月的试错时间)
一、对象生死簿:JVM内存管理必考题
1.1 对象创建的三重奏(高频!)
你以为new一个对象就完事了?Too young!完整的对象创建流程其实是:
- 类加载检查(没加载?赶紧加载!)
- 分配内存(TLAB了解一下?)
- 初始化零值(所有字段归零)
- 设置对象头(年龄、哈希码…)
- 执行方法(这才到构造方法)
举个栗子🌰:
// 你以为的new:
User user = new User();
// 实际发生的步骤:
// 1. 检查User.class是否加载
// 2. 在Eden区划出内存空间
// 3. 所有字段设为默认值(int→0,String→null)
// 4. 对象头写入元数据
// 5. 执行构造方法里的初始化操作
1.2 GC Roots到底是个啥?(面试官最爱!)
记住这四个绝对不能回收的"钉子户":
- 虚拟机栈中的引用(正在执行的方法)
- 本地方法栈的JNI引用
- 方法区静态属性
- 方法区常量引用
比如这个死亡案例💀:
public class OOMDemo {
static class Inner {
byte[] data = new byte[1024*1024];
}
public static void main(String[] args) {
while(true) {
new Inner(); // 疯狂创建匿名对象,没有GC Roots引用!
}
}
}
// 匿名对象没有引用指向,很快就会被回收
二、面向对象三大特征:90%的人答不全!
2.1 多态的实现原理(字节码层面!)
你以为只是重写方法?看字节码才见真章:
public class Animal {
public void eat() {
System.out.println("动物吃饭");
}
}
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
// 反编译后的invokevirtual指令:
// invokevirtual #7 <Animal.eat>
关键点在于:
- 编译时看左边(声明类型)
- 运行时看右边(实际类型)
- 方法表机制实现动态绑定
2.2 封装的最佳实践(阿里规范)
别再用public裸奔字段了!正确姿势:
public class User {
private String name;
private int age;
// 建造者模式YYDS!
public static class Builder {
private String name;
private int age;
public Builder name(String name) {
this.name = name;
return this;
}
public User build() {
return new User(this);
}
}
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
}
}
三、集合框架:HashMap夺命连环问
3.1 JDK1.8的七大优化(必须掌握!)
- 数组+链表+红黑树(阈值8/6)
- 尾插法代替头插法(解决死链)
- 扩容时保持顺序
- 计算hash更高效
- 树化阈值调整
- 新增方法:computeIfAbsent
- 并发修改检测机制
3.2 并发场景下的死亡陷阱
测试下你的水平:
HashMap<String, String> map = new HashMap<>();
// 线程A
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, "value");
}
}).start();
// 线程B
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.get("key" + i);
}
}).start();
这个代码会怎样?
A. 正常运行
B. 偶尔报错
C. 必然死循环
D. 数据丢失
正确答案是C!在JDK1.7中使用头插法扩容时可能产生环形链表,导致get操作死循环。虽然1.8改用尾插法解决了这个问题,但依然不是线程安全的!(使用ConcurrentHashMap才是正道)
四、异常处理:被低估的扣分重灾区
4.1 异常处理七大准则(阿里开发手册)
- 禁用printStackTrace()(日志框架不香吗?)
- 不要用异常做流程控制(性能杀手!)
- catch区分稳定/非稳定代码
- 事务方法内catch异常要回滚
- finally块不要写return
- 注意异常丢失问题(addSuppressed())
- 自定义异常要继承RuntimeException
4.2 异常链的正确玩法
错误示范❌:
try {
// code
} catch (IOException e) {
throw new MyException("出错了");
}
正确姿势✅:
try {
// code
} catch (IOException e) {
throw new MyException("文件操作失败", e); // 保留原始异常
}
五、反射机制:框架的灵魂拷问
5.1 反射性能优化三板斧
- 缓存Class对象(别每次都forName)
- 设置setAccessible(true)(跳过安全检查)
- 使用MethodHandle(JDK7+)
性能对比测试:
操作方式 | 执行100万次耗时 |
---|---|
直接调用 | 15ms |
普通反射 | 850ms |
关闭安全检查 | 350ms |
MethodHandle | 180ms |
5.2 破坏单例的反射攻击
看代码说话:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
// 攻击代码:
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = constructor.newInstance();
Singleton instance2 = constructor.newInstance();
System.out.println(instance1 == instance2); // false!
防御方案:
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("别想用反射搞事情!");
}
}
六、新特性:Java8的三大神器
6.1 Stream性能陷阱(实测数据)
处理100万条数据:
操作 | 耗时 |
---|---|
for循环 | 15ms |
并行流 | 8ms |
串行流 | 35ms |
结论:小数据用循环,大数据用并行流,中间状态用串行流!
6.2 Optional的正确打开方式
错误用法❌:
User user = ...;
Optional<User> opt = Optional.of(user);
if(opt.isPresent()) {
System.out.println(opt.get().getName());
}
正确姿势✅:
Optional<User> opt = ...;
opt.map(User::getName)
.filter(name -> name.length() > 2)
.orElse("默认名字");
七、终极防坑指南(血泪总结)
根据我整理的面试数据,这些是最高频的翻车点:
- ==和equals的区别(80%答不全)
- String的不可变性(65%理解错误)
- 重写equals不重写hashCode(90%会犯)
- ArrayList自动扩容机制(70%说不清)
- volatile的可见性≠原子性(95%混淆)
- 线程池参数设置(85%不会计算)
- 动态代理实现方式(75%只知道JDK代理)
建议各位把这些知识点做成思维导图,每天起床看一遍(别问我怎么知道的,当年我就是这么上岸的)!
写在最后
看完这篇是不是觉得Java基础突然清晰了?(嘿嘿,要的就是这个效果)记住,面试不是背题,而是要理解原理+能说人话+会写代码。下次面试官再问HashMap,你就把红黑树转换阈值、哈希扰动函数、负载因子这些细节甩他脸上,保准让他眼前一亮!
最后送大家一句话:基础不牢,地动山摇。把这篇吃透了,至少能帮你避开初级程序员80%的坑!(别光收藏,赶紧动手写代码验证啊!!!)