判等问题:程序里如何确定你就是你?

ABAP 绝对类型名:用一条路径,把类型精确定位到唯一坐标 ABAP开发中,类型名的遮蔽问题可能导致严重运行时错误。绝对类型名(Absolute Type Name)通过带上下文前缀的路径式命名(如\TYPE=SPFLI、\CLASS=ZCL_FOO),能够唯一标识数据类型,避免同名遮蔽问题。它特别适用于框架开发、动态编程(RTTS)和跨程序类型访问场景,确保类型解析的确定性。RTTS可通过absolute_name属性获取类型的绝对名称,帮助诊断和解决类型相关的问题。 阅读详情

你可能会说,判等不就是一行代码的事情吗,有什么好说的。但,这一行代码如果处理不当,不仅会出现Bug,还可能会引起内存泄露等问题。涉及判等的Bug,即使是使用==这种错误的判等方式,也不是所有时候都会出问题。所以类似的判等问题不太容易发现,可能会被隐藏很久。

今天,我就equals、compareTo和Java的数值缓存、字符串驻留等问题展开讨论,希望你可以理解其原理,彻底消除业务代码中的相关Bug。

注意equals和==的区别

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

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

这就引出了我们必须必须要知道的第一个结论:比较值的内容,除了基本类型只能使用==外,其他类型都需要使用equals。

在开篇我提到了,即使使用==对Integer或String进行判等,有些时候也能得到正确结果。这又是为什么呢?

我们用下面的测试用例深入研究下:

  • 使用==对两个值为127的直接赋值的Integer对象判等;
  • 使用==对两个值为128的直接赋值的Integer对象判等;
  • 使用==对一个值为127的直接赋值的Integer和另一个通过new Integer声明的值为127的对象判等;
  • 使用==对两个通过new Integer声明的值为127的对象判等;
  • 使用==对一个值为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

通过运行结果可以看到,虽然看起来永远是在对127和127、128和128判等,但却没有永远给我们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);
}

第二个案例中,之所以同样的代码128就返回false的原因是,默认情况下会缓存[-128, 127]的数值,而128处于这个区间之外。设置JVM参数加上-XX:AutoBoxCacheMax=1000再试试,是不是就返回true了呢?

private static class IntegerCache {
static final int low = -128;
static final int high;
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;
}

}

第三和第四个案例中,New出来的Integer始终是不走缓存的新对象。比较两个新对象,或者比较一个新对象和一个来自缓存的对象,结果肯定不是相同的对象,因此返回false。

第五个案例中,我们把装箱的Integer和基本类型int比较,前者会先拆箱再比较,比较的肯定是数值而不是引用,因此返回true。

看到这里,对于Integer什么时候是相同对象什么时候是不同对象,就很清楚了吧。但知道这些其实意义不大,因为在大多数时候,我们并不关心Integer对象是否是同一个,只需要记得比较Integer的值请使用equals,而不是==(对于基本类型int的比较当然只能使用==)。

其实,我们应该都知道这个原则,只是有的时候特别容易忽略。以我之前遇到过的一个生产事故为例,有这么一个枚举定义了订单状态和对于状态的描述:

enum StatusEnum {
CREATED(1000, "已创建"),
PAID(1001, "已支付"),
DELIVERED(1002, "已送到"),
FINISHED(1003, "已完成");
private final Integer status; //注意这里的Integer
private final String desc;

StatusEnum(Integer status, String desc) {
    this.status = status;
    this.desc = desc;
}

}

在业务代码中,开发同学使用了==对枚举和入参OrderQuery中的status属性进行判等:

@Data
public class OrderQuery {
private Integer status;
private String name;
}

@PostMapping("enumcompare")
public void enumcompare(@RequestBody OrderQuery orderQuery){
StatusEnum statusEnum = StatusEnum.DELIVERED;
log.info("orderQuery:{} statusEnum:{} result:{}", orderQuery, statusEnum, statusEnum.status == orderQuery.getStatus());
}

因为枚举和入参OrderQuery中的status都是包装类型,所以通过==判等肯定是有问题的。只是这个问题比较隐晦,究其原因在于:

  • 只看枚举的定义CREATED(1000, “已创建”),容易让人误解status值是基本类型;
  • 因为有Integer缓存机制的存在,所以使用==判等并不是所有情况下都有问题。在这次事故中,订单状态的值从100开始增长,程序一开始不出问题,直到订单状态超过127后才出现Bug。

