ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

判等问题:如何确定程序的判断是正确的?

2022-06-10 22:34:17  阅读:204  来源: 互联网

标签:String 判等 127 程序 equals log Integer new 正确


文章内容摘自与极客时间——《Java 业务开发常见错误 100 例》

  判断在我们的代码里随处可见,虽然常见,但是这一行代码处理不当,就可能会出现 Bug,甚至是引起内存泄漏等问题。判等类 Bug 不太容易发现,可能会被隐藏很久。
  今天就来好好聊一聊判等的问题。

注意 equlas 和 == 的区别

  在业务代码中,我们通常使用 == 和 equals 来进行判等操作。equals 是方法,而 == 则是操作符,两者是有区别的:

  • 对于基础数据类型,比如 int、long、double 等基础数据类型,只能够使用 == 进行判等,他们进行时值判断
  • 对于对象类型, == 比较的是两个对象的直接指针,所以是判断两者在内存中的地址;而 equals 通常是用于比较两个对象的内容。

  上面的这段结论应该是我们都知道的一个结论:比较值的内容,除了基本类型只能使用 == 外,其他类型都需要使用 equals。

  接下来我们通过例子进行说明:

  1. 使用 == 对两个值为 127 的直接赋值的 Integer 对象判等;
  2. 使用 == 对两个值为 128 的直接赋值的 Integer 对象判等;
  3. 使用 == 对一个值为 127 的直接赋值的 Integer 和另一个通过 new Integer 声明的值为 127 的对象判等;
  4. 使用 == 对两个通过 new Integer 声明的值为 127 的对象判等;
  5. 使用 == 对一个值为 128 的直接赋值的 Integer 对象和另一个值为 128 的 int 基本类型判等。
Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
log.info("\nInteger a = 127;\n" +
        "Integer b = 127;\n" +
        "a == b ? {}",a == b);    // true

Integer c = 128; //Integer.valueOf(128)
Integer d = 128; //Integer.valueOf(128)
log.info("\nInteger c = 128;\n" +
        "Integer d = 128;\n" +
        "c == d ? {}", c == d);   //false

Integer e = 127; //Integer.valueOf(127)
Integer f = new Integer(127); //new instance
log.info("\nInteger e = 127;\n" +
        "Integer f = new Integer(127);\n" +
        "e == f ? {}", e == f);   //false

Integer g = new Integer(127); //new instance
Integer h = new Integer(127); //new instance
log.info("\nInteger g = new Integer(127);\n" +
        "Integer h = new Integer(127);\n" +
        "g == h ? {}", g == h);  //false

Integer i = 128; //unbox
int j = 128;
log.info("\nInteger i = 128;\n" +
        "int j = 128;\n" +
        "i == j ? {}", i == j); //true

  在第一个案例中,编译器会把 Integer a = 127 转换为 Integer.valueOf(127)。查看源码可以发现,这个转换其实在内部做了缓存,使得两个 Integer 指向了同一个对象,所以结果是 true。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

  第二个案例是因为 Integer 的默认缓存大小是[-128,127] 数值之间,128 在这个区间之外,但这个其实是可以更改的,设置 JVM 参数加上 -XX:AutoBoxCacheMax=1000 再试试,是不是就返回 true 了呢?
  第三第四个案例是因为 New 出来的对象都是不走缓存的,比较两个新对象,结果肯定不是同一个对象。第五个案例则是因为拆箱的原因,比较的是数值而不是引用。
  看到这人,其实只要记住比较 Integer 去使用 equals 而不是 == 能避免大部分的问题。

String 也会有这种问题

  String 也会出现上述这种问题,我们可以用几个用例来测试一下:

  1. 对两个直接声明的值都为 1 的 String 使用 == 判等;
  2. 对两个 new 出来的值都为 2 的 String 使用 == 判等;
  3. 对两个 new 出来的值都为 3 的 String 先进行 intern 操作,再使用 == 判等;
  4. 对两个 new 出来的值都为 4 的 String 通过 equals 判等。
