详细解析包装类及其缓存池,不会还有人不知道吧?
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类等。
感谢阅读,文章对你有帮助的话,不妨一键三连支持一下吧。你们的支持是我最大的动力,祝大家早日富可敌国
以上是关于详细解析包装类及其缓存池,不会还有人不知道吧?的主要内容,如果未能解决你的问题,请参考以下文章