这一条看完感觉内容比较多,所以看完直接开始写笔记了。并没有等全部第三章整理完。后续的坑慢慢填,也欢迎有人一起填。
equals方法是Object对象的基本方法,根据需要,新建的对象可能会重写该方法。
不需要重写equals方法:对象永远只保持和自己相等。
适用于:
当类需要判断逻辑相等,且父类未重写equals方法时,则要在该类重写equals方法。这种类型往往具有“值”的意义,equals往往是在判断“值”的相等,而不是判断对象是否在引用同一对象。p.s. equals方法对对象作为集合中的元素会有影响。
当具有“值”意义的类在如下状况,可以不重写equals:
实例控制(instance control):该类确保每个值对应的对象唯一。
枚举类
重写equals方法时需满足(x, y, z 均为非空对象):
Reflexive 自反性: x.equals(x) == true
Symmetric 对称性: x.equals(y) == y.equals(x)
Transitive 传递性: x.equals(y) == true && y.equals(z) == true then: x.equals(z)
Consistent 一致性: x.equals(y) 的结果是确定的。
Non-null 非空性: x.equals(null) == false
违反上述性质的栗子:
1. 违反自反性:
public class ReflexsiveTest {
@Override
public boolean equals(Object obj) {
return false;
}
public static void main(String[] args) {
List list = new ArrayList();
ReflexsiveTest test = new ReflexsiveTest();
list.add(test);
System.out.println(list.contains(test)); // false
}
}
2. 违反对称性:
public class SymmetricTest {
public static void main(String[] args) {
CaseInsensitiveString test = new CaseInsensitiveString("sss");
String string = "sss";
List l1 = new ArrayList();
l1.add(test);
System.out.println(l1.contains(string)); // false
List l2 = new ArrayList();
l2.add(string);
System.out.println(l2.contains(test)); // true
}
public static final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
// Broken - violates symmetry!
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if (o instanceof String) // One-way interoperability!
{
return s.equalsIgnoreCase((String) o);
}
return false;
}
}
}
3. 违反传递性:
由于equals方法中使用了instanceof方法来判断类型,父类和子类可以通过类型检测的方法。
然而,一旦子类添加了“值”类型的成员,在equals方法中子类的部分成员则不再参与比较。
从而出现 父类.equals(子类1)==true,父类.equals(子类2)==ture,而子类1.equals(子类2)==false。违反传递性。
另一方面 子类.equals(父类) == false,违反了对称性。
有两种途径可以解决上出问题:
使用getClass替代instanceof来进行检验。好处上不再出现上述状况。坏处是无法活用对象的多态性。
使用组合代替继承。将原本子类多余父类的“值”成员与父类进行组合。在组成的类equals方法中调用父类equals方法。
反例:java.sql.Timestamp继承自java.util.Date,添加了nanoseconds字段,其equals方法不满足传递性。
4. 违反一致性:
对于不可变对象,equals方法不能依赖于可变对象。
反例:java.net.URL中的equals方法依赖于IP。需要通过网络判断。可能导致结果不一致。
5. 非空性
对于参数需要判断空, null instanceof Type == false. (还能怎么说?)
综上。高质量的equals方法应有:
用==检查参数是否来自于对象本身的引用:节约比较开销。
用instanceof检查类型是否正确:注意考虑类型之间的集成和接口的实现。
进行类型转换
对需要进行比较的域,进行比较。
对于非float和double的基础类型使用==
对于float/double使用Float/Double.compare(o1, o2)
对于对象使用equals,对象内适用上述规则。
对于数组: 1.对想要比较的元素,适用上述规则进行比较,对于可能为空的元素使用Objects.equals(o1, o2); 2. 对于全组比较的数组,使用Arrays.equals();
可以存储域的范式,来降低比较的消耗。
域的比较顺序会影响性能,优先比较更可能不同的域,或比较代价较小的域。
不比较非逻辑影响的域,如用于同步操作的lock。
若衍生域可以由其他重要域推导,则可以不比较衍生域。如对于其他的域综合描述。
若衍生域可以决定是否相等,则优先比较衍生域。如其他元素的加和。
完成equals方法时,请确认对称性、传递性、一致性。
警告:
重写equals方法时重写HashCode方法
不要试图使equals方法太过“智能”,如在equals方法中处理别称。
注意重写equals的方法参数类型为Object,并添加@Override重写注解——一旦将参数类型写为类型本身,则变成了equals方法重载。可能和预期不符。
比较有意思的地方是EffectiveJava墙裂推荐了google的AutoValue框架,或者说组件。和Lomok有相似的地方。都是对类进行编译期注解。
Lomok是对类自动生成getter/setter和构造函数。
AutoValue不仅可以生成上述方法,还自动生成equals方法和HashCode方法。和生成JSON和解析JSON生成的类。
不过实现方式完全不同。AutoValue需要将原本的类型,写为一个抽象类;将原本的成员写为方法名称。
在IDEA中,要使用AutoValue需要安装插件,maven下添加依赖:
com.google.auto.value
auto-value
1.5.3
provided
比较有意思的是,生成的类型
AutoValue_Typ
的构造函数声明的作用域为
default
对外提供create方法替代够在函数的使用。此举符合effective java中Item 1所提倡的。
Effective Jave认为AutoValue完美的符合Item 1, Item 10, Item 11.
AutoValue详情可以参见:
http://ryanharter.com/blog/2016/03/22/autovalue/
为对象重写equals方法时,重写hashCode方法. 该条主要作用于,对象类作为一些特殊集合(如HashSet, HashMap)的元素时的一些问题.
这些集合都使用元素的hashCode值来对元素进行分桶. 因此,重写hashCode时需要遵照一定的规则,否则使用上述集合时将出现一些鬼畜的问题.
重写时,需要遵从以下规则:
1. hashCode值需要具有一致性. 即: 同一个对象的hashCode应该相同.
2. 若有o1.equals(02) == true, 则o1.hashCode == o2.hashCode.
HashCode在集合中有两处作用:
1 – 元素进入集合时,即调用put()方法时被调用,决定元素所在桶的位置.
2 – 在集合中查找元素,即调用get()方法的时候. get不会遍历所有元素,而是只查找HashCode对应桶挂载的链表/树(JDK8+).
即使巧合查到了同一个的桶, 由于hash值不一致,极大返回null. 特别的HashMap为了优化性能,缓存了元素的Hash值, 若HashCode值不等,则不进行元素的等值校验.
3. 对象不等时,hashCode不一定不等. 不同对象具有不同的hashCode有助于提升上述集合的性能.
@Override
public int hashCode(){
return 42;
}
上述方法是合法的, 它确实能够保证hashCode的一致性. 然而,这种方法本身失去了散列表存储元素的意义.
因此,要注意规则3. 一个良好的hashCode方法,应该能够将不同的元素散列到集合不同的位置上去.
构造一个良好的hashCode方法,需要遵循如下步骤:
1. 声明变量result, 将它初始化为对象中第一个重要域的Hash值. 并按照步骤2.a计算.(回顾item10中重要域的概念)
2. 对其余的重要域f,遵循如下操作:
a. 计算域f的hash值c.
i. 对于基础类型,计算Type.hashCode(f). Type指基础类型对应的封装类型.
ii. 对于对象类型,则有: null的hash值返回0; 非空对象, 若该域的等值判断需要递归调用equals方法,则对应的hash值也递归调用hashCode方法; 对于更复杂的对象,对域f计算一个”标准表示”, 并计算其HashCode.
iii. 对于数组, 选取重要元素,计算其hash值,并按照2.b的方式进行加和, 对于不重要的元素,可以使用常量. 对于所有元素都有意义的数组,可所以使用Arrays.hashCode.
b. result = 31 * result + c;
tips: 使用31的原因: (1). 31 * I == (i<<5) – I; (2). 奇数 (偶数可能导致溢出)且素数 惯性使用.
3. return result;
注:
1. 可以排除衍生域
2. 不在equals方法中进行比较的域必须排除.
更多hash方法: guava – com.google.common.hash.Hashing
其他:
1. Objects.hash(…) 运行速度较慢. 内部调用Arrays.hashCode(), 所有传入参数参与计算, 基础元素增加装拆箱操作.
2. 对于不可变元素, 考虑缓存hash值. 若对象的hash值使用频繁,可以在对象初始化时计算hash值, 也可以延迟计算---- 在第一次调用HashCode方法时计算hash值.
3. 若对象中存在延迟加载的域, 则需要注意线程安全的问题(item83).
4. 不要试图以减少某些重要域的hash计算来提升计算性能.
5. 不应对外暴漏hashCode的计算细节, 为以后扩展留下空间.
Object中的toString方法实现为className@hashCode.
提供良好的toString方法实现,能够使类更加易用,便于debug. toString方法会在打印输出,字符串操作,断言,日志打印等处被调用.
实践中,toString方法应该返回所有在该类上值得关注的信息.比较理想的toString方法返回的信息应该是可以自解释的.
应指明toString方法返回值的格式,并在文档中说明.返回值的格式应富含可以解释和获得的信息.
静态工具类无需实现toString方法.(item 4)
大多数枚举类无需实现toString方法.(item 34)
抽象类应该提供toString方法的实现,为子类提供功能信息的获取.
相关框架: google出品AutoValue, 个人试用了一下,感觉比较迷. 以及IDE提供自动生成toString方法,
Cloneable接口原本作为minin(混入)接口(item 20)而存在, 用于申明一个类可以被复制. 然而该接口最终沦为一个标记接口.
即一颗类可能声明了实现了cloneable接口, 但仍然缺失clone方法;同时, object的clone方法为protected. (object的clone方法是native方法).
object的clone方法的JDK文档中有:
1. object方法,对于未实现cloneable接口的类, 抛出classNotSupportException.
2. 对于已实现cloneable接口的类,若未重写clone方法,则返回一个该类的新的实例, 且两个实例的域一一等值, 然而域本身并不会复制自身. 即: 只是浅拷贝.
3. 对于所有的T[] 实现clone方法
因此, 不能仅仅因为一个类声明实现了就调用其clone方法;即使通过反射, 也无法确认一个对象的clone是accessible的.
通常而言,一个类声明实现cloneable方法, 是期望能够提供一个合适可用且public的clone方法.
实现clone应遵循的机制是很微妙的: 在不调用构造器的前提下, 创建一个新的对象.
clone方法应遵循:
1. x.clone() != x;
2. x.clone().getClass() == x.getClass();
3. x.clone().equals(x) (非必须)
应在子类的clone方法中调用父类clone方法, 若所有父类均遵循上述规则, 则有规则2成立. 然而这种约束是非常脆弱的.
若类型的父类良好的实现了clone方法,且该类的成员都是基础类型或指向不可变对象, 调用super.clone方法后返回的对象,则是我们期望的.
(java支持协变返回类型. 强制类型转换也可以保证程序正常执行)
e.g.
@Override
public PhoneNumber clone(){
try{
return (PhoneNumber) super.clone();
} catch(CloneNotSupportedException e){
throw new AssertionError(); //cannot happen
}
}
注: 不应对不可变对象提供clone方法.
对于包含可变成员的对象, clone方法则相对复杂一些.
若同之前一样, 则可变成员的值只是原对象成员的引用, 即浅拷贝.
解决方案是: clone方法提供和构造器相似的功能而不使用构造器, 即确保对原始对象无影响的情况下, 对复制对象建立不变量.
e.g.
public class Stack{
private Object[] elements;
private int size = 0;
@Override
public Stack clone(){
try{
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch(CloneNotSupportException e){
throw new AssertionError();
}
}
}
T[].clone方法已实现, 且返回T[], 无需格外的实现和类型转换.
值得注意的是: 同序列化一样, 对象的final域与clone方法的架构是矛盾的. 确实存在原对象和复制对象共享final对象的情况, 但该种状况容易使人迷惑. 一个类实现cloneable接口时, 最好考虑移除final域.
有些类存在可变的对象域, 且对象域又含有自己的可变对象域, 即引用层次非常深.
则有clone方法的终极步骤:
1. 调用super.clone
2. 将所有域置为初始状态
3. 重新生成与源对象各个域相同的状态.
clone方法同构造器一样,不应调用可被重写的方法.(item 19). 若clone方法在子类改变自身状态前被调用,则可能会导致与预期结果不一致的状况.
clone方法应该省略抛出异常, 不抛出检查异常的方法更加易用(item 71)
若某个类会被继承, 则:
1. 不对该类声明实现cloneable接口, 让子类自己实现.
2. 实现cloneable接口, 且申明为final方法,
对于涉及线程安全的类, clone方法前应为synchronized.
复制对象更好的方法是提供复制对象构造器或复制工厂方法.
优势:
1. 无需依赖于clone方法不调用构造器的迷之机制.
2. 不需要依赖于脆弱的文档规约.
3. 不会与final域产生冲突.
4. 不抛出不必要的检查异常.
5. 无需要类型转换.
对象类型对应的数组排序和有序集合的存储都基于对对象的排序.
因此,具有值属性且具有明显自然顺序的类应实现comparable接口.
对于compareTo方法, 若对象比当前对象小, 则返回负数; 相等返回0;更大返回正数;
compareTo方法应满足:
1. sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
2. x.compareTo(y) > 0 && y.compareTo(z) > 0 → x.compareTo(z) > 0
3. x.compareTo(y) == 0 → sgn(x.compareTo(z)) == sgn(y.compareTo(z))
4. x.compareTo(y) == 0 →x.equals(y) [非必须]
compareTo方法和equals方法有很多相似的地方, 不同在于: compareTo方法参数不需要跨类型. 若对象不服,可直接抛出类型转换异常
public boolean equals(Object o) / public int compareTo(T o) Comparable
正如违背hash规则, 会影响散列数据结构的功能;违背compareTo的规约, 对于涉及查找和排序算法的数据结构同样会有问题.
规约第四条是非必须的. 然而, 若compareTo方法和equals方法不具有一致性, 则在一些数据结构中, 可能会产生与预期不符合的结构.
比如将两个new BigDecimal(“1.0”) 同时放入HashSet, HashSet会同时持有这两个对象, 因为调用compareTo方法认为两个对象不等.
而若使用TreeSet集合,则只有一个元素, TreeSet调用equals方法来判断两个元素是否等值.
compareTo方法的实现: 逐个比较对象的重要域, 知道不等或比较完所有的域.
java7后, 基础类型对应的封装类型都实现了静态比较方法. 对于两个对象的比较, 无需使用 < 或 > 操作符.
值得注意的是, 有些时候会使用对象的hash值,或者某个域的值进行比较. 不建议使用运算符 – 来得到结果, 可能会出现运算溢出.
java8后, comparator接口结构lambda表达式, 可以实现多种comparator. 可以使用这些comparator来实现compareTo方法.