Welcome everyone

Equals和==的简单比较

java Abfahrt 607浏览 0评论

Equals和==的简单比较

1. equals和 == 的区别

1.1 == 比较的是内存地址。

  • 举例分析:
//首先自定义一个Fruit类
public class Fruit {
    private String name;
    private int number;

    public Fruit(String name, int number) {
        this.name = name;
        this.number = number;
    }
}
  • 验证:
public class Main {
    public static void main(String[] args) {
        Fruit apple1 = new Fruit("apple", 10);
        Fruit apple2 = new Fruit("apple", 10);
        Fruit apple3 = apple1;

        System.out.println(apple1==apple2); //false
        System.out.println(apple1==apple3); //true
    }
}
  • 明显== 比较的是内存地址。

1.2 equals 比较的是可重载的等价关系

  • equals方法实现了的是对一个对象的非空引用的等价关系的判定。
  • 接着上面的例子:
public class Main {
    public static void main(String[] args) {
        Fruit apple1 = new Fruit("apple", 10);
        Fruit apple2 = new Fruit("apple", 10);

        System.out.println(apple1.equals(apple2)); //false
    }
}

  • 依然是false。查看equals方法:
public boolean equals(Object obj) {
    return (this == obj);
}
  • 内部默认调用的是==比较,apple1和apple2显然不是指向同一内存。
  • 接下来重载equals方法:
public class Fruit {
    private String name;
    private int number;

    public Fruit(String name, int number) {
        this.name = name;
        this.number = number;
    }

    @Override
    public boolean equals(Object obj) {
        //内存地址相同肯定相同
        if (this == obj) {
            return true;
        }
        //内容相同,则认为相同
        if (obj instanceof Fruit) {
            Fruit aFruit = (Fruit) obj;
            return name.equals(aFruit.name) && number == aFruit.number;
        }
        return false;
    }
}
  • 再次判断apple1.equals(apple2)结果即为true

2.重载equals时要重载hashCode()方法

  • hashCode()主要作用在我们使用哈希表(散列表)时。
  • 重载hashCode()是为了使我们定义的等价关系更为完整。

2.1为什么需要重载hashCode()?

  • 一般来说,在我们重载equals方法时,也要同时重载hashCode()方法。
  • 当我们只重载了equals方法时:
public class Main {
    public static void main(String[] args) {
        Fruit apple1 = new Fruit("apple", 10);
        Fruit apple2 = new Fruit("apple", 10);

        System.out.println(apple1.equals(apple2)); //true
        System.out.println(apple1.hashCode());     //189568618(不同机器不一定相同)
        System.out.println(apple2.hashCode());     //793589513(不同机器不一定相同)
    }
}
  • 我们重写equals方法后,希望的是:apple1和apple2在最大程度上是等价的。但是,显而易见在hashCode层面上apple1和apple2并不相同。
  • 当我们用到HashMap时会出现以下这种问题:
public class Main {
    public static void main(String[] args) {
        Fruit apple1 = new Fruit("apple", 10);
        Fruit apple2 = new Fruit("apple", 10);

        Map fruits = new HashMap();
        fruits.put(apple1,"Unrelated!");

        System.out.println(fruits.containsKey(apple1)); //true
        System.out.println(fruits.containsKey(apple2)); //false
    }
}
  • 我们重载equals定义的等价关系是期望在最大程度上将apple1和apple2视为相等(除了内存角度,内存角度上apple1肯定不等apple2)。因此,我们期望的结果是,在HashMap中添加apple1后,通过containsKey来判断是否含有apple2时,应该返回true。
  • 重载hashCode()方法:
public class Fruit {
    private String name;
    private int number;

    public Fruit(String name, int number) {
        this.name = name;
        this.number = number;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof Fruit) {
            Fruit aFruit = (Fruit) obj;
            return name.equals(aFruit.name) && number == aFruit.number;
        }
        return false;
    }

    @Override
    public int hashCode() {
        //这里只是展示重载hashCode,并不严谨
        return name.length()+number*1000;
    }
}
  • 再来看之前的结果:
