编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

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
贡献者: yangchong211
上一篇
1.2浮点型数据深入研究
下一篇
1.4泛型由来和设计思想