Java八种包装类、常量池

2024-04-27 00:26:18 浏览数 (1)

还记得 Java八种基本数据类型及对应包装类、四种引用类型吗?如果忘记可以到这里重温复习Java数据类型(八种基本数据类型 四种引用类型)、数据类型转换

一、八种基本数据类型及其包装类

1.1 类型详解

基本类型

存储大小

初始化默认值

取值范围

包装类型

byte

1字节(8位)

0

-128~127

Byte

short

2字节(16位)

0

-32768~32767

Short

int

4字节(32位)

0

-2^31 ~ 2^31 - 1

Integer

long

8字节(64位)

0L。"L"理论上不分大小写,但若写成"l"容易与数字"1"混淆,不容易分辨,所以最好大写。

-2^63 ~ 2^63 - 1

Long

float

4字节(32位)

0.0f

符合IEEE754标准的浮点数,1.4E-45 ~ 3.4028235E38

Float

double

8字节(64位)

0.0d

符合IEEE754标准的浮点数,4.9E-324 ~ 1.7976931348623157E308

Double

char

2字节(16位)

'u0000'

u0000 ~ uffff(十进制等效值为 0~65535,本质也是数值)

Character

boolean

1字节(8位)/4字节(32位)

false

true/false

Boolean

1.2 基本类型与包装类型相互转换——自动装箱拆箱

基本类型与包装类型之间的转换可以通过 自动装箱和拆箱 完成。

  • Integer aInteger = 100; 自动装箱,基本类型-->包装类型【编译器会将int类型的100装箱,变成Interger类型,底层调用 Interger.valueOf(100) 方法
  • int a = aInteger; 自动拆箱,包装类型-->基本类型【编译器会将包装类型aInteger中的数据取出来,赋值给a,底层调用 aInteger.intValue() 方法
代码语言:java复制
Integer a = 18;  //自动装箱。底层执行 Integer a = Integer.valueOf(18);
a = a   6;//自动拆箱、自动装箱,底层 a=Integer.valueOf(a.intValue() 5);

二、Java常量池

Java常量池是Java内存管理中的一个重要概念,主要用于存储字符串常量、基本类型包装类常量、类和方法的全限定名等。在Java中,当创建一个字符串、基本类型包装类或类引用时,JVM会首先检查常量池中是否已存在该对象。如果存在,则直接返回对该对象的引用;如果不存在,则在常量池中创建一个新的对象并返回引用。

Java常量池主要有以下几个优势、特点

  • 节省内存空间:通过共享常量池中的对象,可以避免重复创建相同的对象,从而节省内存空间。
  • 提高性能:由于可以直接从常量池中获取对象引用,而无需每次都创建新对象,因此可以提高程序的运行效率。
  • 保证唯一性:对于字符串和基本类型包装类常量,常量池保证了它们的唯一性。这意味着在Java程序中,两个相等的字符串常量或基本类型包装类常量实际上是指向常量池中同一个对象的引用。

Java常量池主要包括以下几个部分

  • 字符串常量池:用于存储字符串字面量。当使用字面量方式创建字符串时,JVM会首先检查字符串常量池中是否已存在该字符串。如果存在,则返回该字符串的引用;否则,在字符串常量池中创建一个新的字符串并返回引用。
  • 基本类型包装类常量池:对于基本类型的包装类(如Integer、Boolean等),当使用自动装箱创建包装类对象时,如果值的范围在缓存范围内(如Integer的缓存范围是-128到127),则直接从缓存中获取对象;否则会创建一个新的对象。
  • 类元数据常量池:用于存储类的元数据信息,如类名、方法名、字段名等。这些信息在类加载过程中被解析并存储在常量池中,供后续使用。
  • 运行时常量池:是方法区的一部分。JVM为每个已加载的类型(类或接口)维护一个运行时常量池。运行时常量池包含多种常量,包括编译期产生的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。此外,Java语言并不要求常量一定只有编译期才能产生,也就是说,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

需要注意的是,Java 7及以后的版本对String常量池和Integer常量池做了一些优化。例如,对于String常量池,可以通过String类的intern()方法将一个字符串加入到常量池中;对于Integer常量池,缓存范围从Java 5的-128到127扩展到了Java 8及以后版本的-128到Integer.MAX_VALUE。这些优化提高了Java程序的性能和内存使用效率。

java内存模型(JDK7之前、JDK7之后)

JDK7之前

JDK7之后

String常量池已在String、StringBuilder、StringBuffer区别;String底层详解,实例化、拼接、比较;String为什么不可变介绍过,本文重点讨论基本类型包装类常量池。

