Java基本类型比较与哈希处理

Java基本类型有byte, short , long ,int ,char , double , float,boolean基本类型的比较看似简单,其实涉及的知识还是比较零散的,在JVM体系中,基本类型是存放在堆栈的栈区,栈对于线程来说是私有的变量。而堆存放的是引用所指向的复杂对象。

  • 关于Java的调用传递

关于Java的传递网上有很多说基本类型是值传递,引用类型是引用传递。例如看下面的例子:

private static void swap(int a, int b) {
        a = 2;
        b = 1;
    }
        int a = 1;
        int b = 2;

        swap(a, b);
        // 输出结果: a = 1, b = 2
        System.out.println("a = " + a + ", b = " + b);

这里调用了swap方法,并没修改两个属性的值,说明调用时传递的对象是值。对于引用类型的调用:

    private static void swap2(StringBuffer s1, StringBuffer s2) {
        s1.append("a");
        s2.append("b");
    }
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        swap2(s1, s2);
        // 输出结果 s1 = a, s2 = b
        System.out.println("s1 = " + s1 + ", s2 = " + s2);

说明传递进去的引用是一个变量的副本,但是该副本还是指向的堆上的对象,StringBuffer是一个可变对象,修改原来的对象让原先的引用的对象也改变了值。这就看起来像是引用调用。Java栈上面存放的基本类型和引用类型。由此可以确定:Jvm调用是传递的栈上面的值。

  • 关于基本类型的比较问题
    在Java中比较有三种比较, == 与 equals与hashCode。== 表示堆对象地址比较,对于所有类的超类Object定义了另外两个与比较相关的方法,equals默认就是==,hashCode就是给某一个对象定义一个哈希值,这个对于哈希表的操作是重要映射关系。默认的hashCode方法返回的是对象的物理地址对应的一个值:
/**
     * 同样的对象调用此方法应该返回同一个值,
     * 如果两个对象的equals方法相等,那两个对象的
     * hashCode也应该返回一样的值,
     * 虽然并不强制要求equals不同对象的hashCode一定要不相等,
     * 但是应该认识到hashCode尽量不一样可以提升哈希表的性能
     */
public native int hashCode();

关于基本类型的比较:
基本类型在Java类中都有对应的包装类,Short, Integer, Long…等等

        Long l1 = new Long(1L);
        Long l2 = new Long(1L);
        // false
        System.out.println(l1 == l2);

因为都是使用的new关键字,虽然两个变量的值相等,但是==比较的是两个对象地址,不相等返回false。

        Long l1 = 1L;
        Long l2 = 1L;
        // true  
        System.out.println(l1 == l2);

基本类型都会在JVM的常量池内缓存1个字节的数据,这样可以在一定范围内复用这些值,提升效率,但是这样会导致一些问题。

        Long l1 = 128L;
        Long l2 = 128L;
        // false
        System.out.println(l1 == l2);

这里返回了false因为128超出了缓存范围,会隐式构造新的对象,两个对象不想等,返回false。

        Long l1 = 128L;
        Long l2 = 128L;
        // true
        System.out.println(l1.equals(l2));

基本类型的包装类都会重写equals方法,这里比较逻辑上两个对象的值是否相等。我们看看equals的实现:

    public boolean equals(Object obj) {
        if (obj instanceof Long) {
            return value == ((Long)obj).longValue();
        }
        return false;
    }

这里调用longValue()就进行了拆箱操作,就是将包装类拆成了基本类型进行比较。基本类型的比较 == 是比较值是否相等。

        Long l1 = 128L;
        Long l2 = 128L;
        // false
        System.out.println(l1 == l2);
        // true  l1 内部拆箱
        System.out.println(l1.equals(l2));
        // true  l1 直接拆箱
        System.out.println(l1.longValue() == l2);
        // true l1 l2 同时拆箱
        System.out.println(l1.longValue() == l2.longValue());
        Integer a = 1;
        Long b = 1L;
        // true
        System.out.println(a.intValue() == b);

