问题引入
这是一个很简单的问题,也是一个不简单的问题,总之挺有趣的,可以讨论讨论、动手实操一下。
为什么Integer的1000==1000为false,100==100为true?如果你运行了下面的代码,确实会得到这个结果。
基本知识
==和.equals()
== | .equals() |
---|---|
比较的是地址 | 比较的是值 |
即如果两个引用指向同一个对象,用 == 表示它们是相等的;如果两个引用指向不同的对象,用 == 表示它们是不相等的,即使它们的内容相同。
Integer和int
Java是面向对象的编程语言,一切都是对象,但是为了编程的方便还是引入了基本数据类型,为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
区别:
- Integer是int的包装类,int则是java的一种基本数据类型
- Integer变量必须实例化后才能使用,而int变量不需要
- Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
- Integer的默认值是null,int的默认值是0
源码探析
那么照这么理解,应该是两个false。但深入源码Integer.java第769行
1 | /** |
这个内部私有类缓存了-128~127之间的所有整数对象,所以当我们类似于声明
1 | Integer a = 100; |
实际上内部做的是
1 | Integer a = Integer.valueof(100); |
接着再去看看valueof方法
1 | /** |
如果值的范围在-128~127之间,它就从高速缓存中返回实例,所以a、b指向的是同个对象。
1 | Integer a = 100, b = 100; |
你可能会问,为什么这里需要缓存?
合理的理由是,在此范围内的“小”整数使用率比大整数高很多,重复创建与销毁对象是有一定代价的,因此使用相同的底层对象是有价值的,可以减少潜在的内存占用与抖动。
拓展解析
由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
1
2
3Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //falseInteger变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
1
2
3Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
1
2
3Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
1
2
3
4
5
6Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
理解自动装箱、拆箱
自动装箱与拆箱实际上算是一种“语法糖”。所谓语法糖,可简单理解为Java平台为我们自动进行了一些转换,保证不同的写法在运行时等价。因此它们是发生在编译阶段的,也就是说生成的字节码是一致的。
对于整数,javac替我们自动把装箱转换为Integer.valueOf(),把拆箱替换为Integer.intValue()。可以通过将代码编译后,再反编译加以证实。
原则上,建议避免无意中的装箱、拆箱行为,尤其是在性能敏感的场合,创建10万个Java对象和10万个整数的开销可不是一个数量级的。当然请注意,只有确定你现在所处的场合是性能敏感的,才需要考虑上述问题。毕竟大多数的代码还是以开发效率为优先的。
顺带说一下,在32位环境下,Integer对象占用内存16字节;在64位环境下则更大。
值缓存
就像String,Java也为Integer提供了值缓存。
1 | Integer i1 = 1; |
上述代码中第一行与第二行的写法取值使用了值缓存,而第三行的写法则没有利用值缓存。结合刚刚讲到的自动装箱、拆箱的知识,第一行代码用到的自动装箱,等价于调用了Integer.valueOf()。
不仅仅是Integer,Java也为其它包装类提供了值缓存机制,包括Boolean、Byte、Short和Character等。但与String不同的是,默认都只会将绝对值较小的值放入缓存。以Integer为例,默认情况下只会缓存-128到127之间的值。当然如果你愿意也可以通过以下JVM参数进行设置:
-XX:AutoBoxCacheMax=N
原始类型操作线程安全吗?
线程不安全!!
原始数据类型的变量,需要使用并发相关手段才能保证线程安全。特别的是,部分比较宽的数据类型,比如long、float、double,甚至不能保证更新操作的原子性,可能出现程序读取到只更新了一半数据位的数值!
如果有线程安全的计算需要,建议考虑使用类似AtomicInteger、AtomicLong这样线程安全的类。