2.7.2 包装类型
前8种基本数据类型都有相应的包装类,因为Java 的设计理念是一切皆是对象,在很多情况下,需要以对象的形式操作,比如hashCode() 获取哈希值,或者getClass() 获取类等。包装类的存在解决了基本数据类型无法做到的事情:泛型类型参数、序列化、类型转换、高频区间数据缓存。尤其是最后一项,我们都知道Integer会缓存-128~127 之间的值,对于Integer var=? 在-128~127 之间的赋值,Integer 对象由IntegerCache.cache 产生,会复用已有对象,这个区间内的Integer 值可以直接使用== 进行判断,但是这个区间之外的所有数据都会在堆上产生,并不会复用已有对象,这是一个大问题。因此,推荐所有包装类对象之间值的比较,全部使用equals() 方法。
事实上,除Float 和Double 外,其他包装数据类型都会缓存,6 个包装类直接赋值时,就是调用对应包装类的静态工厂方法valueOf(),以Integer 为例,源码如下:
@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
如上源代码,赋值数据i 在缓存区间内直接返回缓存中的Integer 对象,否则就会new 一个对象。在JDK9 直接把new 的构造方法过时,推荐使用valueOf(),合理利用缓存,提升程序性能。各个包装类的缓存区间如下:
- Boolean:使用静态 final 变量定义,valueOf() 就是返回这两个静态值。
- Byte:表示范围是-128~127,全部缓存。
- Short:表示范围是-32768~32767,缓存范围是 -128~127。
- Character:表示范围是 0~65535,缓存范围是 0~127。
- Long:表示范围是 [-263, 263-1],缓存范围是 -128~127。
- Integer:表示范围是 [-231, 231-1]。最后详细介绍 Integer,因为它是 Java 数据世界里应用最广的数据类型,缓存范围是-128~127。但它是唯一可以修改缓存范围的包装类,在VM options 加入参数-XX:AutoBoxCacheMax=7777,即可设置最大缓存值为7777,示例代码如下:
public class LongIntegerCacheTest {
public static void main(String[] args) {
Long a = 127L;
Long b = 127L;
System.out.println("Long max cached value is 127, "
+ "and the result is:" + (a == b));
Long a1 = 128L;
Long b1 = 128L;
System.out.println("Long=128 cache is " + (a1 == b1));
Long c = -128L;
Long d = -128L;
System.out.println("Long min cached value is -128, "
+ "and the result is:" + (c == d));
Long c1 = -129L;
Long d1 = -129L;
System.out.println("Long=-129 cache is " + (c1 == d1));
// Long 类型只缓存-128 ~ 127 之间的数值
Long e = 1000L;
Long f = 1000L;
System.out.println("Long=1000 is " + (e == f));
// JVM AutoBoxCacheMax 只对Integer 对象有效
Integer x = 1001;
Integer y = 1001;
System.out.println("Integer=1001 is " + (x == y));
}
}
执行结果如下:
Long max cached value is 127, and the result is:true
Long=128 cache is false
Long min cached value is -128, and the result is:true
Long=-129 cache is false
Long=1000 is false
Integer=1001 is true
该例很好地说明了Long 只是缓存了-128~127 之间的值,而1000L 没有被缓存;在将Integer 最大缓存值改为7777 后,1001 被成功缓存。合理掌握包装类的缓存策略,防止遇到问题是一个方面,使自己的程序性能最大化,更是程序员的情怀所在。在选择使用包装类还是基本数据类型时,推荐使用如下方式:
(1) 所有的POJO 类属性必须使用包装数据类型。
(2) RPC 方法的返回值和参数必须使用包装数据类型。
(3) 所有的局部变量推荐使用基本数据类型。
与包装类型的相关故障并不鲜见,在淘宝某个团队,曾经出现某些客户的产品页无法打开,存在账号问题。通过预发debug,发现这些正常的账号在执行下面一段逻辑的时候,代码执行结果不符合预期。
public static MemberStatus getStatusFromSuspend(Integer suspend) {
if(suspend == Integer.valueOf(1) || suspend == Integer.valueOf(2)) {
return ENABLED;
}
...
}
suspend 是通过RPC 框架接口调用返回的,而反序列化是new 一个对象的,同一个值在== 判断时,一边是new 出来的新对象,另一边是缓存中已有对象,两者并不相等,导致故障。
2.7.3 字符串
字符串类型是常用的数据类型,它在JVM 中的地位并不比基本数据类型低,JVM 对字符串也做了特殊处理。String 就像是流落到基本数据类型部落的一个副首领,虽然很神气,但是终归难以得到族人对它的认同,毕竟它是堆上分配来的。
字符串相关类型主要有三种:String、StringBuilder、StringBuffer。String 是只读字符串,典型的immutable 对象,对它的任何改动,其实都是创建一个新对象,再把引用指向该对象。String 对象赋值操作后,会在常量池中进行缓存,如果下次申请创建对象时,缓存中已经存在,则直接返回相应引用给创建者。而StringBuffer 则可以在原对象上进行修改,是线程安全的。JDK5 引入的StringBuilder 与StringBuffer 均继承自AbstractStringBuilder,两个子类的很多方法都是通过“super. 方法()”的方式调用抽象父类中的方法,此抽象类在内部与String 一样,也是以字符数组的形式存储字符串的。StringBuilder 是非线程安全的,把是否需要进行多线程加锁交给工程师决定,操作效率比StringBuffer 高。线程安全的对象先产生是因为计算机的发展总是从单线程到多线程,从单机到分布式。
String t1 = new String(“abc”) 与String t2 = “abc”,用== 判断是否相同呢,答案是不同的。如果调用t1.intern() 方法,则是相同的。
在非基本数据类型的对象中,String 是仅支持直接相加操作的对象。这样操作比较方便,但在循环体内,字符串的连接方式应该使用StringBuilder 的append 方法进行扩展。如下的方式是不推荐的:
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
此段代码的内部实现逻辑是每次循环都会new 一个StringBuilder 对象,然后进行append 操作,最后通过toString 方法返回String 对象,不但造成了内存资源浪费,而且性能更差。