解读阿里Java开发手册(v1.1.1) – 异常日志

6. 不要影响彼此

现象

在释放多个IO资源时,都会抛出IOException ,于是可能为了省事如此写:

public static void inputToOutput(InputStream is, OutputStream os,
           boolean isClose) throws IOException {
    BufferedInputStream bis = new BufferedInputStream(is, 1024);
    BufferedOutputStream bos = new BufferedOutputStream(os, 1024);  
    ….
    if (isClose) {
       bos.close();
       bis.close();
    }
}

假设bos关闭失败,bis还能关闭吗?当然不能!

解决办法

虽然抛出的是同一个异常,但是还是各自捕获各的为好。否则第一个失败,后一个面就没有机会去释放资源了。

启示

代码/模块之间可能存在依赖,要充分识别对相互的依赖。

传送门

解读阿里Java开发手册(v1.1.1) –
编程规约

1     public void test() {
2         Point p = new Point(1,2);
3         ColorPoint cp = new ColorPoint(1,2,Color.RED);
4         if (p.equals(cp))
5             System.out.println("p.equals(cp) is true");
6         if (!cp.equals(p))
7             System.out.println("cp.equals(p) is false");
8     }

1. 纠结的同名

现象

很多类的命名相同(例如:常见于异常、常量、日志等类),导致在import时,有时候张冠李戴,这种错误有时候很隐蔽。因为往往同名的类功能也类似,所以IDE不会提示warn。

解决

写完代码时,扫视下import部分,看看有没有不熟悉的。替换成正确导入后,要注意下注释是否也作相应修改。

启示

命名尽量避开重复名,特别要避开与JDK中的类重名,否则容易导入错,同时存在大量重名类,在查找时,也需要更多的辨别时间。

(二) 日志规约

  1. 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);

slf4j是日志门面框架,其仅提供日志记录的API,而不实现日志记录的功能,slf4j需要通过适配库适配到log4j或logback等日至系统来实现日志的记录。
使用slf4j
api能够提升代码和应用的可移植性,在使用不同日志系统的应用之间能够做到无缝的适配。
同时,使用slf4j
api的应用,在切换日志系统时(比如从logback切换到log4j2,不需要代码改造)

  1. 【强制】日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。

  2. 【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。
    logType:日志类型,推荐分类有stats/desc/monitor/visit等;
    logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
    正例:mppserver应用中单独监控时区转换异常,如:
    mppserver_monitor_timeZoneConvert.log
    说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。

  3. 【强制】对trace/debug/info级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
    说明:logger.debug(“Processing trade with id: ” + id + ” symbol: ” +
    symbol);
    如果日志级别是warn,上述日志不会打印,但是会执行字符串拼接操作,如果symbol是对象,会执行toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
    正例:(条件)

    if (logger.isDebugEnabled()) {
        logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
    }

正例:(占位符)

    logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);

占位符方式,log4j2/logback支持,log4j1.x是不直接支持的,只能通过slf4j库适配

  1. 【强制】避免重复打印日志,浪费磁盘空间,务必在log4j.xml中设置additivity=false。
    澳门新葡萄京官网首页,正例:

<logger name="com.taobao.dubbo.config" additivity="false">

additivity默认为true,即通过该logger输出的日志会同时输出到root
logger,如果还为该logger指定了独立的appender,就会导致这部分日志重复输出

  1. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字throws往上抛出。
    正例:

    logger.error(各类参数或者对象toString + "_" + e.getMessage(), e);

记录异常日志的常见错误:

logger.error(e);
logger.error(e.getMessage());
logger.error("上下文"+e.getMessage());

上面这几种都是错的!请确保使用的是两个入参的API,如error(String s,
Throwable t)

  1. 【推荐】谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
    说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

不要认为日志记录不怎么消耗性能,我见过不少事无巨细式的日志把系统性能严重拖慢的案例

  1. 【参考】可以使用warn日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。注意日志输出的级别,error级别只记录系统逻辑出错、异常等重要的错误信息。如非必要,请不要在此场景打出error级别。

     
对于toString返回字符串中包含的域字段,如本例中的areaCode、prefix和lineNumber,应该在该类(PhoneNumber)的声明中提供这些字段的getter方法,以避免toString的使用者为了获取其中的信息而不得不手工解析该字符串。这样不仅带来不必要的效率损失,而且在今后修改toString的格式时,也会给使用者的代码带来负面影响。提到toString返回字符串的格式,有两个建议,其一是尽量不要固定格式,这样会给今后添加新的字段信息带来一定的束缚,因为必须要考虑到格式的兼容性问题,再者就是推荐可以利用toString返回的字符串作为该类的构造函数参数来实例化该类的对象,如BigDecimal和BigInteger等装箱类。
     
这里还有一点建议是和hashCode、equals相关的,如果类的实现者已经覆盖了toString的方法,那么完全可以利用toString返回的字符串来生成hashCode,以及作为equals比较对象相等性的基础。这样的好处是可以充分的保证toString、hashCode和equals的一致性,也降低了在对类进行修订时造成的一些潜在问题。尽管这不是刚性要求的,却也不失为一个好的实现方式。该建议并不是源于该条目,而是去年在看effective
C#中了解到的。
    
