• 深入聊聊java中判等问题:你真的会用==和equals吗


    写在前面

    判等问题是日常开发中遇到的最常见的问题之一,虽然简单但是其中蕴含着很多坑与技巧。

    今天咱们就一起聊聊,判等问题。

    equals 和 == 的区别

    • 对基本类型,比如 int、long,进行判等,只能使用 ==,比较的是直接值。因为基本类型的值就是其数值。
    • 对引用类型,比如 Integer、Long 和 String,进行判等,需要使用 equals 进行内容判等。因为引用类型的直接值是指针,使用 == 的话,比较的是指针,也就是两个对象在内存中的地址,即比较它们是不是同一个对象,而不是比较对象的内容。

    也就是说,比较值的内容,除了基本类型只能使用 == 外,其他类型都需要使用 equals。

    警惕int类型数据的==比较

    我们看以下代码:

    Integer a = 127; //Integer.valueOf(127)
    Integer b = 127; //Integer.valueOf(127)
    log.info(a == b);    // true
    
    Integer c = 128; //Integer.valueOf(128)
    Integer d = 128; //Integer.valueOf(128)
    log.info(c == d);   //false
    
    Integer e = 127; //Integer.valueOf(127)
    Integer f = new Integer(127); //new instance
    log.info(e == f);   //false
    
    // new的对象比较的是地址
    Integer g = new Integer(127); //new instance
    Integer h = new Integer(127); //new instance
    log.info(g == h);  //false
    
    // 把装箱的 Integer 和基本类型 int 比较,前者会先拆箱再比较,比较的肯定是数值而不是引用
    Integer i = 128; //unbox
    int j = 128;
    log.info(i == j); //true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    查看Integer的源码可以发现,这个转换在内部其实做了缓存,使得两个 Integer 指向同一个对象,所以 == 返回 true。
    Integer中有个内部类IntegerCache,缓存-128到127的int类型的数据。

    // Integer源码
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    // Integer内部类
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];
    
        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;
    
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
    
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }
    
        private IntegerCache() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    其中,使用-XX:AutoBoxCacheMax=1000启动参数,可以将Integer的缓存扩展到1000

    对String类型的数据进行==比较

    
    // Java 的字符串驻留机制,直接使用双引号声明出来的两个 String 对象指向常量池中的相同字符串
    String a = "1";
    String b = "1";
    log.info(a == b); //true
    
    // new新的对象用==比较为地址值的比较
    String c = new String("2");
    String d = new String("2");
    log.info(c == d); //false
    
    // intern()方法会将字符串存放在字符串常量池中
    String e = new String("3").intern();
    String f = new String("3").intern();
    log.info(e == f); //true
    
    // equals相当于调用了String的equals方法
    String g = new String("4");
    String h = new String("4");
    log.info(g.equals(h)); //true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    关于intern的详细用法请移步:String的Intern()方法,详解字符串常量池!
    没事别轻易用 intern,如果要用一定要注意控制驻留的字符串的数量,并留意常量表的各项指标。

    实现一个equals

    Object类的equals其实就是比较对象的地址值(用= =比较)。

    String和Integer的equals

    public boolean equals(Object obj) {
        return (this == obj);
    }
    
    • 1
    • 2
    • 3

    之所以 Integer 或 String 能通过 equals 实现内容判等,是因为它们都重写了这个方法。
    比如,String 的 equals 的实现:

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    Integer的equals的实现:

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    自定义类的equals

    对于自定义类型,如果不重写 equals 的话,默认就是使用 Object 基类的按引用的比较方式(Object 超类中的 equals 默认使用 == 判等,比较的是对象的引用)。

    考虑到性能,可以先进行指针判等,如果对象是同一个那么直接返回 true;
    需要对另一方进行判空,空对象和自身进行比较,结果一定是 fasle;
    需要判断两个对象的类型,如果类型都不同,那么直接返回 false;
    确保类型相同的情况下再进行类型强制转换,然后逐一判断所有字段。

    class PointWrong {
        private int x;
        private int y;
        private final String desc;
    
        public PointWrong(int x, int y, String desc) {
            this.x = x;
            this.y = y;
            this.desc = desc;
        }
    
    	// 有问题的equals
        @Override
        public boolean equals(Object o) {
            PointWrong that = (PointWrong) o;
            return x == that.x && y == that.y;
        }
    
    	// 改进后的equals
    	@Override
    	public boolean equals(Object o) {
    	    if (this == o) return true;
    	    if (o == null || getClass() != o.getClass()) return false;
    	    PointRight that = (PointRight) o;
    	    return x == that.x && y == that.y;
    	}    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    hashCode 和 equals 要配对实现

    定义两个 x 和 y 属性值完全一致的 Point 对象 p1 和 p2,把 p1 加入 HashSet,然后判断这个 Set 中是否存在 p2:

    PointWrong p1 = new PointWrong(1, 2, "a");
    PointWrong p2 = new PointWrong(1, 2, "b");
    
    HashSet<PointWrong> points = new HashSet<>();
    points.add(p1);
    log.info("points.contains(p2) ? {}", points.contains(p2)); // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    按照改进后的 equals 方法,这 2 个对象可以认为是同一个,Set 中已经存在了 p1 就应该包含 p2,但结果却是 false。

    出现这个 Bug 的原因是,散列表需要使用 hashCode 来定位元素放到哪个桶。如果自定义对象没有实现自定义的 hashCode 方法,就会使用 Object 超类的默认实现,得到的两个 hashCode 是不同的,导致无法满足需求。

    要自定义 hashCode,我们可以直接使用 Objects.hash 方法来实现,改进后的 Point 类如下:

    
    class PointRight {
        private final int x;
        private final int y;
        private final String desc;
        ...
        @Override
        public boolean equals(Object o) {
            ...
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(x, y);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    不要害怕实现equals和hashCode方法,IDEA 是可以自动生成这俩方法的:
    在这里插入图片描述

    注意 compareTo 和 equals 的逻辑一致性

    定义一个 Student 类,有 id 和 name 两个属性,并实现了一个 Comparable 接口来返回两个 id 的值:

    
    @Data
    @AllArgsConstructor
    class Student implements Comparable<Student>{
        private int id;
        private String name;
    
        @Override
        public int compareTo(Student other) {
            int result = Integer.compare(other.id, id);
            if (result==0)
                log.info("this {} == other {}", this, other);
            return result;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    然后,写一段测试代码分别通过 indexOf 方法和 Collections.binarySearch 方法进行搜索。
    列表中我们存放了两个学生,第一个学生 id 是 1 叫 zhang,第二个学生 id 是 2 叫 wang,搜索这个列表是否存在一个 id 是 2 叫 li 的学生:

    
    @GetMapping("wrong")
    public void wrong(){
    
        List<Student> list = new ArrayList<>();
        list.add(new Student(1, "zhang"));
        list.add(new Student(2, "wang"));
        Student student = new Student(2, "li");
    
        log.info("ArrayList.indexOf");
        int index1 = list.indexOf(student);
        Collections.sort(list);
        log.info("Collections.binarySearch");
        int index2 = Collections.binarySearch(list, student);
    
        log.info("index1 = " + index1);
        log.info("index2 = " + index2);
    }
    
    // 执行结果
    
    [18:46:50.226] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:28  ] - ArrayList.indexOf
    [18:46:50.226] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:31  ] - Collections.binarySearch
    [18:46:50.227] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:67  ] - this CompareToController.Student(id=2, name=wang) == other CompareToController.Student(id=2, name=li)
    [18:46:50.227] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:34  ] - index1 = -1
    [18:46:50.227] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:35  ] - index2 = 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    我们注意到如下几点:
    binarySearch 方法内部调用了元素的 compareTo 方法进行比较;
    indexOf 的结果没问题,列表中搜索不到 id 为 2、name 是 li 的学生;
    binarySearch 返回了索引 1,代表搜索到的结果是 id 为 2,name 是 wang 的学生。

    我们看一下ArrayList的indexOf方法,实际上调用的是equals方法判断相等:

        public int indexOf(Object o) {
            if (o == null) {
                for (int i = 0; i < size; i++)
                    if (elementData[i]==null)
                        return i;
            } else {
                for (int i = 0; i < size; i++)
                    if (o.equals(elementData[i]))
                        return i;
            }
            return -1;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    而Collections.binarySearch实际上是调用的compareTo方法判断相等:

        private static <T>
        int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key)
        {
            int low = 0;
            int high = list.size()-1;
            ListIterator<? extends Comparable<? super T>> i = list.listIterator();
    
            while (low <= high) {
                int mid = (low + high) >>> 1;
                Comparable<? super T> midVal = get(i, mid);
                int cmp = midVal.compareTo(key);
    
                if (cmp < 0)
                    low = mid + 1;
                else if (cmp > 0)
                    high = mid - 1;
                else
                    return mid; // key found
            }
            return -(low + 1);  // key not found
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    修复方式很简单,确保 compareTo 的比较逻辑和 equals 的实现一致即可。重新实现一下 Student 类,通过 Comparator.comparing 这个便捷的方法来实现两个字段的比较:

    
    @Data
    @AllArgsConstructor
    class StudentRight implements Comparable<StudentRight>{
        private int id;
        private String name;
    
        @Override
        public int compareTo(StudentRight other) {
            return Comparator.comparing(StudentRight::getName)
                    .thenComparingInt(StudentRight::getId)
                    .compare(this, other);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    注意contains方法、putIfAbsent、distinct等等

    所有的集合类,判断对象是否相等,基本就是靠着equals或者hashcode或者compareTo,注意具体情况具体分析。

    小心 Lombok 生成代码的“坑”

    Lombok 的 @Data 注解会帮我们实现 equals 和 hashcode 方法,但是有继承关系时,Lombok 自动生成的方法可能就不是我们期望的了。
    @EqualsAndHashCode 默认实现没有使用父类属性。
    为解决这个问题,我们可以手动设置 callSuper 开关为 true,来覆盖这种默认行为:

    
    @Data
    @EqualsAndHashCode(callSuper = true)
    class Employee extends Person {
    
    • 1
    • 2
    • 3
    • 4

    参考资料

    本文参考了朱晔(贝壳金融资深架构师)老师对于判等问题的讲解。

  • 相关阅读:
    【HTTPS】通过OpenSSL在Windows上生成Https证书
    巧用正则表达式
    大小端问题(存储模式)
    SpringCash
    LeetCode每日一题(2256. Minimum Average Difference)
    Android 11.0 下拉状态栏录屏去掉弹窗直接录屏
    C及C++每日练习(3)
    16.(开发工具篇mysql)mysql不同库同步数据的异常记录
    【记录】终端如何 进入conda(base) 环境,如何退出 conda(base)环境,终端快速进入Jupyter notebook的方法
    nginx配置指南
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/126409857