原文: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、分布式缓存、数据结构等等。