三、包装类的常量池技术(缓存池)

Java中八种基本数据类型的包装类大部分都实现了缓存技术,目的是为了避免重复创建对象,提供性能、节省内存。基本数据类型直接存放在栈中,包装类型作为一种引用数据类型 在堆上分配内存(具体内容存放在堆中,栈中存放的是其具体内容所在内存的地址);Java在jdk1.5后包装类常量池使用缓存实现,缓冲池也叫常量池.

  • Byte、Short、Integer、Long、Character、Boolean,均已实现常量池技术(严格来说应该叫对象池,在堆中)。它们维护的常量仅仅是各自缓冲池范围内的常量(如Integer -128~127、Boolean true和false),在这个范围内只要不new对象,都是在常量池中创建;如果常量值超过这个范围,就会从堆中创建对象,而不再从缓存池中取
  • Float和Double没有实现常量池技术,原因在于这两种数据类型的数值是很随意的,就算有常量池命中率也不会高,还浪费额外的堆内存。

3.1 各包装类缓冲池范围

包装类

缓冲池范围

Byte

-128~127(包含边界)

Short

-128~127

Integer

-128~127

Long

-128~127(为了节省内存、提高性能,该范围是基于经验选择的,因为该范围内的数值是最常用的)

Character

u0000 ~ u007F(十进制等效值为 0~127)

Boolean

true和false

(1)内存中有一个java基本类型封装类的常量池。这些类包括Byte, Short, Integer, Long, Character,Boolean,Boolean只有true和false。Float和Double这两个类并没有对应的常量池。

代码语言:java复制
public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

(2)上面6种整型的包装类的对象是存在范围限定的;在限定范围内存在在常量池, 范围以外则在堆区进行分配。

(3)当使用new关键字创建包装类对象时,都会在堆中创建新的对象;只有使用字面量赋值、且在缓冲池范围内 才可使用对象池,否则还是会在堆中创建对象

(4)包装类的两个变量之间的比较 推荐使用equals进行(比较的是值而非地址)。

对于Integer var = ?,在-128~127范围内的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断;但这个区间外的所有数据 都会在堆上产生,并不会复用已有对象,此时==比较的是对象地址值、即对象是否相同,这是一个大坑,推荐使用equals方法进行判断

Integer的equals方法被重写过,比较的是内部value的值,源码如下

代码语言:java复制
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

public int intValue() {
    return value;
}

3.2 Integer i1=new Integer(5) 与 Integer i2=5 的区别

  • Integer i2=5,自动装箱【编译器在自动装箱过程中会调用valueOf()方法,等价于Integer i2=Integer.valueOf(5) 】。先从常量池中查找是否已经存在该值的Integer对象,如果存在则直接返回常量池中的对象;不存在 就创建一个新的Integer对象并存储在常量池中(当范围在-128~127之间时,多次调用会取得同一个对象的引用)
  • Integer i1=new Integer(5) ,每次都会新建一个对象;

对于valueOf方法,只有当数值在 -128~127 之间时,才会被缓存。若超出该范围 仍会创建新的对象

对于超出-128, 127范围的Integer对象,无论是通过valueOf()方法还是new关键字创建,都会在堆中创建新的对象

代码语言:java复制
Integer aInteger = 5;    //在缓存,即常量池。底层调用Integer aInteger = Integer.valueOf(5),里面用到IntegerCache对象池
Integer bInteger = 5;    //在缓存
Integer cInteger = new Integer(5);  //在堆内存中
Integer dInteger = new Integer(5);  //在堆内存中
Integer eInteger = 500;  //不在缓存,在堆中
Integer fInteger = 500;  //不在缓存,在堆中
System.out.println((aInteger == bInteger)   ", "   (cInteger==dInteger));  //true, false
System.out.println(aInteger == cInteger);  //false
System.out.println(eInteger==fInteger);    //false

3.3 Integer.valueOf(int i)方法

通过字面量的方式赋值,实际调用Integer.valueOf方法。Integer i2=5,等价于Integer i2=Integer.valueOf(5),底层用到了IntegerCache,即Integer的缓存池。先判断值是否在缓存池中,如果在 就直接返回缓冲池中的内容,如果不在就new一个对象返回,源码如下

代码语言:java复制
public final class Integer extends Number implements Comparable<Integer> {
    //...
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i   (-IntegerCache.low)];
        return new Integer(i);
    }
    //...
}