只要拆箱了之后都是基本类型的比较,int == 会直接让b也拆箱成基本类型比较,逻辑相等。
但是需要注意以下情况:

扫描二维码关注公众号,回复: 2299753 查看本文章
        // false
       System.out.println(new Integer(1).equals(new Long(1L)));

可以查看equals源码:

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

如果是同类型包装类,即可拆箱,否则仍然比较对象地址。这个问题十分的隐蔽。

  • 哈希比较
    网上很多资料和面试宝典什么的都强调了重写了equals一定要重写hashCode方法。 因为在集合中,很多时候是首先判断hashCode的值然后才可能去进一步比较equals的值来确认两个对象是否真的相等。很多通过hash映射来存储的运算都会用到类似于hash表的结构,对象的hash值能够找到数组对应的数组索引,对应索引位置可能是一个链表,这就是所谓的拉链法存储结构,链表就是因为对象的hash值相等,但是equals方法不相等,这个对象就会被添加到对应索引位置的链表头部。hashCode就是为了尽量散列的存储,提升hash性能,equals是逻辑上相等。例如HashMap的containsKey方法,寻找一个对象在集合中是否存在:
    public boolean containsKey(Object key) {
        // 判断该节点是否存在
        return getNode(hash(key), key) != null;
    }

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 判断hash值对应的索引下有没有链表
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // hash值相等 并且 该对象的地址相等或则该对象equals相等
            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<K,V>)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);
            }
        }
        // hash值对应索引位置没有链表,直接返回null
        return null;
    }

由此看到hashCode就是对象存储在一个hash表中的索引,在HashMap内部,这个hash表的索引为(hash & 哈希表长度) ,这样可以尽量让所有对象的hash值分散在哈希表的各个部分,极端情况下,哈希表长度为1,此时的结构就是一个链表。查询的时间复杂度接近链表,最理想情况下,该结构为一个数组,查询性能O(1)。
基本类型会重写hashCode和equals,这样在存储的时候可以像基本类型一样的去比较。例如下面的例子:

public static class User {
        private Integer id;
        private String name;
        // setter getter
}
        User user1 = new User(1, "micro");
        User user2 = new User(1, "micro");

        Set<User> set = new HashSet<User>();
        set.add(user1);
        // false
        System.out.println(set.contains(user2));

因为默认的hashCode两个对象肯定是不同的,内部映射不到数组的位置,直接就返回false。重写hashCode:

public static class User {
        private Integer id;
        private String name;

        // setter getter
        @Override
        public int hashCode() {
            return this.id;
        }
    }

比较依旧返回false,因为虽然找到了对应的链表,但是equals比较依旧会调用Object的默认==,返回false, 这样依然不能判断该对象相等,因此这里也需要重写equals。重写了equals一定要重写hashCode,否则equals方法写了等于白写,hashCode负责映射对应的数组索引,equals比较对应索引位置的链表值比较。当然只重写了hashCode也不行,只是找到了对应的索引位置,但是却比较不出对应对象的逻辑相等与否。

public static class User {
        private Integer id;
        private String name;
        // ...
        @Override
        public int hashCode() {
            return this.id;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof User) {
                User user = null;
                // 名字和id相等才逻辑相等
                return this.id == (user = (User) obj).getId() && this.name.equals(user.getName());
            }
            return false;
        }

默认id相等即逻辑相等

        User user1 = new User(1, "micro");
        User user2 = new User(1, "micro");

        Set<User> set = new HashSet<User>();
        set.add(user1);
        // true
        System.out.println(set.contains(user2));

hashCode应该尽量平均分布在整个数组,这样可以提升查询性能,但是equals相等的对象对应的hashCode应该要相等,反之则不然。

  • 基本类型的hashCode都是被重写了,并且保证equals相等的对象一定hashCode相等,这样在哈希存储的情况下,才能正常运行。基本类型的处理还需要注意拆箱和常量池缓存等操作,基本类型的传递也是值传递,但是包装类传递的是引用。

猜你喜欢

转载自blog.csdn.net/micro_hz/article/details/79626824