美文网首页JAVA基础
Java基础 - 包装器类型

Java基础 - 包装器类型

作者: HRocky | 来源:发表于2019-02-02 21:04 被阅读0次

    一、基本类型

    类型的作用是修饰变量,不同的类型修饰变量表示这个变量代表不同的数据含义。

    Java是一门面向对象编程的语言,就像《Java编程思想》中说到的那样: "一切都是对象"。但是在程序设计中经常用到一系列的类型,它们需要特殊对待。可以把它们想象成"基本"类型。之所以特殊对待,是因为new将对象存储在"堆"里,故用new创建一个对象——特别是小的、简单的变量,往往不是很有效。因此,对于这些类型,Java采取与C和C++相同的方法。就是说,不用new来创建变量,而是创建一个并非是引用的"自动"变量。这个变量直接存储"值",并置于堆栈中,因此更加高效。

    可以这样理解吧,像下面这样:

    int a = 123;
    

    就可以理解为在内存中开辟了一个空间,这个空间存储值123,用命名"a"代表这个空间,访问a即表示访问这个空间。

    可以比对一下C中的指针,在C语言中可以一个指针,如下:

    int *a;
    

    这个与上面的不同,这个命名a代表的内存空间存储的就不是直接值,而是代表另外一个内存空间的地址,比如这样:

    int a = 123;
    int *b = &a;
    

    总之就这样理解:基本类型修饰的变量代表它所表示的这段内存空间存储的是直接值。

    Java中定义的基本类型

    Java中定义了8种基本类型,分别是数字类型和布尔类型。

    数字类型又可以分为整数类型和浮点类型。
    整数类型包括byte、short、int和long,它们的值分别是8位、16位、32位和64位有符号二进制补码表示的整数;char也是一种整数类型,它的值是16位无符号整数,表示,UTF-16码元。
    浮点数类型包括float和double,前者的值包括32位IEEE 754浮点数,而后者的值包括64位IEEE 754浮点数。

    布尔类型只有两个值:true和false。

    二、包装器类型

    上面说了,一切都是对象。对于基本类型,Java都定义了相对应的包装器类,使得可以在堆中创建一个非基本对象,用来表示对应的基本类型。

    有人会问,有了基本类型为什么还要定义包装类呢?

    以对象的方式来操作基本类型的数据,解决一些特定的问题,这是引入包装类的原因。

    比如说我们定义了一个学生类Student,其中有一个属性表示该学生某学科的考试成绩,成绩我们用整型的数据表示。那么这个数据我们用什么数据类型修饰呢?不提及到包装类的情况下,我们用int类型来修饰这个属性,但是你会发现这样不能解决一个特殊的问题,就是你无法表示这个学生该学科没有成绩(规定不用用其他属性标记表示没有成绩)。如果我们用包装类Integer来表示,空值就可以表示没有成绩。

    0就是表示成绩为0分,不能表示没有成绩。

    上面只是举了某个需要包装类的需求,这种需求的场景还有很多,就不一一举例。

    Java定义的包装器类型

    基本类型对应的包装类如下表所示:

    基本类型 包装器类型
    boolean Boolean
    byte Byte
    char Character
    short Short
    int Integer
    long Long
    float Float
    double Double

    三、装箱和拆箱

    上面我们已经说到了基本类型都有相对于的包装器类型,那么这两种类型可以相互转换吗?

    答案是肯定的,Java提供了装箱和拆箱来实现基本类型与对应包装器类型的转换。

    1. 装箱

    装箱是指把基本类型转换为包装类类型。

    看下面的代码:

    int i = 10;
    

    这里我们定义了一个int的变量i,现在要转换到其对应的包装器类型Integer,我们怎么做,在JDK1.5之前我们可以这样做:

    Integer ii = new Integer(i);
    

    或者这样做:

    Integer ii = Integer.valueOf(i);
    

    JDK1.5中引入了自动装箱的功能来,具体如下:

    init i = 10;
    Integer ii = i;
    

    这里可以看到,直接将基本类型的变量赋值给包装器类型,这种类型相比我们上面写的那两种,体现了"自动"这一概念。

    那么"自动装箱"是怎么做到的呢?

    看下面代码:

    public class E1 {
        public static void main(String[] args) {
            int i = 10;
            Integer ii = i;
        }
    }
    

    我们通过javap指令来看一下生成的字节码(片段):

    {
      public E1();
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 1: 0
    
      public static void main(java.lang.String[]);
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=1, locals=3, args_size=1
             0: bipush        10
             2: istore_1
             3: iload_1
             4: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
             7: astore_2
             8: return
          LineNumberTable:
            line 4: 0
            line 5: 3
            line 6: 8
    }
    

    注意下面红色方框标记:


    自动装箱.png

    从上面可以看到,int类型转换到Integer,直接赋值方式其实还是通过Integer的valueOf方法来操作,只是说这个是编译器为我们处理的,所以相比人为转换这个就叫做"自动"。

    2. 拆箱

    拆箱是指把包装类型转换为基本类型。

    看下面的代码:

    Integer i = 10;
    

    这里我们定义了一个Integer的变量i,现在要转换到其对应的基本类型,我们怎么做,在JDK1.5之前我们可以这样做:

    int ii = i.intValue();
    

    对于其他基本类型的包装类类型,我们会发现都会有相对应的xxValue的方法来实现到基本类型的装换。

    JDK1.5中引入了自动拆箱的功能来,具体如下:

    Integer i = 10;
    init ii = i;
    

    和上面自动装箱一下,体现了自动装换这个概念。

    那么"自动拆箱"是怎么做到的呢?

    看下面代码:

    public class E2 {
        public static void main(String[] args) {
            Integer i = 10;
            int ii = i;
        }
    }
    

    我们通过javap指令来看一下生成的字节码(片段):

    {
      public E2();
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 1: 0
    
      public static void main(java.lang.String[]);
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=1, locals=3, args_size=1
             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
          LineNumberTable:
            line 4: 0
            line 5: 6
            line 6: 11
    }
    

    注意下面红色方框标记:


    自动拆箱.png

    从上面可以看到,Integer类型转换到int,直接赋值方式其实还是通过Integer的intValue方法来操作,只是说这个是编译器为我们处理的,所以相比人为转换这个就叫做"自动"。

    三、包装类缓存池

    先来看一道面试题:

    public class E3 {
        public static void main(String[] args) {
            Integer i1 = 10;
            Integer i2 = 10;
            System.out.println(i1 == i2);
    
            Integer i3 = 128;
            Integer i4 = 128;
            System.out.println(i3 == i4);
        }
    }
    

    输出结果如下:

    rockydeMacBook-Pro:Desktop rocky$ java E3
    true
    false
    

    我们知道"=="比较两个对象是否是同一个对象,从上面的结果我们可以看到i1和i2是同一个对象,i3和i4不是同一个对象。

    为什么结果是这样的?

    上面我们知道基本类型数据赋值给包装类型是通过自动装箱实现的,调用的是相对应的包装类型的valueOf方法,我们来看一下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);
    }
    

    我们看到用到了IntegerCache这个类,只从名字上应该也可以知道这个是做缓存用的。

    JDK做了这样的处理:
    为Byte、Short、Integer和Long缓存了数值范围为[-128,127](闭区间)的对象;
    为Character缓存了数值范围为[0,127](闭区间)的对象。

    包装类与相对应的缓存类如下:

    包装类 包装类对象缓存类
    Byte ByteCache
    Short ShortCache
    Integer IntegerCache
    Long LongCache
    Character CharacterCache

    有了这些知识就可以分析上面的代码:

    Integer  i1 = 10;
    

    将数值为10的Integer对象放入了缓存中。

    Integer i2 = 10;
    

    10在缓存数值区间类,直接从缓存数组中取出对象。

    而128超出了缓存的数值范围,所以:

    Integer i4 = 128;
    

    创建的是一个新对象。

    JDK中没有为Float和Double实现缓存池是因为某个范围内的整型数值的个数是有限的,而浮点数却不是。

    1. 为什么使用缓存池

    这是Flyweight pattern的一种实现:尽量与其他对象共享更多的数据以减少内存的占用。

    同时,是缓存嘛,减少内存占用,同时提升性能。

    2. 可以改变缓存的数值范围吗?

    答案是:一些新版本的Java6和Java7开始可以在启动的时候配置-XX:AutoBoxCacheMax参数来设置Integer最大缓存的数值。这个参数配置的值会设置到java.lang.Integer.IntegerCache.high属性中,从而修改Integer最大的缓存数值。

    其他有缓存的包装类缓存的数值范围都是硬编码的,无法修改。

    使用JDK7我们来测试一下这个设置。

    代码如下:

    public class E4 {
        public static void main(String[] args) {
        Integer i1 = 128;
            Integer i2= 128;
            System.out.println(i1 == i2);
    
            Integer i3 = 1000;
            Integer i4= 1000;
            System.out.println(i3 == i4);
    
            Integer i5 = 1001;
            Integer i6= 1001;
            System.out.println(i5 == i6);
        }
    }
    

    使用下面命令运行:

    java -XX:AutoBoxCacheMax=1000 E4
    

    输出结果如下:

    rockydeMacBook-Pro:Desktop rocky$ java -XX:AutoBoxCacheMax=1000 E4
    true
    true
    false
    

    相关文章

      网友评论

        本文标题:Java基础 - 包装器类型

        本文链接:https://www.haomeiwen.com/subject/hardsqtx.html