java十大常见对象

老刘

Java十大常见对象:写了三年代码才真正搞明白的那些"老熟人"

你要是问我,Java学了这么久,哪些对象是真正天天在用、又最容易踩坑的——我能给你列一串。不是那种教科书式的"常用类概述",而是我在实际项目里被它们反复折磨之后,总结出来的血泪经验。


先说句掏心窝子的话

很多人学Java,上来就背API文档,背完就忘。说白了,不是你记性差,是没在真实场景里被"打过"。等你写了一两年代码回头看,发现翻来覆去打交道的,就那么几个对象。搞懂它们,比背一百页文档管用。

下面这十个,是我根据实际开发经验排的。排名不分先后,但每一个都值得你认真琢磨。


一、String —— 最熟悉的陌生人

几乎每个Java程序的第一行代码里都有它。"Hello World" 那个Hello World,就是个String对象。

java十大常见对象

但你真的了解它吗?

String是不可变对象(immutable),这四个字看着简单,背后坑多得要命。我刚入行那会儿,有个需求是拼接上万条SQL日志,我直接用 String+ 号拼接:

String sql = "";
for (int i = 0; i < 10000; i++) {
    sql += "item" + i + ", ";
}

代码跑完,我盯着内存监控看了半天——GC疯狂回收,程序卡得像幻灯片。后来才知道,每次 += 都会生成一个新的String对象,前面那些全变成垃圾了。

正确姿势: 涉及循环拼接,老老实实用 StringBuilder。这不是高级技巧,这是基本功。

还有一个冷知识:String有常量池。String a = "abc"String b = "abc" 指向的是同一个对象,但 new String("abc") 是另一个。面试考了八百遍了,可到了实际开发,还是会有人踩这个坑。


二、Integer —— 自动装箱的甜蜜陷阱

Integer这个对象,最有意思的地方不在它本身,而在Java的自动装箱和拆箱机制。

Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false

第一次看到这段代码输出的人,表情大概都是"???"

原因很简单:Integer缓存池默认只缓存 -128 到 127 之间的值。在这个范围内,== 比较的是引用(因为指向同一个缓存对象);超出范围,就是两个不同的对象了。

这个坑在项目里出过事故。有一次团队里有个同事用 == 判断两个Integer是否相等,测试环境数据量小,从来没出过问题。上线之后,用户ID超过128的部分全崩了。排查了一下午才找到原因。

记住一句话:Integer比较永远用 equals(),别用 == 这不是代码洁癖,这是血的教训。


三、ArrayList —— 你以为你很懂它

ArrayList大概是你在项目里用得最多的集合类,没有之一。

但很多人对它的理解停留在"能自动扩容的数组"这个层面。说实话,这也够用了,至少80%的场景没问题。可一旦涉及多线程,问题就来了。

ArrayList不是线程安全的。多个线程同时往里add,轻则数据丢失,重则直接抛 ConcurrentModificationException

我见过一个经典bug:一个定时任务在往ArrayList里塞数据,另一个接口在遍历它读数据,每隔几天就报一次ConcurrentModificationException,而且每次复现条件都不一样。最后排查下来,就是因为两个线程共用了同一个ArrayList实例。

解决办法看你场景:

  • 并发不高?用 Collections.synchronizedList()
  • 并发高?上 CopyOnWriteArrayList
  • 先收集再处理?stream().collect(Collectors.toList()) 一把梭

四、HashMap —— 八股文之王,但真有东西

HashMap面试考烂了,但它确实值得多聊几句。

先说个很多人忽略的点:HashMap的key可以是null,但ConcurrentHashMap不行。这个区别在实际开发中很容易忘记。

再来说说底层结构。JDK8之后,HashMap在链表长度超过8且数组长度达到64的时候,会自动转成红黑树。这是为了防止哈希冲突严重时退化成O(n)查询。

