详细解析包装类及其缓存池,不会还有人不知道吧?

Posted 守夜人爱吃兔子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详细解析包装类及其缓存池,不会还有人不知道吧?相关的知识,希望对你有一定的参考价值。

包装类的概念

什么是包装类?它们设计的意义是什么?

Java 是较为纯粹的面向对象设计语言,而其中存在八个原始类型,不归于类的范畴。为了践行 无物不可引用,万类皆是对象,Java 为原始类型提供了对应的引用类型(基本类型又可以称为原始类型),称其为原始类型的包装类

在写法上,除了 int、char,其它六种包装类均为首字母大写,例如 byte 与 Byte。而 int、char 是缩写,其包装类分别是 Integer、Character

相较于基本类型,包装类作为引用类型,可以存在 NULL 值。同时,提供了一些方法,例如

// 求最小求值
int minValue = Integer.MIN_VALUE;
// 求最大取值
int maxValue = Integer.MAX_VALUE;
// 二进制位数
int size = Integer.SIZE;
// 所占字节数
int bytes = Integer.BYTES;

需要注意,Byte、Short、Integer、Long、Float、Double 同属于 Number 数值类,Character、Boolean 不是

明确一点,所有的包装类,都是 final 不可变类这点与 String 是一致的,任何的修改都是新对象的创建

简单些说,引用类型的赋值,实际是获得堆内存的引用地址,而对于不可变类,任何的修改都会创建新的地址,原有的内存空间不再指向

而对于其它的可变引用类型,如数组(数组不可变的是类型、长度,而非内容),内容的修改不会导致新的数组(内存空间)创建

int[] ints1 = new int[3];
// ints 1 与 ints 2 使用了同一份内存空间
int[] ints2 = ints1;
ints1[2] = 3;
System.out.println(Arrays.toString(ints2));
/* [0, 0, 3] */

这些,需要格外注意,什么才是真正的 final 不可变,赋值与声明同步,此后不可修改

如 Integer、String 这些不可变类,是借由 final 完成的修饰。所以,若需要修改其中的内容,必须开辟新的内存空间,这会造成不必要的浪费,也是各种缓存机制存在的必然

// Integer 类已经前缀了 final,意为不可变的类
public final class Integer extends Number implements Comparable<Integer> {}

 

拆箱、装箱

拆、装箱,是原始类型与引用类型之间的相互转换

装箱:基本类型转为其包装类 valueOf()

拆箱:包装类转为其基本类型 intValue()

目前,Java 已经支持了自动装箱、自动拆箱,无须再调用特定的方法进行手动拆、装箱

包装类的拆箱、拆箱 示例,仅作了解即可

/* 手动装箱 */
Integer integer1 = Integer.valueOf(12);
/* 手动拆箱 */
int intValue1 = integer1.intValue();

/* 自动装箱 */
Integer integer3 = 12;
/* 自动拆箱 */
int intValue2 = integer1;

 

缓存池

包装类中,存在缓存池的设置,避免对象的重复创建,以 Integer 为例

在 Integer 类中,存在一个私有静态内部类 private static class IntegerCache {}

简单的理解,Integer 类的装箱操作,会调用 valueOf(),并开辟一块新的堆内存

无论是手动装箱,还是自动装箱,都会调用 valueOf(),只是隐藏了这部分

若装箱后的 Integer 对象存在于 Integer 的缓存池中,则不会创建新对象,而是直接引用自缓存池 IntegerCache

Integer a1 = 120;
Integer a2 = 1200;
Integer b1 = 120;
Integer b2 = 1200;
/* == 对于引用类型,比较的是堆内存地址,Java 中也不支持运算符重载 */
System.out.println(a1 == b1);
System.out.println(a2 == b2);
/* true、false */

上述的包装类示例,违背了以往的认知,这就是缓存池在发挥作用!对于引用类型,== 是比较二者的堆内存地址,同样数值的 Integer 类型,为何会出现堆内存地址相同、相否的情况

当调用 valueOf() 时,会先判断当前创建的对象是否存在于缓存池中

public static Integer valueOf(int i) {
    // 判断当前原始类型数值,是否存在于缓存池中
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

下面是 Integer 缓存池的具体实现源码

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];
    static {
        int h = 127;
        String integerCacheHighPropValue = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
            }
        }
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
        assert IntegerCache.high >= 127;
    }
    private IntegerCache() {}
}

通过一系列的示例、源码,可以很清楚的认识到缓存池。根据包装的原始类型大小,决定是否新建对象,还是从缓存池中取出相同的对象

在 Integer 中,缓存池是 -128~127,一个 byte 的取值范围。可以手动的扩充缓存池的大小,但并不推荐这样做

