第11条:覆盖equals是总要覆盖hashCode
为了对象和所有基于散列的集合一起运作,在覆盖equals时必须覆盖hashCode。如果不修改,那就会出现equals返回true的两个对象,hashCode返回值不一样。这是违反Object规范的。规范如下:
Map<PhoneNumber, String > m = new HashMap<>();
m.put(new PhoneNumber(707,867,5309));
以上是书中的例子,PhoneNumber没覆盖hashCode的情况下,m.get(new PhoneNumber(707,867,5309))可能会返回null。 原因是put和get的是两个对象,没覆盖hashCode导致散列值可能不同,放到不同的散列桶。找出散列桶的情况下就会返回null;
一个好的散列函数倾向于为不同对象产生不同的散列码。这能更好得保证散列表以线性时间运行。书中提供了一个简单的解决方案。
hashCode要排除衍生域,也就是equals计算中没有用到的域。部分衍生域可能出于性能考虑在equals中用到了,但hashCode也不需要。
Object有一个静态方法,通过把关键域作为参数传到方法中就能获得散列码。在不注重性能的情况下可以使用。
@Override
public int hashCode()
{
renturn Objects.hash(lineNum, prefix, areaCode);
}
不可变的类在计算hashCode开销比较大的情况,可以把散列码存在对象内部。如果多数情况会被用作散列键,那创建对象时就可以计算散列码,否则可以在第一次计算时再把散列码存下来。
不要为性能排除关键域。这不符合"一个好的散列函数倾向于为不同对象产生不同的散列码"。排除一个关键域可能导致大量不等的对象的散列码相同,被放到同一个散列桶,影响散列表的正常运作。
不要对hashCode的方法的返回值做具体规定。书中举了Integer的例子,说这样会限制了未来版本中改进散列函数的能力。这块我没理解,但像Integer这样直接用一个对象内的方法来返回散列值的用法,一旦方法实现变了,那就会出问题。
@Override
public int hashCode() {
return Integer.hashCode(value);
}
public static int hashCode(int value) {
return value;
}
总之覆盖equals必须覆盖hashCode,切要遵守Object的通用约定。
第12条:始终要覆盖toString
这条是这一章看到现在日常写代码能用到的条提示。Object自己提供了toString的实现,但它的实现是 类名@散列码,例如Order@46849。无法提供丰富有效的信息。所以toString的约定指出“建议所有子类覆盖”,且toString的实现要简洁但信息丰富并易于阅读。
当对象被传递给printIn、printf、字符操作符+、assert时自动会调用toString,不用再手动去取值拼接字符串了。
在打log的时候会更加容易。以下是tms的一段代码。
EZhuangXieApiGpsTrucksConfig4Kukaexp configObj = getConfigObj();
logger.debug("LtsKukaexpFetchEZhuangXieApiGpsPositionWrapper - configObj:" + JsonUtil.writeValueAsString(configObj));
if (configObj == null || CollectionUtils.isEmpty(configObj.getSupplierName2IdMap())) {
logger.debug("LtsKukaexpFetchEZhuangXieApiGpsPositionWrapper - configObj error!");
return;
}
tms中很多时候打log为了能看到对象的内部状态,调用JsonUtil.writeValueAsString来序列化对象。其实如果EZhuangXieApiGpsTrucksConfig4Kukaexp 覆盖了toString方法。 logger.debug("LtsKukaexpFetchEZhuangXieApiGpsPositionWrapper - configObj:" + configObj)就可以完成相同的任务,且性能通常要比JsonUtil.writeValueAsString好。
toString必须包含对象所有值得关注的信息。如果对象实在太大可以返回摘要信息。
实现toString可以指定返回值的格式,也可以不指定。前者可以更方便解析toString输出的字符串。可以更容易通过文本处理语言从log中提取所需的内容。不指定的话,更加灵活,随时可以改进格式。
无论格式是否指定,都应该在文档中明确表示。主要因为依赖格式的代码程序不可见。在改动toString时,无法知道是否有东西依赖当前格式。有了文档注释,程序员可以很直观地意识到这点,且能根据文档对toString进行修改。
无论是否指定格式,都为toString返回值中所包含的所有信息提供可以通过编程访问之的途径。就是获取对象信息不能只依靠对toString输出的字符串进行解析,而应该单独提供方法。
对于工具类这种不会产生实例的类不需要覆盖toString。
IDE都会提供自动生成toString的方法,我试了下,还是很不错的。推荐在需要打log记录对象状态时去实现一下对应类的toString方法。通过快捷键其实很方便。
@Override
public String toString() {
return "Order{" +
"customerCode='" + customerCode + '\'' +
", customizedRef1='" + customizedRef1 + '\'' +
", deliverySequence=" + deliverySequence +
", isNeedToUpdate=" + isNeedToUpdate +
", cargo=" + cargo +
'}';
}
总之,尽量在每一个可实例化的类中实现toString,使其简洁美观地输出对象丰富的信息。