十二、考虑实现Comparable接口:

2. 想当然的API

现象

有时候调用API时,会想当然的通过名字直接自信满满地调用,导致很惊讶的一些错误:

示例一:flag是true?

boolean flag = Boolean.getBoolean("true");

可能老是false。

示例二:这是去年的今天吗(今年是2012年,不考虑闰年)?结果还是2012年:

Calendar calendar = GregorianCalendar.getInstance();
calendar.roll(Calendar.DAY_OF_YEAR, -365);

下面的才是去年:

calendar.add(Calendar.DAY_OF_YEAR, -365);

解决办法

问自己几个问题,这个方法我很熟悉吗?有没有类似的API?
区别是什么?就示例一而言,需要区别的如下:

Boolean.valueOf(b) VS Boolean.parseBoolean(b) VS Boolean.getBoolean(b);

启示

名字起的更详细点,注释更清楚点,不要不经了解、测试就想当然的用一些API,如果时间有限,用自己最为熟悉的API。

(一) 异常处理

  1. 【强制】Java
    类库中定义的一类RuntimeException可以通过预先检查进行规避,而不应该通过catch
    来处理,比如:IndexOutOfBoundsException,NullPointerException等等。
    说明:无法通过预检查的异常除外,如在解析一个外部传来的字符串形式数字时,通过catch
    NumberFormatException来实现。
    正例:

    if (obj != null) {
        ...
    }

反例:

    try {
        obj.method();
    } catch (NullPointerException e) {
        ...
    }

对于通过入参或全局上下文获取的对象,在使用之前,必须先判null

  1. 【强制】异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低。

使用异常来做流程控制有时用起来很方便,例如进行资格校验的API,可以通过抛出的异常的message来说明资格校验不通过的原因。但这样做会牺牲性能,因为异常对象的产生本身就涉及生成stacktrace等比较耗时的行为,最好避免。

  1. 【强制】对大段代码进行try-catch,这是不负责任的表现。catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。

有些工程的顶层代码中可能存在大段的try-catch,其目的是确保异常不会从业务代码中逃逸,导致没有进入最外层兜底的异常处理逻辑。但考虑代码的简洁和可维护性,最好还是通过框架级的统一异常处理逻辑来进行(例如spring-mvc、Struts等都有通用的全局异常处理机制)。

  1. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。

异常处理的原则之一 – 延迟捕获:
不要在程序有能力处理异常之前捕获它,将异常交由掌握更多信息的作用域处理
所以说,如果处理不了这个异常,那就干脆不要捕获它,让外层的逻辑来处理。当然如果已经是最外层了,那就必须处理

  1. 【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。

使用spring的事务管理能力可以做到在产生异常后自动回滚事务

  1. 【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。
    说明:如果JDK7及以上,可以使用try-with-resources方式。

try-with-resources非常方便

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
}

