深入浅出Java包装类

1.包装类

1.1 基本数据类型对应的包装类都有哪些

基本数据类型 包装类
boolean Boolean
char Character
byte Byte
short Short
int Integer
long Long
float Float
double Double

1.2自动装箱 AutoBoxing,自动拆箱 AutoUnboxing

1.2.1 什么是装箱、拆箱

​ 我们以 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() 自动将对象拆箱为变量。这就是自动装箱以及自动拆箱的原理。

1.2.2 如何触发自动装箱、拆箱

  • ​ 装箱:
    1. 当插入集合当中时,基本数据类型自动装箱为包装类对象。例如 list.add(1)
    2. 以 Integer i = 2;形式初始化时,会自动装箱。
    3. 使用 equals 方法比较 Integer 对象与基本数据类型变量时,变量会自动装箱为包装类。
  • ​ 拆箱:
    1. 当包装类对象进行 +,-,*,/ 等运算时,会拆箱为基本数据类型
    2. 当包装类与基本数据类型进行 == 比较时

1.2.3 自动拆箱的注意事项

我们都知道,自动拆箱是调用 Integer 对象的 intValue() 方法,那么如果有以下这种情况:

Integer unbox = null;
int test = unbox;

以上代码是可以编译通过的,但因为对象为 null ,所以运行会抛出 NullPointerException。如果没有对异常进行捕捉,则会导致程序运行终止。所以在使用自动拆箱时要多加注意,不要让引用指向 null。

1.3 包装类缓存池

1.3.1 缓存池详解

在了解缓存池前,先了解一下享元模式,享元模式是 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 修饰的是成员变量,分为两种情况:

  1. 被 staitc 关键字修饰的成员变量,需要在类加载时就完成初始化。(声明时初始化或者在静态语句块中完成初始化)

    (类的加载详见 https://blog.csdn.net/qq_44707077/article/details/115287769)

  2. 没有被 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

1.3.2 包装类缓存池范围总结

包装类 缓池存范围
Boolean true / false
Character 0 ~ 127
Byte 数据范围内所有值
Short -128 ~ 127
Integer -128 ~ 127(最大值可修改)
Long -128 ~ 127
Float 没有缓存机制
Double 没有缓存机制

1.4 练习

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。

你可能感兴趣的:(Java基础,java,jdk,数据类型)