Effective Java ---- ch03 methods common to all objects笔记

Ch03 Item 10 – 14 methods common to all objects.

 

Item 10 Obey the general contract when override equals

 

这一条看完感觉内容比较多,所以看完直接开始写笔记了。并没有等全部第三章整理完。后续的坑慢慢填,也欢迎有人一起填。

equals方法是Object对象的基本方法,根据需要,新建的对象可能会重写该方法。

 

不需要重写equals方法:对象永远只保持和自己相等。

适用于:

  1. 每个实例都是唯一确定的,即对象本身的存在比值重要。如Thread
  2. 类本身并不需要提供逻辑想等的逻辑。如java.util.regex.Pattern
  3. 父类已经重写了equals方法,且父类的方法在子类仍然适用。如AbstractSet已经实现equals方法,HashSet则不再重写
  4. 类是私有的(private),或者包内私有的(package-private),且确认equals方法不会被调用。这种情况下不用重写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,违反了对称性。

有两种途径可以解决上出问题:

  1. 使用getClass替代instanceof来进行检验。好处上不再出现上述状况。坏处是无法活用对象的多态性。

  2. 使用组合代替继承。将原本子类多余父类的“值”成员与父类进行组合。在组成的类equals方法中调用父类equals方法。

反例:java.sql.Timestamp继承自java.util.Date,添加了nanoseconds字段,其equals方法不满足传递性。

 

4. 违反一致性:

对于不可变对象,equals方法不能依赖于可变对象。

反例:java.net.URL中的equals方法依赖于IP。需要通过网络判断。可能导致结果不一致。

 

5. 非空性

对于参数需要判断空, null instanceof Type ==  false. (还能怎么说?)

 

综上。高质量的equals方法应有:

  1. 用==检查参数是否来自于对象本身的引用:节约比较开销。

  2. 用instanceof检查类型是否正确:注意考虑类型之间的集成和接口的实现。

  3. 进行类型转换

  4. 对需要进行比较的域,进行比较。
    对于非float和double的基础类型使用==
    对于float/double使用Float/Double.compare(o1, o2)
    对于对象使用equals,对象内适用上述规则。
    对于数组: 1.对想要比较的元素,适用上述规则进行比较,对于可能为空的元素使用Objects.equals(o1, o2); 2. 对于全组比较的数组,使用Arrays.equals();

可以存储域的范式,来降低比较的消耗。

域的比较顺序会影响性能,优先比较更可能不同的域,或比较代价较小的域。

不比较非逻辑影响的域,如用于同步操作的lock。

若衍生域可以由其他重要域推导,则可以不比较衍生域。如对于其他的域综合描述。

若衍生域可以决定是否相等,则优先比较衍生域。如其他元素的加和。

 

完成equals方法时,请确认对称性、传递性、一致性。

警告:

  1. 重写equals方法时重写HashCode方法

  2. 不要试图使equals方法太过“智能”,如在equals方法中处理别称。

  3. 注意重写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/

Item 11 Always override hashCode when you override equals

 

为对象重写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的计算细节, 为以后扩展留下空间.

 

Item 12. Always override toString

Object中的toString方法实现为className@hashCode.

 

提供良好的toString方法实现,能够使类更加易用,便于debug. toString方法会在打印输出,字符串操作,断言,日志打印等处被调用.

实践中,toString方法应该返回所有在该类上值得关注的信息.比较理想的toString方法返回的信息应该是可以自解释的.

 

应指明toString方法返回值的格式,并在文档中说明.返回值的格式应富含可以解释和获得的信息.

 

静态工具类无需实现toString方法.(item 4)

大多数枚举类无需实现toString方法.(item 34)

 

抽象类应该提供toString方法的实现,为子类提供功能信息的获取.

 

相关框架: google出品AutoValue, 个人试用了一下,感觉比较迷. 以及IDE提供自动生成toString方法,

 

Item 13. Override clone judiciously

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. 无需要类型转换.

 

Item 14. consider implementing comparable

对象类型对应的数组排序和有序集合的存储都基于对对象的排序.

因此,具有值属性且具有明显自然顺序的类应实现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方法.

你可能感兴趣的:(java)