Java对象

3/22/2022 对象

# equals方法&hashCode方法

# 覆盖equals时需要遵守的约定

equals 方法实现了等价关系(equivalence relation),其属性如下:

  1. 自反性(reflexive): 对于任何非null 的引用值x,x.equals(x)必须返回true。

  2. 对称性(symmetric):对于任何非null 的引用值x和y ,当且仅当y.equals(x)返回true 时,x.equals(y)必须返回true。

  3. 传递性(transitive):对于任何非null的引用值x 、y 和z,如果x.equals(y)返回true ,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。

  4. 一致性(consistent):对于任何非null 的引用值x和y,只要equals 的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。

  5. 对于任何非null 的引用值x, x.equals (null)必须返回false。

# 实现equals的技巧

  1. 可以使用==操作符检查“参数是否是这个对象的引用”

  2. 使用instanceof操作符检查“参数是否是正确的类型”

  3. 把参数转换为正确的类型。因为转换之前进行过instanceof测试,所以转换一定可以成功

  4. 对于该类的每个“关键”域,检查参数中的域与该对象中对应的域是否匹配

    • 对于既不是float也不是double的基本类型,可以直接使用操作符进行比较

    • 对于对象引用域,可以递归地调用equals方法进行比较

    • 对于float域,可以使用静态方法**Float.compare(float,float)进行比较;对于double域,可以使用静态方法Double.compare(double,double)**进行比较

    • 不建议使用Float.equals和Double.equals方法对float和double进行比较,因为每次都需要进行自动装箱,这会导致性能的下降

    • 对于数组域,需要将以上原则应用到每一个元素上

    • 有些对象引用域包含null可能是合法的,所以,为了避免可能导致Null PointerException 异常,则使用静态方法Objects.equals(Object, Object) 来检查这类域的等同性

# 覆盖equals时总要覆盖hashCode方法

  1. 如果不这样做,将会违反hashCode的通用规定,从而导致该类无法结合所有基于散列的集合一起正常运作,这类集合包括HashMap和HashSet。

  2. Object规范

    • 在应用程序的执行期间,只要对象的equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用, hashCode 方法都必须始终返回同一个值。在一个应用程序与另一个程序的执行过程中,执行hashCode 方法所返回的值可以不一致。

    • 如果两个对象根据equals(Object )方法比较是相等的,那么调用这两个对象中的hashCode 方法都必须产生同样的整数结果

    • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的hashCode 方法,则不一定要求hashCode 方法必须产生不同的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表( hashtable )的性能。

  3. 如果没有覆盖hashCode方法,将会违反上述第二条规定即相同的对象必须具有相等的散列码。

  4. 根据类的equals方法可以看到,两个截然不同的实例在逻辑上有可能是相等的,但是根据Object类的hashCode方法,它们仅仅是两个没有任何共同之处的对象。

/**
 * @Author WaleGarrett
 * @Date 2021/12/14 16:56
 */
public class Equals {
    int age;
    String name;
    Equals(){}
    Equals(int age, String name){
        this.age = age;
        this.name = name;
    }
    public static void main(String[] args) {
        HashMap<Equals, Integer> hashMap = new HashMap<>();
        Equals equals = new Equals(22, "wale");
        hashMap.put(equals, 1);
        // 结果输出null
        System.out.println(hashMap.get(new Equals(22, "wale")));
    }
    @Override
    public boolean equals(Object another){
        if(another == null)
            return false;
        if(!(another instanceof Equals))
            return false;
        Equals oth = (Equals) another;
        return age == oth.age && Objects.equals(name, oth.name);
    }
    @Override
    public int hashCode(){
        return super.hashCode();
    }
}