在了解清楚为什么Integer使用==判等有时候也有效的原因之后,我们再来看看为什么String也有这个问题。我们使用几个用例来测试下:

  • 对两个直接声明的值都为1的String使用==判等;
  • 对两个new出来的值都为2的String使用==判等;
  • 对两个new出来的值都为3的String先进行intern操作,再使用==判等;
  • 对两个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会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。这种机制,就是字符串驻留或池化。

再回到刚才的例子,再来分析一下运行结果:

  • 第一个案例返回true,因为Java的字符串驻留机制,直接使用双引号声明出来的两个String对象指向常量池中的相同字符串。
  • 第二个案例,new出来的两个String是不同对象,引用当然不同,所以得到false的结果。
  • 第三个案例,使用String提供的intern方法也会走常量池机制,所以同样能得到true。
  • 第四个案例,通过equals对值内容判等,是正确的处理方式,当然会得到true。

虽然使用new声明的字符串调用intern方法,也可以让字符串进行驻留,但在业务代码中滥用intern,可能会产生性能问题。

写代码测试一下,通过循环把1到1000万之间的数字以字符串形式intern后,存入一个List:

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

可以看到,1000万次intern操作耗时居然超过了44秒。

其实,原因在于字符串常量池是一个固定容量的Map。如果容量太小(Number of buckets=60013)、字符串太多(1000万个字符串),那么每一个桶中的字符串数量会非常多,所以搜索起来就很慢。输出结果中的Average bucket size=167,代表了Map中桶的平均长度是167。

解决方式是,设置JVM参数-XX:StringTableSize,指定更多的桶。设置-XX:StringTableSize=10000000后,重启应用:

[11:09:04.475] [http-nio-45678-exec-1] [INFO ] [.t.c.e.d.IntAndStringEqualController:54  ] - size:10000000 took:5557
StringTable statistics:
Number of buckets       :  10000000 =  80000000 bytes, avg   8.000
Number of entries       :  10030156 = 240723744 bytes, avg  24.000
Number of literals      :  10030156 = 562999472 bytes, avg  56.131
Total footprint         :           = 883723216 bytes
Average bucket size     :     1.003
Variance of bucket size :     1.587
Std. dev. of bucket size:     1.260
Maximum bucket size     :        10

可以看到,1000万次调用耗时只有5.5秒,Average bucket size降到了1,效果明显。

好了,是时候给出第二原则了:没事别轻易用intern,如果要用一定要注意控制驻留的字符串的数量,并留意常量表的各项指标。

实现一个equals没有这么简单

如果看过Object类源码,你可能就知道,equals的实现其实是比较对象引用:

public boolean equals(Object obj) {
return (this == obj);
}

之所以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;
}

对于自定义类型,如果不重写equals的话,默认就是使用Object基类的按引用的比较方式。我们写一个自定义类测试一下。

假设有这样一个描述点的类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;
}

}

定义三个点p1、p2和p3,其中p1和p2的描述属性不同,p1和p3的三个属性完全相同,并写一段代码测试一下默认行为:

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

通过equals方法比较p1和p2、p1和p3均得到false,原因正如刚才所说,我们并没有为Point类实现自定义的equals方法,Object超类中的equals默认使用==判等,比较的是对象的引用。

我们期望的逻辑是,只要x和y这2个属性一致就代表是同一个点,所以写出了如下的改进代码,重写equals方法,把参数中的Object转换为Point比较其x和y属性:

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

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

}

为测试改进后的Point是否可以满足需求,我们定义了三个用例:

  • 比较一个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。

[17:54:39.120] [http-nio-45678-exec-1] [ERROR] [t.c.e.demo1.EqualityMethodController:32  ] - java.lang.NullPointerException
[17:54:39.120] [http-nio-45678-exec-1] [ERROR] [t.c.e.demo1.EqualityMethodController:39  ] - java.lang.ClassCastException: java.lang.Object cannot be cast to org.geekbang.time.commonmistakes.equals.demo1.EqualityMethodController$PointWrong
[17:54:39.120] [http-nio-45678-exec-1] [INFO ] [t.c.e.demo1.EqualityMethodController:43  ] - p1.equals(p2) ? true

