Integer的比较
执行如下代码:
1 | public static void main(String[] args) { |
结果如下:
1 | true |
根据Java编译机制,.java文件在编译以后会生成.class文件给JVM加载执行,于是找到.class文件,反编译看了一下,发现编译器在编译我们的代码时,在我们声明的变量加上了valueOf方法 ,代码变成了如下:
1 | public static void main(String[] args) { |
看一下valueOf方法的实现:
1 | public static Integer valueOf(int i) { |
Integer的作者在写这个类时,为了避免重复创建对象,对Integer值做了缓存,如果这个值在缓存范围内,直接返回缓存好的对象,否则new一个新的对象返回,那究竟这个缓存到底缓存了哪些内容呢?看一下IntegerCache这个类:
1 | private static class IntegerCache { |
这是一个内部静态类,该类只能在Integer这个类的内部访问,这个类在初始化的时候,会去加载JVM的配置,如果有值,就用配置的值初始化缓存数组,否则就缓存-128到127之间的值。
之前的代码给i1和i2赋值时在-128到127之间,取同一个数组里的对象返回,i1和i2指向同一个对象,因此i1==i2返回true,而i3和i4相当于new了两个对象,因此i3==i4返回false
在比较两个Integer对象的值时,无论是怎么声明的,都一定要使用equals去比较,不能用==,在Java中没有重载操作符。如下所示:
1 | System.out.println(i3.equals(i4)); |
字符串的比较
再来看一段代码:
1 | public static void main(String[] args) { |
结果如下:
1 | true |
Java代码是运行在JVM里的,在JVM中,当代码执行到String s1 = “100” 时,会先看常量池里有没有字符串刚好是“100”这个对象,如果没有,在常量池里创建该对象,并把引用指向它,如下字符串在内存中的示意图,绿色部分为常量池,存在于堆内存中。
当执行到String s2 = “100” 时,发现常量池已经有了100这个值,于是不再在常量池中创建这个对象,而是把引用直接指向了该对象,如下图:
这时候我们打印System.out.println(s1 == s2)时,由于==是判断两个对象是否指向同一个引用,所以这儿打印出来的就应该是true。
继续执行到Strings3 = new String(“100”) 这时候我们加了一个new关键字,这个关键字呢就是告诉JVM直接在堆内存里开辟一块新的内存,如下图所示:
继续执行String s4 = new String(“100”)
这时候再打印System.out.println(s3 == s4) 那一定便是false了,因为s3和s4不是指向对一个引用(对象)。
在比较两个String对象内容时,无论是怎么声明的,都一定要使用equals去比较,不能用==。
上面表明了Java字符串两种声明方式在堆内存中不同的体现,在写代码过程中,为了避免重复的创建对象,尽量使用String s1 =”123” 而不是String s1 = new String(“123”),因为JVM对前者给做了优化。
Java里的equals
看如下代码:
1 | public class Person{ |
执行结果如下:
1 | true |
”==”与equals两者之间没有必然的联系,在引用类型中,”==”是比较两个引用是否指向堆内存里的同一个地址(同一个对象),而equals是一个普通的方法,该方法返回的结果依赖于自身的实现。我们先看一下Person这个类,在Java中,如果一个类没有继承其它类,那么它默认继承Object这个类,打开Object这个类看一下,发现如下代码,Person这个类的equals方法就继承自这里:
1 | public boolean equals(Object obj) { |
很简单,判断两个引用是否指向同一个对象,两个Person对象在堆内存中的表现如下图所示:
所以代码person1.equals(person2)等同于person1 == person2,当然打印出来的结果是false。我们再来看看Integer这个类, equals的实现如下:
1 | public boolean equals(Object obj) { |
代码中会判断传入比较的对象是不是Integer类型,如果是,则比较值是否相等,如果不是直接返回false。当代码执行到System.out.println(itr.equals(lon))时,会判断传入的lon这个对象是否是Integer类型,这里的lon是Long类型,所以打印出来的结果当然是false了。
最后是String的实现:
1 | public boolean equals(Object anObject) { |
当代码执行到:System.out.println(s3.equals(s4)),由于字符串底层char数组里存的都是{‘1’,’0’,’0’},打印出来是true。
再谈两个类的比较
将面向对象中的Person类精简,保留了isSame()方法,代码如下:
1 | public class Person { |
执行如下代码:
1 | public static void main(String[] args) { |
结果如下:
1 | strSet.size()1 |
同样是对象,都是放到了set中,一个打印size()是1,另一个打印size()是2。
在Java程序中,有很多的“公约”,我们称之为编程规范,遵守这些规范实现代码,会避开很多坑。要判断两个对象的内容是否相等,不要自己写方法(isSame())去判断,而是应该重写父类的equals方法(这里的父类是Object),String重写了equals()方法,所以打印size结果是1,而Person没有重写,因此Set没法判断这两个”张三”是否是同一个人,打印size结果是2。
像String一样,重写一下Person的equals方法:
1 | public boolean equals(Person other) { |
如果是重写方法,我们在方法上要加上@Override注解,加上该注解,编译器会帮助检查是否真的覆盖了父类的方法。编译一下,居然报错了。原来我们跟本就不是重写(覆盖)了父类的equals方法,而是自己又写了一个参数为Person的equals方法,根本不是重写,只是重载了父类的方法而已。
1 | public boolean equals(Person other) {//重写equals方法这里的参数类型是Object,不能写Person |
重载:就是在同一个类中,方法的名字相同,但参数个数、参数的类型不同。
重写:它是指子类和父类的关系,子类重写了父类的方法,但方法名、参数类型、参数个数必须相同。
正确重写代码如下:
1 |
|
代码测试如下,引入了List:
1 | public static void main(String[] args) { |
运行一下,perList里面我们只添加person1,并没有添加person2,但执行perList.contains(person2)打印的结果居然是true(List里面包含了person2),只因为重写了equals()方法,但pSet.contains(person2))依旧是false。
1 | person1.equals(person2)true |
再次执行本节开始的代码,结果依旧:
1 | strSet.size()1 |
很明显,Person这个类在重写equals()方法后,虽然已经支持List,但还不支持Set。要完美支持HashMap、HashSet、LinkedHashMap、ConcurrentHashMap等这些类,不但要重写equals方法,还需要重写hashCode()方法。
现在我们在Person类里重写一下hashCode()方法:
1 |
|
再次执行代码:
1 | person1.equals(person2)true |
再次执行本节开始的代码,结果已经是我们期望的:
1 | strSet.size()1 |
当我们在实际业务中需要重写(覆盖)equals方法时,根据规范,我们一定要重写(覆盖)hashCode方法。在实际开发过程中,不建议一上来就重写equals方法,除非你有特殊的需求。
hash的相关内容请参考Hash。
参考资料:
清浅池塘 让人疑惑的Java代码、Java字符串那些事儿、说说Java里的equals(上)、说说Java里的equals(中)、