# 覆盖hashCode方法的技巧

  1. 声明一个int 变量并命名为result ,将它初始化为对象中第一个关键域的散列码c ,如步骤2.a 中计算所示(如第10 条所述,关键域是指影响巳quals 比较的域) 。

  2. 对象中剩下的每一个关键域f 都完成以下步骤:

    1. 为该域计算int 类型的散列码C:

      • 如果该域是基本类型,则计算Type. hashCode ( f ),这里的Type 是装箱基本类型的类,与f 的类型相对应。

      • 如果该域是一个对象引用,并且该类的equals 方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode 。如果需要更复杂的比较,则为这个域计算一个“范式”( canonical representation),然后针对这个范式调用hashCode 。如果这个域的值为null, 则返回0 (或者其他某个常数,但通常是0 ) 。

      • 如果该域是一个数组,则要把每一个元素当作单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来。如果数组域中没有重要的元素,可以使用一个常量,但最好不要用0 。如果数组域中的所有元素都很重要,可以使用Arrays . hashCode 方法。

    2. 按照下面的公式,把步骤2 . a 中计算得到的散列码c 合并到result 中:result = 31 * result + c;

      • 这里的乘法计算可以使散列值依赖于域的顺序,比如String中,如果省略了乘法,那么所有只有字母顺序不同的字符串的散列码都会相同。

      • 此外,31有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i = = ( i < < 5 ) - i

    现代的虚拟机可以自动完成这种优化。

  3. 返回result;

/**
 * @Author WaleGarrett
 * @Date 2021/12/14 16:56
 */
public class Equals {
    int age;
    String name;
    Equals(){}
    Equals(int age, String name){
        this.age = age;
        this.name = name;
    }
    public static void main(String[] args) {
        HashMap<Equals, Integer> hashMap = new HashMap<>();
        Equals equals = new Equals(22, "wale");
        hashMap.put(equals, 1);
        System.out.println(hashMap.get(new Equals(22, "wale")));
    }
    @Override
    public boolean equals(Object another){
        if(another == null)
            return false;
        if(!(another instanceof Equals))
            return false;
        Equals oth = (Equals) another;
        return age == oth.age && Objects.equals(name, oth.name);
    }
    @Override
    public int hashCode(){
        int result = 0;
        result = 31 * result + Integer.hashCode(age);
        result = 31 * result + name.hashCode();
        return result;
    }
}

# Java协变

  1. 对于一个类Child来说,它是Parent类的子类,如果Child[]同样是Parent[]的子类,那么就说是协变的。

  2. 也就是说,数组是协变的,以下的语句是合法的,但是可能在运行时报错。

 Object[] objecs = new Long[10];// 可以通过编译  
 objecs[0] = "hello";// 运行时报错

# 列表优于数组

  1. 列表不是协变的,它是可变的,也就是说:对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。

  2. 也就是说,下列的语句将报错:

 List<Object> o = new ArrayList<Long>();// 编译期就报错  
 o.add("hello");

# 创建泛型数组非法

  1. 正是因为数组和泛型的这些区别,导致泛型和数组不能很好的混合使用。比如,创建泛型数组(包括**new List<E>[],new List<String>[] 以及 new E[]**等)是非法的。

  2. 创建泛型数组是非法的原因是,它们不是安全的,编译器在其他正确的程序中进行转换会失败,并且抛出一个ClassCastException异常。

 List<String>[] stringList = new List<String>[1];// 非法创建泛型数组,编译期报错  
 List<Integer> intList = List.of(42);  
 Object[] objects = stringList;  
 objects[0] = intList;  
 String s = stringList[0].get(0);
  1. 我们假设第l 行是合法的,它创建了一个泛型数组。第2 行创建并初始化了一个包含单个元素的List<Integer>。第3 行将List<String>数组保存到一个Object 数组变量中,这是合法的,因为数组是协变的。第4 行将List<Integer>保存到Object 数组里唯一的元素中,这是可以的,因为泛型是通过擦除实现的: List<Integer>实例的运行时类型只是List, List<String>[]实例的运行时类型则是List[],因此这种安排不会产生ArrayStoreException异常。但现在我们有麻烦了。我们将一个List<Integer>实例保存到了原本声明只包含List<String>实例的数组中。在第5 行中,我们从这个数组里唯一的列表中获取了唯一的元素。编译器自动地将获取到的元素转换成String ,但它是一个Integer,因此,我们在运行时得到了一个ClassCastException 异常。为了防止出现这种情况,(创建泛型数组的)第1 行必须产生一条编译时错误。
Last Updated: 3/25/2022, 1:01:36 PM