iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >Java中常见的坑有哪些
  • 148
分享到

Java中常见的坑有哪些

2023-06-27 11:06:58 148人浏览 安东尼
摘要

今天小编给大家分享一下Java中常见的坑有哪些的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。1.前言同一个代码“坑”,踩第一

今天小编给大家分享一下Java中常见的坑有哪些的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

1.前言

同一个代码“坑”,踩第一次叫"长了经验",踩第二次叫"加深印象",踩第三次叫"不长心眼",踩三次以上就叫"不可救药"。在本文中,笔者总结了一些代码坑,描述了问题现象,进行了问题分析,给出了避坑方法。希望大家在日常编码中,遇到了这类代码坑,能够提前避让开来。

1.对象比较方法

jdk1.7  提供的 Objects.equals 方法,非常方便地实现了对象的比较,有效地避免了繁琐的空指针检查。

1.1.问题现象

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

Short shortValue = (short)12345;System.out.println(shortValue == 12345); // trueSystem.out.println(12345 == shortValue); // trueInteger intValue = 12345;System.out.println(intValue == 12345); // trueSystem.out.println(12345 == intValue); // trueLong longValue = 12345L;System.out.println(longValue == 12345); // trueSystem.out.println(12345 == longValue); // true

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

Short shortValue = (short)12345;System.out.println(Objects.equals(shortValue, 12345)); // falseSystem.out.println(Objects.equals(12345, shortValue)); // falseInteger intValue = 12345;System.out.println(Objects.equals(intValue, 12345)); // trueSystem.out.println(Objects.equals(12345, intValue)); // trueLong longValue = 12345L;System.out.println(Objects.equals(longValue, 12345)); // falseSystem.out.println(Objects.equals(12345, longValue)); // false

为什么直接把 == 替换为 Objects.equals 方法会导致输出结果不一样?

1.2.问题分析

通过反编译第一段代码,我们得到语句"System.out.println(shortValue == 12345);"的字节码指令如下:

7   getstatic java.lang.System.out : java.io.PrintStream [22]10  aload_1 [shortValue]11  invokevirtual java.lang.Short.shortValue() : short [28]14  sipush 1234517  if_icmpne 2420  iconst_121  Goto 2524  iconst_025  invokevirtual java.io.PrintStream.println(boolean) : void [32]

原来,编译器会判断包装数据类型对应的基本数据类型,并采用这个基本数据类型的指令进行比较(比如上面字节码指令中的sipushif_icmpne等),相当于编译器自动对常量进行了数据类型的强制转化。

为什么采用 Objects.equals 方法后,编译器不自动对常量进行数据类型的强制转化?通过反编译第二段代码,我们得到语句 "System.out.println(Objects.equals(shortValue, 12345));" 的字节码指令如下:

7   getstatic java.lang.System.out : java.io.PrintStream [22]10  aload_1 [shortValue]11  sipush 1234514  invokestatic java.lang.Integer.valueOf(int) : java.lang.Integer [28]17  invokestatic java.util.Objects.equals(java.lang.Object, java.lang.Object) : boolean [33]20  invokevirtual java.io.PrintStream.println(boolean) : void [39]

原来,编译器根据字面意思,认为常量 12345 默认基本数据类型是 int ,所以会自动转化为包装数据类型 Integer

在 Java 语言中,整数的默认数据类型是 int ,小数的默认数据类型是 double 。

下面来分析一下 Objects.equals 方法的代码实现:

public static boolean equals(Object a, Object b) {   return (a == b) || (a != null && a.equals(b));}

其中,语句 “a.equals(b)” 将会使用到 Short.equals方法。

Short.equals 方法的代码实现为:

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

通过代码实现分析:对应语句"System.out.println(Objects.equals(shortValue, 12345));",因为Objects.equals的两个参数对象类型不一致,一个是包装数据类型 Short ,另一个是包装数据类型 Integer ,所以最终的比较结果必然是 false 。同样,语句 “System.out.println(Objects.equals(intValue, 12345));” ,因为 Objects.equals 的两个参数对象类型一致,都是包装数据类型Integer且取值一样,所以最终的比较结果必然是 true

1.3.避坑方法

保持良好的编码习惯,避免数据类型的自动转化

为了避免数据类型自动转化,更科学的写法是直接声明常量为对应的基本数据类型。

