Java 低级错误案例v1


第 1 页, 共 24 页 软通 Java 低级错误 第 2 页, 共 24 页 修订记录Revision record 日期 Date 修订版本 Revision version 描述 Description 作者 Author 第 3 页, 共 24 页 1 前言 为了规范大家的编程行为,借鉴前人的编程经验,避免低级错误的不断发生,特发 布软通 Java 容易犯的错误,供所有 Java 开发人员学习。低级错误是新老代码都要强制 彻底清除掉的。一级和二级错误根据项目情况也需要全部避免。 2 Java 低级错误 1、 日志和实际情况不一致;捕获异常后没有在日志中记录异常栈。没有在异 常分支记录日志导致问题定位困难。 2、 魔鬼数字。 3、 对文件、IO、数据库等资源进行操作后没有及时、正确进行释放。 4、 数据类没有重载 toString()方法。 5、 嵌套使用 try-catch,或者 try-catch 后面没有必要的 finally 操作(说明:数 据库操作、IO 操作等需要使用结束 close()的对象必须在 try -catch-finally 的 finally 中 close())。 6、 equals 操作时没有将常量放在 equals 操作符的左边(说明:字符串变量 与常量比较时,先写常量,这样可以避免空指针异常)。 7、 用String多次拼接累加修改字符串,导致创建对象过多。 2.1 解读&案例 2.1.1 日志规范性 2.1.1.1 解读 日志是定位问题时最重要的依据,业务流程中缺少必要的日志会给定位问题带来很 多麻烦,甚至可能造成问题完全无法定位。 异常产生后,必须在日志中以 ERROR 或以上级别记录异常栈,否则会导致异常栈 丢失,无法确认异常产生的位置。并不需要在每次捕获异常时都记录异常日志,这样可 能导致异常被多次重复记录,影响问题的定位。但异常发生后其异常栈必须至少被记录 一次。 第 4 页, 共 24 页 和注释一样,日志也不是越多越好。无用的冗余日志不但不能帮助定位问题,还会 干扰问题的定位。而错误的日志更是会误导问题,必须杜绝。 2.1.1.2 案例 下面的例子虽然打印了很多日志,但基本上都是无用的日志,难以帮助定位问题。 甚至还有错误的日志会干扰问题的定位: public void saveProduct1(ProductServiceStruct product) { log.debug("enter method: addProduct()"); log.debug("check product status"); if (product.getProduct().getProductStatus() != ProductFieldEnum.ProductStatus.RELEASE) { throw new PMSException(PMSErrorCode.Product.ADD_ERROR); } log.debug("check tariff"); BooleanResult result = checkTariff(product.getTariffs()); if (!result.getResult()) { throw new PMSException(PMSErrorCode.Product.ADD_ERROR); } log.debug("before add product"); ProductService prodSrv = (ProductService) ServiceLocator.findService(ProductService.class); try { prodSrv.addProduct(product); } catch (BMEException e) { // 未记录异常栈,无法定位问题根源 } log.debug("after add product"); log.debug("exit method: updateProduct()"); // 错误的日志 } 而下面的例子日志打印的不多,但都是关键信息,可以很好的帮助定位问题: public void saveProduct2(ProductServiceStruct product) { if (product.getProduct().getProductStatus() != ProductFieldEnum.ProductStatus.RELEASE) { 第 5 页, 共 24 页 log.error( "product status " + product.getProduct().getProductStatus() + " error, expect " + ProductFieldEnum.ProductStatus.RELEASE); throw new PMSException(PMSErrorCode.Product.ADD_ERROR); } BooleanResult result = checkTariff(product.getTariffs()); if (!result.getResult()) { log.error( "check product tariff error " + result.getResultCode() + ": " + result.getResultDesc()); throw new PMSException(PMSErrorCode.Product.ADD_ERROR); } ProductService prodSrv = (ProductService) ServiceLocator.findService(ProductService.class); try { prodSrv.addProduct(product); } catch (BMEException e) { log.error("add product error", e); throw new PMSException(PMSErrorCode.Product.ADD_ERROR, e); } } 2.1.2 魔鬼数字 2.1.2.1 解读 在代码中使用魔鬼数字(没有具体含义的数字、字符串等)将会导致代码难以理解, 应该将数字定义为名称有意义的常量。 将数字定义为常量的最终目的是为了使代码更容易理解,所以并不是只要将数字定 义为常量就不是魔鬼数字了。如果常量的名称没有意义,无法帮助理解代码,同样是一 种魔鬼数字。 在个别情况下,将数字定义为常量反而会导致代码更难以理解,此时就不应该强求 将数字定义为常量。 第 6 页, 共 24 页 2.1.2.2 案例 public void addProduct(ProductServiceStruct product) { // 魔鬼数字,无法理解 3 具体代表产品的什么状态 if (product.getProduct().getProductStatus() != 3) { throw new PMSException(PMSErrorCode.Product.ADD_ERROR); } BooleanResult result = checkTariff(product.getTariffs()); if (!result.getResult()) { throw new PMSException(PMSErrorCode.Product.ADD_ERROR); } } public void addProduct2(ProductServiceStruct product) { // 仍然是魔鬼数字,无法理解 NUM_THREE 具体代表产品的什么状态 if (product.getProduct().getProductStatus() != NUM_THREE) { throw new PMSException(PMSErrorCode.Product.ADD_ERROR); } BooleanResult result = checkTariff(product.getTariffs()); if (!result.getResult()) { throw new PMSException(PMSErrorCode.Product.ADD_ERROR); } } 下面的例子中虽然将数字定义为了常量,但代码却并不容易理解: /** * 获取将子窗口绘制在父窗口中间时,子窗口的坐标。 * * @param parentWindow 父窗口的位置 * @param clientWindow 子窗口的位置 * @return 子窗口在父窗口中间时的坐标 */ public Point getDrawCenter1(Rect parentWindow, Rect clientWindow) { Point drawCenter = new Point(); 第 7 页, 共 24 页 drawCenter.x = parentWindow.x + (parentWindow.width - clientWindow.width) / HALF_SIZE_DIV; drawCenter.y = parentWindow.y + (parentWindow.height - clientWindow.height) / HALF_SIZE_DIV; return drawCenter; } 直接使用数字,代码反而更容易理解: /** * 获取将子窗口绘制在父窗口中间时,子窗口的坐标。 * * @param parentWindow 父窗口的位置 * @param clientWindow 子窗口的位置 * @return 子窗口在父窗口中间时的坐标 */ public Point getDrawCenter2(Rect parentWindow, Rect clientWindow) { Point drawCenter = new Point(); drawCenter.x = parentWindow.x + (parentWindow.width - clientWindow.width) / 2; drawCenter.y = parentWindow.y + (parentWindow.height - clientWindow.height) / 2; return drawCenter; } 2.1.3 资源释放 2.1.3.1 解读 在使用文件、IO 流、数据库连接等不会自动释放的资源时,应该在使用完毕后马 上将其关闭。关闭资源的代码应该在 try...catch...finally 的 finally 内执行,否则可能造成 资源无法释放。 2.1.3.2 案例 错误案例如下: public void writeProduct1(ProductServiceStruct product) { try { FileWriter fileWriter = new FileWriter(""); fileWriter.append(product.toString()); // 如果 append()抛出异常,close()方法就不会执行,造成 IO 流长时间无法释放 fileWriter.close(); } 第 8 页, 共 24 页 catch (IOException e) { ... } } 关闭 IO 流的正确方法如下: public void writeProduct2(ProductServiceStruct product) { FileWriter fileWriter = null; try { fileWriter = new FileWriter(""); fileWriter.append(product.toString()); } catch (IOException e) { ... } finally { // 不管前面是否发生异常,finally 中的代码一定会执行 if (null != fileWriter) { try { fileWriter.close(); } catch (IOException e) { ... } } //如果有多个资源需要释放,应该与 fileWriter 的释放同级,避免释放 fileWriter 出现异常时,其他资源 释放不了 } } 第 9 页, 共 24 页 2.1.4 数据类重载 toString()方法 2.1.4.1 解读 数据类如果没有重载 toString()方法,在记录日志的时候会无法记录数据对象的属 性值,给定位问题带来困难。 2.1.4.2 案例 public class MdspProductExt { private String key; private String value; public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } class BusinessProcess { private DebugLog log = LogFactory.getDebugLog(BusinessProcess.class); public void doBusiness(MdspProductExt prodExt) { 第 10 页, 共 24 页 try { ... } catch (PMSException e) { // MdspProductExt 未重载 toString()方法,日志中无法记录对象内属性的值,只能记录对象地址 log.error("error while process prodExt " + prodExt); } } } 2.1.5 try-catch 避免嵌套使用 2.1.5.1 解读 嵌套使用 try-catch 会影响程序的执行效率,当内层出现异常 catch 后抛出异常,外 层会继续 catch 住异常,再次做处理。所以应避免 try-catch 的嵌套使用,并且 try 包括 的代码行不宜过多。 catch 异常不宜一次 catch 所有异常,如 catch(Exception ex),这样不利于问题的定 位,应该把异常细化,最后 catch 非细化的异常错误。 2.1.5.2 案例 class BusinessProcess { private DebugLog log = LogFactory.getDebugLog(BusinessProcess.class); public void doBusiness(MdspProductExt prodExt) { try { ... try { ... } catch (PMSException ex) { ... 第 11 页, 共 24 页 throw ex;//这里抛出的异常会在外层的 PMSException 中捕获到 } } catch (PMSException p) { 。。。 } catch (Exception e) { 。。。 } } } 2.1.6 equals 比较 2.1.6.1 解读 当使用 equals 比较两个对象时,应保证 equals 左边的对应不为 null。避免出现空指 针异常。如果是一个对象和一个常量做比较时,应把常量放在 equals 的左边,即使 equals 右边的对象为 null 也不会抛出空指针异常,有效避免空指针异常。 2.1.6.2 案例 class BusinessProcess { public static void error(Object obj) { //当obj为null时会出现空指针异常 if(obj.equals("TestStr")) { ... } ... } public static void right(Object obj) { //当obj为null时不会出现空指针异常 第 12 页, 共 24 页 if("TestStr".equals(obj)) { ... } ... } } 2.1.7 StringBuffer 和 String 使用 2.1.7.1 解读 当构造的字符串易变化时,应使用 StringBuffer 而不是用 String。用 String 多次拼 接累加修改字符串,导致创建对象过多。 2.1.7.2 案例 class BusinessProcess { public static void error(List userList) { String userInfo = ""; for(int i=0; i< userList.size(); i++) { ... userInfo = userInfo + userList.get(i) + "&";//会创建多个对象 ... } } public static void right(List userList) { StringBuffer userInfoBuff = new StringBuffer(); for(int i=0; i< userList.size(); i++) { ... userInfoBuff.append(userList.get(i)).append("&");//不会创建 多个对象 第 13 页, 共 24 页 ... } }} 3 Java 一级错误 1、 缺少类、方法注释,代码修改后没有同步修改注释。 2、 类、方法、变量、常量等命名不能表达具体的含义,或者表达的含义和实 际用途不一致。 3、 空指针异常。 4、 数组下标越界。 5、 将字符串转换为数字时没有捕获 NumberFormatException 异常。 6、 循环体编码时不考虑性能,循环体中包含不需要的重复逻辑。 3.1 解读&案例 3.1.1 注释规范性 3.1.1.1 解读 注释是帮助理解代码的重要资料,缺少必要的注释,可能需要浪费大量的时间阅读、 调试代码,才能真正理解代码的逻辑,造成巨大的浪费,也会给维护代码带来很多困难。 但注释也不是越多约好,在没有必要写注释的地方写冗余注释,或者不规范、甚至和代 码逻辑不一致的注释,会给理解代码造成严重的干扰,不如不写。 类注释和方法注释应该按照编程规范的规定编写。方法注释需要说明方法入参含 义、取值范围、可否为空、是否会被方法修改,以及方法返回值的含义、能否为空等情 况。 对于方法中的注释,建议通过良好的命名规范、代码结构和统一的缩进风格,尽量 使代码自注释,只在代码无法完全表达业务逻辑的地方添加必要的注释。以代码为主, 注释为辅。修改代码时应同步修改注释,杜绝注释和代码不一致的情况。 3.1.1.2 案例 /** 第 14 页, 共 24 页 * <一句话功能简述>。 * <功能详细描述> * // 没有方法注释,应该将自动生成的注释模板删除,添加有意义的类注释 */ public void sendMsg() { ... } /** * 根据内容 ID 和产品类型查询产品列表。 * * @param contentId 内容 ID // 能否为 null?如果为 null 如何处理? * @param productType 产品类型 // 都有哪些类型?能否为 null?为 null 如何处理? * @return 产品列表 // 如果没有查到,是返回空 List 还是返回 null? */ public List queryProduct(String contentId, String productType) { ... if (PRODUCT_TYPE_SUB.equals(productType)) { // 查询点播类产品 // 注释和代码不一致 return querySubProduct(contentId); } ... return null; } /** * 根据内容 ID 查询订购类产品列表。 * * @param contentId 内容 ID,不能为 null * @return 产品列表,如果没有返回空 List */ public List querySubProduct(String contentId) { return ...; } 第 15 页, 共 24 页 3.1.2 命名规范性 3.1.2.1 解读 在代码中,类、方法、变量、常量等的名称是帮助理解代码最直观的要素。命名良 好、逻辑简洁的代码可以达到自注释的目的,通过阅读代码就可以基本理解业务逻辑, 减少编写和维护注释的工作量。在编码时必须重视命名的规范性,名称应该简洁、容易 理解,能够准确表达所命名对象的含义。临时变量的名称应该符合常用的习惯(如 for 循环的整形循环子一般使用 i、j)。 3.1.2.2 案例 下面就是一个命名规范性不好的例子: public void notifyProduct(Integer productId) { if (productId == null) { throw new PMSException(PMSErrorCode.CommonError.PARAM_NULL, "product ID is null"); } ProductService prodSrv = (ProductService) ServiceLocator.findService(ProductService.class); ProductServiceStruct product = prodSrv.queryProduct(productId); if (product == null) { throw new PMSException(PMSErrorCode.CommonError.PARAM_ERROR, "product not exist"); } if (PMSConfig.getInstance().getConfig1()) // getConfig1 没有具体含义 { NotifyService sss1 = (NotifyService) ServiceLocator.findService(NotifyService.class); sss1.notifyProduct(product); if (PMSConfig.getInstance().getNotifyWisg()) // getNotifyWisg 名称和实际含义不一致 { MdspProductExtService sss2 = (MdspProductExtService) ServiceLocator.findService(MdspProductExtService.class); List eee1 = sss2.queryProductExtInfo(productId); sss1.notifyProductExtInfo(eee1); } // sss1、sss2、eee1 没有任何含义,理解困难 } 第 16 页, 共 24 页 } 如果采用规范、能表达含义的名称,代码就变得非常容易理解: public void notifyProduct(Integer productId) { if (productId == null) { throw new PMSException(PMSErrorCode.CommonError.PARAM_NULL, "product ID is null"); } ProductService prodSrv = (ProductService) ServiceLocator.findService(ProductService.class); ProductServiceStruct product = prodSrv.queryProduct(productId); if (product == null) { throw new PMSException(PMSErrorCode.CommonError.PARAM_ERROR, "product not exist"); } if (PMSConfig.getInstance().isNeedNotifyProduct()) { NotifyService notifySrv = (NotifyService) ServiceLocator.findService(NotifyService.class); notifySrv.notifyProduct(product); if (PMSConfig.getInstance().isNeedNotifyProductExtInfo()) { MdspProductExtService prodExtSrv = (MdspProductExtService) ServiceLocator.findService(MdspProductExtService.class); List prodExtInfos = prodExtSrv.queryProductExtInfo(productId); notifySrv.notifyProductExtInfo(prodExtInfos); } } } 3.1.3 空指针异常 3.1.3.1.1 解读 空指针异常是编码过程中最常见的异常,在使用一个对象的时候,如果对象可能为 空,需要先判断对象是否为空,再使用这个对象。 在进行常量和变量的相等判断时,建议将常量定义为 Java 对象封装类型(如将 int 类型的常量定义为 Integer 类型),这样在比较时可以将常量放在左边,调用 equals 方法 进行比较,可以省去不必要的判空。 第 17 页, 共 24 页 3.1.3.1.2 案例 public class NullPointer { static final Integer RESULT_CODE_OK = 0; static final Result RESULT_OK = new Result(); public void printResult(Integer resultCode) { Result result = getResult(resultCode); // result 可能为 null,造成空指针异常 if (result.isValid()) { print(result); } } public Result getResult(Integer resultCode) { // 即使 resultCode 为 null,仍然可以正确执行,减少额外的判空语句 if (RESULT_CODE_OK.equals(resultCode)) { return RESULT_OK; } return null; } public void print(Result result) { ... } } 3.1.4 下标越界 3.1.4.1 解读 访问数组、List 等容器内的元素时,必须首先检查下标是否越界,杜绝下标越界异 常的发生。 第 18 页, 共 24 页 3.1.4.2 案例 public class ArrayOver { public void checkArray(String name) { // 获取一个数组对象 String[] cIds = ContentService.queryByName(name); if(null != cIds) { // 只是考虑到 cids 有可能为 null 的情况,但是 cids 完全有可能是个 0 长度的数组,因此 cIds[0]有可 能数组下标越界 String cid=cIds[0]; cid.toCharArray(); } } } 3.1.5 字符串转数字 3.1.5.1 解读 调用 Java 方法将字符串转换为数字时,如果字符串的格式非法,会抛出运行时异 常 NumberFormatException。 3.1.5.2 案例 错误例子: public Integer getInteger1(String number) { // 如果 number 格式非法,会抛出 NumberFormatException return Integer.valueOf(number); } 正确的处理方法如下: public Integer getInteger2(String number) { try { return Integer.valueOf(number); 第 19 页, 共 24 页 } catch (NumberFormatException e) { ... return null; } } 3.1.6 循环体性能 3.1.6.1 解读 循环体是软件中最容易造成性能问题的地方,所以在进行循环体编码时务必考虑性 能问题。 在循环体内重复使用且不会变化的资源(如变量、文件对象、数据库连接等),应 该在循环体开始前构造并初始化,避免在循环体内重复和构造初始化造成 CPU 资源的 浪费。 除非业务场景需要,避免在循环体内构造 try...catch 块,因为每次进入、退出 try...catch 块都会消耗一定的 CPU 资源,将 try...catch 块放在循环体之外可以节省大量的 执行时间。 3.1.6.2 案例 public void addProducts(List prodList) { for (ProductServiceStruct product : prodList) { // prodSrv 在每次循环时都会重新获取,造成不必要的资源消耗 ProductService prodSrv = (ProductService) ServiceLocator.findService(ProductService.class); // 避免在循环体内 try...catch,放在循环体之外可以节省执行时间 try { prodSrv.addProduct(product); } catch (BMEException e) { ... } 第 20 页, 共 24 页 } } 4 Java 二级错误 1、 用Enumeration来遍历Vector。 2、 一些特殊的乘除法不用位运算。 3、 变量可以定义在循环外的,建议定义在循环外。同时对循环中的业务处理,当处理完成 后应立即跳出循环,提高效率。 4、 日志中不应该有中文。 5、 重复使用的值应定义为变量。 4.1 解读&案例 4.1.1 Vector 的遍历 4.1.1.1 解读 使用枚举遍历速度比较慢,可以调用 API 方法。 4.1.1.2 案例 Enumeration en = vec.elements(); int sum = 0; //循环时多调用一次,耗时 while (en.hasMoreElements()) { sum += ((Integer)(en.nextElement())).intValue(); } return sum; 建议使用: 第 21 页, 共 24 页 int size = vec.size(); int sum = 0; for (int i = 0; i < size; i++) { sum += ((Integer)(vec.get(i))).intValue(); } return sum; 4.1.2 特殊乘除法 4.1.2.1 解读 特殊的乘除法避免使用位运算符。 4.1.2.2 案例 // 不使用temp << 2 temp*4 4.1.3 循环处理 4.1.3.1 解读 变量应定义在循环外。同时循环中一个业务处理完成后应跳出循环。 4.1.3.2 案例 public static void error(List userList) { for(int i=0; i< userList.size(); i++) { //直接在循环体内定义变量 String userName = (String)userList.get(i); ... } 第 22 页, 共 24 页 } public static void test(List userList) { //变量定义在循环体外 String userName = ""; for(int i=0; i< userList.size(); i++) { userName = (String)userList.get(i); ... } } 循环内业务处理完成后应跳出循环: public static void error(List userList) { String userName = ""; for(int i=0; i< userList.size(); i++) { userName = (String)userList.get(i); if("Tom".equals(userName)) { ... } // 当Tom匹配成功做处理后,后面的Mark等仍会做判断 if("Mark".equals(userName)) { ... } if("Cat".equals(userName)) { ... } ... } } public static void test(List userList) { String userName = ""; for(int i=0; i< userList.size(); i++) { 第 23 页, 共 24 页 userName = (String)userList.get(i); if("Tom".equals(userName)) { ... } // 当Tom匹配成功做处理后,后面的Mark等不再会做判断 else if("Mark".equals(userName)) { ... } else if("Cat".equals(userName)) { ... } ... } } 4.1.4 日志不能有中文 4.1.4.1 解读 日志中直接记录中文信息,可能会引起乱码,不利于问题的定位。 4.1.4.2 案例 private static final Logger log = Logger.getLogger(ServTransService.class); public String queryUserInfo() throws CurrentlyException { try { ... } catch (CurrentlyException e) { //英文信息日志 log.error(“Query userInfomaction is error!”); //中文信息日志 第 24 页, 共 24 页 log.error(“查询用户信息错误!”); log.error(e.toString()); ... } return ""; } 4.1.5 变量重复使用 4.1.5.1 解读 程序中多次从对象中获取的信息,应当定义为变量,提高程序效率。 4.1.5.2 案例 public void passwordReset(Data data) throws CurrentlyException { // 这里应该把data.get("UserID")定义为一个变量,下面都使用该变量 if("1".equals(data.get("UserID"))) { ... } if("2".equals(data.get("UserID"))) { ... } if("3".equals(data.get("UserID"))) { ... } ... }
还剩23页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 9 人已下载

下载pdf

pdf贡献者

wangran

贡献于2012-04-06

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf