Java的基本数据类型、拆装箱(深入版)

前言

本文主要总结了Java的八种基本数据类型以及它们在虚拟机中的标记,还会对:“什么是浮点型?什么是单精度和双精度?为什么不能用浮点型表示金额?”这些问题进行解释。

在Java中已经提供基本数据类型,为什么还要提供包装类型?Integer的缓存机制是什么样的?

(若文章有不正之处,或难以理解的地方,请多多谅解,欢迎指正)

八种基本数据类型、包装类

Java基本数据类型如下:

类型 值域 位数 默认值 虚拟机内部符号 包装类
boolean {false, true} 1 false Z Boolean
byte [-128, 127] 8 0 B Byte
short [-32768, 32767] 16 0 S Short
char [0, 65535] 16 ‘\u0000’ C Character
int [-2^31, 2^31-1] 32 0 I Integer
long [-2^63, 2^63-1] 64 0L J Long
float ~[-3.4E38, 3.4E38] 32 +0.0F F Float
double ~[1.8E308, 1.8E308] 64 +0.0D D Double

怎么知道基本数据类型在虚拟机内部的符号呢?

public class Test{
    boolean a;
    byte b;
    short c;
    char d;
    int e;
    long g;
    float f;
    double h;
}

将上述代码进行编译后,使用下边的语句反编译即可:

javap -verbose Test.class

什么是浮点型?

首先我们得先知道什么是浮点型:浮点型简单来说就是带有小数的数据,而这小数可以在相应的二进制的不同位置浮动。而浮点型在计算机里的存储方式,一般是按照IEEE754标准中浮点数表示格式来进行存储,即用符号(+或-)、指数和尾数来表示,底数确定为2。

什么单精度双精度?

举个栗子,标准4字节(32位)浮点型在计算机的存储方式如下:
在这里插入图片描述
标准8字节(64位)浮点型的计算机的存储方式如下:
在这里插入图片描述
所以对于Java中的float和double类型来说,它们在计算机中的存储具体格式如下:

符号域 指数域 小数域 指数偏移量 Java基本类型
单精度浮点数 1位[31] 8位[30-23] 23位[22-00] 127(2^8-1) float
双精度浮点数 1位[63] 11位[62-52] 52位[51-00] 1023(2^10-1) double

为什么不能用浮点型表示金额呢?

举个栗子:

public class Test{
    static float f = 1234.123456789f;
    static double d = 1234567.1234567890123456789d;
    public static void main(String[] args){
        System.out.println(f);
        System.out.println(d);
    }
}

运行结果为:

1234.1234
1234567.123456789

大家能看到,精度缺失了很多,而这种精度缺失在金融等行业是绝对不能被容忍的。那么可以怎么解决呢?

一般是用BigDecimal或者Long来解决相关的精度缺失和小数相加问题。

基本数据类型有什么好处?

其实在Java语言中,对象是存储在堆中的,我们需要通过栈中的引用来使用对象,所以如果我们每次使用基本数据类型,如int,都需要new一个对象出来,那就比较消耗资源了。

所以Java提供了基本数据类型,这种数据的变量不需要使用new来创建,它们不在堆上创建,而是直接存储在栈内存中,这样数据创建、存取和销毁更加高效

那么Java基本数据类型在虚拟机中的栈里,又是怎么存储的呢?

一般老师上课的时候会粗浅地跟我们说,Java内存分为“堆内存”和“栈内存”,但其实Java内存区域的划分比这复杂点,这里先简单介绍虚拟机的运行时数据区域
在这里插入图片描述
在运行时数据区域中,方法区和堆属于线程公有区域,而虚拟机栈、本地方法栈和程序计数器属于线程私有区域。其中“栈内存”一般只是的虚拟机栈里的局部变量表

我们将镜头向虚拟机栈拉近,可以看到一个虚拟机栈中有多个栈帧(Stack Frame),因为每个方法在执行的同时都会创建一个栈帧,栈帧中存储局部变量表、操作数栈、动态链接、方法出口等信息。

其中,局部变量表存放了编译期可知的八种基本数据类型、对象引用和returnAddress类型。

32位hotspot虚拟机中,对于32位以内的数据类型:boolean、byte、short、char、int、float占用4个字节,相当于1个slot,而对于64位的数据类型:long、double占用2个slot

64位hotspot虚拟机中,对于32位以内的数据类型占用的空间是8个字节,即一个slot,而64位的数据类型占用的空间是2个slot

为什么需要包装类型

每个基本数据类型都有对应的类,统称为包装类(Wrapper Class)。上文我们说到,基本数据类型相比对象来说,更加节省资源且存取方便,那么为什么要还会有包装类型呢?

Java是一种面向对象的语言,很多地方都需要使用到对象而不是数据类型,如在集合类中是不允许存储int、double等基本数据类型的数据,因为集合容器所要求的的元素是Object类型。

为了让基本类型也具有对象的特征,于是就出现了包装类型,它像是将基本数据类型“包装”起来后,使其具有对象的性质,并添加了属性和方法,丰富了基本数据类型的操作

拆箱与装箱

什么是拆箱和装箱?

上文提到包装类需要的原因,但二者之间还是会有些时候需要进行转换。

装箱(boxing)就是把基本数据类型转化成包装类的过程

Integer num1 = Integer.valueOf(123);

拆箱(unboxing)就是把包装类转换成基本数据类型的过程

int num = num1.intValue();

自动拆箱与自动装箱

在Java SE5中,Java提供了自动拆箱与自动装箱功能。

自动装箱就是将基本数据类型自动转换成对应的包装类

即,调用valueOf()方法将原始类型值转换成对象

Integer i = 10; //自动装箱
//其实就相当于Integer i = new Integer.valueOf(10);

自动拆箱就是将包装类自动转换成对应的基本数据类型

即,调用intValue()(或者xxxValue())方法将对象转换成原始类型。

int b = i;  //自动拆箱
//其实就相当于int b = i.intValue();

读到这里,相信有读者想到了“语法糖”。语法糖可以简单理解为,Java为了方便开发者编写程序,特地设计了一些语法来”减轻”开发者的开发压力,但是其实这些改变仅限于在开发者层面,经过编译后的Class文件依然会让虚拟机按照它原本运行的方式运行代码。

Integer的缓存机制

Java SE的自动拆装箱还提供了一个和缓存有关的功能,让我们先做道题先:

public class Test {
    public static void main(String[] args) {
        Integer a = 2;
        Integer b = 2;

        if (a == b)
            System.out.println("a == b");
        else
            System.out.println("a != b");
        
        Integer c = 200;
        Integer d = 200;
        if (c == d)
            System.out.println("c == d");
        else
            System.out.println("c != d");
    }
}

运行结果为:

a == b
c != d

我们知道,在Java中"=="一般用于比较对象应用,而"equals()"用于比较值。那么为什么这段代码只是数值发生了变化,但结果却不一样呢?因为在Java5中引入的新功能:整型对象通过使用相同的对象引用来实现了缓存和重用

适用于整数值区间在-128 至 127,且只适用于自动装箱,使用构造函数创建对象不适用。

我们在上文了解到**Integer.valueOf()**是实现装箱的关键,那我们来看看这个方法是怎么实现的:

/**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

我们可以看到,这个方法表示在IntegerCache.low和IntegerCache.high这个区间的整数数值,则按照IntegerCache.cache来返回整数实例,否则返回一个数值为i的包装类。代码的注释可参考以下拙译:

本方法将返回一个代表特定值(int)的实例(Integer)。如果并不要求创造一个新的Integer实例,则该方法应比构造方法优先使用,因为这个方法可通过频繁使用缓存可能会在空间和时间上的节省有着显著效果。

这个方法会将范围在-128~127的数字缓存下来,并且不在这个范围的数值也会缓存下来。

我们再看一下IntegerCache这个类,这个类是Integer类中private static类型的类:

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() {}
    }

其中,最大值127可以通过**-XX:AutoBoxCacheMax=size**修改。

可以看到,为了尽可能创建多的整数并存储进一个整数数组中,程序先确定整数区间low和high,并且通过for循环从低到高创建整数存储到数组中。这个缓存会在Integer第一次调用的时候初始化出来。以后,就可以使用缓存中包含的实例对象,而不是创建一个新的实例(在自动装箱的情况下)。

除了Integer,其他整数类型也有相应的缓存机制:

ByteCache可用于缓存Byte对象

ShortCache可用于缓存Short对象

LongCache可用于缓存Long对象

CharacterCache可用于缓存Character对象

Byte、Short、Long都有固定范围:-128至127,对于Character范围是0至127。也就是除了Integer以外,缓存范围都不能改变。

结语

拆装箱实际上就是一种语法糖,而基本数据类型是一个我们总是理所当然地使用但少有人去深究其实现原理的知识。因为知识量和技术有限,所以对于基本数据类型的介绍也只能到虚拟机的程度,如果有读者对基本数据类型的实现原理有更深入的理解,不妨在评论区留下相关的资料链接,大家一起学习~

如果觉得文章不错,请点一个赞吧,这会是我最大的动力~

参考资料:

Java的8种基本数据类型

浮点型数据

java中的基本数据类型与JVM中的基本数据类型

什么是Java中的自动拆装箱

Java Integer Cache

[译]Java中整型的缓存机制

《深入理解Java虚拟机》

posted @ 2020-03-11 17:18  NYfor2018  阅读(397)  评论(0编辑  收藏  举报