溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java中常見的坑有哪些

發布時間:2022-01-26 15:34:28 來源:億速云 閱讀:177 作者:iii 欄目:開發技術

今天小編給大家分享一下Java中常見的坑有哪些的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

1.前言

同一個代碼“坑”,踩第一次叫"長了經驗",踩第二次叫"加深印象",踩第三次叫"不長心眼",踩三次以上就叫"不可救藥"。在本文中,筆者總結了一些代碼坑,描述了問題現象,進行了問題分析,給出了避坑方法。希望大家在日常編碼中,遇到了這類代碼坑,能夠提前避讓開來。

1.對象比較方法

JDK1.7  提供的 Objects.equals 方法,非常方便地實現了對象的比較,有效地避免了繁瑣的空指針檢查。

1.1.問題現象

在  JDK1.7  之前,在判斷一個短整型、整型、長整型包裝數據類型與常量是否相等時,我們一般這樣寫:

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

從  JDK1.7 之后,提供了 Objects.equals 方法,并推薦使用函數式編程,更改代碼如下:

Short shortValue = (short)12345;
System.out.println(Objects.equals(shortValue, 12345)); // false
System.out.println(Objects.equals(12345, shortValue)); // false
Integer intValue = 12345;
System.out.println(Objects.equals(intValue, 12345)); // true
System.out.println(Objects.equals(12345, intValue)); // true
Long longValue = 12345L;
System.out.println(Objects.equals(longValue, 12345)); // false
System.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 12345
17  if_icmpne 24
20  iconst_1
21  goto 25
24  iconst_0
25  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 12345
14  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.避坑方法

1、保持良好的編碼習慣,避免數據類型的自動轉化

為了避免數據類型自動轉化,更科學的寫法是直接聲明常量為對應的基本數據類型。

第一段代碼可以這樣寫:

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

第二段代碼可以這樣寫:

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

2、借助開發工具或插件,及早地發現數據類型不匹配問題

Eclipse 的問題窗口中,我們會看到這樣的提示:

Unlikely argument type for equals(): int seems to be unrelated to Short
Unlikely argument type for equals(): Short seems to be unrelated to int
Unlikely argument type for equals(): int seems to be unrelated to Long
Unlikely 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]

3、進行常規性單元測試,盡量把問題發現在研發階段

“勿以善小而不為”,不要因為改動很小就不需要進行單元測試了,往往 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 33
21  aload_2 [value1]
22  invokevirtual java.lang.Double.doubleValue() : double [24]
25  aload_3 [value2]
26  invokevirtual java.lang.Double.doubleValue() : double [24]
29  dmul
30  goto 38
33  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.避坑方法

1、盡量避免使用三元表達式,可以采用 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;
}

2、盡量使用基本數據類型,避免數據類型的自動轉化

如果三元表達式中有算術計算和包裝數據類型,可以考慮利用 if-else 語句代替。改寫代碼如下:

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

3、進行覆蓋性單元測試,盡量把問題發現在研發階段

像這種問題,只要編寫一些單元測試用例,進行一些覆蓋性測試,是完全可以提前發現的。

(推薦教程:Java教程)

3.泛型對象賦值

Java 泛型是 JDK1.5 中引入的一個新特性,其本質是參數化類型,即把數據類型做為一個參數使用。

3.1.問題現象

在做用戶數據分頁查詢時,因為筆誤編寫了如下代碼:

1、PageDataVO.java

/** 分頁數據VO類 */
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class PageDataVO<T> {
   /** 總共數量 */
   private Long totalCount;
   /** 數據列表 */
   private List<T> dataList;
}

2、UserDAO.java

/** 用戶DAO接口 */
@Mapper
public interface UserDAO {
   /** 統計用戶數量 */
   public Long countUser(@Param("query") UserQueryVO query);
   /** 查詢用戶信息 */
   public List<UserDO> queryUser(@Param("query") UserQueryVO query);
}

3、UserService.java

/** 用戶服務類 */
@Service
public class UserService {
   /** 用戶DAO */
   @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);
  }
}

4、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 編譯器對以上兩種類型進行了兼容,不會出現編譯錯誤,但會出現編譯告警。但是,我的開發工具在編譯時真沒出現過告警。

再來分析我們遇到的問題,實際上同時命中了兩種情況:

1、把 List<UserDO> 對象賦值給 List ,命中了第一種情況;

2、把 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.問題現象

根據數據庫開發規范,數據庫表格必須包含 id,gmt_create,gmt_modified 三個字段。其中, id這個字段,可能根據數據量不同,采用 intlong 類型(注意:阿里規范要求必須是 long 類型,這里為了舉例說明,允許為 int 或 long 類型)。

所以,把這三個字段抽出來,定義了一個 BaseDO 基類:

/** 基礎DO類 */
@Getter
@Setter
@ToString
public class BaseDO<T> {
   private T id;
   private Date gmtCreate;
   private Date gmtModified;
}

針對 user 表,定義了一個 UserDO 類:

/** 用戶DO類 */
@Getter
@Setter
@ToString
public class UserDO extends BaseDO<Long>{
   private String name;
   private String description;
}

對于查詢接口,定義了一個 UserVO 類:

/** 用戶VO類 */
@Getter
@Setter
@ToString
public static class UserVO {
   private Long id;
   private String name;
   private String description;
}

實現查詢用戶服務接口,實現代碼如下:

