啃透这十个Java老骨头,才算真的入门了
说实话,我当年学Java那会儿,最怕的不是新知识学不会,而是那些翻来覆去被问的“经典问题”——每次面试前都得背一遍,跟应付考试似的。但真等自己开始带新人、做项目了才发现,这些“老骨头”能传这么多年,不是没有道理的。
它们就像Java江湖里的基本功,你绕不过去。今天咱不搞那些教科书式的罗列(那玩意儿网上随便一搜一堆),我就用我这几年踩过的坑、面过的人,跟你聊聊这十个问题到底为啥重要,以及怎么在实际里用明白。
1. “==”和equals(),到底差在哪儿?
这问题老掉牙了吧?但上个月我司一个实习生,就因为没搞清这个,在用户比对时闹了个大笑话——明明两个用户ID一样,死活登录不进去。

说白了: “==”看的是“身份证号”(内存地址),equals()看的是“长相”(内容)。
但坑就坑在,很多新手以为equals()天生就是比内容的。不是的!你得自己写(或者用人家写好的)。Java里默认的equals(),其实干的也是“==”的活儿。
我举个最生活的例子:你和你双胞胎兄弟,身份证号(==)肯定不一样,但要是你妈用“长得像不像”(equals())来判断,那在她眼里,你俩就是“相等”的。
所以,记住第一条: 用String、Integer这些包装类时,放心用equals(),人家已经帮你写好了;但要是你自己写了个User类,想比两个用户是不是同一个人,记得重写equals()方法,不然比出来的结果能让你怀疑人生。
(对了,重写equals()的时候,记得顺手把hashCode()也重写了,不然HashMap、HashSet这些家伙又要给你使绊子。这是另一个故事了,回头单聊。)
2. String为啥是不可变的?
我第一次听说“String不可变”时,第一反应是:这不有病吗?我改个字符串还得new个新的,多浪费内存。
后来被现实毒打了。你想啊,Java里字符串用得多频繁啊,要是能随便改,那得多乱套?
- 安全: 你的数据库密码、连接信息要是能被随便改,吓人不?
- 性能: 因为不可变,所以JVM才敢放心地搞“字符串常量池”。你写一千遍
String s = "hello",内存里其实就一个"hello"。这省了多少地方! - 线程安全: 不用加锁就能到处传,多省心。
说白了,这就是一种设计上的“牺牲”——牺牲一点修改的灵活性,换来安全、性能和稳定。值。
3. ArrayList和LinkedList,到底该选谁?
这俩简直是面试官的最爱。标准答案谁都会背:“ArrayList查得快,增删慢;LinkedList增删快,查得慢。”
但现实是,我工作这么多年,95%的情况用的都是ArrayList。
为啥?因为现在程序里,“查”的操作远远多于“增删”。你想想,你从数据库拉一列表数据展示,是不是主要就是遍历、点开看?真需要频繁在列表中间插来插去的场景,少。
而且,LinkedList那个“增删快”,是有条件的。你得先“查”到那个位置,才能删啊!这个查找过程,本身就是O(n)的遍历。所以,除非你真要在已知节点的前后疯狂插入删除(比如实现个LRU缓存),否则,无脑选ArrayList,基本不会错。
(别被那些八股文带偏了,实践出真知。)
4. 多线程:SYNchronized和Lock,怎么选?
这问题能吵一架。我刚学的时候,觉得Lock又新又酷,功能还多(能尝试锁、能公平锁),肯定完爆老旧的synchronized。
其实吧, 在绝大多数普通业务场景下,synchronized就够了。它写法简单,不容易出错(锁自动释放),而且JDK后来一直在优化它,性能早就不差了。你搞个电商秒杀,锁个库存,用synchronized完全hold住。
那Lock啥时候用?当你需要那些“高级功能”的时候。比如:
- 你想尝试着去锁一下,锁不到就算了(
tryLock()),别傻等。 - 你想让线程按申请锁的顺序来(公平锁),防止饿死。
- 你得在代码里灵活控制加锁解锁,一个锁里套多个条件(
Condition)。
所以,选型原则很简单: 先用synchronized,等它真的满足不了你了,再考虑Lock。别为了“高级”而“高级”,把简单问题复杂化。
5. HashMap的死循环,是真的吗?
真的。但这事儿得说清楚,这是在JDK 1.7及之前,在多线程环境下扩容时可能发生的。1.8之后,数据结构从“头插法”改成了“尾插法”,基本杜绝了这个恐怖的bug。
但为啥我还要提它?因为这是一个绝佳的教训:HashMap在设计上就不是线程安全的。你别看它用着爽,在高并发场景下往同一个HashMap里乱写,就算不死循环,也会导致数据错乱、丢失。
所以,记住这条铁律:只要有多线程可能操作同一个Map,就用ConcurrentHashMap。别抱侥幸心理。我见过太多人为了省事,觉得“我这儿并发不高”,结果线上爆出诡异问题,查到头秃。
6. 接口和抽象类,傻傻分不清?
这可能是最让人纠结的设计问题之一。书上说的那些“区别”(单继承、多实现,有没有状态)你都懂,但用的时候还是懵。
我的经验是,想两个问题:
- 你想不想写默认实现? 如果想提供一些所有子类都该有的通用方法(哪怕是个空壳),用抽象类。如果只是定个规矩,大家爱怎么实现怎么实现,用接口。
- 你未来的扩展方向是什么? 如果是一类有“血缘关系”的东西(比如各种“车”),用抽象类打基础。如果是给不同类别的对象,赋予同一种“能力”(比如“会飞”、“能序列化”),用接口。
举个不恰当的例子:抽象类像你爸,给你留了套房(具体属性)和一份家训(部分实现);接口像你的驾照,只证明你有“开车”这个能力,至于你开奔驰还是开五菱宏光,我不管。
7. 垃圾回收(GC),到底在干吗?
别被那些“标记-清除”、“复制算法”、“G1/ZGC”给吓住了。你就把它想象成你妈收拾你房间:
- 有些东西(对象)你明确说不要了(
obj = null),她下次打扫直接扔。 - 大部分东西,是她看你很久没动了,觉得你没用了,就给你收走了(这就是GC的“可达性分析”)。
- 她收拾的频率和方式(GC算法),决定了你房间(程序)是偶尔卡一下大扫除(Full GC),还是每天都随手整理一点(Minor GC)。
对我们程序员来说,核心就两点:
- 别自己瞎“帮”GC: 比如动不动就
System.gc(),这相当于强行把你妈拽来打扫,很可能打乱她的节奏。 - 警惕“内存泄漏”: 就是你以为不要了,但其实还被别人偷偷指着的东西。比如往全局的静态Map里不停地放数据,又不清理。这就像你把废纸都塞床底下,你妈看不见,以为房间很干净,最后床底炸了。
8. 异常处理:catch了然后呢?
这是我见过最普遍的坏习惯:catch (Exception e) { e.printStackTrace(); },然后就当没事发生了。
这比不处理还可怕! 因为程序表面上还在跑,但其实已经处于一个未知的错误状态了,天知道后面会出什么妖蛾子。
正确的态度是:把异常当成你程序的一部分逻辑来处理。
- 如果是预料之中的错误(比如用户输入格式不对),给个友好提示,让流程继续。
- 如果是严重的、不可恢复的错误(比如数据库连不上了),该抛就往上抛,或者转换成业务异常,让最外层统一处理(比如告诉用户“系统开小差了”)。
- 最忌讳的就是“吞掉”异常,当哑巴。
printStackTrace()只在调试时有用,线上日志请用日志框架,记录好错误信息和上下文。
9. 反射,这“后门”该不该用?
反射(Reflection)功能强大,能让你在运行时“为所欲为”:调私有方法、改私有字段。框架(Spring、MyBatis)全靠它吃饭。
但对我们日常开发来说,能不用,尽量不用。原因就一个:它破坏了封装,而且有性能开销。代码可读性会变差,编译器也帮不了你做检查,所有错误都得等到运行时才爆出来。
那什么时候用呢?当你写一些通用框架、工具,或者处理一些极度灵活的配置时(比如根据配置文件里的类名动态创建对象)。记住,它是“药”,能治病,但也有副作用,别当饭吃。
10. 为什么Java要用“双亲委派”加载类?
这可能是最“学院派”的一个问题,但理解它,能帮你避开很多诡异的ClassNotFound和类冲突问题。
想象一下,你公司(JVM)有个规矩:员工(类)要找个工具(加载类),必须先问自己的直属领导(应用类加载器),领导再问部门总监(扩展类加载器),总监再问公司大BOSS(启动类加载器)。BOSS说“我这有”,就用BOSS的;BOSS说“我这没有”,才一层层往下派。
好处太明显了:
- 安全: 防止你随便写个
java.lang.String就把核心类库给替换了。 - 效率: 避免重复加载,领导那有了,你就别费劲了。
- 清晰: 保证了Java核心库的优先级最高。
你平时可能感觉不到它,但如果你自己玩过类加载器,或者遇到过不同Jar包里有同名类打架的情况,就知道这个机制多重要了。它保证了Java世界最基本的秩序。
行了,一口气聊了这么多,其实就想说,这些“经典问题”不是用来背的,每一个背后都是Java设计者踩过的坑、权衡过的选择。把它们啃透了,你写出的代码会更稳,遇到问题也更能直击要害。
别再死记硬背了,找个小项目,亲手写写,改改,跑跑,体会一下。编程这事儿,手感比答案重要得多。
去吧,写代码去。遇到新坑,咱再聊。

还没有评论,来说两句吧...