java避坑

1.对象比较方法

问题现象
在 JDK1.7 之前,在判断一个短整型、整型、长整型包装数据类型与常量是否相等时,我们一般这样写:

Short shortValue = (short)12345;
System.out.println(shortValue == 12345); // true
Integer intValue = 12345;
System.out.println(intValue == 12345); // true
Long longValue = 12345L;
System.out.println(longValue == 12345); // true

从 JDK1.7 之后,提供了 Objects.equals 方法,并推荐使用函数式编程,更改代码如下:

Short shortValue = (short)12345;
System.out.println(Objects.equals(shortValue, 12345)); // false
Integer intValue = 12345;
System.out.println(Objects.equals(intValue, 12345)); // true
Long longValue = 12345L;
System.out.println(Objects.equals(longValue, 12345)); // false

问题分析:

在 Java 语言中,整数的默认数据类型是 int,小数的默认数据类型是 double。
通过分析Objects.equals方法的源代码可知:语句 System.out.println(Objects.equals(shortValue, 12345)),因为 Objects.equals 的两个参数对象类型不一致,一个是包装数据类型 Short,另一个是包装数据类型 Integer,所以最终的比较结果必然是false;而语句 System.out.println(Objects.equals(intValue, 12345)),因为 Objects.equals 的两个参数对象类型一致,都是包装数据类型 Integer 且取值相同,所以最终的比较结果必然是 true。

1)保持良好的编程习惯,避免数据类型的基本转换

为了避免数据类型的基本转换,更科学的方法时直接申明常量为对应的基本数据类型。

第一段代码可以这样写:

Short shortValue = (short)12345;
System.out.println(shortValue == (short)12345); // true
Integer intValue = 12345;
System.out.println(intValue == 12345); // true
Long longValue = 12345L;
System.out.println(longValue == 12345L); // true

第二段代码可以这样写:

Short shortValue = (short)12345;
System.out.println(Objects.equals(shortValue, (short)12345)); // true
Integer intValue = 12345;
System.out.println(Objects.equals(intValue, 12345)); // true
Long longValue = 12345L;
System.out.println(Objects.equals(longValue, 12345L)); // true

2)借助开发工具或插件,及早的发现基本数据类型不匹配

在 Eclipse 的问题窗口中,我们会看到这样的提示:

1.Unlikely argument type for equals(): int seems to be unrelated to Short
2.Unlikely argument type for equals(): int seems to be unrelated to Long

3)进行常规性单元测试,尽量把问题发现在研发阶段

注意:进行必要单元测试,适用于以下所有案例

2.三元表达式拆包

三元表达式是 Java 编码中的一个固定语法格式:

条件表达式?表达式1:表达式2

问题现象

boolean condition = false;
Double value1 = 1.0D;
Double value2 = 2.0D;
Double value3 = null;
Double result = condition ? value1 * value2 : value3; // 抛出空指针异常

问题分析
三元表达式的类型转化规则:

1.若两个表达式类型相同,返回值类型为该类型;
2.若两个表达式类型不同,但类型不可转换,返回值类型为 Object 类型;
3.若两个表达式类型不同,但类型可以转化,先把包装数据类型转化为基本数据类型,然后按照基本数据类型的转换规则 (byte < short(char)< int < long < float < double) 来转化,返回值类型为优先级最高的基本数据类型。

根据规则分析,表达式 1(value1 * value2)的类型为基础数据类型 double,表达式 2(value 3)的类型为包装数据类型 Double,根据三元表达式的类型转化规则判断,最终的表达式类型为基础数据类型 double。所以,当条件表达式 condition 为 false 时,需要把空 Double 对象 value 3 转化为基础数据类型 double,于是就调用了 value 3 的 doubleValue 方法进行拆包,当然会抛出空指针异常。

1)尽量避免使用三元表达试,可以使用if-else代替

如果三元表达式中有包装数据类型的算术计算,可以考虑利用 if-else 语句代替。改写代码如下:

if (condition) {
  result = value1 * value2;
} else {
  result = value3;
}

2)尽量使用基本数据类型,避免包装数据类型的拆装包

如果在三元表达式中有算术计算,尽量使用基本数据类型,避免包装数据类型的拆装包。改写代码如下:

boolean condition = false;
double value1 = 1.0D;
double value2 = 2.0D;
double value3 = 3.0D;
double result = condition ? value1 * value2 : value3;

3.泛型对象赋值

1)在初始化泛型对象时,推荐使用diamond语法

在《 Java 开发手册》中,有这么一条推荐规则:

【推荐】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略。
说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。

正例:

// <> diamond 方式
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式ArrayList<User> users = new ArrayList(10);

其实,初始化泛型对象时,全省略是不推荐的。这样会避免类型检查。

4.泛型属性拷贝

Spring 的 BeanUtils.copyProperties 方法,是一个很好用的属性拷贝工具方法。