第一段代码可以这样写:

Short shortValue = (short)12345;System.out.println(shortValue == (short)12345); // trueSystem.out.println((short)12345 == shortValue); // trueInteger intValue = 12345;System.out.println(intValue == 12345); // trueSystem.out.println(12345 == intValue); // trueLong longValue = 12345L;System.out.println(longValue == 12345L); // trueSystem.out.println(12345L == longValue); // true

第二段代码可以这样写:

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

借助开发工具插件,及早地发现数据类型不匹配问题

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

Unlikely argument type for equals(): int seems to be unrelated to ShortUnlikely argument type for equals(): Short seems to be unrelated to intUnlikely argument type for equals(): int seems to be unrelated to LongUnlikely argument type for equals(): Long seems to be unrelated to int

通过 FindBugs 插件扫描,我们会看到这样的警告:

Call to Short.equals(Integer) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]Call to Integer.equals(Short) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]Call to Long.equals(Integer) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]Call to Integer.equals(Long) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]

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

“勿以善小而不为”,不要因为改动很小就不需要进行单元测试了,往往 Bug 都出现在自己过度自信的代码中。像这种问题,只要进行一次单元测试,是完全可以发现问题的。

2.三元表达式拆包

三元表达式是 Java 编码中的一个固定语法格式:“条件表达式?表达式 1 :表达式 2 ”。

三元表达式的逻辑为:“如果条件表达式成立,则执行表达式 1 ,否则执行表达式 2 ”。

2.1.问题现象

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

当条件表达式 condition 等于 false 时,直接把 Double 对象 value3 赋值给 Double 对象result,按道理没有问题呀,为什么会抛出空指针异常(NullPointerException)?

2.2.问题分析

通过反编译代码,我们得到语句"Double result = condition ? value1 * value2 : value3;"的字节码指令如下:

17  iload_1 [condition]18  ifeq 3321  aload_2 [value1]22  invokevirtual java.lang.Double.doubleValue() : double [24]25  aload_3 [value2]26  invokevirtual java.lang.Double.doubleValue() : double [24]29  dmul30  goto 3833  aload 4 [value3]35  invokevirtual java.lang.Double.doubleValue() : double [24]38  invokestatic java.lang.Double.valueOf(double) : java.lang.Double [16]41  astore 5 [result]43  getstatic java.lang.System.out : java.io.PrintStream [28]46  aload 5 [result]

在第 33 行,加载 Double 对象 value3 到操作数栈中;在第 35 行,调用 Double 对象 value3doubleValue 方法。这个时候,由于 value3 是空对象 null ,调用 doubleValue 方法必然抛出抛出空指针异常。但是,为什么要把空对象 value3 转化为基础数据类型 double

查阅相关资料,得到三元表达式的类型转化规则:

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

根据规则分析,表达式 1(value1 * value2)计算后返回基础数据类型 double ,表达式 2(value3) 返回包装数据类型 double ,根据三元表达式的类型转化规则判断,最终的返回类型为基础数据类型 double 。所以,当条件表达式 condition 等于 false 时,需要把空对象 value3 转化为基础数据类型 double ,于是就调用了 value3doubleValue 方法抛出了空指针异常。

可以用以下案例验证三元表达式的类型转化规则:

boolean condition = false;Double value1 = 1.0D;Double value2 = 2.0D;Double value3 = null;Integer value4 = null;// 返回类型为Double,不抛出空指针异常Double result1 = condition ? value1 : value3;// 返回类型为double,会抛出空指针异常Double result2 = condition ? value1 : value4;// 返回类型为double,不抛出空指针异常Double result3 = !condition ? value1 * value2 : value3;// 返回类型为double,会抛出空指针异常Double result4 = condition ? value1 * value2 : value3;

2.3.避坑方法

尽量避免使用三元表达式,可以采用 if-else 语句代替

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

boolean condition = false;Double value1 = 1.0D;Double value2 = 2.0D;Double value3 = null;Double result;if (condition) {   result = value1 * value2;} else {   result = value3;}

尽量使用基本数据类型,避免数据类型的自动转化

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

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

进行覆盖性单元测试,尽量把问题发现在研发阶段

像这种问题,只要编写一些单元测试用例,进行一些覆盖性测试,是完全可以提前发现的。

(推荐教程:Java教程)

