1.3数据装箱和拆箱原理
目录介绍
- 01.先看个案例分析
- 1.1 思考一些问题
- 1.2 来看案例题1
- 1.3 来看案例题2
- 1.4 来看案例题3
- 1.5 来看案例题4
- 1.6 int和Integer区别
- 02.理解装箱和拆箱
- 2.1 什么是装箱&拆箱
- 2.2 装箱和拆箱实现
- 2.3 在编程中注意点
- 2.4 为何要设计Integer
- 03.Integer设计思想
- 3.1 来看一个案例
- 3.2 引入了缓存设计
- 3.3 IntegerCache类
- 3.4 其他包装类型
- 04.int类型安全如何保证
- 4.1 int是安全的吗
- 4.2 安全的基本类型
- 4.3 AtomicInteger安全版
- 05.int和Integer局限性
- 5.1 原始类型局限性
- 5.2 引用类型局限性
- 5.3 回答1.1的问题
01.先看个案例分析
1.1 思考一些问题
- 基础问题思考
- 编译阶段、运行时,自动装箱 / 自动拆箱是发生在什么阶段?int和Integer的区别
- 使用静态工厂方法 valueOf 会使用到缓存机制,那么自动装箱的时候,缓存机制起作用吗?
- Integer 源码分析下类或某些方法的设计要点?
- 一些高级点的问题
- 为什么我们需要原始数据类型,Java 的对象似乎也很高效,应用中具体会产生哪些差异?
1.2 来看案例题1
- 1、由于Integer变量实际上是对一个Integer对象的引用
- 两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
Integer i = new Integer(100); Integer j = new Integer(100); System.out.print(i == j); //false
1.3 来看案例题2
- 2、Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true
- 因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
Integer i = new Integer(100); int j = 100; System.out.print(i == j); //true
1.4 来看案例题3
- 3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。
- 因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
Integer i = new Integer(100); Integer j = 100; System.out.print(i == j); //false
1.5 来看案例题4
- 4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
Integer i = 100; Integer j = 100; System.out.print(i == j); //true Integer i = 128; Integer j = 128; System.out.print(i == j); //false
- 对于第4条的原因: java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下:
public static Integer valueOf(int i){ assert IntegerCache.high >= 127; if (i >= IntegerCache.low && i <= IntegerCache.high){ return IntegerCache.cache[i + (-IntegerCache.low)]; } return new Integer(i); }
- java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了
1.6 int和Integer区别
- 1、Integer是int的包装类,int则是java的一种基本数据类型
- 2、Integer变量必须实例化后才能使用,而int变量不需要
- 3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
- 4、Integer的默认值是null,int的默认值是0
02.理解装箱和拆箱
2.1 什么是装箱&拆箱
- 装箱就是
- 自动将基本数据类型转换为包装器类型;
- 拆箱就是
- 自动将包装器类型转换为基本数据类型。
- 如下所示
//装箱:将基本类型转化为包装类型 Integer i = 10; //拆箱:将包装类型转化为基本类型 int c = i;
2.2 装箱和拆箱实现
- 以Integer类为例,下面看一段代码来了解装箱和拆箱的实现
public class Foo { public static void main(String[] args) { Integer i = 10; int c = i; } }
- 然后来编译一下,看下图所示:
public class Foo { public Foo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 10 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: astore_1 6: aload_1 7: invokevirtual #3 // Method java/lang/Integer.intValue:()I 10: istore_2 11: return }
- 从反编译得到的字节码内容可以看出,在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。
- 因此可以用一句话总结装箱和拆箱的实现过程:装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue 方法实现的(xxx代表对应的基本数据类型)。
2.3 在编程中注意点
- 建议避免无意中的装箱、拆箱行为
- 尤其是在性能敏感的场合,创建 10 万个 Java 对象和 10 万个整数的开销可不是一个数量级的,不管是内存使用还是处理速度,光是对象头的空间占用就已经是数量级的差距了。
- 性能开销:装箱和拆箱操作会引入一定的性能开销,因为它们涉及到对象的创建和销毁。频繁进行装箱和拆箱操作可能会对性能产生影响,特别是在大量数据处理的情况下。在性能敏感的代码中,可以考虑手动进行装箱和拆箱操作,以避免不必要的开销。
- 空指针异常:在进行拆箱操作时,如果包装类对象为 null,会抛出空指针异常(NullPointerException)。因此,在拆箱之前,应该确保包装类对象不为 null,可以使用条件判断或者使用 Optional 类来处理可能为 null 的情况。
2.4 为何要设计Integer
- 设计 Integer 类型是为了在 Java 中提供对整数的封装和操作
- Integer 是一个包装类,它将 int 类型的原始数据封装成对象,使其具有对象的特性和功能。
- 它在泛型、集合、空值表示和类型转换等方面提供了便利,但也需要注意其特性和性能开销。
- 需要注意它的一些特性
- 如自动装箱和拆箱的性能开销,以及在比较两个 Integer 对象时需要使用 equals() 方法而不是 == 运算符。
03.Integer设计思想
3.1 来看一个案例
- new Integer(123) 与 Integer.valueOf(123) 的区别在于:
- new Integer(123) 每次都会新建一个对象;
- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Integer x = new Integer(123); Integer y = new Integer(123); System.out.println(x == y); // false Integer z = Integer.valueOf(123); Integer k = Integer.valueOf(123); System.out.println(z == k); // true
- valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
- 在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
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; }
- 编译器会在自动装箱过程调用 valueOf() 方法,因此多个Integer实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
Integer m = 123; Integer n = 123; System.out.println(m == n); // true
- 基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
- 在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
3.2 引入了缓存设计
- 如何你是Integer的开发,你将如何设计它,如何提高它的性能和节省内存?
- 节省内存:一般思路是,添加缓存,有即复用,没有则新建
- 提高性能:减少对象创建,及时释放对象
- 在 Java 5 中,为 Integer 的操作引入了一个新的特性,用来节省内存和提高性能。
- 整型对象在内部实现中通过使用相同的对象引用实现了缓存和重用。
- 这种 Integer 缓存策略仅在自动装箱(autoboxing)的时候有用,使用构造器创建的 Integer 对象不能被缓存。
3.3 IntegerCache类
- 在创建新的 Integer 对象之前会先在 IntegerCache.cache (是个Integer类型的数组)中查找。有一个专门的 Java 类来负责 Integer 的缓存。
- 这个类是用来实现缓存支持,并支持 -128 到 127 之间的自动装箱过程。最大值 127 可以通过 JVM 的启动参数 -XX:AutoBoxCacheMax=size 修改。
- 缓存通过一个 for 循环实现。从小到大的创建尽可能多的整数并存储在一个名为 cache 的整数数组中。
- 这个缓存会在 Integer 类第一次被使用的时候被初始化出来。以后,就可以使用缓存中包含的实例对象,而不是创建一个新的实例(在自动装箱的情况下)。
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } 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() {} }
3.4 其他包装类型
- 这种缓存行为不仅适用于Integer对象。我们针对所有整数类型的类都有类似的缓存机制。
- 有 ByteCache 用于缓存 Byte 对象
- 有 ShortCache 用于缓存 Short 对象
- 有 LongCache 用于缓存 Long 对象
- 有 CharacterCache 用于缓存 Character 对象
- Byte,Short,Long 有固定范围: -128 到 127。对于 Character, 范围是 0 到 127。除了 Integer 可以通过参数改变范围外,其它的都不行。
04.int类型安全如何保证
4.1 int是安全的吗
- 200个线程,每个线程对共享变量 count 进行 50 次 ++ 操作
- int 作为基本类型,直接存储在内存栈,且对其进行+,-操作以及++,–操作都不是原子操作,都有可能被其他线程抢断,所以不是线程安全。
- int 用于单线程变量存取,开销小,速度快。
int count = 0; private void startThread() { for (int i = 0;i < 200; i++){ new Thread(new Runnable() { @Override public void run() { for (int k = 0; k < 50; k++){ count++; } } }).start(); } // 休眠10秒,以确保线程都已启动 try { Thread.sleep(1000*10); } catch (InterruptedException e) { e.printStackTrace(); }finally { Log.e("打印日志----",count+""); } } //期望输出10000,最后输出的是9818 //注意:打印日志----: 9818
4.2 安全的基本类型
- Java自带的线程安全的基本类型包括:
- AtomicInteger, AtomicLong, AtomicBoolean, AtomicIntegerArray,AtomicLongArray等
4.3 AtomicInteger安全版
- AtomicInteger类中有有一个变量valueOffset,用来描述AtomicInteger类中value的内存位置 。
- 当需要变量的值改变的时候,先通过get()得到valueOffset位置的值,也即当前value的值.给该值进行增加,并赋给next
- compareAndSet()比较之前取到的value的值当前有没有改变,若没有改变的话,就将next的值赋给value,倘若和之前的值相比的话发生变化的话,则重新一次循环,直到存取成功,通过这样的方式能够保证该变量是线程安全的
- value使用了volatile关键字,使得多个线程可以共享变量,使用volatile将使得VM优化失去作用,在线程数特别大时,效率会较低。
private static AtomicInteger atomicInteger = new AtomicInteger(1); static Integer count1 = Integer.valueOf(0); private void startThread1() { for (int i = 0;i < 200; i++){ new Thread(new Runnable() { @Override public void run() { for (int k = 0; k < 50; k++){ // getAndIncrement: 先获得值,再自增1,返回值为自增前的值 count1 = atomicInteger.getAndIncrement(); } } }).start(); } // 休眠10秒,以确保线程都已启动 try { Thread.sleep(1000*10); } catch (InterruptedException e) { e.printStackTrace(); }finally { Log.e("打印日志----",count1+""); } } //期望输出10000,最后输出的是10000 //注意:打印日志----: 10000 //AtomicInteger使用了volatile关键字进行修饰,使得该类可以满足线程安全。 private volatile int value; /** * Creates a new AtomicInteger with the given initial value. * * @param initialValue the initial value */ public AtomicInteger(int initialValue) { value = initialValue; }
05.int和Integer局限性
5.1 原始类型局限性
- 原始数据类型和 Java 泛型并不能配合使用
- Java 的原始数据类型和泛型是不能直接配合使用的。原始数据类型是指没有使用泛型的普通数据类型,如int、char、boolean等。
- 而泛型是一种参数化类型的机制,允许我们在定义类、接口或方法时使用类型参数,以增加代码的灵活性和重用性。
- Java 的泛型某种程度上可以算作伪泛型,它完全是一种编译期的技巧,Java 编译期会自动将类型转换为对应的特定类型,这就决定了使用泛型,必须保证相应类型可以转换为Object。
- 例如,不能使用原始数据类型作为泛型类型参数
- 如
List<int>
是不合法的。相反,应该使用包装类 Integer 来代替,即List<Integer>
。 - 另外,原始数据类型也不能用于泛型类的实例化,如不能创建一个
ArrayList<int>
,而应该使用ArrayList<Integer>
。
- 如
- 来看一个案例,如下所示
List<Integer> numbers = new ArrayList<>(); // 使用包装类 Integer numbers.add(10);
5.2 引用类型局限性
- 无法高效地表达数据,也不便于表达复杂的数据结构
- Java 的对象都是引用类型,如果是一个原始数据类型数组,它在内存里是一段连续的内存,而对象数组则不然,数据存储的是引用,对象往往是分散地存储在堆的不同位置。
- 这种设计虽然带来了极大灵活性,但是也导致了数据操作的低效,尤其是无法充分利用现代 CPU 缓存机制。
- Java 为对象内建了各种多态、线程安全等方面的支持,但这不是所有场合的需求,尤其是数据处理重要性日益提高,更加高密度的值类型是非常现实的需求。
5.3 回答1.1的问题
- 为什么我们需要原始数据类型,Java 的对象似乎也很高效,应用中具体会产生哪些差异?
- 原始数据类型(Primitive data types)在Java中是指基本的数据类型,如整数类型(int、long)。相比之下,Java的对象是通过类来表示的,具有更复杂的结构和功能。
- 原始数据类型的主要原因是效率和内存占用。原始数据类型在内存中占用的空间较小,且操作速度更快。这是因为原始数据类型直接存储数据的值,而不需要额外的内存来存储对象的引用和其他元数据。此外,原始数据类型的操作通常是直接在处理器级别上执行的,而不需要通过方法调用和对象引用的间接访问。
- Java的对象也具有其优势,特别是在面向对象编程和复杂数据结构方面。对象可以封装数据和行为,提供更高级的功能和灵活性。对象还可以通过继承、多态和接口实现更复杂的关系和功能。
更多内容
- GitHub:https://github.com/yangchong211
- 博客:https://juejin.cn/user/1978776659695784
- 博客汇总:https://github.com/yangchong211/YCBlogs
- 设计模式专栏:https://github.com/yangchong211/YCDesignBlog
- Java进阶专栏:https://github.com/yangchong211/YCJavaBlog