java8中,Integer缓冲池的最小值为-128,最大值默认为127,可以通过jvm参数调整上限。在IntegerCache的静态代码段中,为-128~127的所有整数生成一个Integer对象,然后添加到cache数据中,当调用Integer.valueof()时会判断数值是否在这个区间内,如果在就直接返回已经缓存好的对象,如果不再就直接新创建一个Integer对象

编译器在自动装箱过程中会调用valueOf()方法,因此如果多个Integer实例使用自动装箱来创建、在-128~127范围内、并且值相同,就会引用相同的对象。

而对于通过new关键字创建的对象,自然不会使用缓存池中的对象

IntegerCache源码如下:

代码语言:java复制
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer 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;

        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;
    }

    private IntegerCache() {}
}
代码语言:java复制
Integer a = 127;
Integer b = 127;
System.out.println(a == b);  //true,常量池中同一个对象

Integer c = 128;  
Integer d = 128;
//由于Integer只缓存-128~127之间的值,因此128对应的数据没有被缓存
System.out.println(c == d);  //false,在堆中 不同对象

Integer e = new Integer(127);
Integer f = new Integer(127);
System.out.println(e == f);  //false,通过new关键字 都会创建一个新对象、在堆中

Byte,Short,Integer,Long,Character,Boolean这 6 种包装类都实现了缓存池技术,原理及使用与上面介绍的Integer类似;两种浮点数类型的包装类 Float、Double没有实现缓存池技术

3.4 包装类与对应基本数据类型进行运算

当包装类和对应基本数据类型进行 ==、运算符 等计算时,包装类会出现自动拆装箱,此时即便是堆中数和常量池数运算也是true

代码语言:java复制
int aint = 127;
Integer i1 = 127;   //在缓存。底层调用Integer cInteger = Integer.valueOf(127),里面用到IntegerCache对象池
Integer i2 = 127;   //在缓存
Integer i3 = 0;     //在缓存
Integer i4 = new Integer(127);   //不在缓存
Integer i5 = new Integer(127);   //不在缓存
Integer i6 = new Integer(0);     //不在缓存
//Integer是int的封装类,当Integer与int进行==比较时(无论Integer是直接赋值、还是new),Integer会拆箱成一个int类型,所以还是相当于两个int类型进行比较
System.out.println((aint == i1)   ", "   (aint==i4));   //true, true。  
//2个不同的Integer对象,“==”会校验Integer地址是否相同
System.out.println((i1 == i2)   ", "   (i1==i4)   ", "   (i4==i5));  //true, false, false      
System.out.println(i1 == i2   i3);   //true
//堆创建,但Integer对象无法直接计算 故拆箱,比较数值
System.out.println(i4 == i5   i6);   //true   
System.out.println(127 == i5   i6);  //true   同上


//Boolean类也实现了对象池技术
Boolean bool1 = true;
Boolean bool2 = true;
System.out.println(bool1 == bool2);  //true

//浮点类型的包装类Float、Double没有实现对象池技术
Double d1 = 2.0;
Double d2 = 2.0;
System.out.println(d1 == d2);  //false

i1、i2、i3都是常量池中的对象,i4、i5、i6是堆中的对象。

i4 == i5 i6 返回true是因为,涉及运算 i5、i6会进行自动拆箱操作、数值相加,即i4 == 127。Integer对象无法直接与数值进行比较,故i4同样自动拆箱为int值127,最终转为 127==127 进行数值比较。

四、总结

  • 基本类型与包装类型可通过 自动装箱拆箱 相互转换
  • 为提高性能、节省内存,Java中Byte、Short、Integer、Long、Character、Boolean包装类实现了缓存技术,Float和Double没有实现常量池技术
  • 当使用new关键字创建包装类对象时,都会在堆中创建新的对象;只有使用字面量赋值、且在缓冲池范围内 才可使用对象池,否则还是会在堆中创建对象
  • 包装类的两个变量之间的比较 推荐使用equals进行(比较的是值而非地址)
  • Integer i2=5,自动装箱,底层调用Integer i2=Integer.valueOf(5) 方法。先从常量池中查找是否已经存在该值的Integer对象,如果存在则直接返回常量池中的对象;不存在 就创建一个新的Integer对象并存储在常量池中(当范围在-128~127之间时,多次调用会取得同一个对象的引用)
  • Integer i1=new Integer(5) ,每次都会新建一个对象;
  • 对于Integer.valueOf方法,只有当数值在 -128~127 之间时,才会被缓存。若超出该范围 仍会创建新的对象。编译器在自动装箱过程中会调用valueOf()方法,因此如果多个Integer实例使用自动装箱来创建、在-128~127范围内、并且值相同,就会引用相同的对象。

0 人点赞