3.泛型对象赋值

Java 泛型是 JDK1.5 中引入的一个新特性,其本质是参数化类型,即把数据类型做为一个参数使用。

3.1.问题现象

在做用户数据分页查询时,因为笔误编写了如下代码:

PageDataVO.java

@Getter@Setter@ToString@NoArgsConstructor@AllArgsConstructorpublic class PageDataVO<T> {      private Long totalCount;      private List<T> dataList;}

UserDAO.java

@Mapperpublic interface UserDAO {      public Long countUser(@Param("query") UserQueryVO query);      public List<UserDO> queryUser(@Param("query") UserQueryVO query);}

UserService.java

@Servicepublic class UserService {      @Autowired   private UserDAO userDAO;      public PageDataVO<UserVO> queryUser(UserQueryVO query) {       List<UserDO> dataList = null;       Long totalCount = userDAO.countUser(query);       if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {           dataList = userDAO.queryUser(query);      }       return new PageDataVO(totalCount, dataList);  }}

UserController.java

@Controller@RequestMapping("/user")public class UserController {      @Autowired   private UserService userService;      @ResponseBody   @RequestMapping(value = "/query", method = RequestMethod.POST)   public Result<PageDataVO<UserVO>> queryUser(@RequestBody UserQueryVO query) {       PageDataVO<UserVO> pageData = userService.queryUser(query);       return ResultBuilder.success(pageData);  }}

以上代码没有任何编译问题,但是却把 UserDO中一些涉密字段返回给前端。细心的读者可能已经发现了,在 UserService 类的 queryUser 方法的语句" return new PageDataVO(totalCount, dataList);"中,我们把List<UserDO>对象dataList赋值给了PageDataVO<UserVO>List<UserVO>字段dataList

问题是:为什么开发工具不报编译错误啦?

3.2.问题分析

由于历史原因,参数化类型和原始类型需要兼容。我们以 ArrayList 举例子,来看看如何兼容的。

以前的写法:

ArrayList list = new ArrayList();

现在的写法:

ArrayList<String> list = new ArrayList<String>();

考虑到与以前的代码兼容,各种对象引用之间传值,必然会出现以下的情况:

// 第一种情况ArrayList list1 = new ArrayList<String>();// 第二种情况ArrayList<String> list2 = new ArrayList();

所以, Java 编译器对以上两种类型进行了兼容,不会出现编译错误,但会出现编译告警。但是,我的开发工具在编译时真没出现过告警。

再来分析我们遇到的问题,实际上同时命中了两种情况:

List<UserDO> 对象赋值给 List ,命中了第一种情况;

PageDataVO 对象赋值给PageDataVO<UserVO> ,命中了第二种情况。

最终的效果就是:我们神奇地把 List<UserDO> 对象赋值给了 List<UserVO>

问题的根源就是:我们在初始化 PageDataVO 对象时,没有要求强制进行类型检查。

3.3.避坑方法

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

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

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


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

其实,初始化泛型对象时,全省略是不推荐的。这样会避免类型检查,从而造成上面的问题。

在初始化泛型对象时,推荐使用diamond语法,代码如下:

return new PageDataVO<>(totalCount, dataList);

现在,在 Eclipse 的问题窗口中,我们会看到这样的错误:

Cannot infer type arguments for PageDataVO<>

于是,我们就知道忘了把 List<UserDO> 对象转化为 List<UserVO> 对象了。

2、在进行单元测试时,需要对比数据内容

在进行单元测试时,运行正常是一个指标,但数据正确才是更重要的指标。

4.泛型属性拷贝

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

4.1.问题现象

根据数据库开发规范,数据库表格必须包含 idgmt_creategmt_modified 三个字段。其中, id这个字段,可能根据数据量不同,采用 intlong 类型(注意:阿里规范要求必须是 long 类型,这里为了举例说明,允许为 int 或 long 类型)。

所以,把这三个字段抽出来,定义了一个 BaseDO 基类:

@Getter@Setter@ToStringpublic class BaseDO<T> {   private T id;   private Date gmtCreate;   private Date gmtModified;}

针对 user 表,定义了一个 UserDO 类:

@Getter@Setter@ToStringpublic class UserDO extends BaseDO<Long>{   private String name;   private String description;}

对于查询接口,定义了一个 UserVO 类:

