ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java开发十年,我整理了一份避坑清单

Java开发十年,我整理了一份避坑清单 十年Java我踩过的坑比写过的代码还多。回头想想真正让我长记性的从来不是某个惊艳的设计模式而是一个个看似不起眼的基础问题。刚入行时我只求代码能跑工作几年后我开始害怕“能跑”这两个字——因为能跑和没病之间隔着无数个线上事故。大多数线上故障都藏在开发者以为“理所当然”的代码里。集合越常用越危险ArrayList和LinkedList之争网上一搜一大片现实里依然有人选错。默认使用LinkedList的代码在随机访问场景下性能差到让人怀疑人生。更不用说循环里随手调用list.remove(i)索引变化会让你错过一半元素。这个错误太常见许多人写了好几年都没意识到。没有数据结构认知的所谓优化都是拆东墙补西墙。subList是另一个不被认真对待的陷阱。它返回的不是新列表而是原列表的视图。修改subList的值会直接反映到原列表对subList做add或remove还可能抛出ConcurrentModificationException。我看过不止一个同事因为subList的“视图”特性清空数据失败后只能靠重启缓存解决。Java类库并不复杂复杂的是人们总爱脑补它的行为。HashMap更是重灾区。默认加载因子0.75很多人背得滚瓜烂熟但要问为什么是0.75能答上来的不多。这背后是时间与空间的折中如果你能预判数据量却不指定初始容量HashMap就会多次resize白白消耗性能。Java 7并发下的resize还能形成环形链表让CPU飙到100%。小细节里藏着大事故放在HashMap身上再合适不过。自定义对象当key也容易爆雷。hashCode和equals必须同时覆盖算是常识但真正让人崩溃的是对象在放入HashMap之后被修改了hashCode导致这个key永远取不出来。所有诡异bug最后都会指向你对基本约定的漠视。并发别让线程池变成怪兽并发编程是Java重灾区。很多人以为给变量加上volatile就万事大吉却不知道volatile只保证可见性不保证原子性。i这种操作十个线程并发就能演示什么叫意外。AtomicInteger虽然能保证单步原子可你要是先判断再扣减check和act之间没有锁照样超卖。并发问题的本质是竞态线程数量只是背锅侠。线程池的坑更隐蔽。newFixedThreadPool用无界队列承接任务请求洪峰一到队列无限增长最终OOM。newCachedThreadPool不限制线程数量高并发下能创建数万个线程把系统拖垮。我见过一个支付回调服务就是被突发流量这么打死的。永远不要用Executors的快捷方法创建核心线程池。手动new ThreadPoolExecutor并设置有限队列和拒绝策略才是能抗事的做法。子线程里的异常也不会自动抛给主线程定时任务静默失败往往就是没人捕获Future的异常所致。JVM调优先看日志再改参数JVM调优是个被神化的领域。网上铺天盖地谈G1、讲ZGC可真到排查时很多人连GC日志都不会打。有次系统Full GC频率高得吓人所有人都围着堆内存参数打转最后dump堆才发现代码里每次请求都往静态Map塞临时对象形成事实上的内存泄漏。调优之前不分析堆你改的每个参数都在掷骰子。类加载器引发的问题更隐蔽。框架为了支持热部署不断生成新类如果老引用没清理元空间会慢慢耗尽线上Metaspace OOM往往很难定位只能靠重启拖延。经历一次你就会明白动态派生的能力越强运维的噩梦也越深。所以我对无脑热部署一直充满警惕。时间SimpleDateFormat的经典翻车时间处理是Java另一串陈年老雷。很多人喜欢把SimpleDateFormat定义成static字段多线程调用format低并发时没事一旦并发上来轻则拿到错乱的时间重则直接抛NumberFormatException。格式化时间看着简单翻车却最无声无息。如果还在维护老代码遇到这种问题先怀疑并发共享的格式化器新代码请直接使用线程安全的DateTimeFormatter。日期区间判断也常出问题。查某一天订单时如果只拿日期字符串做等值比较很容易漏掉带时分秒的数据。正确做法是用时间范围大于等于当天零点且小于次日零点。你以为数据库存的是日期实际上它存了一个完整的时间点。这些边界问题往往不会在联调时暴露直到业务人员拿着Excel来质问你才发现自己错得有多冤。Spring Boot自动化越强越要懂它的底牌Spring Boot让开发变舒服却也惯坏了一大批人。有次一个公共配置类放在启动类扫描包外面整个团队Bean全部找不到排查了一整天。组件扫描再方便也逃不开basePackage这个约定。自动化越强你越需要知道它内部那根线连到哪里。条件注解同样令人头疼。ConditionalOnProperty里的属性名拼错一个字符配置不会生效但Spring不会告诉你。ConfigurationProperties缺少setter时绑定会失败很多字段悄悄用着默认值。你看到的日志风平浪静实际跑的配置却差了十万八千里。自动化配置最可怕的地方不是它错了而是它错了也一声不吭。所以关键配置最好在启动时显式打印出来让“没生效”无处躲藏。事务越普通越容易失效Spring事务堪称静默失效重灾区。同一个类里两个方法互相调用被调用方法标了Transactional事务却不生效——因为Spring事务基于代理内部调用绕过了代理对象。批量导入场景里一个事务方法每插一条提交一次数据量一大就一半成一半败想回滚都没辙。你不是给方法加了注解你是给逻辑加了一层幻觉。回滚边界也常被误解。Spring默认只对RuntimeException和Error回滚像IOException这种受检异常不会自动触发回滚。再加上很多人习惯catch住异常再返回false事务更是无从谈起。事务失效往往静默无声等发现数据脏了你可能已经重新上线三次。要让事务稳定可靠就得让异常按预期向外部抛出别把失败吞进业务逻辑里。数据库慢SQL与分页的纠缠数据库最典型的坑就是深分页。offset一变大MySQL要扫描并丢弃大量记录limit 100000,20可能比limit 20,1000慢上几十倍。简单加索引并不能解决因为索引只能帮你找到起点后面十万行还是要扫。分页翻得越深数据库越痛苦。解决办法通常是记住上一页最大ID或用覆盖索引延迟关联。慢SQL的另一个来源是隐式类型转换。varchar字段配数字条件MySQL会先把字段转成数字再比较索引直接失效。字符集不一致的join更麻烦就算两边都有索引也没法高效关联。所以我要求团队上线前对核心SQL执行EXPLAIN因为优化器已经用结果告诉你慢SQL不是凭空出现的是开发者亲手写出来的。架构分库分表不是银弹数据量刚到百万就有人急着分库分表。可很多系统的瓶颈根本不在数据量。一张表配合合适的索引、读写分离和缓存扛到千万级很常见。引入分布式后跨库join消失分布式事务需求出现数据迁移的坑一个接一个。没有完美的架构只有愿意承担的代价。在拆分之前先优化分页、缓存热点、清洗慢SQL别拿架构升级掩盖业务代码的懒。依赖与重构技术债迟早要还依赖管理是另一片雷区。jar包冲突导致NoSuchMethodError许多Java工程师都遇过。有时只是引入一个公共组件背地里带了三十个传递依赖版本一碰撞全乱套。两个日志实现同时存在日志会随机走一个线上错误连影子都看不到。解决依赖冲突没有捷径只有mvn dependency:tree。每次引入新依赖都要像审查代码一样审查依赖树。工具类也容易成为毒瘤。团队每人一套DateUtil和StringUtil久而久之变成谁也不敢改、谁也说不清的野代码。最贵的代码就是那些没人敢动又没人能解释的辅助类。与其各写各的不如统一工具库或者把公共方法沉淀到基础组件里。重构也一样老系统没有单元测试想动结构心里都发慌。没有测试的重构等于在高空走钢丝。哪怕先给核心业务流程补几个集成测试再开始动手也比裸奔要强得多。十年之后再看这份清单它其实不是什么高深秘笈而是一堆失败经验的汇总。我见过无数工程师在代码里横冲直撞也见过优秀系统因为一个小坑而崩塌。技术在不断更新但“想当然”这三个字永远致命。真正可靠的代码永远来自对失败案例的尊重。希望这份清单能帮你在踩坑之前先看见坑。如果不小心踩了那就拍掉身上的土把坑也标记在地图上留给后来的人。
返回列表