当然,若是直接使用 new Integer(),可以创建一个新的 Integer 对象,但已经废弃。关键字 new,为强制创建,无视缓存池机制

值得注意的是,并非所有的包装类都存在缓存池,浮点型的 Float、Double 与 Boolean 不存在缓存池的概念。Byte、Short、Integer、Long、Character 存在缓存池,默认范围都是 -128~127

 

重写 equals()

同样的以 Integer 为例,探讨其中的方法重写。在 Integer 类、String 类中,对于 Object 类的 equals() 方法已经被重写

默认的 equals() 方法,对于引用类型,比较的是 变量是否引用自同一对象,判断的依据是堆内存地址。在重写之后,根据实际的数值判断

Integer a = 12;
Integer b = 12;
Integer c = new Integer(12);
/* == 不可重写,依旧是根据对象的引用地址判断 */
System.out.println(a == b);
System.out.println(a == c);
/* equals() 已被重写,根据对象的值进行判断 */
System.out.println(a.equals(b));
System.out.println(a.equals(c));
/*
true、false、true、true
*/

上述的示例可以看出

  • a、b 由于 Integer 的缓存池机制,引用的是同一个对象,堆内存地址与实际数值皆一致
  • c 通过关键字 new,没有引用缓存池,而是全新的堆内存地址,但实际数值依旧保持一致
  • 在 == 的判断中,不存在问题,a 与 b 相等,与 c 不相等

可是,equals() 的默认实现,是根据对象的引用地址进行的。而经过 Integer 的重写,根据实际的数值判断,以至于堆内存不同的变量在 equals() 的判断中为 true

所以,请看 Integer 类中的重写实现,与 Object 类中的默认实现,二者的区别

/* Object 类的默认 equals() 方法,其中 this 指代当前对象 */
public boolean equals(Object obj) {
    return (this == obj);
}
/* Integer 重写后的 equals() 方法 */
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        /* Integer 类的 equals() 根据值判断二者的相等 */
        return value == ((Integer)obj).intValue();
    }
    return false;
}

 

重写 hashCode()

Object 类中,也存在一个方法,hashCode(),负责返回对象的哈希值

共同的认知是,重写 equals() 的同时,必须重写 hashCode()

哈希值是根据对象的属性,在通过哈希算法生成的,在 Integer 重写之后,规则发生改变,Integer 的哈希值等于它的数值本身

@Override
public int hashCode() {
    return Integer.hashCode(value);
}

值得注意的是

  • 相同的对象,哈希值一定相同
  • 而哈希值相同的对象,也可能不是同一个对象
// Arrays.hashCode() 也重写 hashCode(),暂时不用
int[] ints = new int[2];
System.out.println(ints.hashCode());
System.out.println(new int[2].hashCode());
/*
哈希值:2083562754、1239731077
*/

hashCode() 的默认实现,是将对象的引用地址转换为整数值。引用地址右虚拟机生成,可覆盖

可以参考 HashMap 中的键值存储形式,它允许存在重复的 hash 值,以单向链表的形式存储

对于 equals() 与 hashCode(),优先是通过对象的哈希值判断相等性。若哈希值不同,则并非同一个对象;若哈希值相同,则根据 equals() 二次判断。这样,可以达到性能与安全的平衡

若 equals() 重写,而 hashCode() 不重写,根据对象的值进行判断,则会出现,equals() 判断为 true,而 hashCode() 不相同,以至于操作失误

简单的理解为:equals() 判断为 true,则 hashCode() 必须为 true;而 hashCode() 为 true时,equals() 存在为 false 的可能

 

最后

给大家分享一篇一线开发大牛整理的java高并发核心编程神仙文档,里面主要包含的知识点有:多线程、线程池、内置锁、JMM、CAS、JUC、高并发设计模式、Java异步回调、CompletableFuture类等。

领取文档地址:【GitHub标星12.5K+Java高并发核心编程知识笔记助我提升,感觉之前学的都是渣渣】 

感谢阅读,文章对你有帮助的话,不妨一键三连支持一下吧。你们的支持是我最大的动力,祝大家早日富可敌国

以上是关于详细解析包装类及其缓存池,不会还有人不知道吧?的主要内容,如果未能解决你的问题,请参考以下文章

不会吧,不会吧,还有人不知道 binlog ?

不会吧,不会吧,还有人不知道 binlog ?

不会吧,不会吧,还有人不知道 binlog ?

不会还有人不知道这些计算机网络的专业名词吧!!!不会吧!!

不会吧不会吧,不会还有人不知道这个超赞的主题吧?

不会还有人不知道mybatis的关联查询吧?