通过这些失效的用例,我们大概可以总结出实现一个更好的equals应该注意的点:

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

修复和改进后的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;
}		

改进后的equals看起来完美了,但还没完。我们继续往下看。

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

按照改进后的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);
}

}

改进equals和hashCode后,再测试下之前的四个用例,结果全部符合预期。

[18:25:23.091] [http-nio-45678-exec-4] [INFO ] [t.c.e.demo1.EqualityMethodController:54  ] - p1.equals(null) ? false
[18:25:23.093] [http-nio-45678-exec-4] [INFO ] [t.c.e.demo1.EqualityMethodController:61  ] - p1.equals(expression) ? false
[18:25:23.094] [http-nio-45678-exec-4] [INFO ] [t.c.e.demo1.EqualityMethodController:67  ] - p1.equals(p2) ? true
[18:25:23.094] [http-nio-45678-exec-4] [INFO ] [t.c.e.demo1.EqualityMethodController:71  ] - points.contains(p2) ? true

看到这里,你可能会觉得自己实现equals和hashCode很麻烦,实现equals有很多注意点而且代码量很大。不过,实现这两个方法也有简单的方式,一是后面要讲到的Lombok方法,二是使用IDE的代码生成功能。IDEA的类代码快捷生成菜单支持的功能如下:

注意compareTo和equals的逻辑一致性

除了自定义类型需要确保equals和hashCode要逻辑一致外,还有一个更容易被忽略的问题,即compareTo同样需要和equals确保逻辑一致性。

我之前遇到过这么一个问题,代码里本来使用了ArrayList的indexOf方法进行元素搜索,但是一位好心的开发同学觉得逐一比较的时间复杂度是O(n),效率太低了,于是改为了排序后通过Collections.binarySearch方法进行搜索,实现了O(log n)的时间复杂度。没想到,这么一改却出现了Bug。

我们来重现下这个问题。首先,定义一个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;
}

}

然后,写一段测试代码分别通过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

我们注意到如下几点:

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

修复方式很简单,确保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);
}

}

其实,这个问题容易被忽略的原因在于两方面:

  • 一是,我们使用了Lombok的@Data标记了Student,@Data注解(详见这里)其实包含了@EqualsAndHashCode注解(详见这里)的作用,也就是默认情况下使用类型所有的字段(不包括static和transient字段)参与到equals和hashCode方法的实现中。因为这两个方法的实现不是我们自己实现的,所以容易忽略其逻辑。
  • 二是,compareTo方法需要返回数值,作为排序的依据,容易让人使用数值类型的字段随意实现。

我再强调下,对于自定义的类型,如果要实现Comparable,请记得equals、hashCode、compareTo三者逻辑一致。

小心Lombok生成代码的“坑”

Lombok的@Data注解会帮我们实现equals和hashcode方法,但是有继承关系时,Lombok自动生成的方法可能就不是我们期望的了。

我们先来研究一下其实现:定义一个Person类型,包含姓名和身份证两个字段:

@Data
class Person {
    private String name;
    private String identity;
public Person(String name, String identity) {
    this.name = name;
    this.identity = identity;
}

}

对于身份证相同、姓名不同的两个Person对象:

Person person1 = new Person("zhuye","001");
Person person2 = new Person("Joseph","001");
log.info("person1.equals(person2) ? {}", person1.equals(person2));

使用equals判等会得到false。如果你希望只要身份证一致就认为是同一个人的话,可以使用@EqualsAndHashCode.Exclude注解来修饰name字段,从equals和hashCode的实现中排除name字段:

@EqualsAndHashCode.Exclude
private String name;

修改后得到true。打开编译后的代码可以看到,Lombok为Person生成的equals方法的实现,确实只包含了identity属性:

public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof LombokEquealsController.Person)) {
return false;
} else {
LombokEquealsController.Person other = (LombokEquealsController.Person)o;
if (!other.canEqual(this)) {
return false;
} else {
Object this$identity = this.getIdentity();
Object other$identity = other.getIdentity();
if (this$identity == null) {
if (other$identity != null) {
return false;
}
} else if (!this$identity.equals(other$identity)) {
return false;
}
return true;
    }
}

}

