专业在线打字练习软件-巧手打字通,只输出有价值的知识。
前言
本次巧手打字通课堂
将和您一起聊一聊,java程序员日常开发过程中最容易踩坑的10大致命陷阱。
1 资源泄漏风险
在Java中,对文件或任何需要手动关闭的资源(如数据库连接、文件输入/输出流等)进行操作时,确实需要确保资源在使用完毕后被正确释放,以防止资源泄露。
- 需要手动释放的资源及链接时,要在finally中进行了释放,防止抛出异常,导致资源释放的逻辑没有走到,导致资源泄漏;
try{
//io操作:InputStream,Socket等
//out.flush();
}catch(XxxException e){
//异常处理
throw e;
}finally{
if(in != null){
in.close();
}
}
- Java 7 引入了 try-with-resources 语句,这是一个自动管理资源的非常便利的特性,它确保每个资源在语句结束时都会自动关闭,无论是正常结束还是由于异常而结束。
try (
InputStream fis = new FileInputStream(source);
OutputStream fos = new FileOutputStream(target)) {
} catch (Exception e) {
e.printStackTrace();
}
具体实现是对于实现AutoCloseable接口的类的实例,将其放到try后面,在try结束的时候,会自动将这些资源关闭(调用close方法)。
2 空指针异常(NPE)问题
在程序抛出的所有异常里面,空指针异常应该是最常见的一类了。具体示例如下:
public static void main(String[] args) {
// 异常示例1:空值比较
String nullString = null;
if (nullString.equals("targetString")) {
// 这里建议使用:Objects.equals(nullString,"targetString")或者"targetString".equals(nullString)两种方式
}
// 异常示例2:自动拆包
Integer nullInt = null;
// do something
int num = nullInt;// 自动拆包,导致出现异常
// 异常示例3:容器规则
Map<String,String> map = new ConcurrentHashMap();
map.put(nullString,"");// 不允许key为null
// 异常示例4:异常数据
List<Integer> haveNullElementList = new ArrayList();
haveNullElementList.add(1);
haveNullElementList.add(null);
for (int i : haveNullElementList) { // 没有空值判断,导致异常
}
}
任何NPE问题,都由使用者来保证。原因是在开发过程中,很难约束上游严格按照各种约定执行。
3 死循环、递归调用风险
java中的方法指令运行栈上,栈的大小一般在1-2M左右(取决于操作系统和JVM)。如果方法执行出现了死循环或者递归函数没有出口,不管栈空间多大,栈溢出是必然的。
public class Test {
private static long loop(int n) {
return n * loop(n - 1);
}
public static void main(String[] args) {
loop(100000);
}
}
以上示例代码就会抛出: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这样的浮点类型参数进行数值计算时,有时会遇到计算结果不够精确的问题,这主要是因为计算机内部是基于二进制系统工作的,而并非所有的浮点数都能被二进制完美地精确表示。这种表示上的局限性,就导致了在计算过程中可能会出现精度丢失的情况。简单来说,就是计算机在处理这些浮点数时,有时候无法完全准确地存储或计算它们,从而导致了结果的不精确。
public class Test {
public static void main(String[] args) {
System.out.println(0.06+0.01); // 精度丢失,结果:0.06999999999999999
System.out.println(303.1/1000); // 精度丢失,结果:0.30310000000000004
}
}
这在计算订单金额,描述产品重量等关键业务场景是不能够接受的。在大多数的商业计算中,一般采用java.math.BigDecimal 类来进行精确计算。
在使用BigDecimal时,建议使用BigDecimal(String var) 的String参数构造方法来构建对象。不要用BigDecimal(double var)的double参数的构造方法。这是因为0.1无法准确地表示为 double类型。
6 对象拷贝导致的风险
在业务实现的过程中,对象的拷贝或转换是很常见的应用场景。对象拷贝实现方式有两种,一种是通过原生支持的Cloneable接口实现,另一种是自定义实现。
对于实现了Cloneable接口的方案,以下几个陷阱:
- 这种方式并不是通常所理解的深拷贝,而是浅拷贝;
- 浅拷贝的一个主要问题是新的对象和原对象共享实例变量的值。原对象对共享实例变量对象的引用被修改了,那么新对象也会受到影响;
对于自定义实现对象的拷贝,需要注意一下几点:
- 序列化(Serialization):将对象序列化到一个字节流中,然后再从字节流中反序列化出新的对象。陷阱是必须有可序列化的接口(Serializable),而且如果对象中包含不可序列化的成员变量,那么在序列化过程中就可能抛出异常;
- 非序列化对象的深拷贝,需要自己实现深拷贝。如果对象之间存在循环引用,那么在进行深拷贝时,可能会出现无限递归的问题;
除此之外,对象拷贝还需要考虑性能问题,深拷贝操作可能会非常消耗性能,特别是在处理大型对象时。因此在需要频繁进行深拷贝的场景下,可能需要使用其他方式进行处理。
7 编码格式导致的风险
相信吗?编码习惯和风格的不统一往往也会导致bug。最典型的就是逻辑执行语句后面是否可以省略大括号,看下面示例:
public class Test {
public static void main(String[] args) {
int random = new Random().nextInt(10);
if (random > 5)
random = random - 5;
random = random * 2;
// 以上在语句,if作用域里面和外面的计算逻辑,将会大不相同,而这种问题往往不太容易被人察觉是真实的逻辑就这样,还是写出来了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等方法,不要在有调整数组对象的场景下使用。举例:
List<String> list = Arrays.asList("a","b","c");
list.add("d");// 错误:原因是Arrays.asList返回的List对象是一个内部类,其实现不支持数组的更新操作。
// 如果非要进行更新操作,可以使用ArrayList包装一下;
list = new ArrayList<>(list);
- 不能在遍历Collection,Map容器的过程中,对容器内的元素进行新增或删除操作。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
// 错误用例
for (String t : list) {
if ("b".equals(t)) {
list.remove(t);// 错误(ConcurrentModificationException):不能在遍历的过程中对数组进行新增或删除操作
}
}
// 正常用例
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String t = iterator.next();
if ("b".equals(t)) {
iterator.remove();// 可以使用迭代器方式进行操作
}
}
}
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的滥用也可能导致内存泄漏等风险,需谨慎使用。
最后,希望本文对读者有所启发和帮助。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。