基本数据类型 | 包装类 |
---|---|
boolean | Boolean |
char | Character |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
我们以 Integer 为例。
首先,什么是装箱、拆箱。又何来自动一说?让我们通过下面的例子开始了解。
//装箱
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
--------------------------
//拆箱
String s = new String("a");
System.out.println(s == "a"); //output:false
Integer i1 = new Integer(200);
double i2 = 200.0;
System.out.println(i1 == i2); //output:true
1.装箱:我们创建了一个泛型为 Integer 的 ArrayList,说明在这个集合中只能存储 Integer 类型的对象。但我们却可以通过 list.add(1) 的方式向集合中插入数据。那么我们可以揣测,int 类型的变量一定经历了某个过程,自动转换成 Integer 类型的对象后再存入集合,不然为什么不会报错。
2.拆箱:了解过 ‘= =’ 的同学知道,’= =’ 比较的永远是 ‘值’。当比较对象时,比较的是对象中存储的 ‘值’ (即内存地址);当比较变量时,比较的是变量的值。那么上面的代码中,我们首先让 String 对象与 字符串常量 进行比较,结果如我们预想的一样,地址与值相比较,结果不可能为 true。接下来我们让 Integer 对象与 double 变量相比较,输出却是 true ???。那么我们可以根据这个结果进行揣测,不是 Integer 对象自动转换为变量,就是 double 变量自动转换为对象。
究竟事实是什么呢,我们可以通过编译、反编译以上代码进行了解。
编译为字节码文件,并通过 IDEA 打开:
ArrayList var1 = new ArrayList();
var1.add(1);
String var2 = new String("a");
System.out.println(var2 == "a");
Integer var3 = new Integer(200);
double var4 = 200.0D;
System.out.println((double)var3 == var4); //强转为 double
以下是反编译结果:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."":()V,创建了 ArrayList 实例
7: astore_1
8: aload_1
9: iconst_1 // 准备好常量 1
10: invokestatic #4 // Method java/lang/Integer.valueOf(I)Ljava/lang/Integer;,
//调用了 Integer 的静态方法 valueOf !!!
13: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)
Z
16: pop
17: new #6 // class java/lang/String
20: dup
21: ldc #7 // String a
23: invokespecial #8 // Method java/lang/String."":(Ljava/lang/String
;)V
26: astore_2
27: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload_2
31: ldc #7 // String a
33: if_acmpne 40
36: iconst_1
37: goto 41
40: iconst_0
41: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
44: new #11 // class java/lang/Integer
47: dup
48: sipush 200
51: invokespecial #12 // Method java/lang/Integer."":(I)V
54: astore_3
55: ldc2_w #13 // double 200.0d
58: dstore 4
60: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
63: aload_3
64: invokevirtual #15 // Method java/lang/Integer.intValue:()I,
//调用了 Integer 的实例方法 intValue !!!
67: i2d
68: dload 4
70: dcmpl
71: ifne 78
74: iconst_1
75: goto 79
78: iconst_0
79: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
82: return
}
在反编译结果中序号为 10、36对应行,我们可以看到,Integer "偷偷"调用了两个方法 1. valueOf 2. intValue。接下来让我们进入 Integer 源码查看,这两个方法到底做了什么。
1.valueOf(int i)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]; //缓存池
return new Integer(i);
}
通过源码我们可以看到,该方法传入了 int 类型变量,判断当前变量是否存在于缓存池中(先不用了解,本文 1.3 缓存池中有详细讲解),然后通过 Integer 的有参构造,创建一个 Integer 对象并返回。
2.intValue()
public int intValue() {
return value;
}
通过源码我们可以看到,该方法将 Integer 对象中的成员变量 value 返回。
至此,我们明白了,Integer 会通过我们看不见的方式,通过调用 valueOf(int i) 自动对变量进行装箱;通过调用 intValue() 自动将对象拆箱为变量。这就是自动装箱以及自动拆箱的原理。
我们都知道,自动拆箱是调用 Integer 对象的 intValue() 方法,那么如果有以下这种情况:
Integer unbox = null;
int test = unbox;
以上代码是可以编译通过的,但因为对象为 null ,所以运行会抛出 NullPointerException。如果没有对异常进行捕捉,则会导致程序运行终止。所以在使用自动拆箱时要多加注意,不要让引用指向 null。
在了解缓存池前,先了解一下享元模式,享元模式是 Java 23 种设计模式中最简单的一个,池化思想就是基于这个设计模式的。享元模式的动机是:避免系统中存在大量相同或相似的对象。在 Java 中,符合这种 “存在大量相同或相似” 的类有很多,比如:1. Connection(数据库连接) 2. 包装类 3. String 4. Thread 等等 。我们经常需要使用这些类的实例,如果频繁创建及销毁,会大大降低效率。通过享元模式的核心:享元池,可以避免这种情况。享元池的原理是,定义一个缓存区域(即享元池)提前创建并存储享元对象,需要使用享元对象前,先去缓存中寻找对象是否已存在,如果存在则直接将该对象返回;如果不存在,则在享元池中添加新的享元对象,以供下次使用。
基于享元模式提出的享元池有很多,比如数据库连接池,线程池,包装类缓存池,常量池等等。
下面我们来了解包装类缓存池,以 Integer 为例。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
上面我们讲到 valueOf 方法时,当传入变量 i 时,需要在缓存中先判断 i 对应的值是否在缓存范围内。如果存在则返回缓存池中的对象,如果不存在则新创建一个 Integer 对象返回。(这里并没有将新创建的 Integer 对象存入缓存池,原因如下)
字面意思就可以看出来,IntegerCache 就是 Integer 的缓存类。下面是源码:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static{
...
}
}
IntegerCache 被定义为私有静态内部类,外部不能访问(可以暴力反射)。IntegerCache 中定义了三个静态成员,并且都被 static、final 修饰。
首先解析这里 static 关键字的含义:当 Integer 类被加载时,类中的静态成员就已经被确定,有且只有一个。
其次解析这里 final 关键字的含义:被 final 修饰的变量只会被初始化一次。当 final 修饰的是成员变量,分为两种情况:
被 staitc 关键字修饰的成员变量,需要在类加载时就完成初始化。(声明时初始化或者在静态语句块中完成初始化)
(类的加载详见 https://blog.csdn.net/qq_44707077/article/details/115287769)
没有被 static 关键字修饰的成员变量,需要在构造函数执行完毕前,完成初始化。(声明时初始化或者在构造函数中完成初始化)
了解了 static及final 的含义后我们知道,此处 high、cache[] 没有在声明时完成初始化,那么只有一种可能:在静态代码块中完成初始化。以下为静态代码块源码:
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;
//创建 Integer 缓存池!!!
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;
}
在静态代码块中,大部分代码是对于 high 值的处理:当 integerCacheHighPropValue 为空时,high 值默认设置为 128;但当 integerCacheHighPropValue 不为空时,high 值会被修改。
那么如何设置 integerCacheHighPropValue 并手动修改 high 值呢?我们可以在 JVM 虚拟机初始化时,传入启动参数,根据需求修改缓存范围的最大值。以下为示例代码:
public class IntegerTest {
//只有 Integer 的缓存 max 可修改
public static void main(String[] args) {
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);
Integer i5 = 200;
Integer i6 = 200;
System.out.println(i5 == i6);
Integer i7 = 20000;
Integer i8 = 20000;
System.out.println(i7 == i8);
}
//output 正常运行,不传参数
true
false
false
//通过命令行运行程序,并传入参数 AutoBoxCacheMax
cmd 指令分为两步,1.编译 IntegerTest.java 为字节码文件 IntegerTest.class 2.通过指令运行字节码文件
javac IntegerTest.java
java -server -XX:AutoBoxCacheMax=200 IntegerTest
//output
true
true
false
//output 通过命令行运行程序,并传入参数 +AggressiveOpts(设置缓存范围最大值为20000)
javac IntegerTest.java
java -server -XX:+AggressiveOpts IntegerTest
//output
true
true
true
包装类 | 缓池存范围 |
---|---|
Boolean | true / false |
Character | 0 ~ 127 |
Byte | 数据范围内所有值 |
Short | -128 ~ 127 |
Integer | -128 ~ 127(最大值可修改) |
Long | -128 ~ 127 |
Float | 没有缓存机制 |
Double | 没有缓存机制 |
public static void main(String[] args) {
Integer a = new Integer(200);
int b = 200;
Integer c = 200;
Integer d = 200;
Integer e = 100;
Integer f = 100;
Integer g = new Integer(127);
Integer h = 127;
System.out.println(a.equals(b));
System.out.println(a == b);
System.out.println(e == f);
System.out.println(c == d);
System.out.println(g == h);
}
//output
true
true
true
false
false
1.equals 比较的是两个对象,当比较对象与基本数据类型类型变量时,变量会自动装箱为包装类;并且 Integer 重写了 equals 方法如下:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
两个 Integer 对象如果值相同,那么通过 equals 方法判断,结果为 true。
2.答错的同学,回看一下 本文1.2.2。此时 a == b,触发了 a 自动拆箱,那么两个 int 类型相比较,200 = 200,显而易见,true。
3.两个 Integer 对象相比较,不会触发自动拆箱。所以比较的是两个对象存储的内存地址,判断 e,f是否指向同一个内存地址。100 在缓存池范围内,所以两个对象指向缓存池中同一个位置,两个对象是同一个。true
4.两个 Integer 对象相比较,不会触发自动拆箱。所以比较的是两个对象存储的内存地址,判断 a,c是否指向同一个内存地址。false
5.g 以有参构造的形式创建对象,这样做会导致 JVM 在堆中开辟一个空间,并且 g 指向堆中对应的地址。此时 g 中存储的是堆中的内存地址。h 通过自动装箱的方式创建对象,指向缓存池中的内存地址。结果显而易见,g,h 中存储的内存地址不同,结果为 false。