不过说实话,绝大多数业务场景下,你遇到的HashMap冲突概率低到几乎可以忽略。真到了需要考虑这个级别的时候,你大概率已经在用Redis或者其他分布式缓存了。

但有两个实际问题值得注意:

第一,初始容量的设置。 默认初始容量是16,负载因子是0.75。如果你明确知道要放多少数据,初始化时就指定容量,能减少不必要的扩容。别小看这个,高并发下频繁rehash会吃掉不少CPU。

第二,HashMap遍历时的删除问题。 经典中的经典——遍历时删除元素,用for-each直接删,必抛异常。得用迭代器的 remove(),或者用Java8的 removeIf()


五、LocalDateTime —— 日期处理的正确答案

Date和Calendar这两个老家伙,我强烈建议你忘掉它们。不是说不能用,而是有更好的选择。

LocalDateTimeLocalDateLocalTime(Java 8引入的时间API)才是现代Java日期处理的正道。

为什么Date不好用?

Date date = new Date();
date.setMonth(date.getMonth() + 1); // 可变!

跟String一样,时间对象也应该追求不可变性。Date是可变的,而且它的月份从0开始(0代表一月),这个设计简直是反人类。

LocalDateTime的好处太多了:

  • 不可变,线程安全
  • 方法命名直观,plusDays()withMonth()
  • DurationPeriod 配合,算时间间隔特别方便

唯一要小心的是时区问题LocalDateTime 不带时区信息,如果你的应用需要处理跨时区数据,记得用 ZonedDateTime

我在之前一个项目里就吃过这个亏:国内服务和海外服务之间传时间,大家都用LocalDateTime序列化,结果因为时区差异,时间对不上。排查下来发现,服务端在东八区,客户端在美国西海岸,一个用系统默认时区,一个没设时区——整整差了8个小时。


六、BigDecimal —— 涉及钱的都归它管

如果你在做电商、金融、支付相关的开发,BigDecimal就是你的命根子。

为什么不能用double算钱?

double result = 0.1 + 0.2;
System.out.println(result); // 0.30000000000000004

计算机用二进制表示小数,天然存在精度丢失。0.1在二进制里是无限循环小数,就像你没法用十分之一的标准精确表示1/3一样。日常计算这点误差无所谓,但涉及金额——差一分钱都是事故。

BigDecimal用起来也有讲究:

// 别这样,用double构造会继承精度问题
BigDecimal bad = new BigDecimal(0.1);

// 要这样,传字符串
BigDecimal good = new BigDecimal("0.1");

还有除法:divide() 如果除不尽,不指定精度和舍入模式,直接抛 ArithmeticException。我见过不止一个新人被这个异常打蒙的。

BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
// 千万别这样
// a.divide(b);  // ArithmeticException!

// 要这样
a.divide(b, 2, RoundingMode.HALF_UP); // 3.33

七、File —— 文件操作的起点与终点

File这个类,说实话有点"古老"了。它能创建文件、删除文件、检查文件是否存在,但读写文件本身它干不了。

很多人搞混这一点。File只是文件系统的一个抽象表示,代表某个路径,至于这个路径上有没有东西、东西是什么,它管不着。真正要读写,得靠 FileInputStreamFileOutputStream,或者Java NIO的 Files 工具类。

Java 7之后,Files 提供了一堆实用方法,比以前方便太多:

// 一行代码读完一个文件
String content = Files.readString(Path.of("config.txt"));

还有个经常被忽略的问题:文件路径的跨平台兼容。Windows用反斜杠 \,Linux/Mac用正斜杠 /。如果你在代码里硬编码路径分隔符,换台机器可能就挂了。用 File.separator 或者 Path.of(),让它自己处理。


八、Exception —— 最怕它,又离不开它

Exception家族在Java里是个庞大的体系。实际开发中,你最常打交道的大概是这几类:

NullPointerException——Java世界的头号杀手。虽然现代IDE已经很智能了,会提示可能的空指针,但真到了运行时,该炸还是炸。