1)不要盲目地相信第三方工具包,任何工具包都可能存在问题

在 Java 中,存在很多第三方工具包,比如:Apache 的 commons-lang3、commons-collections,Google 的 guava……都是很好用的第三方工具包。但是,不要盲目地相信第三方工具包,任何工具包都有可能存在问题。

2)如果需要拷贝的属性较少,可以手动编码进行属性拷贝

用 BeanUtils.copyProperties 反射拷贝属性,主要优点是节省了代码量,主要缺点是导致程序性能下降。所以,如果需要拷贝的属性较少,可以手动编码进行属性拷贝。

5.Set对象排重

在 Java 语言中,Set 数据结构可以用于对象排重,常见的 Set 类有 HashSet、LinkedHashSet 等。
问题分析

当向集合 Set 中增加对象时,首先集合计算要增加对象的 hashCode,根据该值来得到一个位置用来存放当前对象。如在该位置没有一个对象存在的话,那么集合 Set 认为该对象在集合中不存在,直接增加进去。如果在该位置有一个对象存在的话,接着将准备增加到集合中的对象与该位置上的对象进行 equals 方法比较:如果该 equals 方法返回 false,那么集合认为集合中不存在该对象,就把该对象放在这个对象之后;如果 equals 方法返回 true,那么就认为集合中已经存在该对象了,就不会再将该对象增加到集合中了。所以,在哈希表中判断两个元素是否重复要使用到 hashCode 方法和 equals 方法。hashCode 方法决定数据在表中的存储位置,而 equals 方法判断表中是否存在相同的数据。

1)当确定数据唯一时,可以使用list代替set


List<City> citySet = new ArrayList<>(1024);
Iterator<CSVRecord> iterator = parser.iterator();
while (iterator.hasNext()) {
    citySet.add(parseCity(iterator.next()));
}
return citySet;

2)当确定数据不唯一时,可以使用map代替set

当确定解析的城市数据不唯一时,需要安装城市名称进行排重操作,可以直接使用 Map 进行存储。为什么不建议实现 City 类的 hashCode 方法,再采用 HashSet 来实现排重呢?首先,不希望把业务逻辑放在模型 DO 类中;其次,把排重字段放在代码中,便于代码的阅读、理解和维护。

Map<String, City> cityMap = new HashMap<>(1024);
Iterator<CSVRecord> iterator = parser.iterator();
while (iterator.hasNext()) {
    City city = parseCity(iterator.next());
    cityMap.put(city.getCode(), city);
}
return cityMap.values();

3)遵循 Java 语言规范,重写 hashCode 方法和 equals 方法

不重写 hashCode 方法和 equals 方法的自定义类不应该在 Set 中使用。

6.公有方法代理

SpringCGLIB 代理生成的代理类是一个继承被代理类,通过重写被代理类中的非 final 的方法实现代理。所以,SpringCGLIB 代理的类不能是 final 类,代理的方法也不能是 final 方法,这是由继承机制限制的。

1)严格遵循 CGLIB 代理规范,被代理的类和方法不要加 final 修饰符

严格遵循 CGLIB 代理规范,被代理的类和方法不要加 final 修饰符,避免动态代理操作对象实例不同(原始对象实例和代理对象实例),从而导致数据不一致或空指针问题。

2)缩小 CGLIB 代理类的范围,能不用被代理的类就不要被代理

缩小 CGLIB 代理类的范围,能不用被代理的类就不要被代理,即可以节省内存开销,又可以提高函数调用效率。

7.公有字段代理

在 fastjson 强制升级到 1.2.60 时踩过一个坑,作者为了开发快速,在 ParseConfig 中定义了:

public class ParseConfig {
    public final SymbolTable symbolTable = new SymbolTable(4096);
    ......
}

在我们的项目中继承了该类,同时又被 AOP 动态代理了,于是一行代码引起了一场“血案”。

1)当确定字段不可变时,可以定义为公有静态常量

当确定字段不可变时,可以定义为公有静态常量,并用类名称 + 字段名称访问。类名称 + 字段名称访问公有静态常量,与类实例的动态代理无关。

2)当确定字段不可变时,可以定义为私有成员变量

当确定字段不可变时,可以定义为私有成员变量,提供一个公有 Getter 方法获取该变量值。当该类实例被动态代理时,代理方法会调用被代理的 Getter 方法,从而返回被代理类的成员变量值。

3)遵循 JavaBean 编码规范,不要定义公有成员变量

遵循 JavaBean 编码规范,不要定义公有成员变量。JavaBean 规范如下:

1.JavaBean 类必须是一个公共类,并将其访问属性设置为 public,如:public class User{…}
2.JavaBean 类必须有一个空的构造函数:类中必须有一个不带参数的公用构造器
3.一个 JavaBean 类不应有公共实例变量,类变量都为 private,如:private Integer id;
4.属性应该通过一组 getter / setter 方法来访问

8.引用链接

链接: link.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值