/** 用戶服務類 */
@Service
public class UserService {
   /** 用戶DAO */
   @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.避坑方法

1、不要盲目地相信第三方工具包,任何工具包都有可能存在問題

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

2、如果需要拷貝的屬性較少,可以手動編碼進行屬性拷貝

BeanUtils.copyProperties 反射拷貝屬性,主要優點是節省了代碼量,主要缺點是導致程序性能下降。所以,如果需要拷貝的屬性較少,可以手動編碼進行屬性拷貝。

3、一定要進行單元測試,一定要對比數據內容

在編寫完代碼后,一定要進行單元測試,一定要對比數據內容。切莫想當然地認為:工具包很成熟、代碼也很簡單,不可能出現問題。

5.Set對象排重

Java 語言中, Set 數據結構可以用于對象排重,常見的 Set 類有 HashSet 、 LinkedHashSet 等。

5.1.問題現象

編寫了一個城市輔助類,從 CSV 文件中讀取城市數據:

/** 城市輔助類 */
@Slf4j
public 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
@ToString
private 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();

3、遵循Java語言規范,重寫hashCode方法和equals方法

不重寫hashCode方法和equals方法的自定義類不應該在Set中使用。

(推薦微課:Java微課)

6.公有方法代理

SpringCGLIB 代理生成的代理類是一個繼承被代理類,通過重寫被代理類中的非 final 的方法實現代理。所以, SpringCGLIB  代理的類不能是 final 類,代理的方法也不能是final 方法,這是由繼承機制限制的。

6.1.問題現象

這里舉例一個簡單的例子,只有超級用戶才有刪除公司的權限,并且所有服務函數被 AOP 攔截處理異常。例子代碼如下:

1、UserService.java:

/** 用戶服務類 */
@Service
public class UserService {
   /** 超級用戶 */
   private User superUser;


/** 設置超級用戶 */
   public void setSuperUser(User superUser) {
       this.superUser = superUser;
  }


   /** 獲取超級用戶 */
   public final User getSuperUser() {
       return this.superUser;
  }
}

2、CompanyService.java:

/** 公司服務類 */
@Service
public class CompanyService {
   /** 公司DAO */
   @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);
  }
}

3、AopConfiguration.java:

/** AOP配置類 */
@Slf4j
@Aspect
@Configuration
public 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.避坑方法

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 動態代理了,于是一行代碼引起了一場“血案”。

7.1.問題現象

仍然使用上章的例子,但是把獲取、設置方法刪除,定義了一個公有字段。例子代碼如下:

1、UserService.java

/** 用戶服務類 */
@Service
public class UserService {
   /** 超級用戶 */
   public final User superUser = new User(0L, "admin", "超級用戶");
  ......
}

2、CompanyService.java

/** 公司服務類 */
@Service
public class CompanyService {
   /** 公司DAO */
   @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);
  }
}

3、AopConfiguration.java

同上一章 AopConfiguration.java 。

當我們調用 CompanyService的deleteCompany 方法時,居然拋出空指針異常(NullPointerException)。經過調試打印,發現是 UserServicesuperUser 變量為null。如果把AopConfiguration刪除,就不會出現空指針異常,說明這個問題是由AOP代理導致的。

7.2.問題分析

使用 SpringCGLIB 代理類時, Spring 會創建一個名為 UserService$$EnhancerBySpringCGLIB$$????????的代理類。這個代理類繼承了 UserService 類,并覆蓋了 UserService 類中的所有非 finalpublic 的方法。但是,這個代理類并不調用 super 基類的方法;相反,它會創建的一個成員 userService 并指向原始的 UserService 類對象實例?,F在,內存中存在兩個對象實例:一個是原始的 UserService 對象實例,另一個指向 UserService 的代理對象實例。這個代理類只是一個虛擬代理,它繼承了 UserService 類,并且具有與 UserService 相同的字段,但是它從來不會去初始化和使用它們。所以,一但通過這個代理類對象實例獲取公有成員變量時,將返回一個默認值 null 。

7.3.避坑方法

1、當確定字段不可變時,可以定義為公有靜態常量

當確定字段不可變時,可以定義為公有靜態常量,并用類名稱+字段名稱訪問。類名稱+字段名稱訪問公有靜態常量,與類實例的動態代理無關。

/** 用戶服務類 */
@Service
public class UserService {
   /** 超級用戶 */
   public static final User SUPER_USER = new User(0L, "admin", "超級用戶");
  ......
}


/** 使用代碼 */
if (!Objects.equals(operatorId, UserService.SUPER_USER.getId())) {
   throw new ExampleException("只有超級用戶才能刪除公司");
}

2、當確定字段不可變時,可以定義為私有成員變量

當確定字段不可變時,可以定義為私有成員變量,提供一個公有方法獲取該變量值。當該類實例被動態代理時,代理方法會調用被代理方法,從而返回被代理類的成員變量值。

/** 用戶服務類 */
@Service
public 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("只有超級用戶才能刪除公司");
}

3、遵循 JavaBean 編碼規范,不要定義公有成員變量

遵循 JavaBean 編碼規范,不要定義公有成員變量。JavaBean 規范如下:

(1)JavaBean類必須是一個公共類,并將其訪問屬性設置為public,如:public class User{......}

(2)JavaBean類必須有一個空的構造函數:類中必須有一個不帶參數的公用構造器

(3)一個JavaBean類不應有公共實例變量,類變量都為private,如:private Integer id;

(4)屬性應該通過一組getter/setter方法來訪問。

以上就是“Java中常見的坑有哪些”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女