String a = "1";
String b = "1";
log.info("\nString a = \"1\";\n" +
        "String b = \"1\";\n" +
        "a == b ? {}", a == b); //true

String c = new String("2");
String d = new String("2");
log.info("\nString c = new String(\"2\");\n" +
        "String d = new String(\"2\");" +
        "c == d ? {}", c == d); //false

String e = new String("3").intern();
String f = new String("3").intern();
log.info("\nString e = new String(\"3\").intern();\n" +
        "String f = new String(\"3\").intern();\n" +
        "e == f ? {}", e == f); //true

String g = new String("4");
String h = new String("4");
log.info("\nString g = new String(\"4\");\n" +
        "String h = new String(\"4\");\n" +
        "g == h ? {}", g.equals(h)); //true

  在分析结果之前,你需要知道 java 的常量池机制,设计常量池的初衷是为了节省内存。当代码中出现双引号形式创建的字符串对象时,JVM就会对这个字符串进行检测,如果常量池中已经存在相同内容字符串的引用时,就返回该字符串的引用;否则,创建新的字符串。这种机制叫字符串池化。

  紧接着回到刚才的案例。
  那么第一个案例是通过双引号声明的两个 String 类型的对象,那么因为 java 的字符串池化机制,结果是 true;第二个案例,new 出来的对象引用当然不同,结果是 false;第三个案例,intern方法走的也是常量池机制,所以结果也是 true;第四个案例,通过 equals 对值内容进行判等,是正确的形式,结果是true。

  这里提一下 intern 方法,我的建议是能不用就不要用。首先是该方法我在日常开发中真的很少看到有人去使用该方法,其次是滥用该方法,可能产生性能问题。
  写个代码测试一下:

List<String> list = new ArrayList<>();

@GetMapping("internperformance")
public int internperformance(@RequestParam(value = "size", defaultValue = "10000000")int size) {
    //-XX:+PrintStringTableStatistics
    //-XX:StringTableSize=10000000
    long begin = System.currentTimeMillis();
    list = IntStream.rangeClosed(1, size)
            .mapToObj(i-> String.valueOf(i).intern())
            .collect(Collectors.toList());
    log.info("size:{} took:{}", size, System.currentTimeMillis() - begin);
    return list.size();
}

  在启动程序时设置 JVM 参数 -XX:+PrintStringTableStatistic,程序退出时可以打印出字符串常量表的统计信息。调用接口后关闭程序,输出如下:

[11:01:57.770] [http-nio-45678-exec-2] [INFO ] [.t.c.e.d.IntAndStringEqualController:54  ] - size:10000000 took:44907
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :  10030230 = 240725520 bytes, avg  24.000
Number of literals      :  10030230 = 563005568 bytes, avg  56.131
Total footprint         :           = 804211192 bytes
Average bucket size     :   167.134
Variance of bucket size :    55.808
Std. dev. of bucket size:     7.471
Maximum bucket size     :       198

  intern 操作耗时达到 44 秒。其实,原因在于字符串常量是一个固定容量的 Map。如果容量太小,字符串太多,那么每一个桶中的字符串数量会非常多,所以搜索起来就很慢。解决方式是,设置 JVM 参数 -XX:StringTableSize,指定更多的桶。

实现一个 equals 没有那么简单

  如果你看到 Object 的源码,你就会知道 equals 方法实际上用的是 == 进行判断。之所以 Integer 或者 String 这类对象能够进行内容判断,是因为他们重写了 equals 方法。
  我们也是能经常碰到需要自己重写 equals 的场景,写个案例,假设有这样一个描述点的类 Point,有 x、y 和描述三个属性:

class Point {
    private int x;
    private int y;
    private final String desc;

    public Point(int x, int y, String desc) {
        this.x = x;
        this.y = y;
        this.desc = desc;
    }
}

  现在我们希望只要 x 和 y 这 2 个属性一致就代表这是同一个点,所以我们需要重写 equals 方法,而不是用 Object 的原始的 equals。

