原文:https://mp.weixin.qq.com/s/yDFurUS3HXAZsDfoEQkzSQ前端
中國有句老話叫"事不過三",指一我的犯了一樣的錯誤,一次兩次三次還能夠原諒,超過三次就不可原諒了。有人指出這個「三」是虛數,用來泛指屢次,因此"事不過三"不包括「三」。至於"事不過三"包不包括「三」,可能跟每一個人的底線有關係,屬於哲學範疇,不在本文的討論範圍以內。java
寫代碼也是如此,同一個代碼「坑」,踩第一次叫"長了經驗",踩第二次叫"加深印象",踩第三次叫"不長心眼",踩三次以上就叫"不可救藥"。在本文中,筆者總結了一些代碼坑,描述了問題現象,進行了問題分析,給出了避坑方法。但願你們在平常編碼中,遇到了這類代碼坑,可以提早避讓開來。程序員
JDK1.7 提供的 Objects.equals 方法,很是方便地實現了對象的比較,有效地避免了繁瑣的空指針檢查。面試
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複製代碼
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複製代碼
經過反編譯第一段代碼,咱們獲得語句"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]複製代碼
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]複製代碼
在 Java 語言中,整數的默認數據類型是 int ,小數的默認數據類型是 double 。
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}複製代碼
public boolean equals(Object obj) {
if (obj instanceof Short) {
return value == ((Short)obj).shortValue();
}
return false;
}複製代碼
(1)保持良好的編碼習慣,避免數據類型的自動轉化
spring
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複製代碼
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複製代碼
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)進行常規性單元測試,儘可能把問題發如今研發階段
編程
三元表達式是 Java 編碼中的一個固定語法格式:「條件表達式?表達式 1 :表達式 2 」。三元表達式的邏輯爲:「若是條件表達式成立,則執行表達式 1 ,不然執行表達式 2 」。
json
boolean condition = false;
Double value1 = 1.0D;
Double value2 = 2.0D;
Double value3 = null;
Double result = condition ? value1 * value2 : value3; // 拋出空指針異常複製代碼
經過反編譯代碼,咱們獲得語句"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]複製代碼
若兩個表達式類型相同,返回值類型爲該類型; 若兩個表達式類型不一樣,但類型不可轉換,返回值類型爲Object類型; 若兩個表達式類型不一樣,但類型能夠轉化,先把包裝數據類型轉化爲基本數據類型,而後按照基本數據類型的轉換規則(byte<short(char)<int<long<float<double)來轉化,返回值類型爲優先級最高的基本數據類型。
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;複製代碼
(1)儘可能避免使用三元表達式,能夠採用 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;
}複製代碼
boolean condition = false;
double value1 = 1.0D;
double value2 = 2.0D;
double value3 = 3.0D;
double result = condition ? value1 * value2 : value3;複製代碼
Java 泛型是 JDK1.5 中引入的一個新特性,其本質是參數化類型,即把數據類型作爲一個參數使用。
在作用戶數據分頁查詢時,由於筆誤編寫了以下代碼:
/** 分頁數據VO類 */
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class PageDataVO<T> {
/** 總共數量 */
private Long totalCount;
/** 數據列表 */
private List<T> dataList;
}複製代碼
/** 用戶DAO接口 */
@Mapper
public interface UserDAO {
/** 統計用戶數量 */
public Long countUser(@Param("query") UserQueryVO query);
/** 查詢用戶信息 */
public List<UserDO> queryUser(@Param("query") UserQueryVO query);
}複製代碼
/** 用戶服務類 */
@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);
}
}複製代碼
/** 用戶控制器類 */
@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);
}
}複製代碼
因爲歷史緣由,參數化類型和原始類型須要兼容。咱們以 ArrayList 舉例子,來看看如何兼容的。
ArrayList list = new ArrayList();複製代碼
ArrayList<String> list = new ArrayList<String>();複製代碼
// 第一種狀況
ArrayList list1 = new ArrayList<String>();
// 第二種狀況
ArrayList<String> list2 = new ArrayList();複製代碼
最終的效果就是:咱們神奇地把 List<UserDO> 對象賦值給了 List<UserVO> 。
(1)在初始化泛型對象時,推薦使用 diamond 語法
【推薦】集合泛型定義時,在 JDK7 及以上,使用 diamond 語法或全省略。說明:菱形泛型,即 diamond,直接使用<>來指代前邊已經指定的類型。正例:// <> diamond 方式 HashMap<String, String> userCache = new HashMap<>(16); // 全省略方式 ArrayList<User> users = new ArrayList(10);複製代碼
return new PageDataVO<>(totalCount, dataList);複製代碼
如今,在 Eclipse 的問題窗口中,咱們會看到這樣的錯誤:
Cannot infer type arguments for PageDataVO<>複製代碼
Spring 的 BeanUtils.copyProperties 方法,是一個很好用的屬性拷貝工具方法。
根據數據庫開發規範,數據庫表格必須包含 id,gmt_create,gmt_modified 三個字段。其中, id 這個字段,可能根據數據量不一樣,採用 int 或 long 類型(注意:阿里規範要求必須是 long 類型,這裏爲了舉例說明,容許爲 int 或 long 類型)。
/** 基礎DO類 */
@Getter
@Setter
@ToString
public class BaseDO<T> {
private T id;
private Date gmtCreate;
private Date gmtModified;
}複製代碼
/** 用戶DO類 */
@Getter
@Setter
@ToString
public class UserDO extends BaseDO<Long>{
private String name;
private String description;
}複製代碼
/** 用戶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;
}
}複製代碼
[{"description":"This is a tester.","name":"tester"},...]複製代碼
按道理,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);
}複製代碼
Object value = readMethod.invoke(source);
if (Objects.nonNull(value) && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], value.getClass())) {
... // 賦值相關代碼
}複製代碼
(1)不要盲目地相信第三方工具包,任何工具包都有可能存在問題
在 Java 語言中, Set 數據結構能夠用於對象排重,常見的 Set 類有 HashSet 、 LinkedHashSet 等。
編寫了一個城市輔助類,從 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;
}
}複製代碼
編碼,名稱
010,北京
020,廣州
010,北京複製代碼
[{"code":"010","name":"北京"},{"code":"020","name":"廣州"},{"code":"010","name":"北京"}]複製代碼
可是,並無對城市「北京」進行排重。
當向集合 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);
}複製代碼
那麼,咱們就重寫把 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);
}
}複製代碼
[{"code":"010","name":"北京"},{"code":"020","name":"廣州"}]複製代碼
(1)當肯定數據惟一時,可使用List代替Set
List<City> citySet = new ArrayList<>(1024);
Iterator<CSVRecord> iterator = parser.iterator();
while (iterator.hasNext()) {
citySet.add(parseCity(iterator.next()));
}
return citySet;複製代碼
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方法
SpringCGLIB 代理生成的代理類是一個繼承被代理類,經過重寫被代理類中的非 final 的方法實現代理。因此, SpringCGLIB 代理的類不能是 final 類,代理的方法也不能是final 方法,這是由繼承機制限制的。
這裏舉例一個簡單的例子,只有超級用戶纔有刪除公司的權限,而且全部服務函數被 AOP 攔截處理異常。例子代碼以下:
/** 用戶服務類 */
@Service
public class UserService {
/** 超級用戶 */
private User superUser;
/** 設置超級用戶 */
public void setSuperUser(User superUser) {
this.superUser = superUser;
}
/** 獲取超級用戶 */
public final User getSuperUser() {
return this.superUser;
}
}複製代碼
/** 公司服務類 */
@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);
}
}複製代碼
/** 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);
}
}
}複製代碼
使用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);
}
}
......
}複製代碼
(1)嚴格遵循 CGLIB 代理規範,被代理的類和方法不要加 final 修飾符
在 fastjson 強制升級到 1.2.60 時踩過一個坑,做者爲了開發快速,在 ParseConfig 中定義了:
public class ParseConfig {
public final SymbolTable symbolTable = new SymbolTable(4096);
......
}複製代碼
仍然使用上章的例子,可是把獲取、設置方法刪除,定義了一個公有字段。例子代碼以下:
/** 用戶服務類 */
@Service
public class UserService {
/** 超級用戶 */
public final User superUser = new User(0L, "admin", "超級用戶");
......
}複製代碼
/** 公司服務類 */
@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:
(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("只有超級用戶才能刪除公司");
}複製代碼
/** 用戶服務類 */
@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("只有超級用戶才能刪除公司");
}複製代碼
(1)JavaBean類必須是一個公共類,並將其訪問屬性設置爲public,如:public class User{......}(2)JavaBean類必須有一個空的構造函數:類中必須有一個不帶參數的公用構造器(3)一個JavaBean類不該有公共實例變量,類變量都爲private,如:private Integer id;(4)屬性應該經過一組getter/setter方法來訪問。
人類受益於「類比」思惟,觸類旁通就是人類的智慧,每當遇到新生事物時,人們每每用相似的已知事物做爲參考,可以加速對新生事物的認知。而人類又受制於「定勢」思惟,由於已知事物並不能表明新生事物,而人們又容易造成先入爲主的概念,最終致使對新生事物產生誤判。
這些資料的內容都是面試時面試官必問的知識點,篇章包括了不少知識點,其中包括了有基礎知識、Java集合、JVM、多線程併發、spring原理、微服務、Netty 與RPC 、Kafka、日記、設計模式、Java算法、數據庫、Zookeeper、分佈式緩存、數據結構等等。