public class Main {
    public static void main(String[] args) {
        Fruit apple1 = new Fruit("apple", 10);
        Fruit apple2 = new Fruit("apple", 10);

        System.out.println(apple1.hashCode());//10005
        System.out.println(apple2.hashCode());//10005

        Map fruits = new HashMap();
        fruits.put(apple1,"Unrelated!");

        System.out.println(fruits.containsKey(apple1)); //true
        System.out.println(fruits.containsKey(apple2)); //true
    }
}
  • 这样,结果与我们的预期便一样了!
  • 这是因为HashMap的containsKey方法实际上调用的是HashMap的getNode(int hash, Object key)方法:
//上述例子中hash对应参数传入的是:根据apple2的hashCode计算出的值
final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {//first是根据hashCode计算出来的
        //在hash值相等的情况下才会去进一步比较相等!
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
  • 同理在向HashMap加入键值对的时候,apple1和apple2也会被视为相同的键:
public class Main {
    public static void main(String[] args) {
        Fruit apple1 = new Fruit("apple", 10);
        Fruit apple2 = new Fruit("apple", 10);

        Map fruits = new HashMap();
        fruits.put(apple1,"First!");
        fruits.put(apple2,"Second!");

        System.out.println(fruits.size());      //1
        System.out.println(fruits.get(apple1)); //Second!
        System.out.println(fruits.get(apple2)); //Second!
    }
}
  • HashMap的put方法,实际上调用的是HashMap的putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)方法。该方法也会先根据键的hashCode判断,再结合==equals判断在HashMap中是否已经存在该键。

2.2 重载hashCode()应注意什么?

  • hashCode()方法会为Object返回一个哈希值。该方法是为了支持哈希表的优点
    • 哈希表的主要特点就是访问速度快,这得益于其散列的特点。HashMap正式根据插入键值对的键的hashCode来完成散列存储的操作。因此,等价的对象(比如apple1和apple2)应该存储在哈希表相同的位置,而对象位置是决定于对象的hashCode,因此等价的对象的hashCode需要相同(即重载equals后,A.equals(B)为true,那么A和B的hashCode()应该相等)。
    • 哈希表的优势同样受制于碰撞的情况,应该尽量减少碰撞现象的发生。不等价的对象应该散列在不同位置,因此不等价的对象的hashCode应尽量不同(即重载equals后,A.equals(B)为false,那么A和B的hashCode()应该尽量不相等)。如果所有对象的hashCode返回相同的值,那么HashMap中存储的所有对象都互相碰撞,HashMap将优势全无。
  • hashCode()的计算尽量稳定。示例会出现这样一种情况:
public class Main {
    public static void main(String[] args) {
        Fruit apple1 = new Fruit("apple", 10);
        System.out.println(apple1.hashCode());          //10005

        Map fruits = new HashMap();
        fruits.put(apple1,"First!");

        System.out.println(fruits.containsKey(apple1)); //true

        apple1.setNumber(12);
        System.out.println(apple1.hashCode());          //12005

        System.out.println(fruits.containsKey(apple1)); //false
    }
}
  • 当我们修改apple1对象number属性值时,会使hashCode()返回值改变,这会导致使用HashMap的containsKey方法判断apple1的键是否存在时出现错误。(新的hashCode值导致散列表查找操作命中位置改变,从而错过HashMap中存储的对象,误以为不存在)。
  • 改进hashCode()方法,使之相对稳定:
public class Fruit {
    private String name;
    private int number;

    public Fruit(String name, int number) {
        this.name = name;
        this.number = number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof Fruit) {
            Fruit aFruit = (Fruit) obj;
            return name.equals(aFruit.name) && number == aFruit.number;
        }
        return false;
    }

    @Override
    public int hashCode() {
        /**
         * number会经常变动,而name不会变动。
         * 通过不易变动的值计算的hash值会更稳定
         */
        return name.hashCode();
    }
}
  • 再看示例:
public class Main {
    public static void main(String[] args) {

        Fruit apple1 = new Fruit("apple", 10);
        System.out.println(apple1.hashCode());          //93029210

        Map fruits = new HashMap();
        fruits.put(apple1,"First!");

        System.out.println(fruits.containsKey(apple1)); //true

        apple1.setNumber(12);
        System.out.println(apple1.hashCode());          //93029210

        System.out.println(fruits.containsKey(apple1)); //true
    }
}
  • 结果符合预期!

3.有关字符串的一些比较

3.1 创建字符串 = 和 new 的区别

  • 通过 s1="abc";创建字符串时,字符串被存在常量池中,s1指向常量池中字符串的地址。
  • 通过s2=new String("abc"); 创建字符串时,首先字符串被存在常量池中,然后在堆中保存字符串的副本(副本存的是常量池中字符串的地址),最后s2指向中字符串副本的地址。