等价于
```java
BufferedReader br = new BufferedReader(new FileReader(path));
try {
    return br.readLine();
} finally {
    if (br != null)
        br.close();
}
  1. 【强制】不能在finally块中使用return,finally块中的return返回后方法结束执行,不会再执行try块中的return语句。

方法的退出方式有两种:return或抛出异常,而finally块中的代码是在return或抛出异常之后执行的,所以如果finally块中有return,会把之前return过的返回值覆盖掉,如果之前抛出了异常,也会被吞掉

  1. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
    说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。

  2. 【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。调用方需要进行null判断防止NPE问题。
    说明:本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。

防止NPE是调用者的责任,这一点很对。如果API的提供者拍胸脯说“绝对不会返回null”,你就敢不进行null判断了吗?

  1. 【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景:
    1)
    返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。
    反例:public int f() { return
    Integer对象},如果为null,自动解箱抛NPE。
    2) 数据库的查询结果可能为null。
    3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。
    4) 远程调用返回对象时,一律要求进行空指针判断,防止NPE。
    5) 对于Session中获取的数据,建议NPE检查,避免空指针。
    6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。
    正例:可以使用JDK8的Optional类来防止NPE问题。

简单来说,拿到的对象只要不是你自己的代码产生的,那么都有可能是null,均需要进行NPE检查
Optional类既可以用来装B,又实实在在的有用。如果升级JDK8有困难,google
guava库中也提供了Optional类。
关于Optional类的具体使用,可参考http://www.tuicool.com/articles/uIzeYjf

  1. 【推荐】定义时区分unchecked / checked
    异常,避免直接使用RuntimeException抛出,更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException
    / ServiceException等。

这一条规约分解一下,有几条:

  • 自定义异常时,想好要定义的异常是unchecked还是checked异常,如果是前者,继承RuntimeException,如果是后者,继承Exception
  • 尽量不要在抛出异常时throw new RuntimeException(“xxxx”);
    应该使用具备业务含义的自定义异常类,这样做可以在捕获异常时提供方便
  • 绝对不要在抛出异常时throw new Exception(“xxx”)或throw new
    Throwable(“xxx”),这样做不仅仅是屏蔽了异常本身的业务含义,同时也屏蔽了异常的分类(checked/unchecked),甚至连Exception和Error的区别也屏蔽了

如果不清楚Throwable/Exception/Error的关系,或不清楚unchecked/checked异常的含义,建议先阅读笔者的另一篇文章Java异常控制机制和异常处理原则

  1. 【参考】在代码中使用“抛异常”还是“返回错误码”,对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess、“错误码”、“错误简短信息”。
    说明:关于RPC方法返回方式使用Result方式的理由:
    1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
    2)如果不加栈信息,只是new自定义异常,加入自己的理解的error
    message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。

  2. 【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。
    说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是共用模块。
    正例:一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取:

private boolean checkParam(DTO dto) {...}

说的很对,但为啥放在异常处理分类下……?

 1     public final class PhoneNumber {
 2         private final short areaCode;
 3         private final short prefix;
 4         private final short lineNumber;
 5         private final int myHashCode;
 6         public PhoneNumber(int areaCode,int prefix,int lineNumber) {
 7             //做一些基于参数范围的检验。
 8             this.areaCode = areaCode;
 9             this.prefix = prefix;
10             this.lineNumber = lineNumber;
11             myHashCode = 17;
12             myHashCode = 31 * myHashCode + areaCode;
13             myHashCode = 31 * myHashCode + prefix;
14             myHashCode = 31 * myHashCode + lineNumber;
15         }
16         @Override public boolean equals(Object o) {
17             if (o == this)
18                 return true;
19             if (!(o instanceof PhoneNumber)) 
20                 return false;
21             PhoneNumber pn = (PhoneNumber)o;
22             return pn.lineNumber = lineNumber && pn.prefix == prefix && pn.areaCode = areaCode;
23         }
24         @Override public int hashCode() {
25             return myHashCode;
26         }
27     }

9. 忽视日志记录时机、级别

现象

存在下面两则示例:

示例一:该不该记录日志?

catch (SocketException e)
{
    LOG.error("server error", e);
    throw new ConnectionException(e.getMessage(), e);
}

示例二:记什么级别日志?

在用户登录系统中,每次失败登录:

LOG.warn("Failed to login by "+username+");

解决

  1. 移除日志记录:在遇到需要re-throw的异常时,如果每个人都按照先记录后throw的方式去处理,那么对一个错误会记录太多的日志,所以不推荐如此做;但是如果re-throw出去的exception没有带完整的trace(
    即cause),那么最好还是记录下。
  2. 如果恶意登录,那系统内部会出现太多WARN,从而让管理员误以为是代码错误。可以反馈用户以错误,但是不要记录用户错误的行为,除非想达到控制的目的。

启示

日志改不改记?记成什么级别?如何记?这些都是问题,一定要根据具体情况,需要考虑:

  1. 是用户行为错误还是代码错误?
  2. 记录下来的日志,能否能给别人在不造成过多的干扰前提下提供有用的信息以快速定位问题。

  3. 忘设初始容量


现象

在JAVA中,我们常用Collection中的Map做Cache,但是我们经常会遗忘设置初始容量。

cache = new LRULinkedHashMap< K, V>(maxCapacity);

解决

初始容量的影响有多大?拿LinkedHashMap来说,初始容量如果不设置默认是16,超过16×LOAD_FACTOR,会resize(2
* table.length),扩大2倍:采用 Entry[] newTable = new
Entry[newCapacity]; transfer(newTable),即整个数组Copy,
那么对于一个需要做大容量CACHE来说,从16变成一个很大的数量,需要做多少次数组复制可想而知。如果初始容量就设置很大,自然会减少resize,
不过可能会担心,初始容量设置很大时,没有Cache内容仍然会占用过大体积。其实可以参考以下表格简单计算下,
初始时还没有cache内容, 每个对象仅仅是4字节引用而已。

  • memory for reference fields (4 bytes each);
  • memory for primitive fields
Java type Bytes required
boolean 1
byte
char 2
short
int 4
float
long 8
double

启示

不仅是map,
还有stringBuffer等,都有容量resize的过程,如果数据量很大,就不能忽视初始容量可以考虑设置下,否则不仅有频繁的
resize还容易浪费容量。

在Java编程中,除了上面枚举的一些容易忽视的问题,日常实践中还存在很多。相信通过不断的总结和努力,可以将我们的程序完美呈现给读者。

前言

阿里Java开发手册谈不上圣经,但确实是大量程序员踩坑踩出来的一部非常有价值的宝典。其从代码规范性、性能、健壮性、安全性等方面出发,对程序员提出了一系列简单直观的要求,对于人员流动性强,程序员技术水平参差不齐的团队来说,尤其具备价值。

阿里Java开发手册中,有一部分规约是针对阿里自己的工程环境特点设置的,其他团队可以用于借鉴,无需照搬,而大部分的规约,都是具备推广价值的。

然而这本手册中的规约众多,部分搭配了简短的说明,相当一部分规约则对原理说明的不够详细。本着“知道为什么要这样做”强于“知道应该这样做”的思想,本文在列出阿里Java开发手册的同时,对其中部分语焉不详的规约进行了比较详细的说明,并尽可能搭配代码样例。

本文覆盖阿里Java开发手册中的前两章,即编程规约和异常日志两章,后三章MySQL规约、工程规约、安全规约不列入主要有两个考虑,一是这三章的内容与Java不紧密相关,二是这三章中除MySQL之外的规约与阿里现行的技术架构捆绑的比较紧,普适性较低。

本文中,在阿里Java开发手册基础上增加的说明内容全部以引用的形式出现,即

引用部分的文字是本文作者对阿里Java规约的附加说明

 1     public class Point {
 2         private final int x;
 3         private final int y;
 4         public Point(int x,int y) {
 5             this.x = x;
 6             this.y = y;
 7         }
 8         @Override public boolean equals(Object o) {
 9             if (!(o instanceof Point)) 
10                 return false;
11             Point p = (Point)o;
12             return p.x == x && p.y == y;
13         }
14     }

7. 用断言取代参数校验

现象

如题所提,作为防御式编程常用的方式:断言,写在产品代码中做参数校验等。例如:

private void send(List< Event> eventList)  {
    assert eventList != null;
}

解决

换成正常的统一的参数校验方法。因为断言默认是关闭的,所以起不起作用完全在于配置,如果采用默认配置,经历了eventList
!= null结果还没有起到作用,徒劳无功。

启示

有的时候,代码起不起作用,不仅在于用例,还在于配置,例如断言是否启用、log级别等,要结合真实环境做有用编码。

二、异常日志

 1     public final class PhoneNumber {
 2         private final short areaCode;
 3         private final short prefix;
 4         private final short lineNumber;
 5         public PhoneNumber(int areaCode,int prefix,int lineNumber) {
 6             //做一些基于参数范围的检验。
 7             this.areaCode = areaCode;
 8             this.prefix = prefix;
 9             this.lineNumber = lineNumber;
10         }
11         @Override public boolean equals(Object o) {
12             if (o == this)
13                 return true;
14             if (!(o instanceof PhoneNumber)) 
15                 return false;
16             PhoneNumber pn = (PhoneNumber)o;
17             return pn.lineNumber = lineNumber && pn.prefix == prefix && pn.areaCode = areaCode;
18         }
19     }
20     public static void main(String[] args) {
21         Map<PhoneNumber,String> m = new HashMap<PhoneNumber,String>();
22         PhoneNumber pn1 = new PhoneNumber(707,867,5309);
23         m.put(pn1,"Jenny");
24         PhoneNumber pn2 = new PhoneNumber(707,867,5309);
25         if (m.get(pn) == null)
26             System.out.println("Object can't be found in the Map");
27     }

3. 有时候溢出并不难

现象

有时候溢出并不难,虽然不常复现:

示例一:

long x=Integer.MAX_VALUE+1;
System.out.println(x);

x是多少?竟然是-2147483648,明明加上1之后还是long的范围。类似的经常出现在时间计算:

数字1×数字2×数字3… 

示例二:

在检查是否为正数的参数校验中,为了避免重载,选用参数number,
于是下面代码结果小于0,也是因为溢出导致:

Number i=Long.MAX_VALUE;
System.out.println(i.intValue()>0);

解决

  1. 让第一个操作数是long型,例如加上L或者l(不建议小写字母l,因为和数字1太相似了);
  2. 不确定时,还是使用重载吧,即使用doubleValue(),当参数是BigDecimal参数时,也不能解决问题。

启示

对数字运用要保持敏感:涉及数字计算就要考虑溢出;涉及除法就要考虑被除数是0;实在容纳不下了可以考虑BigDecimal之类。

1     @Override public boolean equals(Object o) {
2         if (o == null || o.getClass() == getClass()) 
3             return false;
4         Point p = (Point)o;
5         return p.x == x && p.y == y;
6     }

8. 用户认知负担有时候很重

现象

先来比较三组例子,看看那些看着更顺畅?

示例一:

public void caller(int a, String b, float c, String d) {
    methodOne(d, z, b);
    methodTwo(b, c, d);
}
public void methodOne(String d, float z, String b)  
public void methodTwo(String b, float c, String d)

示例二:

public boolean remove(String key, long timeout) {
             Future< Boolean> future = memcachedClient.delete(key);
public boolean delete(String key, long timeout) {
             Future< Boolean> future = memcachedClient.delete(key);

示例三:

public static String getDigest(String filePath, DigestAlgorithm algorithm)
public static String getDigest(String filePath, DigestAlgorithm digestAlgorithm)

解决

  1. 保持参数传递顺序;
  2. remove变成了delete,显得突兀了点, 统一表达更好;
  3. 保持表达,少缩写也会看起来流畅点。

启示

在编码过程中,不管是参数的顺序还是命名都尽量统一,这样用户的认知负担会很少,不要要用户容易犯错或迷惑。例如用枚举代替string从而不让用户迷惑到底传什么string,
诸如此类。

 1     class MyTest {
 2         private static final Set<Point> unitCircle;
 3         static {
 4             unitCircle = new HashSet<Point>();
 5             unitCircle.add(new Point(1,0));
 6             unitCircle.add(new Point(0,1));
 7             unitCircle.add(new Point(-1,0));
 8             unitCircle.add(new Point(0,-1));
 9         }
10         public static boolean onUnitCircle(Point p) {
11             return unitCircle.contains(p);
12         }
13     }

在Java编码中,我们容易犯一些错误,也容易疏忽一些问题,因此笔者对日常编码中曾遇到的一些经典情形归纳整理成文,以共同探讨。

     
再次看输出结果,传递性确实被打破了。如果我们在Point.equals中不使用instanceof而是直接使用getClass呢?

4. 日志跑哪了?

现象

有时候觉得log都打了,怎么找不到?

示例一:没有stack trace!

 } catch (Exception ex) {
    log.error(ex);
 }

示例二:找不到log!

} catch (ConfigurationException e) {
    e.printStackTrace();
}

解决

  1. 替换成log.error(ex.getMessage(),ex);
  2. 换成普通的log4j吧,而不是System.out。

启示

  1. API定义应该避免让人犯错,如果多加个重载的log.error(Exception)自然没有错误发生
  2. 在产品代码中,使用的一些方法要考虑是否有效,使用e.printStackTrace()要想下终端(Console)在哪。

  3. 遗忘的Volatile


现象

在DCL模式中,总是忘记加一个Volatile。

private static CacheImpl instance;  //lose volatile
public static CacheImpl getInstance() {
    if (instance == null) {
        synchronized (CacheImpl.class) {
            if (instance == null) {
                instance = new CacheImpl (); 
            }
        }
    }
    return instance;
}

解决

毋庸置疑,加上一个吧,synchronized
锁的是一块代码(整个方法或某个代码块),保证的是这”块“代码的可见性及原子性,但是instance
== null第一次判断时不再范围内的。所以可能读出的是过期的null。

启示

我们总是觉得某些低概率的事件很难发生,例如某个时间并发的可能性、某个异常抛出的可能性,所以不加控制,但是如果可以,还是按照前人的“最佳实践”来写代码吧。至少不用过多解释为啥另辟蹊径。

     
对于Object类中提供的equals方法在必要的时候是必要重载的,然而如果违背了一些通用的重载准则,将会给程序带来一些潜在的运行时错误。如果自定义的class没有重载该方法,那么该类实例之间的相等性的比较将是基于两个对象是否指向同一地址来判定的。因此对于以下几种情况可以考虑不重载该方法:
      1.    类的每一个实例本质上都是唯一的。
     
不同于值对象,需要根据其内容作出一定的判定,然而该类型的类,其实例的自身便具备了一定的唯一性,如Thread、Timer等,他本身并不具备更多逻辑比较的必要性。
      2.    不关心类是否提供了“逻辑相等”的测试功能。
     
如Random类,开发者在使用过程中并不关心两个Random对象是否可以生成同样随机数的值,对于一些工具类亦是如此,如NumberFormat和DateFormat等。
      3.  
 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
     
如Set实现都从AbstractSet中继承了equals实现,因此其子类将不在需要重新定义该方法,当然这也是充分利用了继承的一个优势。
      4.  
 类是私有的或是包级别私有的,可以确定它的equals方法永远不会被调用。
    
     
那么什么时候应该覆盖Object.equals呢?如果类具有自己特有的“逻辑相等”概念,而且超类中没有覆盖equals以实现期望的行为,这是我们就需要覆盖equals方法,如各种值对象,或者像Integer和Date这种表示某个值的对象。在重载之后,当对象插入Map和Set等容器中时,可以得到预期的行为。枚举也可以被视为值对象,然而却是这种情形的一个例外,对于枚举是没有必要重载equals方法,直接比较对象地址即可,而且效率也更高。
      在覆盖equals是,该条目给出了通用的重载原则:
      1.    自反性:对于非null的引用值x,x.equals(x)返回true。
     
如果违反了该原则,当x对象实例被存入集合之后,下次希望从该集合中取出该对象时,集合的contains方法将直接无法找到之前存入的对象实例。
      2.  
 对称性:对于任何非null的引用值x和y,如果y.equals(x)为true,那么x.equals(y)也为true。

 1     public final class CaseInsensitiveString {
 2         private final String s;
 3         public CaseInsensitiveString(String s) {
 4             this.s = s;
 5         }
 6         @Override public boolean equals(Object o) {
 7             if (o instanceof CaseInsensitiveString) 
 8                 return s.equalsIgnoreCase((CaseInsensitiveString)o).s);
 9             if (o instanceof String) //One-way interoperability
10                 return s.equalsIgnoreCase((String)o);
11             return false;
12         }
13     }
14     public static void main(String[] args) {
15         CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
16         String s = "polish";
17         List<CaseInsensitiveString> l = new ArrayList<CaseInsensitiveString>();
18         l.add(cis);
19         if (l.contains(s)) 
20             System.out.println("s can be found in the List");
21     }

八、覆盖equals时请遵守通用约定:

     
另外,该条目还建议不要仅仅利用某一域字段的部分信息来计算hashCode,如早期版本的String,为了提高计算哈希值的效率,只是挑选其中16个字符参与hashCode的计算,这样将会导致大量的String对象具有重复的hashCode,从而极大的降低了哈希集合的存取效率。
    
十、始终要覆盖toString:

     
注意以上代码中的instanceof判断,由于在后面的实现中需要将参数o进行类型强转,如果类型不匹配则会抛出ClassCastException,导致equals方法提前退出。在此需要指出的是instanceof还有一个潜在的规则,如果其左值为null,instanceof操作符将始终返回false,因此上面的代码可以优化为:

     
如果在ColorPoint中没有重载自己的equals方法而是直接继承自超类,这样的相等性比较逻辑将会给使用者带来极大的迷惑,毕竟Color域字段对于ColorPoint而言确实是非常有意义的比较性字段,因此该类重载了自己的equals方法。然而这样的重载方式确实带来了一些潜在的问题,见如下代码:

 1     @Override public boolean equals(Object o) {
 2         if (o == this) 
 3             return true;
 4         
 5         if (!(o instanceof MyType))
 6             return false;
 7             
 8         MyType myType = (MyType)o;
 9         return objField.equals(o.objField) && intField == o.intField 
10             && Double.compare(doubleField,o.doubleField) == 0 
11             && Arrays.equals(arrayField,o.arrayField);
12     }

     
对于上例,如果执行cis.equals(s)将会返回true,因为在该class的equals方法中对参数o的类型针对String作了特殊的判断和特殊的处理,因此如果equals中传入的参数类型为String时,可以进一步完成大小写不敏感的比较。然而在String的equals中,并没有针对CaseInsensitiveString类型做任何处理,因此s.equals(cis)将一定返回false。针对该示例代码,由于无法确定List.contains的实现是基于cis.equals(s)还是基于s.equals(cis),对于实现逻辑两者都是可以接受的,既然如此,外部的使用者在调用该方法时也应该同样保证并不依赖于底层的具体实现逻辑。由此可见,equals方法的对称性是非常必要的。以上的equals实现可以做如下修改:

1     @Override public int hashCode() {
2         int result = 17;
3         result = 31 * result + areaCode;
4         result = 31 * result + prefix;
5         result = 31 * result + lineNumber;
6         return result;
7     }
1     @Override public boolean equals(Object o) {
2         if (o instanceof CaseInsensitiveString) 
3             return s.equalsIgnoreCase((CaseInsensitiveString)o).s);
4         return false;
5     }

      鉴于之上所述,该条目中给出了重载equals方法的最佳逻辑:
      1.  
 使用==操作符检查”参数是否为这个对象的引用”,如果是则返回true。由于==操作符是基于对象地址的比较,因此特别针对拥有复杂比较逻辑的对象而言,这是一种性能优化的方式。
      2.  
 使用instanceof操作符检查”参数是否为正确的类型”,如果不是则返回false。
      3.  
 把参数转换成为正确的类型。由于已经通过instanceof的测试,因此不会抛出ClassCastException异常。
      4.  
 对于该类中的每个”关键”域字段,检查参数中的域是否与该对象中对应的域相匹配。
      如果以上测试均全部成功返回true,否则false。见如下示例代码:

     
经过这样的修改,对称性确实得到了保证,但是却牺牲了传递性,见如下代码:

 1     public class ColorPoint extends Point {
 2         private final Color c;
 3         public ColorPoint(int x,int y,Color c) {
 4             super(x,y);
 5             this.c = c;
 6         }
 7         @Override public boolean equals(Object o) {
 8             if (!(o instanceof ColorPoint)) 
 9                 return false;
10             return super.equals(o) && ((ColorPoint)o).c == c;
11         }
12     }

     
在上面的代码中,可以看到参与hashCode计算的域字段也同样参与了PhoneNumber的相等性(equals)比较。对于生成的散列码,推荐不同的对象能够尽可能生成不同的散列,这样可以保证在存入HashMap或HashSet中时,这些对象被分散到不同的散列桶中,从而提高容器的存取效率。对于有些不可变对象,如果需要被频繁的存取于哈希集合,为了提高效率,可以在对象构造的时候就已经计算出其hashCode值,hashCode()方法直接返回该值即可,如:

     
对于该类的equals重载是没有任何问题了,该逻辑可以保证传递性,然而在我们试图给Point类添加新的子类时,会是什么样呢?

      这样修改之后,cis.equals(s)和s.equals(cis)都将返回false。    
      3.  
 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,同时y.equals(z)也返回true,那么x.equals(z)也必须返回true。

     
从上面的示例中可以看出,如果域字段为Object对象,则使用equals方法进行两者之间的相等性比较,如果为int等整型基本类型,可以直接比较,如果为浮点型基本类型,考虑到精度和Double.NaN和Float.NaN等问题,推荐使用其对应包装类的compare方法,如果是数组,可以使用JDK
1.5中新增的Arrays.equals方法。众所周知,&&操作符是有短路原则的,因此应该将最有可能不相同和比较开销更低的域比较放在最前面。
     
最后需要提起注意的是Object.equals的参数类型为Object,如果要重载该方法,必须保持参数列表的一致性,如果我们将子类的equals方法写成:public
boolean equals(MyType
o);Java的编译器将会视其为Object.equals的过载(Overload)方法,因此推荐在声明该重载方法时,在方法名的前面加上@Override注释标签,一旦当前声明的方法因为各种原因并没有重载超类中的方法,该标签的存在将会导致编译错误,从而提醒开发者此方法的声明存在语法问题。
    
九、覆盖equals时总要覆盖hashCode:

1     @Override String toString() {
2         return String.format("(%03d) %03d-%04d",areaCode,prefix,lineNumber);
3     }
1     @Override public boolean equals(Object o) {
2         if (!(o instanceof Point)) 
3             return false;
4         if (!(o instanceof ColorPoint))
5             return o.equals(this);
6         return super.equals(o) && ((ColorPoint)o).c == c;
7     }

     
从输出结果来看,ColorPoint.equals方法破坏了相等性规则中的对称性,因此需要做如下修改:

1     @Override public boolean equals(Object o) {
2         if (o == null)
3             return false;
4         if (!(o instanceof MyType)) 
5             return false;
6         ...
7     }

     
与equals和hashCode不同的是,该条目推荐应该始终覆盖该方法,以便在输出时可以得到更明确、更有意义的文字信息和表达格式。这样在我们输出调试信息和日志信息时,能够更快速的定位出现的异常或错误。如上一个条目中PhoneNumber的例子,如果不覆盖该方法,就会输出PhoneNumber@163b91
这样的不可读信息,因此也不会给我们诊断问题带来更多的帮助。以下代码重载了该方法,那么在我们调用toString或者println时,将会得到”(408)867-5309″。

1     @Override public boolean equals(Object o) {
2         if (!(o instanceof MyType)) 
3             return false;
4         ...
5     }

     
如果此时我们测试的不是Point类本身,而是ColorPoint,那么按照目前Point.equals(getClass方式)的实现逻辑,ColorPoint对象在被传入onUnitCircle方法后,将永远不会返回true,这样的行为违反了”里氏替换原则”(敏捷软件开发一书中给出了很多的解释),既一个类型的任何重要属性也将适用于它的子类型。因此该类型编写的任何方法,在它的子类型上也应该同样运行的很好。
     
如何解决这个问题,该条目给出了一个折中的方案,既复合优先于继承,见如下代码:

1     public void test() {
2         ColorPoint p1 = new ColorPoint(1,2,Color.RED);
3         Point p2 = new Point(1,2);
4         ColorPoint p1 = new ColorPoint(1,2,Color.BLUE);
5         if (p1.equals(p2) && p2.equals(p3))
6             System.out.println("p1.equals(p2) && p2.equals(p3) is true");
7         if (!(p1.equals(p3))
8             System.out.println("p1.equals(p3) is false");
9     }

     
从以上示例的输出结果可以看出,新new出来的pn2对象并没有在Map中找到,尽管pn2和pn1的相等性比较将返回true。这样的结果很显然是有悖我们的初衷的。如果想从Map中基于pn2找到pn1,那么我们就需要在PhoneNumber类中覆盖缺省的hashCode方法,见如下代码:

 1     public class ColorPoint {
 2         //包含了Point的代理类
 3         private final Point p;
 4         private final Color c;
 5         public ColorPoint(int x,int y,Color c) {
 6             if (c == null)
 7                 throw new NullPointerException();
 8             p = new Point(x,y);
 9             this.c = c;
10         }
11         //提供一个视图方法返回内部的Point对象实例。这里Point实例为final对象非常重要,
12 //可以避免使用者的误改动。视图方法在Java的集合框架中有着大量的应用。
13         public Point asPoint() {
14             return p;
15         }
16         @Override public boolean equals(Object o) {
17             if (!(o instanceof ColorPoint)) 
18                 return false;
19             ColorPoint cp = (ColorPoint)o;
20             return cp.p.equals(p) && cp.c.equals(c);
21         }
22     }

     
一个通用的约定,如果类覆盖了equals方法,那么hashCode方法也需要被覆盖。如果将会导致该类无法和基于散列的集合一起正常的工作,如HashMap、HashSet。来自JavaSE6的约定如下:
      1.  
 在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象多次调用,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
      2.  
 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
      3.  
 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。
     
如果类没有覆盖hashCode方法,那么Object中缺省的hashCode实现是基于对象地址的,就像equals在Object中的缺省实现一样。如果我们覆盖了equals方法,那么对象之间的相等性比较将会产生新的逻辑,而此逻辑也应该同样适用于hashCode中散列码的计算,既参与equals比较的域字段也同样要参与hashCode散列码的计算。见下面的示例代码:

     
和之前提到的通用方法equals、hashCode和toString不同的是compareTo方法属于Comparable接口,该接口为其实现类提供了排序比较的规则,实现类仅需基于内部的逻辑,为compareTo返回不同的值,既A.compareTo(B)

> 0可视为A > B,反之则A < B,如果A.compareTo(B) == 0,可视为A

B。在C++中由于提供了操作符重载的功能,因此可以直接通过重载操作符的方式进行对象间的比较,事实上C++的标准库中提供的缺省规则即为此,如bool
operator>(OneObject
o)。在Java中,如果对象实现了Comparable接口,即可充分利用JDK集合框架中提供的各种泛型算法,如:Arrays.sort(a);
即可完成a对象数组的排序。事实上,JDK中的所有值类均实现了该接口,如Integer、String等。
     
Object.equals方法的通用实现准则也同样适用于Comparable.compareTo方法,如对称性、传递性和一致性等,这里就不做过多的赘述了。然而两个方法之间有一点重要的差异还是需要在这里提及的,既equals方法不应该抛出异常,而compareTo方法则不同,由于在该方法中不推荐跨类比较,如果当前类和参数对象的类型不同,可以抛出ClassCastException异常。在JDK
1.5
之后我们实现的Comparable<T>接口多为该泛型接口,不在推荐直接继承1.5
之前的非泛型接口Comparable了,新的compareTo方法的参数也由Object替换为接口的类型参数,因此在正常调用的情况下,如果参数类型不正确,将会直接导致编译错误,这样有助于开发者在coding期间修正这种由类型不匹配而引发的异常。
     
在该条目中针对compareTo的相等性比较给出了一个强烈的建议,而不是真正的规则。推荐compareTo方法施加的等同性测试,在通常情况下应该返回和equals方法同样的结果,考虑如下情况:

 1     public static void main(String[] args) {
 2         HashSet<BigDecimal> hs = new HashSet<BigDecimal>();
 3         BigDecimal bd1 = new BigDecimal("1.0");
 4         BigDecimal bd2 = new BigDecimal("1.00");
 5         hs.add(bd1);
 6         hs.add(bd2);
 7         System.out.println("The count of the HashSet is " + hs.size());
 8         
 9         TreeSet<BigDecimal> ts = new TreeSet<BigDecimal>();
10         ts.add(bd1);
11         ts.add(bd2);
12         System.out.println("The count of the TreeSet is " + ts.size());
13     }
14     /*    输出结果如下:
15         The count of the HashSet is 2
16         The count of the TreeSet is 1
17 */    

     
由以上代码的输出结果可以看出,TreeSet和HashSet中包含元素的数量是不同的,这其中的主要原因是TreeSet是基于BigDecimal的compareTo方法是否返回0来判断对象的相等性,而在该例中compareTo方法将这两个对象视为相同的对象,因此第二个对象并未实际添加到TreeSet中。和TreeSet不同的是HashSet是通过equals方法来判断对象的相同性,而恰恰巧合的是BigDecimal的equals方法并不将这个两个对象视为相同的对象,这也是为什么第二个对象可以正常添加到HashSet的原因。这样的差异确实给我们的编程带来了一定的负面影响,由于HashSet和TreeSet均实现了Set<E>接口,倘若我们的集合是以Set<E>的参数形式传递到当前添加BigDecimal的函数中,函数的实现者并不清楚参数Set的具体实现类,在这种情况下不同的实现类将会导致不同的结果发生,这种现象极大的破坏了面向对象中的”里氏替换原则”。
     
在重载compareTo方法时,应该将最重要的域字段比较方法比较的最前端,如果重要性相同,则将比较效率更高的域字段放在前面,以提高效率,如以下代码:

 1     public int compareTo(PhoneNumer pn) {
 2         if (areaCode < pn.areaCode)
 3             return -1;
 4         if (areaCode > pn.areaCode)
 5             return 1;
 6             
 7         if (prefix < pn.prefix)
 8             return -1;
 9         if (prefix > pn.prefix)
10             return 1;
11             
12         if (lineNumber < pn.lineNumer)
13             return -1;
14         if (lineNumber > pn.lineNumber)
15             return 1;
16         return 0;
17     }

     
上例给出了一个标准的compareTo方法实现方式,由于使用compareTo方法排序的对象并不关心返回的具体值,只是判断其值是否大于0,小于0或是等于0,因此以上方法可做进一步优化,然而需要注意的是,下面的优化方式会导致数值类型的作用域溢出问题。

 1     public int compareTo(PhoneNumer pn) {
 2         int areaCodeDiff = areaCode - pn.areaCode;
 3         if (areaCodeDiff != 0)
 4             return areaCodeDiff;
 5         int prefixDiff = prefix - pn.prefix;
 6         if (prefixDiff != 0)
 7             return prefixDiff;
 8     
 9         int lineNumberDiff = lineNumber - pn.lineNumber;
10         if (lineNumberDiff != 0)
11             return lineNumberDiff;
12         return 0;
13     }    

     
这样的Point.equals确实保证了对象相等性的这几条规则,然而在实际应用中又是什么样子呢?

      4.  
 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被改变,多次调用x.equals(y)就会一致的返回true,或者一致返回false。
     
在实际的编码中,尽量不要让类的equals方法依赖一些不确定性较强的域字段,如path。由于path有多种表示方式可以指向相同的目录,特别是当path中包含主机名称或ip地址等信息时,更增加了它的不确定性。再有就是path还存在一定的平台依赖性。
      5.  
 非空性:很难想象会存在o.equals(null)返回true的正常逻辑。作为JDK框架中极为重要的方法之一,equals方法被JDK中的基础类广泛的使用,因此作为一种通用的约定,像equals、toString、hashCode和compareTo等重要的通用方法,开发者在重载时不应该让自己的实现抛出异常,否则会引起很多潜在的Bug。如在Map集合中查找指定的键,由于查找过程中的键相等性的比较就是利用键对象的equals方法,如果此时重载后的equals方法抛出NullPointerException异常,而Map的get方法并未捕获该异常,从而导致系统的运行时崩溃错误,然而事实上,这样的问题是完全可以通过正常的校验手段来避免的。综上所述,很多对象在重载equals方法时都会首先对输入的参数进行是否为null的判断,见如下代码:

发表评论

电子邮件地址不会被公开。 必填项已用*标注