Java 开发者必看!避开十大致命陷阱的实战指南
专业在线打字练习软件-巧手打字通,只输出有价值的知识。
前言
本次巧手打字通课堂
将和您一起聊一聊,java 程序员日常开发过程中最容易踩坑的 10 大致命陷阱。
1 资源泄漏风险
在 Java 中,对文件或任何需要手动关闭的资源(如数据库连接、文件输入/输出流等)进行操作时,确实需要确保资源在使用完毕后被正确释放,以防止资源泄露。
需要手动释放的资源及链接时,要在 finally 中进行了释放,防止抛出异常,导致资源释放的逻辑没有走到,导致资源泄漏;
Java 7 引入了 try-with-resources 语句,这是一个自动管理资源的非常便利的特性,它确保每个资源在语句结束时都会自动关闭,无论是正常结束还是由于异常而结束。
具体实现是对于实现 AutoCloseable 接口的类的实例,将其放到 try 后面,在 try 结束的时候,会自动将这些资源关闭(调用 close 方法)。
2 空指针异常(NPE)问题
在程序抛出的所有异常里面,空指针异常应该是最常见的一类了。具体示例如下:
任何 NPE 问题,都由使用者来保证。原因是在开发过程中,很难约束上游严格按照各种约定执行。
3 死循环、递归调用风险
java 中的方法指令运行栈上,栈的大小一般在 1-2M 左右(取决于操作系统和 JVM)。如果方法执行出现了死循环或者递归函数没有出口,不管栈空间多大,栈溢出是必然的。
以上示例代码就会抛出:Exception in thread "main" java.lang.StackOverflowError。递归层数比较多的场景下,建议使用循环替代递归。
4 数值越界风险
首先,使用
Integer
时要知道,虽然-128 到 127 之间的值因为IntegerCache
的缓存机制会复用对象,可以用==
来比较它们是否相等,但超出这个范围后,每次都会在堆上创建新的Integer
实例,这时就不能再用==
来比较了。为了避免出现潜在的逻辑错误,无论在什么情况下,还是建议统一使用equals
方法来进行数值比较。其次,设计业务系统的主键生成策略时,特别是选择
int
类型作为主键时,得仔细考虑其最大值(Integer.MAX_VALUE
)的限制。你得确保业务增长不会碰到这个天花板,否则数据可能会溢出,或者出现类型不匹配的问题。再者,数据库设计与 Java 对象之间的映射也是个需要注意的点。如果数据库中的
id
字段是bigint unsigned
类型,但 Java 类里对应的属性却是Integer
类型,那么随着数据越来越多,id
的值有可能会超过Integer
能表示的范围,变成负数。这听起来就很糟糕,对吧?所以,要确保 Java 类中的属性类型能和数据库字段类型相匹配,比如用Long
类型来安全地存储大范围的整数。最后,在电商或金融类系统中,订单价格的计算绝对是个重头戏。如果计算逻辑出错,或者精度处理不当,价格可能会变成负数,这不仅会让用户感到困惑和不满,还可能引发严重的商业问题。因此,在开发这类系统时,一定要非常仔细地检查和测试价格计算的逻辑和精度处理,确保无论什么情况下都能得到准确无误的结果。
5 数值精度丢失风险
当我们直接使用 double 或 float 这样的浮点类型参数进行数值计算时,有时会遇到计算结果不够精确的问题,这主要是因为计算机内部是基于二进制系统工作的,而并非所有的浮点数都能被二进制完美地精确表示。这种表示上的局限性,就导致了在计算过程中可能会出现精度丢失的情况。简单来说,就是计算机在处理这些浮点数时,有时候无法完全准确地存储或计算它们,从而导致了结果的不精确。
这在计算订单金额,描述产品重量等关键业务场景是不能够接受的。在大多数的商业计算中,一般采用 java.math.BigDecimal 类来进行精确计算。
在使用 BigDecimal 时,建议使用 BigDecimal(String var) 的 String 参数构造方法来构建对象。不要用**BigDecimal(double var)**的 double 参数的构造方法。这是因为 0.1 无法准确地表示为 double 类型。
6 对象拷贝导致的风险
在业务实现的过程中,对象的拷贝或转换是很常见的应用场景。对象拷贝实现方式有两种,一种是通过原生支持的 Cloneable 接口实现,另一种是自定义实现。
对于实现了 Cloneable 接口的方案,以下几个陷阱:
这种方式并不是通常所理解的深拷贝,而是浅拷贝;
浅拷贝的一个主要问题是新的对象和原对象共享实例变量的值。原对象对共享实例变量对象的引用被修改了,那么新对象也会受到影响;
对于自定义实现对象的拷贝,需要注意一下几点:
序列化(Serialization):将对象序列化到一个字节流中,然后再从字节流中反序列化出新的对象。陷阱是必须有可序列化的接口(Serializable),而且如果对象中包含不可序列化的成员变量,那么在序列化过程中就可能抛出异常;
非序列化对象的深拷贝,需要自己实现深拷贝。如果对象之间存在循环引用,那么在进行深拷贝时,可能会出现无限递归的问题;
除此之外,对象拷贝还需要考虑性能问题,深拷贝操作可能会非常消耗性能,特别是在处理大型对象时。因此在需要频繁进行深拷贝的场景下,可能需要使用其他方式进行处理。
7 编码格式导致的风险
相信吗?编码习惯和风格的不统一往往也会导致 bug。最典型的就是逻辑执行语句后面是否可以省略大括号,看下面示例:
与示例代码中类似的还有 for,switch default 语句,都有编码风格导致程序逻辑出现问题的情况。最好都使用大括号进行作用域的圈定。
8 集合容器并发与性能风险
如果多个线程同时修改同一个集合或映射,可能会导致数据的不一致性,出现数据丢失或者不可预知的行为。所以,多个线程操作同一个 Collcetion 或 Map 时务必要保证线程安全!
比如在有并发场景下的使用的对象容器,可以选择实现了线程安全的 ConcurrentHashMap,CopyOnWriteArrayList 等。
另外,也要关注容器的实现原理,以方便我们进行性能优化,看下面例子。
arrayList.removeAll(set)的速度远高于 arrayList.removeAll(list)。
这是因为前者的删除,使用了 HashSet 的 contain 方法,复杂度是 O(1),后者使用的是循环遍历,复杂度是 O(n),在要删除的数据比较多的情况下,性能差距就会变得非常明显。知道了它们之间的差距了,就可以写出更好的实现了,可以将 subList 封装为 HashSet:arrayList.removeAll(new HashSet(subList))。
同样的道理,在进行 HashMap 的创建时,为什么建议指定 initialCapacity 大小呢?这是因为我们往容器里添加元素时,数组的大小会自动增长。数组的大小每次增长都导致内存重新分配和复制,那么就会带来性能的开销。预先合理设置 HashMap 的大小,就可以避免这种开销。
9 集合容器操作异常风险
对于 Arrays.asList,Collections.EMPTY_LIST 等方法,不要在有调整数组对象的场景下使用。举例:
不能在遍历 Collection,Map 容器的过程中,对容器内的元素进行新增或删除操作。
10 ThreadLocal 使用风险
1. 内存泄漏风险
ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。
使用 static 的 ThreadLocal,延长了 ThreadLocal 的生命周期,可能导致的内存泄漏。
分配使用了 ThreadLocal 又不再调用 get(),set(),remove()方法,那么就会导致内存泄漏。
避免内存泄露解决方案:ThreadLocal 要在接口结束时,通过 finally 进行 remove 操作,可以防止内存泄漏;
2. ThreadLocal 跨线程传递问题
ThreadLocal 的子类 InheritableThreadLocal 可以实现父子线程之间的数据传递,但它仅适用于 new Thread 手动创建线程的时候!实际上,日常更多的是使用线程池,线程池里面的线程都预创建好的,就没法直接用 InheritableThreadLocal 了。
如何往线程池内的线程传递 ThreadLocal?
JDK 的类库没提供这个功能,可以自己实现或者使用第三方库 TransmittableThreadLocal 实现跨线程参数传递。
总结
本文探讨了软件开发中常见的十大风险,涵盖了从资源管理到并发编程的多个方面。
强调了资源泄漏风险,提醒开发者需妥善管理资源以避免内存泄露等问题。
空指针异常(NPE)作为常见错误之一,要求编程时进行充分的空值检查。
还指出了死循环和递归调用可能导致的性能瓶颈甚至程序崩溃风险。
数值处理方面,数值越界和精度丢失风险不容忽视,需根据具体场景选择合适的数据类型。
对象拷贝时需注意浅拷贝与深拷贝的区别,避免数据共享引发的问题。
编码格式不一致也可能带来业务逻辑的歧义和错误,统一代码发风格还是很有必要的。
集合容器的并发访问和不当操作则可能导致数据不一致或性能下降。
ThreadLocal 的滥用也可能导致内存泄漏等风险,需谨慎使用。
最后,希望本文对读者有所启发和帮助。
版权声明: 本文为 InfoQ 作者【巧手打字通】的原创文章。
原文链接:【http://xie.infoq.cn/article/e4e46bb0772f968fdd354e4b6】。文章转载请联系作者。
评论