public class Main {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = new String("abc");

        System.out.println(s1==s2);                     //false
        System.out.println(System.identityHashCode(s1));//284720968
        System.out.println(System.identityHashCode(s2));//189568618
    }
}

3.2 字符串在常量池中和堆中的区别

  • 常量池中的字符串对象是唯一的。
  • 中的字符串对象是不唯一的。
public class Main {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
        String s3 = new String("abc");
        String s4 = new String("abc");

        System.out.println(s1 == s2); //true
        System.out.println(s3 == s4); //false

        System.out.println(System.identityHashCode(s1));//284720968
        System.out.println(System.identityHashCode(s2));//284720968
        System.out.println(System.identityHashCode(s3));//189568618
        System.out.println(System.identityHashCode(s4));//793589513
    }
}

3.3 intern()方法

  • 字符串存在于常量池中,intern()方法会返回一个对象引用,该对象引用指向常量池中的字符串。
public class Main {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = new String("abc");

        System.out.println(s1 == s2);           //false
        System.out.println(s1 == s2.intern());  //true

        System.out.println(System.identityHashCode(s1));//284720968
        System.out.println(System.identityHashCode(s2));//189568618
        System.out.println(System.identityHashCode(s2.intern()));//284720968
    }
}
  • 字符串不存在于常量池中,intern()方法首先会在常量池中保存该字符串对象的引用,然后返回一个对象引用,该对象引用指向常量池中的引用
public class Main {
    public static void main(String[] args) {
        String s1 = new String("ab")+new String("cd");//1
        System.out.println(s1 == s1.intern());//true
        System.out.println(System.identityHashCode(s1));//1349393271
        System.out.println(System.identityHashCode(s1.intern()));//1349393271

        String s2 = new String("ab")+new String("cd");//2
        System.out.println(s2 == s2.intern());          //false
        System.out.println(System.identityHashCode(s2));//1338668845
        System.out.println(System.identityHashCode(s2.intern()));//1349393271

        System.out.println(s1 == s2.intern());//true

        /**
         * 执行完1时,常量池中存在"ab"和"cd";堆中存在"abcd";s1指向堆中"abcd"的地址。
         * s1.intern()执行,因为常量池中并不存在"abcd",所以会将堆中"abcd"的引用保存在常量池中。
         * 此时常量池中存在的是:"ab"和"cd"和一个象征"abcd"引用,这个引用指向的是堆中保存的"abcd"。
         * 所以: s1 和 s1.intern() 实际指向同一个对象。
         */
        /**
         * 执行2后,常量池中存在的是:"ab"和"cd"和一个象征"abcd"引用(s1的);
         * 堆中存在两个"abcd"对象(一个s1的,一个s2的)。
         * s2.intern()执行,因为常量池中存在一个象征"abcd"引用,
         * 会返回一个对象引用,该对象引用指向那个象征"abcd"引用;
         * 实际上,s2.intern()返回的对象引用指向的是堆中的"abcd"(s1的)。
         * 所以: s2 和 s2.intern() 实际指向非同一个对象;
         * s1 和 s2.intern() 实际指向同一个对象。
         */
    }
}

3.4 使用中间变量也会返回指向堆中对象的引用

  • 使用” “形式声明的字符串相加,在编译阶段会被直接合并。eg.”ab”+”cd”—>”abcd”。
  • 通过中间变量,字符串相加,在编译阶段不会被合并处理,会返回堆中对象的引用。(类似new)
public class Main {
    public static void main(String[] args) {
        String s1 = "ab";
        String s2 = "ab"+"cd";
        String s3 = s1 +"cd"; //使用中间变量s1,相当于s3 = new String("XXXX")
        String s4 = "abcd";

        System.out.println(s2==s3); //false
        System.out.println(s2==s4); //true
        System.out.println(System.identityHashCode(s1));//960604060
        System.out.println(System.identityHashCode(s2));//1349393271
        System.out.println(System.identityHashCode(s3));//1338668845
        System.out.println(System.identityHashCode(s4));//1349393271
    }
}

转载请注明:汪明鑫的个人博客 » Equals和==的简单比较

喜欢 (3)

说点什么

您将是第一位评论人!

提醒
avatar
wpDiscuz