@Getter@Setter@ToStringpublic static class UserVO {   private Long id;   private String name;   private String description;}

实现查询用户服务接口,实现代码如下:

@Servicepublic class UserService {      @Autowired   private UserDAO userDAO;      public List<UserVO> queryUser(UserQueryVO query) {       // 查询用户信息       List<UserDO> userDOList = userDAO.queryUser(query);       if (CollectionUtils.isEmpty()) {           return Collections.emptyList();      }       // 转化用户列表       List<UserVO> userVOList = new ArrayList<>(userDOList.size());       for (UserDO userDO : userDOList) {           UserVO userVO = new UserVO();           BeanUtils.copyProperties(userDO, userVO);           userVOList.add(userVO);      }       // 返回用户列表       return userVOList;  }}

通过测试,我们会发现一个问题——调用查询用户服务接口,用户 ID 的值并没有返回。

[{"description":"This is a tester.","name":"tester"},...]

4.2.问题分析

按道理,UserDO 类和 UserVO 类的 id 字段,类型都是 Long 类型,不存在类型不可转化,应该能够正常赋值。尝试手工赋值,代码如下:

for (UserDO userDO : userDOList) {   UserVO userVO = new UserVO();   userVO.setId(userDO.getId());   userVO.setName(userDO.getName());   userVO.setDescription(userDO.getDescription());   userVOList.add(userVO);}

经过测试,上面代码返回结果正常,用户ID的值成功返回。

那么,就是 BeanUtils.copyProperties 工具方法的问题了。用 Debug 模式运行,进入到 BeanUtils.copyProperties 工具方法内部,得到以下数据:

原来, UserDO 类的 getId 方法返回类型不是 Long类型,而是被泛型还原成了 Object 类型。而下面的 ClassUtils.isAssignable 工具方法,判断是否能够把 Object 类型赋值给 Long 类型,当然会返回 false 导致不能进行属性拷贝。

为什么作者不考虑"先获取属性值,再判断能否赋值”?建议代码如下:

Object value = readMethod.invoke(source);if (Objects.nonNull(value) && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], value.getClass())) {  ... // 赋值相关代码}

4.3.避坑方法

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

Java 中,存在很多第三方工具包,比如:Apachecommons-lang3commons-collectionsGoogleguava …… 都是很好用的第三方工具包。但是,不要盲目地相信第三方工具包,任何工具包都有可能存在问题。

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

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

一定要进行单元测试,一定要对比数据内容

在编写完代码后,一定要进行单元测试,一定要对比数据内容。切莫想当然地认为:工具包很成熟、代码也很简单,不可能出现问题。

5.Set对象排重

Java 语言中, Set 数据结构可以用于对象排重,常见的 Set 类有 HashSetLinkedHashSet 等。

5.1.问题现象

编写了一个城市辅助类,从 CSV 文件中读取城市数据:

@Slf4jpublic class CityHelper {      public static void main(String[] args) {       Collection<City> cityCollection = readCities2("cities.csv");       log.info(JSON.tojsONString(cityCollection));  }   public static Collection<City> readCities(String fileName) {       try (FileInputStream stream = new FileInputStream(fileName);           InputStreamReader reader = new InputStreamReader(stream, "GBK");           CSVParser parser = new CSVParser(reader, CSVFORMat.DEFAULT.withHeader())) {           Set<City> citySet = new HashSet<>(1024);           Iterator<CSVRecord> iterator = parser.iterator();           while (iterator.hasNext()) {               citySet.add(parseCity(iterator.next()));          }           return citySet;      } catch (IOException e) {           log.warn("读取所有城市异常", e);      }       return Collections.emptyList();  }   private static City parseCity(CSVRecord record) {       City city = new City();       city.setCode(record.get(0));       city.setName(record.get(1));       return city;  }      @Getter   @Setter   @ToString   private static class City {              private String code;              private String name;  }}

代码中使用 HashSet 数据结构,目的是为了避免城市数据重复,对读取的城市数据进行强制排重。

当输入文件内容如下时:

编码,名称010,北京020,广州010,北京

解析后的 JSON 结果如下:

[{"code":"010","name":"北京"},{"code":"020","name":"广州"},{"code":"010","name":"北京"}]

但是,并没有对城市“北京”进行排重。

5.2.问题分析

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

分析上面的问题,由于没有重写 City 类的 hashCode 方法和 equals 方法,就会采用 Object 类的 hashCode 方法和 equals 方法。其实现如下:

public native int hashCode();public boolean equals(Object obj) {   return (this == obj);}

可以看出:Object 类的 hashCode 方法是一个本地方法,返回的是对象地址;Object 类的 equals 方法只比较对象是否相等。所以,对于两条完全一样的北京数据,由于在解析时初始化了不同的 City 对象,导致 hashCode 方法和 equals 方法值都不一样,必然被 Set 认为是不同的对象,所以没有进行排重。

那么,我们就重写把 City 类的hashCode 方法和 equals 方法,代码如下:

@Getter@Setter@ToStringprivate static class City {      private String code;      private String name;      @Override   public boolean equals(Object obj) {       if (obj == this) {           return true;      }       if (Objects.isNull(obj)) {           return false;      }       if (obj.getClass() != this.getClass()) {           return false;      }       return Objects.equals(this.code, ((City)obj).code);  }      @Override   public int hashCode() {       return Objects.hashCode(this.code);  }}

重新支持测试程序,解析后的JSON结果如下:

[{"code":"010","name":"北京"},{"code":"020","name":"广州"}]

结果正确,已经对城市“北京”进行排重。

5.3.避坑方法

1、当确定数据唯一时,可以使用List代替Set

当确定解析的城市数据唯一时,就没有必要进行排重操作,可以直接使用 List 来存储。

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();

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

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

(推荐微课:Java微课)

6.公有方法代理

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

6.1.问题现象

这里举例一个简单的例子,只有超级用户才有删除公司的权限,并且所有服务函数被 aop 拦截处理异常。例子代码如下:

UserService.java:

@Servicepublic class UserService {      private User superUser;   public void setSuperUser(User superUser) {       this.superUser = superUser;  }      public final User getSuperUser() {       return this.superUser;  }}

CompanyService.java:

@Servicepublic class CompanyService {      @Autowired   private CompanyDAO companyDAO;      @Autowired   private UserService userService;      public void deleteCompany(Long companyId, Long operatorId) {       // 设置超级用户       userService.setSuperUser(new User(0L, "admin", "超级用户"));       // 验证超级用户       if (!Objects.equals(operatorId, userService.getSuperUser().getId())) {           throw new ExampleException("只有超级用户才能删除公司");      }       // 删除公司信息       companyDAO.delete(companyId, operatorId);  }}

AopConfiguration.java:

@Slf4j@Aspect@Configurationpublic class AopConfiguration {      @Around("execution(* org.changyi.SpringBoot.service..*.*(..))")   public Object around(ProceedingJoinPoint joinPoint) {       try {           log.info("开始调用服务方法...");           return joinPoint.proceed();      } catch (Throwable e) {           log.error(e.getMessage(), e);           throw new ExampleException(e.getMessage(), e);      }  }}

当我们调用 CompanyService的deleteCompany 方法时,居然也抛出空指针异常(NullPointerException),因为调用 UserService 类的 getSuperUser 方法获取的超级用户为 null 。但是,我们在 CompanyService 类的 deleteCompany 方法中,每次都通过 UserService 类的 setSuperUser 方法强制指定了超级用户,按道理通过 UserService 类的 getSuperUser 方法获取到的超级用户不应该为 null 。其实,这个问题也是由 AOP 代理导致的。

6.2.问题分析

使用SpringCGLIB代理类时,Spring会创建一个名为 UserService$$EnhancerBySpringCGLIB$$????????的代理类。反编译这个代理类,得到以下主要代码:

public class UserService$$EnhancerBySpringCGLIB$$a2c3b345 extends UserService implements SpringProxy, Advised, Factory {  ......   public final void setSuperUser(User var1) {       MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;       if (var10000 == null) {           CGLIB$BIND_CALLBACKS(this);           var10000 = this.CGLIB$CALLBACK_0;      }       if (var10000 != null) {           var10000.intercept(this, CGLIB$setSuperUser$0$Method, new Object[]{var1}, CGLIB$setSuperUser$0$Proxy);      } else {           super.setSuperUser(var1);      }  }  ......}

可以看出,这个代理类继承了 UserService 类,代理了 setSuperUser 方法,但是没有代理 getSuperUser 方法。所以,当我们调用 setSuperUser 方法时,设置的是原始对象实例的 superUser 字段值;而当我们调用 getSuperUser 方法时,获取的是代理对象实例的 superUser 字段值。如果把这两个方法的 final 修饰符互换,同样存在获取超级用户为 null 的问题。

6.3.避坑方法

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

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

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

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

7.公有字段代理

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

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

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

7.1.问题现象

仍然使用上章的例子,但是把获取、设置方法删除,定义了一个公有字段。例子代码如下:

UserService.java

@Servicepublic class UserService {      public final User superUser = new User(0L, "admin", "超级用户");  ......}

CompanyService.java

@Servicepublic class CompanyService {      @Autowired   private CompanyDAO companyDAO;      @Autowired   private UserService userService;      public void deleteCompany(Long companyId, Long operatorId) {       // 验证超级用户       if (!Objects.equals(operatorId, userService.superUser.getId())) {           throw new ExampleException("只有超级用户才能删除公司");      }       // 删除公司信息       companyDAO.delete(companyId, operatorId);  }}

AopConfiguration.java

同上一章 AopConfiguration.java

当我们调用 CompanyService的deleteCompany 方法时,居然抛出空指针异常(NullPointerException)。经过调试打印,发现是 UserServicesuperUser 变量为null。如果把AopConfiguration删除,就不会出现空指针异常,说明这个问题是由AOP代理导致的。

7.2.问题分析

使用 SpringCGLIB 代理类时, Spring 会创建一个名为 UserService$$EnhancerBySpringCGLIB$$????????的代理类。这个代理类继承了 UserService 类,并覆盖了 UserService 类中的所有非 finalpublic 的方法。但是,这个代理类并不调用 super 基类的方法;相反,它会创建的一个成员 userService 并指向原始的 UserService 类对象实例。现在,内存中存在两个对象实例:一个是原始的 UserService 对象实例,另一个指向 UserService 的代理对象实例。这个代理类只是一个虚拟代理,它继承了 UserService 类,并且具有与 UserService 相同的字段,但是它从来不会去初始化和使用它们。所以,一但通过这个代理类对象实例获取公有成员变量时,将返回一个默认值 null

7.3.避坑方法

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

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

@Servicepublic class UserService {      public static final User SUPER_USER = new User(0L, "admin", "超级用户");  ......}if (!Objects.equals(operatorId, UserService.SUPER_USER.getId())) {   throw new ExampleException("只有超级用户才能删除公司");}

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

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

@Servicepublic class UserService {      private User superUser = new User(0L, "admin", "超级用户");      public User getSuperUser() {       return this.superUser;  }  ......}if (!Objects.equals(operatorId, userService.getSuperUser().getId())) {   throw new ExampleException("只有超级用户才能删除公司");}

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

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

(1)JavaBean类必须是一个公共类,并将其访问属性设置为public,如:public class User{......}

(2)JavaBean类必须有一个空的构造函数:类中必须有一个不带参数的公用构造器

(3)一个JavaBean类不应有公共实例变量,类变量都为private,如:private Integer id;

(4)属性应该通过一组getter/setter方法来访问。

以上就是“Java中常见的坑有哪些”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注编程网精选频道。

--结束END--

本文标题: Java中常见的坑有哪些

本文链接: https://www.lsjlt.com/news/309865.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Java中常见的坑有哪些
    今天小编给大家分享一下Java中常见的坑有哪些的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。1.前言同一个代码“坑”,踩第一...
    99+
    2023-06-27
  • Java中ReentrantLock常见的坑有哪些
    这篇文章主要介绍“Java中ReentrantLock常见的坑有哪些”,在日常操作中,相信很多人在Java中ReentrantLock常见的坑有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java中Re...
    99+
    2023-06-30
  • Java Stream流式编程常见的坑有哪些
    本篇内容介绍了“Java Stream流式编程常见的坑有哪些”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!...
    99+
    2022-10-19
  • java中的常见异常有哪些
    1、java.lang.nullpointerexception这个异常大家肯定都经常遇到,异常的解释是"程序遇上了空指针",简单地说就是调用了未经初始化的对象或者是不存在的对象,这个错误经常出现在创建图片,调用数组这些操作中,比如图片未经...
    99+
    2015-01-05
    java基础 java 常见 异常
  • Java中有哪些常见的异常
    这篇文章主要介绍“Java中有哪些常见的异常”,在日常操作中,相信很多人在Java中有哪些常见的异常问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java中有哪些常见的异常”的疑惑有所帮助!接下来,请跟着小编...
    99+
    2023-06-03
  • C++构造函数常见的坑有哪些
    今天就跟大家聊聊有关C++构造函数常见的坑有哪些,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。某一天我们接到了一个需求,需要开发一个类似于STL中string的类。我们很快写好了代码...
    99+
    2023-06-22
  • java中常见的锁有哪些
    java中常见的锁有:1.乐观锁;2.悲观锁;3.自旋锁;4.偏向锁;5.公平锁;java中常见的锁有以下几种乐观锁java中乐观锁是一种乐观思想,总认为资源和数据不会被修改,并不会对数据进行上锁,但进行写入操作的时会判断数据是否被修改。悲...
    99+
    2022-10-12
  • Java中ReentrantLock4种常见的坑
    目录前言Lock 简介ReentrantLock 使用ReentrantLock 中的坑1.ReentrantLock 默认为非公平锁2.在 finally 中释放锁3.锁不能被释放...
    99+
    2022-11-13
  • 常见的Java异常有哪些
    本篇文章为大家展示了常见的Java异常有哪些,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。异常是 Java 程序中经常遇到的问题,我想每一个 Java 程序员都讨厌异常,一 个异常就是一个 BUG,...
    99+
    2023-06-16
  • java的常见异常有哪些
    1、NullPointerException:空指针异常,当操作一个 null 对象的方法或属性时会抛出这个异常。是一个很头疼的异常,因为它是运行时异常,不需要手动捕获,但运行时碰到这个异常会中断程序。 (推荐学习:j...
    99+
    2018-01-05
    java入门 java
  • Java常见异常有哪些
    这篇文章主要介绍Java常见异常有哪些,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!一、概述异常指不期而至的各种状况,它在程序运行的过程中发生。作为开发者,我们都希望自己写的代码 永远都不会出现 bug,然而现实告诉...
    99+
    2023-06-15
  • 在java中常见的异常类有哪些
    java中常见的异常类有:NullPointerException:当应用程序试图访问空对象时,则抛出该异常。SQLException:提供关于数据库访问错误或其他错误信息的异常。IndexOutOfBoundsException:指示某排...
    99+
    2022-10-05
  • Java中有哪些常见的语法糖
    Java中有哪些常见的语法糖,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。语法糖语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Pe...
    99+
    2023-06-16
  • java中常见的几种锁有哪些
    公平锁/非公平锁公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,有可能,会造成优先级反转或者饥饿现象。独享锁/共享锁独享锁是指该锁一次只能被一...
    99+
    2017-11-14
    java入门 java 常见
  • 存储方面,Java 和 JavaScript 有哪些常见的坑点需要避免?
    在软件开发中,存储是一个非常重要的方面。在使用Java和JavaScript进行开发时,存在许多常见的存储坑点,需要我们避免。在本文中,我们将介绍Java和JavaScript中存在的一些常见的存储坑点,并提供相应的解决方案。 一、Java...
    99+
    2023-08-05
    存储 面试 javascript
  • Java中StringBuilder()常见方法有哪些
    在Java中,StringBuilder类提供了多个常见的方法用于字符串的操作,以下是一些常用的方法:1. append(Strin...
    99+
    2023-09-13
    Java
  • Java常见问题有哪些
    这篇文章给大家分享的是有关Java常见问题有哪些的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。问: 如何设置Java 2(JDK1.2)的环境变量    答: Java 2安装后,需要设置PATH和JA...
    99+
    2023-06-03
  • Java CPP的坑有哪些
    这篇文章主要讲解了“Java CPP的坑有哪些”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java CPP的坑有哪些”吧!1.分清楚System.load与System.loadLibra...
    99+
    2023-06-04
  • java常见异常类型有哪些
    Java常见的异常类型包括:1. NullPointerException(空指针异常):当引用变量为null时,使用该变量进行操作...
    99+
    2023-09-27
    java
  • Java 中有哪些常见的进制数值常量
    今天就跟大家聊聊有关Java 中有哪些常见的进制数值常量,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。package cn.nxl2018;class Test{...
    99+
    2023-06-06
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作