但到这里还没完,如果类型之间有继承,Lombok会怎么处理子类的equals和hashCode呢?我们来测试一下,写一个Employee类继承Person,并新定义一个公司属性:

@Data
class Employee extends Person {
private String company;
public Employee(String name, String identity, String company) {
    super(name, identity);
    this.company = company;
}

}

在如下的测试代码中,声明两个Employee实例,它们具有相同的公司名称,但姓名和身份证均不同:

Employee employee1 = new Employee("zhuye","001", "bkjk.com");
Employee employee2 = new Employee("Joseph","002", "bkjk.com");
log.info("employee1.equals(employee2) ? {}", employee1.equals(employee2));	

很遗憾,结果是true,显然是没有考虑父类的属性,而认为这两个员工是同一人,说明@EqualsAndHashCode默认实现没有使用父类属性。

为解决这个问题,我们可以手动设置callSuper开关为true,来覆盖这种默认行为:

@Data
@EqualsAndHashCode(callSuper = true)
class Employee extends Person {

修改后的代码,实现了同时以子类的属性company加上父类中的属性identity,作为equals和hashCode方法的实现条件(实现上其实是调用了父类的equals和hashCode)。

重点回顾

现在,我们来回顾下对象判等和比较的重点内容吧。

首先,我们要注意equals和== 的区别。业务代码中进行内容的比较,针对基本类型只能使用==,针对Integer、String在内的引用类型,需要使用equals。Integer和String的坑在于,使用判等有时也能获得正确结果。

其次,对于自定义类型,如果类型需要参与判等,那么务必同时实现equals和hashCode方法,并确保逻辑一致。如果希望快速实现equals、hashCode方法,我们可以借助IDE的代码生成功能,或使用Lombok来生成。如果类型也要参与比较,那么compareTo方法的逻辑同样需要和equals、hashCode方法一致。

最后,Lombok的@EqualsAndHashCode注解实现equals和hashCode的时候,默认使用类型所有非static、非transient的字段,且不考虑父类。如果希望改变这种默认行为,可以使用@EqualsAndHashCode.Exclude排除一些字段,并设置callSuper = true来让子类的equals和hashCode调用父类的相应方法。

在比较枚举值和POJO参数值的例子中,我们还可以注意到,使用来判断两个包装类型的低级错误,确实容易被忽略。所以,我建议你在IDE中安装阿里巴巴的Java规约插件(详见这里),来及时提示我们这类低级错误:

 

gnuradio+b210实现FM收音机 gnuradio+b210实现FM收音机环境介绍FM接收1 Flow Graph2 变量参数FM发射1 Flow Graph 环境介绍 Name Version gnuradio 3.7.14.0 uhd 3.15.0.0 ubuntu 18.04 usrp b210 参考 https://www.ijedr.org/papers/IJEDR1702038.pdf Audio File Transmission using GNURADIO and USRP FM接 阅读详情

相关推荐

一生一芯预学习:从零搭建Verilator仿真环境与NVBoard实战

本文详细介绍了如何从零搭建Verilator仿真环境,并完成NVBoard实战项目,为'一生一芯'预学习提供完整指导。内容包括环境配置、工具安装、波形生成、自动化构建及可视化调试技巧,帮助初学者快速掌握数字逻辑设计与仿真验证的核心技能。

keras9composer的博客 701

《开发实战》08 | 判等问题:程序里如何确定你就是你?

之前遇到过一个坑:重启后,两个对象相等,结果是true,但是修改了某些东西后,热加载(不用重启即可生效)后,再次执行equals,返回就是false,因为热加载使用的类加载器和程序正常启动的类加载器不同。这种机制,就是字符串驻留或池化。因为引用类型的直接值是指针,使用 == 的话,比较的是指针,也就是两个对象在内存中的地址,即比较它们是不是同一个对象,而不是比较对象的内容。对基本类型,比如 int、long,进行判等,只能使用 ==,比较的是直接值。对象的判断,默认的equals比较的是对象的引用。

weixin_39924752的博客 188

从工艺到器件:用Silvaco ATLAS仿真PIN二极管击穿电压的保姆级指南

本文详细介绍了使用Silvaco ATLAS仿真PIN二极管击穿电压的全流程指南,从工艺仿真基础搭建到器件结构构建,再到击穿特性仿真和结果分析。通过Silvaco工具链,工程师可以在设计阶段精准预测器件性能,优化击穿电压,提升研发效率。

weixin_34259816的博客 355

Java业务开发常见错误100例------①代码篇 (23讲)

01 | 使用了并发工具类库,线程安全就高枕无忧了吗? ThreadLocal 使用的注意点 ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。符合操作时需要注意原子性 使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。 诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下,这些方

weixin_43719015的博客 5413

Windows 11下WSL2安装CUDA避坑指南:从驱动版本匹配到环境变量配置

本文详细解析了在Windows 11的WSL2中搭建CUDA开发环境的完整流程与核心避坑要点。关键在于理解“驱动在Windows,工具链在Linux”的独特架构,并确保显卡驱动与CUDA Toolkit版本严格匹配。指南涵盖了从驱动验证、WSL-Ubuntu专用仓库安装、环境变量配置到GCC版本冲突、GPU算力不匹配等常见问题的解决方案,帮助开发者高效构建稳定的深度学习与高性能计算环境。

smartcontract5的博客 467

USB2.0信号完整性测试之眼图

名称线缆颜色引脚描述红色绿色白色GND黑色。

qq_53766840的博客 3286

AI与自动化本质区别:从确定性执行到不确定性应对

人工智能(AI)与自动化虽常被混用,但二者在问题域、数学本质和工程目标上存在根本分野。自动化解决的是确定性映射问题,依赖预设规则与可验证逻辑,强调重复性、零歧义与高可靠性;而AI面向不确定性映射,核心在于泛化能力、环境反馈驱动的决策演化,以及对认知边界的自主觉察。技术价值不仅体现为精度提升,更在于重构人机协作范式——从‘执行指令’转向‘共同定义问题’。典型应用场景包括预测性设备维护、跨模态情绪识别、主动异常发现等需应对分布偏移与隐性经验的领域。本文基于工业一线实践,厘清Artificial Intellig

anheku1562的博客 397

【数据集】CMIP6气候模式数据下载

简介CMIP6及其气候模式数据下载

WW、forever的博客 9356

Python程序里的注释和#号

Python程序里的注释是很重要的。它们可以用自然语言告诉你某段代码的功能是什么。在你想要临时移除一段代码时,你还可以用注解的方式将这段代码临时禁用。接下来的练习将让你学会注释 : 从现在开始,我将用这样的方式来写代码。我一直在强调“完全相同”,不过你也不必按照字面意思理解。你的程序在屏幕上的显示可能会有些不同,不过重要的是你在文本编辑器中输入的文本的正确性。实际上,我可以用任何编辑器...

python学习者的博客 9384

1、开启 Microsoft Power Automate 自动化之旅

本文介绍了 Microsoft Power Automate 的基本概念和其在信息时代自动化需求中的重要作用。Power Automate 是 Power Platform 的一部分,能够通过无代码/低代码的方式自动化处理重复性任务,例如邮件处理、文件复制等,从而提升工作效率。文章还详细讲解了 Power Automate 的核心术语、登录方式、创建第一个流程的操作步骤,并通过示例帮助用户快速上手。无论是终端用户还是管理员,都可以通过本文了解如何利用 Power Automate 实现业务流程自动化。

corn8的博客 169

【ARM CoreLink 系列 7 -- TZC-400控制器简介】

为了确保内存能够正确识别总线的信号控制位,新增一个TrustZone地址空间控制器(TrustZone Address Space Controller,TZASC)组件来实现对内存地址的安全扩展,将内存区域划分为安全内存区域和非安全内存区域,安全世界可以访问安全内存和非安全内存,而普通世界只能访问非安全内存,如下图所示:图 1-1内存区域的划分是在系统启动时通过配置 TZASC 组件(TZC-400/TZC-380)来实现的。TZC-400由和使能情况;安全访问权限;Base地址;

CodingCos的博客 1634

我遭遇的离奇Bug:程序里的幽灵

同时,这次经历也让我意识到,作为一名软件工程师,我们不仅要具备扎实的技术功底,还需要拥有敏锐的洞察力和解决问题的能力。这次“幽灵Bug”的经历,不仅让我收获了宝贵的技术经验,更让我在职业生涯中迈出了坚实的一步。在未来的软件开发道路上,我会继续努力,不断提升自己的技术水平和团队合作能力,为公司的发展贡献更多的力量。为了解决这个问题,我查阅了大量的资料,参考了许多前人的经验,甚至向一些行业内的专家请教。它不仅考验了我的技术实力,更锻炼了我的耐心和毅力,让我对软件开发有了更深入的认识。

一个梦想做程序员的编程小白 441

AI 大模型应用数据中心的数据清洗工具

1. 背景介绍 在人工智能大模型应用的浪潮中,数据清洗作为数据预处理的重要环节,对于提升模型性能和可靠性具有至关重要的作用。数据中心作为人工智能模型的运行环境,面临着海量数据流和多样化的数据类型,如何高效、准确地进行数据清洗,成为应用大模型的关键问题之一。本文将详细介绍AI大模型应用数据中心的数据清洗工具,包括核心概念、算法原理、具体操作步骤、应用场

SuperAGI2025 1804

关于C/C++程序里如何实现模糊查找

相信学过C/C++的都应该了解如何在数组里面去查找数据,利用for循环便能很容易实现查找的功能,但若数组里的数据是字符串类型,而你又恰好需要实现的是根据关键字进行查找的话,那么按部就班对数组里的字符串进行一个个字符的对比是很难实现的,这时候就要想其他的办法了,C里面有一个函数便可以从侧面实现模糊查找,它就是strstr()函数,该函数搜索一个字符串在另一个字符串里的第一次出现,并返回所搜索到字符串

jinjidexiaocainiao的博客 1万+

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

SAP SD基础知识之文本确定(Text Determination)

SAP SD基础知识之文本确定(Text Determination) 一,文本Texts SAP系统中,文本存储在很多的不同对象中,例如在客户主记录,物料主记录,客户-物料信息记录以及所有业务单据中。 客户主记录包含central texts,Accounting特有的texts,销售分销特有的texts以及联系人特有的texts;你可以为每个areas定义不同的text ...

喜欢打酱油的老鸟 2914

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

【项目问题】程序里,oracle相关的内容,为什么有些能显示表有些不能显示表,但是用oracle工具就可以看到所有表

1、oracle数据源链接成功了,但是在产品使用过程中发现,没有表,空空如也 先看一下用户:zd202207 2、再去看看数据库工具里面 1、java程序里面jdbc,指定的是某一个用户下面(zd202207),的table,并不是,这个用户的权限下可以看到的table 2、工具为什么能看得到,因为zd202207这个用户,被system赋予了可以查看其他表的权限,所以可以看得到1、首先确定jdbc直接想通过程序是没法看到的 2、可以通过执行sql进行查询,因为sql的执行是可以看到的格式:select

吃素的哈士奇de博客 844

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

工业设备停止检测全攻略:从传感器选型到软件判停与堵转识别

在工业自动化领域,设备状态感知是保障生产精度与安全的基础。除了常见的运动检测,如何可靠地判断设备是否真正停止,同样是工程师必须面对的核心问题。停止检测涉及电气零速、机械到位与程序判停等多个维度,需要结合传感器选型、信号处理算法及电机运行特性综合实现。光电传感器、电感式接近开关、编码器与磁栅尺等器件各有适用场景,而基于编码器的零速判定、死区阈值设置与滑动窗口滤波等软件手段,则能有效滤除干扰、提升判停准确性。电机堵转时,电流信号的变化比位置反馈更为灵敏,合理设置堵转阈值与复位策略可显著降低设备故障风险。从传感器

weixin_30444105的博客 316

怎样在程序里获得一个空指针?

6.2 怎样在程序里获得一个空指针? 根据语言定义, 在指针上下文中的常数 0 会在编译时转换为空指针。也就是说, 在初始化、 赋值或比较的时候, 如果一边是指针类型的值或表达式, 编译器可以 确定另一边的常数 0 为空指针并生成正确的空指针值。因此下边的代码段完全 合法: char *p = 0; if(p != 0) 参见问题 5.3。 然而, 传入函数的...

Sakila 231

C语言虚词怎么在程序里表示,文言虚词用法推断

一、文言虚词用法推断在文言文中,大多数的虚词不表示明确的意义,但它们能组织实词完成句子结构,体现出句子的语气,表达出作者的感情。尽管虚词的数量比实词少得多,但因为虚词使用的频率高,语法功能强,且古今变化大,用法灵活多样,所以掌握既是学习古汉语的一大难点,又是一大重点,也是考试的热点。考纲对此考点的要求是了解常见文言虚词在文中的用法,能力层次为B级(理解),即准确判断虚词的意义和用法。高考对此知识的...

weixin_29663061的博客 730

MATLAB代码:电力系统 低碳调度 源荷不确定 风电 程序语言:matlab+yalmip(...

内容:参照考虑源荷两侧不确定性的含风电的低碳调度,引入模糊机会约束,程序包括储能、风光、火电机组及水电机组,解决了目标函数含有分类特征的约束问题、非线性约束/目标的线性转化问题,且考虑了机组的启停时间约束,目标函数考虑运行成本、弃风弃光和碳成本,有详细的资料,程序完整性好、模块化编程、注释清楚、方便学习!这就是典型的分段线性化套路,用lambda做权重系数把连续变量离散化,既保留了精度又维持了线性。代码里还藏了个小彩蛋:在计算火电机组煤耗时,原本的非线性函数被分段线性化了,这个技巧让求解速度直接提升三倍。

2509_94268171的博客 942

c# winform程序里嵌入FLASH

<br /><br />---------------------------------flash c#嵌入-------------------------------------- <br />要在c# winform程序里嵌入FLASH,其实就是直接引用Flash的ocx控件。<br />那你就需要使用“COM 组件” Shockwave Flash Object。不过这个"Flash.ocx"控件不能直接在“COM 组件”窗口中找到.。<br />需要我们手动添加,选择菜单中的“工具”--“自定义

princelintb的专栏 4818

深入拆解:一个汇编交通灯程序里的‘时间片’与‘状态机’思想

本文深入解析了汇编语言编写的交通灯控制程序,重点探讨了其中的‘时间片’与‘状态机’设计思想。通过分析8254定时器中断和状态变量POINT的实现,揭示了嵌入式系统中并发控制与状态管理的核心技术,为开发者提供了硬件层、逻辑层和架构层的全面视角。

weixin_28692817的博客 339

MATLAB代码:计及源-荷双重不确定性的虚拟电厂日前鲁棒优化调度 关键词:虚拟电厂/微网调度...

今天咱们要聊的这个虚拟电厂调度模型,正是一个典型的"备胎"方案:既要伺候好光伏发电的"公主病",又得应付负荷需求的"善变脸"。主要内容:代码主要做的是一个虚拟电厂或者微网单元的日前鲁棒经济调度的模型,考虑了光伏出力和负荷功率的双重不确定性,采用鲁棒优化法处理不确定性变量,构建了虚拟电厂鲁棒优化调度模型。主要内容:代码主要做的是一个虚拟电厂或者微网单元的日前鲁棒经济调度的模型,考虑了光伏出力和负荷功率的双重不确定性,采用鲁棒优化法处理不确定性变量,构建了虚拟电厂鲁棒优化调度模型。看到那个1e5了吗?

2508_94216203的博客 1290

简要了解C语言程序里全局变量、局部变量、堆、栈等概念,并在ubuntu系统中编程

简要了解C语言程序里全局变量、局部变量、堆、栈等概念,并在ubuntu系统中编程内存分类不同内存的各自特点概念的验证关键字volatile 内存分类 一个由C/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。 其操作方式类似于数据结构中的栈。 2、堆区(heap) — 在内存开辟另一块存储区域。 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。 注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 3

优快云zgnb的博客 223

RAII本质:用作用域绑定资源生命周期的确定性哲学

资源管理是程序设计的核心基础问题,其本质在于协调‘时间’(何时释放)与‘所有权’(谁负责释放)两大维度。传统手动管理(如C的malloc/free)依赖开发者路径全覆盖,易遗漏;垃圾回收(如C# GC)以牺牲确定性为代价换取内存安全,却对文件句柄、GPU纹理等非托管资源无能为力。RAII(Resource Acquisition Is Initialization)则另辟蹊径——将资源生命周期严格锚定在栈对象作用域内,通过构造函数获取、析构函数释放,实现编译期可验证的确定性保障。它不依赖运行时猜测,也不考验

526
上一篇: Java性能优化:Stream如何提高遍历集合效率?
下一篇: 数据库索引:索引并不是万能药
JavaShark
博客等级 码龄4年 184粉丝 438原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值