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

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

你可能会说,判等不就是一行代码的事情吗,有什么好说的。但,这一行代码如果处理不当,不仅会出现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规约插件(详见这里),来及时提示我们这类低级错误:

​

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

相关推荐

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

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

527

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

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

weixin_39924752的博客 189

ABAP 绝对类型名:用一条路径,把类型精确定位到唯一坐标

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

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 2612

Python程序里的注释和#号

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

python学习者的博客 9384

S32DS 3.5.8离线包怎么用?保姆级教程教你手动安装S32K3开发环境(附资源下载)

本文详细介绍了S32DS 3.5.8离线包的安装方法,适用于S32K3开发环境的手动搭建。从资源获取、完整性验证到分步安装指南,提供了全面的离线部署解决方案,特别适合军工/车企内网开发等场景。文章还包含版本选择建议、常见问题排查及团队协作方案,帮助开发者高效完成环境配置。

weixin_30471561的博客 480

避坑指南:时间序列互相关分析(CCF)常见的5个误用场景

本文详细解析了时间序列互相关分析(CCF)中常见的5个误用场景,包括忽视平稳性检验、混淆同期相关与因果关联、误读滞后阶数、忽略自相关干扰以及样本量不足等问题。通过实际案例和技术建议,帮助数据分析师避免这些陷阱,确保CCF分析的准确性和可靠性。

weixin_29064009的博客 429

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

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

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

DSSP 工具:蛋白质二级结构信息计算与批量处理教程

DSSP(Dictionary of Secondary Structure in Proteins)是一款经典的蛋白质二级结构计算工具,由荷兰癌症研究所(NKI Research)Perrakis 团队维护。它通过分析蛋白质的 PDB 结构文件,能够自动标注每个氨基酸残基的二级结构类型、相对溶剂可及性(暴露面积)等关键信息,为后续的拓扑图绘制、疏水 / 亲水表面分析、二级结构预测模型训练等提供核心数据支持。

good_rrrrr的博客 1839

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

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

jinjidexiaocainiao的博客 1万+

AD20 Channel-Offset Error 解决办法

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言AD20 PCB相同电路模块ROOM格式复用时出现 Channel-Offset Error 解决办法一、问题现象二、解决办法 前言 AD20 PCB相同电路模块ROOM格式复用时出现 Channel-Offset Error 解决办法 一、问题现象 我们经常在使用Room格式快速布局相同模块电路时出现以下问题, 二、解决办法 相同模块电路PCB布局复制的过程网上有很多,这里不再做赘述,步骤都差不多,只说问题根源; 1:.

m0_47201213的博客 7634

UE4-(反射)场景采集之场景采集2D

一、场景采集分为两种:场景采集2D、场景采集立方体 场景采集2D:采集2D图片模式。 操作: 1.选择场景采集2D,是一个摄像机的物体,摆放到场景中。 2.在文件夹空白处创建纹理,一定要注意创建纹理的格式, 纹理分为立方体渲染目标和渲染目标,此处我们使用的是2D模式,所以选择渲染目标(Texture Render Target2D)。创建后重命名为RenderTextue2D。 3.选择场景中刚拖入的 SceneCapture2D 摄像机在细节面板的Scene Capture 将..

Stone的博客 4601

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

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

吃素的哈士奇de博客 844

在摩尔线程 MTT S80 上使用 Ollama 进行 DeepSeek R1 蒸馏版模型推理

本文主要介绍了如何在摩尔线程 MTT S80 上通过 Ollama 快速完成DeepSeek R1 蒸馏系列模型的推理。

摩尔线程blog中心 3603

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

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

喜欢打酱油的老鸟 2914

【ROS2】RViz2加载URDF模型文件

1)运行RViz2rviz22)添加组件:RobotModel3)选择通过文件添加4)选择URDF文件,此时会报错,需要修改Fixed Frame为map即可

郭老二 1451

【ComfyUI】Wan2.2 Animate 背景保留动作迁移视频生成

本文介绍了一个基于ComfyUI的高精度视频生成工作流"Wan2.2 Animate",该系统融合动作捕捉、图像拼接和LoRA加权等技术,能自动识别人脸并完成精准动作迁移,同时保持背景稳定。工作流采用模块化设计,包含输入提取、图像融合、模型加载等关键节点,核心模型Wan2.2-Animate-14B支持fp8精度与BlockSwap控制。流程分为图像输入、动作提取、多图合成等五个阶段,通过文本提示编码器和图像语义编码器实现语义驱动的动作生成,最终输出高质量视频,适用于虚拟形象驱动和

Mr数据杨 2280

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

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

Sakila 232

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

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

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

weixin_29663061的博客 730

Statistics and Machine Learning Toolbox

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

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

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的专栏 4819

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

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

2508_94216203的博客 1290

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

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

优快云zgnb的博客 223

myeclipse写配置文件,在程序里找到路径,并读取配置文件的内容

写配置文件,在程序里找到路径,并读取配置文件的内容: String classPath = this.getClass().getClassLoader().getResource("/").getPath(); //得到当前路径 FileInputStream fin = new FileInputStream(classPath+"portconfig");//读取配置文件路径,portc

没有如果 3787
上一篇: 数据库索引:索引并不是万能药
下一篇: 20%的业务代码的Spring声明式事务,可能都没处理正确
JavaMonsterr
博客等级 码龄4年 211粉丝 481原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值