序文
Javaプログラマーとして、あなたがいくつかの基本的な知識の落とし穴を踏んだかどうかはわかりません。
バグが長期間チェックされ、最終的に低レベルのエラーであることが判明する場合があります。
一部のコードでは、このデータバッチは正常に機能する場合がありますが、データバッチが変更されると例外が発生します。
時々、あなたはぼんやりとコードの行を見つめるかもしれません、考えて:なぜこのコードの行は間違っているのですか?
今日は、99%のJavaプログラマーが踏んだ、または踏もうとしている6つの落とし穴についてお話します。
1.==記号と比較したピット
あなたがプロジェクトでそれを見たかどうかはわかりませんが、一部の同僚Integer
は、タイプの2つのパラメーターの数の比較を==
等しくするために使用しますか?
とにかく見たのですが、この使い方は正しいですか?
私の答えは、それは特定の状況に依存するということであり、それが正しいか間違っているかは言えません。
次のようないくつかのステータスフィールド:orderStatusには、-1(配置されていない)、0(順序付けられている)、1(支払われている)、2(完了)、3(キャンセルされている)、5つの状態があります。
このとき、==を使用してそれらが等しいかどうかを判断する場合:
Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1 == orderStatus2);
返される結果は本当ですか?
回答:はい、誤りです。
一部の学生は反論するかもしれません、整数に範囲がありません:-128-127
キャッシュ?
なぜそれは間違っているのですか?
まず、Integerのコンストラクターを見てください。
実際には使用されていません缓存
。
では、キャッシュはどこで使用されますか?
答えは次のvalueOf
方法にあります。
上記の判断がこれに変更された場合:
String orderStatus1 = new String("1");
String orderStatus2 = new String("1");
System.out.println(Integer.valueOf(orderStatus1) == Integer.valueOf(orderStatus2));
返される結果は本当ですか?
回答:それは本当です。
上記の非常に特殊なシナリオでのみ、優れたコーディング習慣を身に付け、==lessを使用して2つの整数データが等しいかどうかを判断する必要があります。
equals
代わりに、メソッド判断を使用するように変更する必要があります。
Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1.equals(orderStatus2));
操作の結果は真です。
2.Objects.equalsのピット
ここで、そのような要件があるとします。現在ログインしているユーザーを特定するために、指定したシステム管理者である場合は、電子メールを送信します。システム管理者には特別なフィールドIDはなく、ユーザーID = 888であり、値は開発、テスト、および実稼働環境で同じです。
この要件を達成するのは本当に簡単すぎます。
UserInfo userInfo = CurrentUser.getUserInfo();
if(Objects.isNull(userInfo)) {
log.info("请先登录");
return;
}
if(Objects.equals(userInfo.getId(),888L)) {
sendEmail(userInfo):
}
現在ログインしているユーザーのコンテキストからユーザー情報を取得し、ユーザー情報が空の場合は直接戻るかどうかを判断します。
取得したユーザー情報が空でない場合は、ユーザーIDが888に等しいかどうかを判別してください。
- 如果等于888,则发送邮件。
- 如果不等于888,则啥事也不干。
当我们用id=888的系统管理员账号登录之后,做了相关操作,满怀期待的准备收邮件的时候,却发现收了个寂寞。
后来,发现UserInfo类是这样定义的:
@Data
public class UserInfo {
private Integer id;
private String name;
private Integer age;
private String address;
}
此时,有些小伙伴可能会说:没看出什么问题呀。
但我要说的是这个代码确实有问题。
什么问题呢?
下面我们重点看看它的equals方法:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
equals方法的判断逻辑如下:
- 该方法先判断对象a和b的引用是否相等,如果相等则直接返回true。
- 如果引用不相等,则判断a是否为空,如果a为空则返回false。
- 如果a不为空,调用对象的equals方法进一步判断值是否相等。
这就要从Integer
的equals
方法说起来了。
它的equals方法具体代码如下:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
先判断参数obj是否是Integer类型,如果不是,则直接返回false。如果是Integer类型,再进一步判断int值是否相等。
而上面这个例子中b是long类型,所以Integer的equals方法直接返回了false。
也就是说,如果调用了Integer的equals方法,必须要求入参也是Integer类型,否则该方法会直接返回false。
除此之外,还有Byte、Short、Double、Float、Boolean和Character也有类似的equals方法判断逻辑。
常见的坑有:
- Long类型和Integer类型比较,比如:用户id的场景。
- Byte类型和Integer类型比较,比如:状态判断的场景。
- Double类型和Integer类型比较,比如:金额为0的判断场景。
如果你想进一步了解Objects.equals方法的问题,可以看看我的另一篇文章《Objects.equals有坑》。
3. BigDecimal的坑
通常我们会把一些小数类型的字段(比如:金额),定义成BigDecimal
,而不是Double
,避免丢失精度问题。
使用Double时可能会有这种场景:
double amount1 = 0.02;
double amount2 = 0.03;
System.out.println(amount2 - amount1);
正常情况下预计amount2 - amount1应该等于0.01
但是执行结果,却为:
0.009999999999999998
实际结果小于预计结果。
Double类型的两个参数相减会转换成二进制,因为Double有效位数为16位这就会出现存储小数位数不够的情况,这种情况下就会出现误差。
常识告诉我们使用BigDecimal
能避免丢失精度。
但是使用BigDecimal能避免丢失精度吗?
答案是否定的。
为什么?
BigDecimal amount1 = new BigDecimal(0.02);
BigDecimal amount2 = new BigDecimal(0.03);
System.out.println(amount2.subtract(amount1));
这个例子中定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。
结果:
0.0099999999999999984734433411404097569175064563751220703125
不科学呀,为啥还是丢失精度了?
Jdk
中BigDecimal
的构造方法
上有这样一段描述:
大致的意思是此构造函数的结果可能不可预测,可能会出现创建时为0.1,但实际是0.1000000000000000055511151231257827021181583404541015625的情况。
由此可见,使用BigDecimal构造函数初始化对象,也会丢失精度。
那么,如何才能不丢失精度呢?
BigDecimal amount1 = new BigDecimal(Double.toString(0.02));
BigDecimal amount2 = new BigDecimal(Double.toString(0.03));
System.out.println(amount2.subtract(amount1));
我们可以使用Double.toString
方法,对double类型的小数进行转换,这样能保证精度不丢失。
其实,还有更好的办法:
BigDecimal amount1 = BigDecimal.valueOf(0.02);
BigDecimal amount2 = BigDecimal.valueOf(0.03);
System.out.println(amount2.subtract(amount1));
使用BigDecimal.valueOf
方法初始化BigDecimal类型参数,也能保证精度不丢失。在新版的阿里巴巴开发手册中,也推荐使用这种方式创建BigDecimal参数。
4. Java8 filter的坑
对于Java8
中的Stream
用法,大家肯定再熟悉不过了。
我们通过对集合
的Stream
操作,可以实现:遍历集合、过滤数据、排序、判断、转换集合等等,N多功能。
这里重点说说数据的过滤。
在没有Java8之前,我们过滤数据一般是这样做的:
public List<User> filterUser(List<User> userList) {
if(CollectionUtils.isEmpty(userList)) {
return Collections.emptyList();
}
List<User> resultList = Lists.newArrayList();
for(User user: userList) {
if(user.getId() > 1000 && user.getAge() > 18) {
resultList.add(user);
}
}
return resultList;
}
通常需要另一个集合辅助完成这个功能。
但如果使用Java8的filter
功能,代码会变得简洁很多,例如:
public List<User> filterUser(List<User> userList) {
if(CollectionUtils.isEmpty(userList)) {
return Collections.emptyList();
}
return userList.stream()
.filter(user -> user.getId() > 1000 && user.getAge() > 18)
.collect(Collectors.toList());
}
代码简化了很多,完美。
但如果你对过滤后的数据,做修改了:
List<User> userList = queryUser();
List<User> filterList = filterUser(userList);
for(User user: filterList) {
user.setName(user.getName() + "测试");
}
for(User user: userList) {
System.out.println(user.getName());
}
你当时可能只是想修改过滤后的数据,但实际上,你会把元素数据一同修改了。
意不意外,惊不惊喜?
其根本原因是:过滤后的集合中,保存的是对象的引用,该引用只有一份数据。
也就是说,只要有一个地方,把该引用对象的成员变量
的值,做修改了,其他地方也会同步修改。
如下图所示:
5. 自动拆箱的坑
Java5
之后,提供了自动装箱
和自动拆箱
的功能。
自动装箱是指:JDK会把基本类型,自动变成包装类型。
比如:
Integer integer = 1;
等价于:
Integer integer = new Integer(1);
而自动拆箱是指:JDK会把包装类型,自动转换成基本类型。
例如:
Integer integer = new Integer(2);
int sum = integer + 5;
等价于:
Integer integer = new Integer(2);
int sum = integer.intValue() + 5;
但实际工作中,我们在使用自动拆箱时,往往忘记了判空,导致出现NullPointerException
异常。
5.1 运算
很多时候,我们需要对传入的数据进行计算,例如:
public class Test2 {
public static void main(String[] args) {
System.out.println(add(new Integer(1), new Integer(2)));
}
private static Integer add(Integer a, Integer b) {
return a + b;
}
}
如果传入了null值:
System.out.println(add(null, new Integer(2)));
则会直接报错。
5.2 传参
有时候,我们定义的某个方法是基本类型,但实际上传入了包装类,比如:
public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = null;
System.out.println(add(a, b));
}
private static Integer add(int a, int b) {
return a + b;
}
如果出现add方法报NullPointerException
异常,你可能会懵逼,int类型怎么会出现空指针异常呢?
其实,这个问题出在:Integer类型的参数,其实际传入值为null,JDK字段拆箱,调用了它的intValue
方法导致的问题。
6. replace的坑
很多时候我们在使用字符串时,想把字符串比如:ATYSDFA*Y中的字符A替换成字符B,第一个想到的可能是使用replace方法。
如果想把所有的A都替换成B,很显然可以用replaceAll方法,因为非常直观,光从方法名就能猜出它的用途。
那么问题来了:replace方法会替换所有匹配字符吗?
jdk的官方给出了答案。
该方法会替换每一个匹配的字符串。
既然replace和replaceAll都能替换所有匹配字符,那么他们有啥区别呢?
replace
有两个重载
的方法。
- 其中一个方法的参数:char oldChar 和 char newChar,支持字符的替换。
source.replace('A', 'B')
- 另一个方法的参数是:CharSequence target 和 CharSequence replacement,支持字符串的替换。
source.replace("A", "B")
而replaceAll
方法的参数是:String regex 和 String replacement,即基于正则表达式
的替换。
例如对普通字符串进行替换:
source.replaceAll("A", "B")
使用正则表达替换(将*替换成C):
source.replaceAll("\\*", "C")
顺便说一下,将*
替换成C
使用replace方法也可以实现:
source.replace("*", "C")
小伙们看到看到二者的区别了没?使用replace方法无需对特殊字符进行转义。
不过,千万注意,切勿使用如下写法:
source.replace("\\*", "C")
这种写法会导致字符串无法替换。
还有个小问题,如果我只想替换第一个匹配的字符串该怎么办?
这时可以使用replaceFirst
方法:
source.replaceFirst("A", "B")
说实话,这里内容都很基础,但越基础的东西,越容易大意失荆州,更容易踩坑。
最后,统计一下,这些坑一个都没踩过的同学,麻烦举个手。
此外,我的所有文章已经开源了。更多精彩内容收录在我的GitHub,访问地址: github.com/dvsusan/sus… ,欢迎小伙伴们给我一个****star 。