@Override
public boolean equals(Object o) {
	PointWrong that = (PointWrong) o;
	return x == that.x && y == that.y;
}

  但这样其实还是存在一些小问题,我们安排三个测试案例:

  • 比较一个 Point 对象和 null;
  • 比较一个 Object 对象和一个 Point 对象;
  • 比较两个 x 和 y 属性值相同的 Point 对象。
PointWrong p1 = new PointWrong(1, 2, "a");
try {
    log.info("p1.equals(null) ? {}", p1.equals(null));
} catch (Exception ex) {
    log.error(ex.getMessage());
}

Object o = new Object();
try {
    log.info("p1.equals(expression) ? {}", p1.equals(o));
} catch (Exception ex) {
    log.error(ex.getMessage());
}

PointWrong p2 = new PointWrong(1, 2, "b");
log.info("p1.equals(p2) ? {}", p1.equals(p2));

  通过日志中的结果可以看到,第一次比较出现了空指针异常,第二次比较出现了类型转换异常,第三次比较符合预期输出了 true。
  通过这些失效的案例,我们大概可以总结出

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

hashCode 和 equals 要配对实现

  定义后 equlas 方法后,我们还需要注意一点,比如现在定义两个 x 和 y 属性完全一致的 Point 对象 p1 和 p2,按照改进之后的 equlas 方法,它们一定是一致的,现在把 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));

  按理来说,是肯定为 true 的,但结果却是 false。原因很简单,散列表需要使用 hashCode 来定位元素放那个桶。如果自定义对象没有实现自定义 hashCode 方法,就会使用 Object 超类的默认实现,那么得到的 hashcode 就是不同的值(这也是我们面试题里超级常见的一个问题了)。
  要自定义 hashCode,我们可以直接使用 Objects.hash 方法来实现。

注意 compareTo 和 equals 的逻辑一致性

  除了 hashcode 和 equals 方法之外,还有个更容易被我们忽略的问题,即 ompareTo 同样需要和 equals 确保逻辑一致性。
  我之前遇到过这么一个问题,代码里本来使用了 ArrayList 的 indexOf 方法进行元素搜索,但是一位好心的开发同学觉得逐一比较的时间复杂度是 O(n),效率太低了,于是改为了排序后通过 Collections.binarySearch 方法进行搜索,实现了 O(log n) 的时间复杂度。没想到,这么一改却出现了 Bug。
  那么现在来重现一下问题:

@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;
    }
}

  然后,写一段测试代码分别通过 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. binarySearch 方法内部调用了元素的 compareTo 方法进行比较;
  2. indexOf 的结果没问题,列表中搜索不到 id 为 2、name 是 li 的学生;
  3. binarySearch 返回了索引 1,代表搜索到的结果是 id 为 2,name 是 wang 的学生。

  修复方式很简单,确保 compareTo 的比较逻辑和 equals 的实现一致即可:

@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);
    }
}

  其实,这个问题容易被忽略的原因在于两方面:一方面是我们习惯于用 @Data 来标记对象,也就是默认情况下使用类型所有的字段(不包括 static 和 transient 字段)参与到 equals 和 hashCode 方法的实现中。因为这两个方法的实现不是我们自己实现的,所以容易忽略其逻辑;另一方面,compareTo 方法需要返回数值,作为排序的依据,容易让人使用数值类型的字段随意实现。
  强调一下,,对于自定义的类型,如果要实现 Comparable,请记得 equals、hashCode、compareTo 三者逻辑一致

  最后,如果你想编码时能够及时回避掉这些问题,我建议你在 IDE 中安装阿里巴巴的 Java 规约插件(详见这里),来及时提示我们这类低级错误:
image

标签:String,判等,127,程序,equals,log,Integer,new,正确
来源: https://www.cnblogs.com/lastboss/p/16364833.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有