乐趣区

关于java:Java程序员线上惨痛踩坑记录你也一定遇到过

线上问题年年有,往年特地多。记几次线上惨痛的踩坑记录,心愿大家以史为鉴。


1. 包装类型主动解箱导致空指针异样

public int getId() {
    Integer id = null;
    return id;
}

如果调用下面的办法会产生什么?id 是 Integer 类型,而办法的返回值 int 类型,会主动拆箱转换,因为 id 是 null,转换成 int 类型的时候,就会报 NullPointerException 异样。

无论是《阿里 Java 开发手册》、《代码整洁之道》还是《Effective Java》都倡议办法返回值类型尽量写成包装类型,相似 Integer。还有实体类、接管前端传参类、给前端的响应类中的属性都要写成包装类型,防止拆箱出错。

2. 包装类型用 == 判断相等,导致判断不正确

先看一段代码运行后果:

public class IntegerTest {public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        Integer c = 200;
        Integer d = 200;
        System.out.println(a == b); // 输入 true
        System.out.println(c == d); // 输入 false
    }
}

很多人会很纳闷,为什么输入的两个后果会不一样?

当给 Integer 类型赋值时,会调用 Integer.valueOf() 办法

static final int low = -128;
static final int high = 127;

public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

当 value 值在 -128 到 127 之间时,会复用缓存。当不在这个区间时,才会创建对象。

而 == 比拟的是内存地址,不同的对象的内存地址不雷同,所以就呈现上述的后果。

Integer 重写了 equals() 办法:

public boolean equals(Object obj) {if (obj instanceof Integer) {return value == ((Integer)obj).intValue();}
    return false;
}

当应用 equals() 办法时,比拟的是 int 值是否相等。

所以,包装类判断是否相等的时候,绝不能用 == 判断,肯定要用 equals() 办法判断。

3. Switch 传参是 null 导致空指针异样

猜一下上面代码的运行后果:

public class Test {public static void main(String[] args) {
        String name = null;
        switch (name) {
            case "yideng":
                System.out.println("一灯");
                break;
            default:
                System.out.println("default");
        }
    }
}

你是不是认为会输入 default,其实代码会抛出 NullPointerException 异样。

当 switch 比拟两个对象是否相等的时候,会调用 name.hashCode() 办法和 name.equals() 办法,因为 name 是 null,后果就抛出了 NullPointerException 异样。

所以调用 switch 办法前,肯定要对传参进行判空。

4. 创立 BigDecimal 类型时精度失落

猜一下上面代码的运行后果:

public class Test {public static void main(String[] args) {BigDecimal bigDecimal = new BigDecimal(0.1);
        System.out.println(bigDecimal);
    }
}

你认为会输入 0.1,其实输入后果是:

0.1000000000000000055511151231257827021181583404541015625

What?这么一大串是什么货色?

为什么会呈现这种状况呢?起因是,当咱们用 new BigDecimal(0.1) 创建对象是,会调用 BigDecimal 的这个构造方法:

public BigDecimal(double val) {this(val,MathContext.UNLIMITED);
}

把传参 0.1 当成了 double 类型,double 计算的时候会把数值转换成二进制,而 0.1 转换成二进制是无奈除尽的,所以就带了一大串小数位。

当须要创立 BigDecimal 类型时,应该怎么做呢?

能够先把数值转换成字符串类型,再创立 BigDecimal 对象,相似这样:

BigDecimal bigDecimal = new BigDecimal(String.valueOf(0.1));

又来一个问题,BigDecimal 是怎么解决精度失落问题?

答案是 BigDecimal 会先把数值乘以 10 的整数倍,去除小数位,转换成 long 类型,而后进行运算,最初把运算后果除以 10 的整数倍。

5. group 分组时主键反复,导致异样

上面代码的分组能胜利吗?

public class SteamTest {
  
    static class User {
        // 用户 ID
        private Integer id;
        // 用户名
        private String name;
    }

    public static void main(String[] args) {
        List<User> users = Arrays.asList(new User(1, "Tom"),
                new User(1, "Tony"),
                new User(2, "Jerry")
        );
         // 用户汇合按 id 进行分组
        Map<Integer, User> userMap = users.stream()
                .collect(Collectors.toMap(User::getId, user -> user));
      System.out.println(userMap);
    }
}

后果报异样了,Exception in thread “main” java.lang.IllegalStateException: Duplicate key SteamTest.User(id=1, name=Tom)

起因是主键抵触,有两个 id= 1 的数据,按 id 进行分组时程序就不晓得怎么解决了。

能够这样做

public class SteamTest {
  
    static class User {
        // 用户 ID
        private Integer id;
        // 用户名
        private String name;
    }

    public static void main(String[] args) {
        List<User> users = Arrays.asList(new User(1, "Tom"),
                new User(1, "Tony"),
                new User(2, "Jerry")
        );
         // 用户汇合按 id 进行分组,主键抵触的时候,取第一个 user
        Map<Integer, User> userMap = users.stream()
                .collect(Collectors.toMap(User::getId, user -> user, (user1, user2) -> user1));
      System.out.println(userMap); // 输入 {1:{"id":1,"name":"Tom"},2:{"id":2,"name":"Jerry"}}
    }
}

6. 虚实 ArrayList 导致增加异样

上面的 add() 办法能增加胜利吗?

public class Test {public static void main(String[] args) {List<Integer> list = Arrays.asList(1, 2);
        list.add(3);
    }
}

后果是抛异样了,Exception in thread “main” java.lang.UnsupportedOperationException

抛出了不反对这个办法的异样,为什么呢?咱们看一下 Arrays.asList() 办法的源码:

public static <T> List<T> asList(T... a) {return new ArrayList<>(a);
}

返回了一个 ArrayList,为什么还不能增加胜利了?

假相是此 ArrayList 非彼 ArrayList,跟咱们罕用的 ArrayList 只是重名,这个 ArrayList 只是 Arrays 对象一个外部类,外部并没有实现 add() 办法,所以增加的时候会报错。

这不是明摆着坑人吗?实现了 list 接口,为啥不实现 add() 办法?

其实作者是成心这样设计的,除了没有实现 add() 办法,还没有实现 addAll()、remove()、clear() 等批改办法,目标就是创立后再不让用户批改,这样的汇合有什么用呢?

其实在某些不可变场景还是很实用的,比方已完结的订单状态汇合:

List<String> list = Arrays.asList("Failure", "Cancelled","Completed");

这种汇合个别不会变的,应用过程中也不容许批改,防止出错。

7. 总结

每一次踩坑,背地都有至多一次的线上问题记录,这些总结都是用教训换来的,不只是本人,其他人必定也遇到过。咱们如何能力防止在当前的开发中再呈现相似的问题呢?

  • 站在使用者的角度,编写具体的单元测试,打印必要日志,追踪代码执行后果
  • 站在创造者的角度,探索框架的架构设计和源码实现,了解作者的用意

你在线上还踩过那些坑?

退出移动版