ConcurrentModificationException——上面聊ArrayList的时候提过。多线程下集合操作的噩梦。

ClassCastException——类型转换失败。泛型用得好,能少遇到很多次。

不过我想说的是,异常处理这块,很多人的习惯其实是错的。

try {
    // 一堆业务逻辑
} catch (Exception e) {
    e.printStackTrace();
}

e.printStackTrace() 在生产环境是大忌。它只输出到标准错误流,如果用日志框架部署的服务,你可能根本看不到这个输出。而且它吞掉了异常的上下文信息,排查问题时非常痛苦。

正确做法:用日志框架记录,带上必要的上下文。如果需要抛出异常,用自定义异常类型,并附带有意义的错误码和消息。

另外,try-with-resources 这个语法从Java 7就有了,用它来处理 InputStreamConnection 这类需要关闭的资源,比在finally块里手动close优雅太多。


九、Thread —— 看似简单,水深得很

Thread这个对象,说它是Java并发的入口一点不夸张。但老实说,实际项目里直接 new Thread() 的场景越来越少了。

为什么?因为手动管理线程太容易出错了。线程池(ExecutorService)才是正解。线程池的 submit()execute() 帮你管理了线程的生命周期,避免了频繁创建销毁线程的开销。

但Thread本身的几个概念你得清楚:

线程状态: NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED。不是说start了就立刻运行,还得等操作系统调度。

中断机制: Thread.interrupt() 不是强制停止线程,而是设置一个中断标志。线程自己决定怎么响应这个中断。很多人以为调了interrupt线程就会停,结果写了死循环的线程永远不会停——因为没有代码去检查中断状态。

我之前接手过一个项目,有个后台线程在跑一个定时任务,服务要停的时候关不掉,一直卡在那里。查了半天发现,那个线程在一个while(true)里死循环,既没检查 Thread.currentThread().isInterrupted(),也没设超时机制。最后只能 kill -9 强杀。

所以现在的最佳实践是:尽量用 CompletableFutureExecutorServiceScheduledExecutorService 这些高级抽象,别直接玩Thread。除非你在写底层框架。


十、BigDecimal的室友——System.out.println

我知道这个不算"对象",但它太有代表性了,必须提一嘴。

刚学Java的人,调试方式就是疯狂打印。代码里到处是 System.out.println(),跑一遍控制台刷刷刷输出一大堆。

这不丢人,大家都这么干过。但等你开始写真正的项目,会发现这种方式有几个致命问题:

  • 控制台输出有限,日志多了根本看不过来
  • 没有级别区分(debug/info/warn/error)
  • 没有时间戳、线程信息
  • 不能按级别过滤
  • 生产环境根本看不到控制台

所以,尽早学会用日志框架。SLF4J + Logback 是目前Java世界最主流的日志方案,几行配置就能搞定。我带过的新人,第一个要求就是:代码里不允许出现 System.out.println()


写在最后

Java的常用对象远不止这十个,但搞懂了上面这些,你的Java基本功至少能上一个台阶。

说到底,编程这事儿,看文档看教程是输入,写代码踩坑是输出。光看不练,永远停留在"我好像懂了"的阶段。

上面提到的那些坑——String拼接性能、Integer缓存陷阱、HashMap遍历删除、BigDecimal精度问题——每一个都是真实项目里出过事的。记住它们最好的方式不是背下来,而是亲手踩一次。

当然,最好是别踩。所以这篇文章,就当是提前帮你排雷了。

行了,去写代码吧。

文章版权声明:文章内容均来源于各大短视频平台搜集以及修改和删减新增,如有侵权或者违规,请联系站长进行删除,如需转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
AddoilApplauseBadlaughBombCoffeeFabulousFacepalmFecesFrownHeyhaInsidiousKeepFightingNoProbPigHeadShockedSinistersmileSlapSocialSweatTolaughWatermelonWittyWowYeahYellowdog
验证码
评论列表 (暂无评论,5人围观)

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

目录[+]