# code-optimization **Repository Path**: cwbshare/code-optimization ## Basic Information - **Project Name**: code-optimization - **Description**: 代码走查及代码优化 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2019-07-09 - **Last Updated**: 2024-09-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 项目中代码问题及优化 # FindBugs-IDEA插件 使用IDEA的FindBugs-IDEA插件检查出我们开发项目过程中存在的问题,从案例分析开始,最后给出修复或优化方案。 * (1)插件的安装可以参考:https://blog.csdn.net/feibendexiaoma/article/details/72821781 * (2)FindBugs查找出的代码问题分为如下九类: | NO | **bug** | 问题说明 | | -------------------------------- | ------------ | ------------ | | 1 | **Bad practice** | 不好的习惯 | | 2 | **Correctness** | 代码的正确性 | | 3 | **Dodgy** | 小问题 | | 4 | **Malicious code vulnerability** | 恶意代码 | | 5 | **Internationalization** | 国际化问题 | | 6 | **Performance** | 性能问题 | | 7 | **Security** | 安全性问题 | | 8 | **Multithreaded currectness** | 线程问题 | | 9 | **Experrimental** | 实验性问题 | 下面就从这九个方面进行案例分析。 # 1 不良习惯(Bad practice Warnings) ## Comparison of String parameter using == or != ### 案例 ``` java public static MessageOperateTypeEnum getEnumeById(String code) { MessageOperateTypeEnum[] enumArray = MessageOperateTypeEnum.values(); for (MessageOperateTypeEnum enums : enumArray) { if (code == enums.getCode()) { return enums; } } return null; } ``` ```java if (gridSubmitDTO.getPictureId() == "" || gridSubmitDTO.getGridCoordinates() == null) { throw new BizException(CommonError.PARAMS_ERROR); } ``` ```java if (null != seachObj.get("keyWord") && seachObj.get("keyWord").toString() != "") { keyword = seachObj.get("keyWord").toString(); } ``` ```java if (messageOperateObjectEnum.getCode() == MessageOperateObjectEnum.GRID_RELATION_QUESTION_PICTURE.getCode()) { saveSendMessageLog(id, cooperator.getId(), SendDataTypeEnum.GRID_DATA.getType()); } ``` ### 分析 String类型的比较不能使用==或!=比较。如下: ```java public class StringTest { public static void main(String[] argv){ String s = new String(""); if(s != ""){ System.out.println("不相等"); } if(s.equals("")){ System.out.println("相等"); } } } ``` 输出结果: ```java 相等 不相等 ``` ### 改进 改用equals方法进行串的比较。 --- ## Non-transient non-serializable instance field in serializable class ### 案例 ```java @Data public class AllAssistantBookForWordPluginDTO implements Serializable { /** * 学段列表 */ List stageList; /** * 学科列表 */ List subjectList; /** * 教辅列表 */ List assistantBookList; } ``` ### 分析 在序列化的类中有非序列化的对象,并且没有对非序列化对象标注Transient ,这样做的后果是这个类的实列不能被序列化。 ### 改进 这里的SubjectForWordPluginDTO没有序列化,增加序列化代码即可: ```java public class SubjectForWordPluginDTO implements Serializable { .... } ``` ### 扩展 (1)序列化的目的 ​ 序列化就为了将对象存在磁盘,或从磁盘恢复回来,或用于网络传输,或用于缓存,如果对象中存在非序列化的对象,那这个非序列化的对象不能被序列化出去。 (2)serialVersionUID的作用 ​ serialVersionUID就是对象序列化的身份证号,序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。 ​ serialVersionUID有两种显示的生成方式: 一是默认的1L,比如: ```java private static final long serialVersionUID = 1L; ``` 二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如: ```java private static final long serialVersionUID = -2696291713632457436L; ``` 这两种都是指定版本号的方式,反序列化时只要serialVersionUID没有人为改变,就是同一个对象,能序列化成功,当写serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果Class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的,一量类有变化,再反序化原来的对象就会报错,因为反序列化时最先比较的版本号已经不一致,故报错。 --- ## Class defines compareTo(...) and uses Object.equals() ### 案例 ```java public class GridSubmitDTO implements Comparable { private String pictureId; private GridDTO gridCoordinates; private String gridId; private Integer spreadFlag; private Integer classId; private Integer type; @Override public int compareTo(GridSubmitDTO dto) { float a = this.getGridCoordinates().getStartY() - dto.getGridCoordinates().getStartY(); if (a > 0) { return 1; } else if (a < 0) { return -1; } else { float b = this.getGridCoordinates().getStartX() - dto.getGridCoordinates().getStartX(); if (b > 0) { return 1; } else { return -1; } } } } ``` ### 分析 GridSubmitDTO类实现了Comparable接口,但却使用了父类(java.lang.Object类)的equals方法,没有覆盖父类的实现,违反了接口的使用规则。 没有重写equals方法的都会直接使用Object的equals方法,就是直接的引用地址的比较。所以如果要比较是否相等时,这里要重写equals方法。 重写equals方法又要重写hashCode,hashCode是为了提高比较效率而生,我们可以参考String类的hashCode的写法。所以想重写好equals和hashCode不是这么容易。 ### 改进 添加注解 ```java @EqualsAndHashCode ``` ### 扩展 (1)添加完了@EqualsAndHashCode可以到target文件夹查看到@EqualsAndHashCode是如何生成equals和hashCode的。 ```java public boolean equals(final Object o) { if (o == this) { return true; } else if (!(o instanceof GridSubmitDTO)) { return false; } else { GridSubmitDTO other = (GridSubmitDTO)o; if (!other.canEqual(this)) { return false; } else { Object this$pictureId = this.getPictureId(); Object other$pictureId = other.getPictureId(); if (this$pictureId == null) { if (other$pictureId != null) { return false; } } else if (!this$pictureId.equals(other$pictureId)) { return false; } Object this$gridCoordinates = this.getGridCoordinates(); Object other$gridCoordinates = other.getGridCoordinates(); if (this$gridCoordinates == null) { if (other$gridCoordinates != null) { return false; } } else if (!this$gridCoordinates.equals(other$gridCoordinates)) { return false; } Object this$gridId = this.getGridId(); Object other$gridId = other.getGridId(); if (this$gridId == null) { if (other$gridId != null) { return false; } } else if (!this$gridId.equals(other$gridId)) { return false; } label62: { Object this$spreadFlag = this.getSpreadFlag(); Object other$spreadFlag = other.getSpreadFlag(); if (this$spreadFlag == null) { if (other$spreadFlag == null) { break label62; } } else if (this$spreadFlag.equals(other$spreadFlag)) { break label62; } return false; } label55: { Object this$classId = this.getClassId(); Object other$classId = other.getClassId(); if (this$classId == null) { if (other$classId == null) { break label55; } } else if (this$classId.equals(other$classId)) { break label55; } return false; } Object this$type = this.getType(); Object other$type = other.getType(); if (this$type == null) { if (other$type != null) { return false; } } else if (!this$type.equals(other$type)) { return false; } return true; } } } protected boolean canEqual(final Object other) { return other instanceof GridSubmitDTO; } public int hashCode() { int PRIME = true; int result = 1; Object $pictureId = this.getPictureId(); int result = result * 59 + ($pictureId == null ? 43 : $pictureId.hashCode()); Object $gridCoordinates = this.getGridCoordinates(); result = result * 59 + ($gridCoordinates == null ? 43 : $gridCoordinates.hashCode()); Object $gridId = this.getGridId(); result = result * 59 + ($gridId == null ? 43 : $gridId.hashCode()); Object $spreadFlag = this.getSpreadFlag(); result = result * 59 + ($spreadFlag == null ? 43 : $spreadFlag.hashCode()); Object $classId = this.getClassId(); result = result * 59 + ($classId == null ? 43 : $classId.hashCode()); Object $type = this.getType(); result = result * 59 + ($type == null ? 43 : $type.hashCode()); return result; } ``` (2)java语言规范要求equals方法具有下面的特性: - (1)自反性:对于任何非空引用x,x.equals(x)应该返回true; - (2)对称性:对于任何引用x,和y,当且仅当,y.equals(x)返回true,x.equals(y)也应该返回true; - (3)传递性:对于任何引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true; - (4)一致性:如果x,y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果; - (5)对于任意非空引用x,x.equals(null)返回false; (3)参考了博客 http://www.jackieathome.net/archives/348.html?utm_source=tuicool&utm_medium=referral --- ## compareTo()/compare() incorrectly handles float or double value ### 案例 ```java @Override public int compareTo(GridSubmitDTO dto) { float a = this.getGridCoordinates().getStartY() - dto.getGridCoordinates().getStartY(); if (a > 0) { return 1; } else if (a < 0) { return -1; } else { float b = this.getGridCoordinates().getStartX() - dto.getGridCoordinates().getStartX(); if (b > 0) { return 1; } else { return -1; } } } ``` ### 分析 (1)浮点型数值比较潜在的最大问题是精度丢失,float在JAVA中使用的是科学计数法,使用四个字节(32位)来存储,有一位为符号位,有八位为指数位,剩下23位为基数部分,所以float数虽然能表示很大范围的,但精度有可能丢失。 (2)由于精度问题,double/float比较相等不能直接使用==,但是比较大小可以用<、 >号 ### 改进 ```java @Override public int compareTo(GridSubmitDTO dto) { return Float.compare(getGridCoordinates().getStartY(), dto.getGridCoordinates().getStartY()); } ``` ### 扩展 查看Float.compareTo源码,更加理解浮点数的比较方式。 --- ## Method names should start with a lower case letter ### 案例 ```java @ApiOperation("获取系统角色") @GetMapping("/common/role/list") public ApiResponse RoleList() { ApiResponse apiResponse = new ApiResponse(); Platform platform = platformService.selectByName(platformName); com.sunducation.seac.common.dto.PageDTO pageDTO = roleService.fetchRoleList(platform.getId(), 0L, 1L, 0, 100000, null); apiResponse.setData(pageDTO.getContent()); return apiResponse; } ``` ### 分析 方法名,变量名,参数名为驼峰结构,首字母要小写。 ### 改进 方法名改为roleList --- # 2 代码的正确性(Correctness Warnings) ## Call to equals() comparing different types ### 案例 ```java Integer parentsId .... if (parentsId == null || parentsId.equals("")) { //新增seac用户,状态为禁用状态 User user = new User(); user.setUsername(parents1.getPhone()); user.setName(parents1.getPhone()); user.setPassword(MD5.getMD5(DEFAULT_PASSWORD)); user.setPhone(parents1.getPhone()); user.setGmtCreate(new Date()); user.setIsDelete(IsDeleteEnum.NORMAL.getId()); user.setStatus(UserStatusEnum.DISABLE.getId()); user.setPlatformId(PARENTS_PLATFORMID); Long seacId = userService.addUser(user); parents.setSeacId(seacId); parents.setMainChildId(busStudent.getId()); parents.setUserName(parents1.getPhone()); parents.setPhone(parents1.getPhone()); parents.setNickname(parents1.getUserName()); parents.setPassword(MD5.getMD5(DEFAULT_PASSWORD)); parents.setStatus(StudentStatusEnum.NOMAL.getCode()); parents.setGmtCreate(new Date()); parentsMapper.insert(parents); //发送消息新增家长信息 List afterList = new ArrayList<>(); Map body = new HashMap<>(); List studentIds = new ArrayList<>(); studentIds.add(busStudent.getId()); body.put("id", parents.getId()); body.put("name", parents.getNickname()); body.put("userName", parents.getPhone()); body.put("phone", parents.getPhone()); body.put("password", parents.getPassword()); body.put("status", 1); body.put("studentIds", studentIds); afterList.add(body); sendMessage.sendMsgOfAsyncData(MessageOperateTypeEnum.ADD, MessageOperateObjectEnum.PARENTS, null, afterList, "新增家长", SendDataTypeEnum.USER_DATA, studentAndParentDTO.getSchoolId()); } ``` ### 分析 除了串类型,不要什么类型都用习惯性的跟空串比较。不同类型进行比较永远是false ### 改进 删除判断条件parentsId.equals("") --- ## Non-virtual method call passes null for non-null parameter ### 案例 ```java public class ApiResponse implements Serializable { private int code = 0; private String msg = "ok"; private Object data; public ApiResponse() { } public ApiResponse(Object data) { this.data = data; } public ApiResponse(CommonError error) { this.code = error.getId(); this.msg = error.getMsg(); } .... } ``` TaskController类使用ApiResponse: ```java return new ApiResponse(null); ``` ### 分析 因为重载的方法执行,优先执行子类,所以这里会调用到 ```java public ApiResponse(CommonError error) ``` 就会出现空指针异常。 使用简单示例来理解: ```java public class ParamTest { public void mathodChoose(String name){ System.out.println("类型类型是String"); } public void mathodChoose(Object name){ System.out.println("类型类型是Object"); } public static void main(String[] argv){ ParamTest paramTest = new ParamTest(); paramTest.mathodChoose(null); } } ``` 输出结果为: ``` 类型类型是String ``` ### 改进 ```java return new ApiResponse(); ``` --- ## Suspicious reference comparison ### 案例 ```java if (partDTO.getParentId() == id) { //把题目挂到子目录上面 List paperQuestionList = fetchQuestionList(assistantBookId, paperId, partDTO.getId()); partDTO.setQuestionList(paperQuestionList); childList.add(partDTO); } ``` ### 分析 getParentId()返回值是Integer类型,id也是Integer类型,两对象类型,不能直接使用==来比较,不然比较的是地址。 ### 改进 改用equals方法进行值的比较,而不是引用地址的比较。 ### 扩展 ```java public class IntegerTest { /** * -128~127这256个常用到的值做了缓存,会直接从缓存返回对象回来,再进行地址比较就返回true **/ public static void compare1(){ Integer a = 1; Integer b = 1; System.out.println(a == b); } /** * 大于127后会新建一个整形对象,再进行地址比较就返回false **/ public static void compare2(){ Integer a = 128; Integer b = 128; System.out.println(a == b); } public static void main(String[] agrv){ compare1(); compare2(); } } ``` --- ## "." or "|" used for regular expression ### 案例 ```java String[] nodeName = node.getName().split("|"); ``` ### 分析 split参数是正则参数,对于正则的特殊字符需要转义 ### 改进 ```java String[] nodeName = node.getName().split("\\|"); ``` 这里使用两斜杠是因为斜杠也是特殊字符,它先转义,再转义| ### 扩展 正则表达式中的特殊字符 详见:https://www.cnblogs.com/zengzhihua/p/9037856.html ------ ## equals method overrides equals in superclass and may not be symmetric ### 案例 ```java @Data public class AssistantBookReqDTO extends PageInfoDTO implements Serializable { /** * 教师id */ private Integer teacherId; ...... } ``` ### 分析 (1)BUG含义:子类覆盖了父类中的equals方法,而两个类中都是用了instanceof来判断两个对象是否相等,这个操作存在风险。equals方法应该具有对称性(a.equals(b) == b.equals(a)),但是当B是A的子类时,A的equals方法检查参数是A的实例,B的equals方法检查参数是B的实例,则这些方法定义的equal关系很可能不是对称的。 (2)主要问题:@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集。当使用@Data注解时,则有了@EqualsAndHashCode注解,那么就会在此类中存在equals(Object other)和 hashCode()方法,且不会使用父类的属性,这就导致了可能的问题。如不示例: ```java @Data @NoArgsConstructor @AllArgsConstructor public class TV { /** * 编号 */ private Integer id; /** * 名称 */ private String name; } ``` ``` java @Data @NoArgsConstructor @AllArgsConstructor public class XiaoMiTV extends TV { /** * 价格 */ private float price; /** * 颜色 */ private String color; public XiaoMiTV(Integer id , String name, float price, String color){ super(id, name); this.price = price; this.color = color; } } ``` ```java public class EqualsAndHashCodeTest { public static void main(String[] args) { XiaoMiTV tv1 = new XiaoMiTV(123, "小米", 1f, "白"); XiaoMiTV tv2 = new XiaoMiTV(456, "中米", 1f, "白"); /** * 当XiaoMiTV类添加@EqualsAndHashCode(callSuper = true)输出正确结果false * 否则结果为true,即两对象相等 */ System.out.println(tv1.equals(tv2)); } } ``` ### 改进 添加@EqualsAndHashCode(callSuper = true)注解 ```java @Data @EqualsAndHashCode(callSuper = true) public class AssistantBookReqDTO extends PageInfoDTO implements Serializable { /** * 教师id */ private Integer teacherId; ...... } ``` --- ## Method call passes null for non-null parameter ### 案例 ```java @Override public PageDTO ResourceDeveloperList(Map seachObj) throws BizException { Integer type, stageId, subjectId, page, size, status; type = stageId = subjectId = page = size = status = null; String keyword = null; try { if (null != seachObj.get("type")) { type = Integer.parseInt(seachObj.get("type").toString()); } if (null != seachObj.get("stageId")) { stageId = Integer.parseInt(seachObj.get("stageId").toString()); } if (null != seachObj.get("keyWord") && seachObj.get("keyWord").toString() != "") { keyword = seachObj.get("keyWord").toString(); } if (null != seachObj.get("subjectId")) { subjectId = Integer.parseInt(seachObj.get("subjectId").toString()); } if (null != seachObj.get("status")) { status = Integer.parseInt(seachObj.get("status").toString()); } if (null != seachObj.get("page")) { page = Integer.parseInt(seachObj.get("page").toString()); } if (null != seachObj.get("pageSize")) { size = Integer.parseInt(seachObj.get("pageSize").toString()); } } catch (Exception e) { throw new BizException(CommonError.PARAMS_ERROR); } return selestList(status, type, stageId, keyword, subjectId, page, size); } ``` ```java @Override public PageDTO selestList(Integer status, Integer type, Integer stageId, String keyword, Integer subjectId, Integer page, Integer pageSize) throws BizException { PageDTO result = new PageDTO(); Page queryPage = PageHelper.startPage(page, pageSize); try { keyword = SearchKeywordFilterUtil.filter(keyword); resourceDeveloperMapper.selestList(status, type, stageId, keyword, subjectId); } catch (BizException e) { throw new BizException(CommonError.DATABASE_ERROR); } result.setContent(queryPage.getResult()); result.setTotalPage(queryPage.getPages()); result.setPage(queryPage.getPageNum()); result.setCount(queryPage.getTotal()); result.setSize(queryPage.getPageSize()); return result; } ``` ### 分析 调用方法selestList里,参数都可能为空,当page或pageSize为空时,就会抛异常,因为 ```java PageHelper.startPage(page, pageSize) ``` 参数类型是int ### 改进 将会引起异常的Integer参数,一定要判断值存在才往下走,不然直接返回。 --- ## Nullcheck of value previously dereferenced ### 案例 ```java Sheet sheet = wb.getSheetAt(0); int rowCount = sheet.getLastRowNum(); System.err.println("行:" + rowCount); if (sheet == null || rowCount < 0) { response.setCode(1); response.setMsg("excel不能为空!"); return response; } ``` ### 分析 sheet变量需要在使用的时候判断是否为空。 ### 改进 判空条件放在变量定义之后。 --- ## Invocation of toString on an array ### 案例 ```java int[] coordinates = ImageUtil.findGridCoordinatesBySmallPic(grid, bigImage, 0, 0); System.out.println(coordinates); ``` ### 分析 对数组调用toString()方法。代码在对数组调用toString()方法时,将产生一个相当无用的形如 [C@16f0472 的结果。考虑使用 Arrays.toString方法 ### 改进 ```java Arrays.asList(coordinates).toString() ``` --- # 3 小问题(Dodgy code Warnings) ## Dead store to local variable ### 案例 ```java @Override public DataSendAssistantBookDTO getAssistantBookDTO(Integer assistantId) { AssistantBook book = assistantBookService.getById(assistantId); if (book == null) { throw new BizException("根据教辅id[" + assistantId + "]未找到教辅"); } List paperCooperators = new ArrayList<>(); QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("assistant_book_id", assistantId); queryWrapper.eq("type", AssistantBookCooperatorTypeEnum.QUESTION_DATA.getType()); List cooperators = relationAssistantBookCooperatorService.list(queryWrapper); if (CollectionUtils.isEmpty(cooperators)) return null; DataSendAssistantBookDTO result = new DataSendAssistantBookDTO(); result.setAssistantId(assistantId); result.setAssistantName(book.getName()); List content = assistantBookService.fetchCanSendMessagePaperList(assistantId); if (CollectionUtils.isEmpty(content)) { return null; } result.setContent(content); return result; } ``` ### 分析 本地变量存储了闲置不用的对象 ### 改进 删除掉没用到的对象 :paperCooperators ------ ## Code contains a hard coded reference to an absolute pathname ### 案例 ```java File file = new File("/home/notuser/nginx-download-template/class-template.xlsx"); ``` ### 分析 硬编码指向绝对路径 ### 改进 改为配置文件配置,再使用@Value的方式注入 ------ ## Write to static field from instance method ### 案例 ```java @Component public class OssUtil { public static final String accessUrl = "http://xxx.com/"; //bucket名称 private static String bucketName; //文件上传地址 private static String endpoint; //accessKeyId 阿里云连接key private static String accessKeyId; //连接秘钥 private static String accessKeySecret; @Value("${wts.bucketName}") public void setBucketName(String bucketName) { OssUtil.bucketName = bucketName; } .... } ``` ### 分析 不能直接对static成员进行赋值,当创建多个实例时,多个实例调用该方法会造成一些不可预知的隐患。 ### 改进 变量定义为非静态变量,使用配置文件引入。 ------ ## Condition has no effect ### 案例 ```java int questionCount = containFwdSpreadFlag && containSpreadFlag ? count.intValue() + grids.size() - 1 : count.intValue() + grids.size(); ``` ### 分析 containSpreadFlag根据前面条件判断它一定是false,所以这个判断条件一定是false,是一个无效的判断。 ### 改进 去掉containSpreadFlag判断 ------ ## instanceof will always return true ### 案例 ```java public static String filter(String keyword) { if (keyword != null && keyword instanceof String && (keyword.toString().contains("%") || keyword.toString().contains("_"))) { keyword = keyword.toString().replaceAll("%", "/%").replaceAll("_", "/_"); } return keyword; } ``` ### 分析 ``` keyword instanceof String ``` 这个判断条件恒真,多余的判断 ### 改进 干掉keyword instanceof String条件判断 ------ ## Redundant nullcheck of value known to be non-null ### 案例 ```java if (assistantBookId == null || type == null) { throw new BizException(CommonError.PARAMS_ERROR); } if (type != null && type == 2) { gridToolService.deleteAssistantBookPictures(assistantBookId); //训练分割完后设置分割状态为1 gridToolService.updateDivdeStatus(assistantBookId, 0); gridQuestionRelService.updateIsRecognition(assistantBookId, 0); gridToolService.deleteGridRelationQuestionPicture(assistantBookId); } ``` ### 分析 type不可能为空,无效判断。 ### 改进 干掉条件type != null的判断。 ------ ## Exception is caught when Exception is not thrown ### 案例 ```java try { score = Float.valueOf(nodeName[2]); if (score < 0 || score > 1) { throw new BizException("请填写正确分值!"); } } catch (Exception e) { throw new BizException("请填写正确分值!"); } ``` ### 分析 不管什么异常直接使用catche Exception这样很省事,但是JAVA规范中并不推荐这样做,这样是属于“过泛地捕获异常”,因为try{}中可能出现的异常种类有很多,上面的做法不利于分别处理各种异常,建议根据业务需求,分别捕获需要特别处理的异常。 ### 改进 ```java try { score = Float.valueOf(nodeName[2]); if (score < 0 || score > 1) { throw new BizException("请填写正确分值!"); } } catch (NumberFormatException ne) { throw new BizException("请填写正确分值!"); } catch (Exception e) { throw new BizException("请填写正确分值!"); } ``` ------ ## Switch statement found where default case is missing ### 案例 ```java switch (userInfoChangedMqDTO.getType()) { // 处理SEAC添加用户消息 case USER_ADD_MQ_TYPE_ENUM: userDtos.stream().forEach(userdto -> asyncResourceDeveloperService.addResourceDeveloper(userdto)); break; // 处理SEAC修改用户、修改角色、删除角色消息 case USER_INFO_CHANGED_MQ_TYPE_ENUM: case ROLE_DELETE_MQ_TYPE_ENUM: case ROLE_INFO_CHANGED_MQ_TYPE_ENUM: userDtos.stream().forEach(userdto -> asyncResourceDeveloperService.updateResourceDeveloper(userdto)); break; // 处理SEAC用户删除消息 case USER_DELETE_MQ_TYPE_ENUM: List ids = userDtos.stream().map(e -> e.getId().intValue()).collect(Collectors.toList()); asyncResourceDeveloperService.deleteResourceDeveloper(ids); break; } ``` ### 分析 Switch没有默认情况下执行的case语句。 ### 改进 添加上default分支。 ------ ## Useless object created ### 案例 ```java 略 ``` ### 分析 存在没使用到的对象 ### 改进 干掉 --- # 4 恶意代码(Malicious code vulnerability Warnings) ## May expose internal representation by returning reference to mutable object ### 案例 ```java @Data public class GridDataSendDTO { private Integer assistantId; private Integer[] cooperatorIds; } ``` ```java @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("res_assistant_book") public class AssistantBook extends Model { ..... /** * 数据创建时间 */ private Date gmtCreate; } ``` ### 分析 (1)BUG直译:可能因使引用可指向多个对象而暴露内部存储结构。 这代码使一个指向外部多个对象的引用指向了一个内部对象存储地址。 如果实例被未被信任代码访问或多个对象发生了未经检查的改变就会危及安全性或其它重要属性, 你需要去做一些不同的事情。存储一个对象的拷贝在许多情况下会是一个更好的方法。 (2)BUG说明:对象里面存在对象,这些对象的赋值是由@Data产生的set来赋值的,这些当传进来的对象发生变化时,这个对象里的属性也会跟着变化。如下: ```java @Data public class MutableObjectTest { private Integer[] ids; private Date gmtCreate; public static void main(String[] argv){ MutableObjectTest mutableObjectTest = new MutableObjectTest(); Date date = new Date(); mutableObjectTest.setGmtCreate(date); System.out.println(mutableObjectTest.getGmtCreate()); date.setYear(12); System.out.println(mutableObjectTest.getGmtCreate()); Integer[] ids = {1,2,3}; mutableObjectTest.setIds(ids); System.out.println(Arrays.asList(mutableObjectTest.getIds())); ids[1] = 0; System.out.println(Arrays.asList(mutableObjectTest.getIds())); } } ``` 输出结果: ``` Tue Jul 09 14:54:54 CST 2019 Tue Jul 09 14:54:54 CST 1912 [1, 2, 3] [1, 0, 3] ``` 可见结果变改了,存在被恶意篡改的风险。 ### 改进 重写set方法,将对象拷贝再赋值: ```java @Data public class MutableObjectTest2 { private Integer[] ids; private Date gmtCreate; public void setIds(Integer[] ids) { if(ids != null){ this.ids = ids.clone(); } else { this.ids = null; } } public void setGmtCreate(Date gmtCreate) { if(gmtCreate != null){ this.gmtCreate = (Date) gmtCreate.clone(); } else { this.gmtCreate = null; } } public static void main(String[] argv){ MutableObjectTest2 mutableObjectTest = new MutableObjectTest2(); Date date = new Date(); mutableObjectTest.setGmtCreate(date); System.out.println(mutableObjectTest.getGmtCreate()); date.setYear(12); System.out.println(mutableObjectTest.getGmtCreate()); Integer[] ids = {1,2,3}; mutableObjectTest.setIds(ids); System.out.println(Arrays.asList(mutableObjectTest.getIds())); ids[1] = 0; System.out.println(Arrays.asList(mutableObjectTest.getIds())); } } ``` --- # 5 国际化问题(Internationalization Warnings) ## Reliance on default encoding ### 案例 ```java byte[] input = message.getBytes(); ``` ### 分析 String.getBytes()依赖于系统编码,系统的默认编码是不可预知的。避免这个错误,需要将编码指定好,即:String.getBytes("UTF-8");这个getBytes的一个重载方法,可以指定getBytes使用的编码。 ### 改进 ```java byte[] input = message.getBytes("utf-8"); ``` ------ # 6 性能问题(Performance Warnings) ## Boxing/unboxing to parse a primitive ### 案例 ```java String currentPage = data.get("page") == null ? "1" : data.get("page").toString(); String pageSize = data.get("pageSize") == null ? "20" : data.get("pageSize").toString(); Page page = PageHelper.startPage(Integer.valueOf(currentPage), Integer.valueOf(pageSize)); ``` ### 分析 - 这个问题主要涉及到拆箱和封箱知识:java里有8种基本数据类型,4种整型(byte,shot,int,long),2种浮点类型(float,double),1种用于表示Unicode编码的字符单元的字符类型(char)和1种用于表示真值的boolean类型,对应着8种包装类(Byte,Shot,Integer,Long,Float,Double,Character,Boolean)。 - 基本类与包装类之前的转换,就是装包(封箱)或拆包(拆箱)的过程。 - 针对这个案例,Integer.valueOf方法返回的是Integer类型,而PageHelper.startPage要的是int的参数类型,故这里处理逻辑:**String => int => Integer => int** ### 改进 ```java Page page = PageHelper.startPage(Integer.parseInt(currentPage), Integer.parseInt(pageSize)); ``` 目标是减少了不必要的装箱转换 ------ ## Boxed value is unboxed and then immediately reboxed ### 案例 ```java Integer pictureId = gridSubmitDTO.getPictureId(); Map picInfo = gridQuestionRelService.getPicGridInfosByPicId(Integer.valueOf(pictureId)); ``` ```java QuestionDetailDTO questionDetailDTO = questionService.fetchOneQuestionByQuestionId(ParamQuestionSearchTypeEnum.INDEX_PAGE.getCode(), developer.getId().intValue(), developer.getUserName(), questionId, true); ``` ### 分析 装箱的值被拆箱,然后立刻重新装箱 ### 改进 (1) 去掉Integer.valueOf方法 (2)developer.getId().intValue()改为:developer.getId() ------ ## Method invokes inefficient Number constructor; use static valueOf instead ### 案例 ```java List intIds = Arrays.asList(arr).stream().map(e -> new Integer(e)).collect(Collectors.toList()); ``` ### 分析 推荐使用Integer.ValueOf(int)代替new Integer(int),因为这样可以提高性能。如果当你的int值介于-128~127时,Integer.ValueOf(int)的效率比Integer(int)快大约3.5倍,因为从Integer.ValueOf源码可以看出对-128~127这256个值做了缓存(IntegerCache),如果int值的范围是:-128~127,调用Integer.ValueOf方法时,他会直接返回IntegerCache的缓存给你。 ### 改进 ```java List intIds = Arrays.asList(arr).stream().map(e -> Integer.ValueOf(e)).collect(Collectors.toList()); ``` ------ ## Inefficient use of keySet iterator instead of entrySet iterator ### 案例 ```java Map map = new HashMap<>(); .... List list = new ArrayList<>(); for (Integer id : map.keySet()) { list.add(map.get(id)); } ``` ### 分析 很多人都这样遍历Map,效率比较低,先一个一个的把key遍历,然后在根据key去查找value,这完全没有必要,直接遍历entry(桶)然后直接从entry得到value,它们的执行效率大概为2:1。 ### 改进 ```java Map map = new HashMap<>(); .... List list = new ArrayList<>(); //最优遍历方式 for(ChildrenOfParentDTO value:map.values()){ list.add(value); } //次优遍历方式 for(Map.Entry e: map.entrySet()){ list.add(e.getValue()); } ``` ### 扩展 三种遍历方式,耗时测试示例: ```java /** * @description: HashMap遍历取值的方式及性能比较 * 这三个类HashMapTest,HashMapTest1,HashMapTest2,不能放在一起,不然运行的结果会跟调用的遍历的顺序有关,所以才分开成三个类来测试 * 同一个类,多次运行耗时不一样,可能跟运行时散列的内存位置有关 * @author:chenwenbiao * @createTime:2019/7/9 16:48 * @version:1.0 **/ public class HashMapTest { /** * 使用keySet方式遍历 */ public static long keySet(){ HashMap map = new HashMap(); for (int i = 0; i < 1000000; i++) { map.put("" + i, "keySet" + i); } long startTime = System.currentTimeMillis(); Iterator keySetIterator = map.keySet().iterator(); while (keySetIterator.hasNext()) { String key = keySetIterator.next(); String value = map.get(key); // System.out.println("2:" + value); } return System.currentTimeMillis() - startTime; } public static void main(String[] args) { long keySetSpentTimes = keySet(); System.out.println("key set spent times:" + keySetSpentTimes); } } ``` ```java /** * @description: HashMap遍历取值的方式及性能比较 * 这三个类HashMapTest,HashMapTest1,HashMapTest2,不能放在一起,不然运行的结果会跟调用的遍历的顺序有关,所以才分开成三个类来测试 * 同一个类,多次运行耗时不一样,可能跟运行时散列的内存位置有关 * @author:chenwenbiao * @createTime:2019/7/9 16:48 * @version:1.0 **/ public class HashMapTest2 { /** * 使用直接values方式 */ public static long valueSet(){ HashMap map = new HashMap(); for (int i = 0; i < 1000000; i++) { map.put("" + i, "keySet" + i); } long startTime = System.currentTimeMillis(); for (String value:map.values()) { // System.out.println("1:" + value); } return System.currentTimeMillis() - startTime; } public static void main(String[] args) { long valueSpentTimes = valueSet(); System.out.println("value set spent times:" + valueSpentTimes); } } ``` ```java /** * @description: HashMap遍历取值的方式及性能比较 * 这三个类HashMapTest,HashMapTest1,HashMapTest2,不能放在一起,不然运行的结果会跟调用的遍历的顺序有关,所以才分开成三个类来测试 * 同一个类,多次运行耗时不一样,可能跟运行时散列的内存位置有关 * @author:chenwenbiao * @createTime:2019/7/9 16:48 * @version:1.0 **/ public class HashMapTest3 { /** * 使用entrySet方式遍历 */ public static long entrySet(){ HashMap map = new HashMap(); for (int i = 0; i < 1000000; i++) { map.put("" + i, "keySet" + i); } long startTime = System.currentTimeMillis(); Iterator> entryKeyIterator = map.entrySet().iterator(); while (entryKeyIterator.hasNext()) { Map.Entry e = entryKeyIterator.next(); // System.out.println("3:"+ e.getValue()); } return System.currentTimeMillis() - startTime; } public static void main(String[] args) { long entrySetSpentTimes = entrySet(); System.out.println("entry set spent times:" + entrySetSpentTimes); } } ``` 耗时结果: ```java valueset spent times:25 keyset spent times:37 entrySet spent times:26 ``` # 8 线程问题(Multithreaded correctness Warnings) ## Call to static DateFormat ### 案例 ```java @Service public class GridQuestionRelServiceImpl implements GridQuestionRelService { private static SimpleDateFormat simpleDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); ...... /** * 新增格子问题关联关系 * 识别特殊格子坐标 */ private void insertGridRelationQuestionPicture(Integer bookId, Integer bookNodeId, Integer questionId, Integer spreadFlag, Integer pictureId, Integer classId, float startX, float startY, float endX, float endY) throws IOException { GridRelationQuestionPicture grqp = new GridRelationQuestionPicture(); ..... grqp.setGmtCreate(Timestamp.valueOf(simpleDate.format(new Date()))); } ``` ### 分析 DateFormat不是线程安全的,这里定义成静态对象,当多线程情况下使用,就可能发生错误。 ### 改进 去掉static ### 扩展 ```java /** * @description: 演示SimpleDateFormat线程不安全的场景 * @author:chenwenbiao * @createTime:2019/7/9 15:28 * @version:1.0 **/ public class SimpleDateFormatTest{ private SimpleDateFormat dateFormat ; public static void main(String[] args) { SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy-MM-dd"); String todayString = "2019-07-09"; String tomorrowString = "2019-07-10"; try { Date today = dateFormat.parse(todayString); Date tomorrow = dateFormat.parse(tomorrowString); //打印出当时串的时间,方便对比 System.out.println("today=" + today); System.out.println("tomorrow=" + tomorrow); //开启格式化线程,格式化日期 Thread thread1 = new Thread(new Thread1(dateFormat,today)); thread1.start(); Thread thread2 = new Thread(new Thread2(dateFormat,tomorrow)); thread2.start(); } catch (ParseException e) { e.printStackTrace(); } } } class Thread1 implements Runnable{ private SimpleDateFormat dateFormat; private Date date; public Thread1(SimpleDateFormat dateFormat,Date date){ this.dateFormat = dateFormat; this.date = date; } @Override public void run() { while (true){ String strDate = dateFormat.format(date); System.out.println("today=" + strDate); // 如果不等于2019-07-09,证明出现线程安全问题了!!!! if(!"2019-07-09".equals(strDate)){ System.exit(1); } } } } class Thread2 implements Runnable{ private SimpleDateFormat dateFormat; private Date date; public Thread2(SimpleDateFormat dateFormat,Date date){ this.dateFormat = dateFormat; this.date = date; } @Override public void run() { while (true){ String strDate = dateFormat.format(date); System.out.println("tomorrow=" + strDate); // 如果不等于2019-07-10,证明出现线程安全问题了!!!! if(!"2019-07-10".equals(strDate)){ System.exit(1); } } } } ``` ---