2.7 数据类型
2.7.1 基本数据类型
虽然Java 是面向对象编程语言,一切皆是对象,但是为了兼容人类根深蒂固的数据处理习惯,加快常规数据的处理速度,提供了9 种基本数据类型,它们都不具备对象的特性,没有属性和行为。基本数据类型是指不可再分的原子数据类型,内存中直接存储此类型的值,通过内存地址即可直接访问到数据,并且此内存区域只能存放这种类型的值。Java 的9 种基本数据类型包括boolean、byte、char、short、int、long、float、double 和refVar。前8 种数据类型表示生活中的真假、字符、整数和小数,最后一种refVar 是面向对象世界中的引用变量,也叫引用句柄。本书认为它也是一种基本数据类型。前8 种都有相应的包装数据类型,除char 的对应包装类名为Character,int 为Integer 外,其他所有对应的包装类名就是把首字母大写即可。这8种基本数据类型的默认值、空间占用大小、表示范围及对应的包装类等信息如表2-4所示。
默认值虽然都与0 有关,但是它们之间是存在区别的。比如,boolean 的默认值以0 表示的false,JVM 并没有针对boolean 数据类型进行赋值的专用字节码指令,boolean flag = false 就是用ICONST_0,即常数0 来进行赋值;byte 的默认值以一个字节的0 表示,在默认值的表示上使用了强制类型转化;float 的默认值以单精度浮点数0.0f 表示,浮点数的0.0 使用后缀f 和d 区别标识;char 的默认值只能是单引号的'\u0000'表示NUL,注意不是null,它就是一个空的不可见字符,在码表中是第一个,其码值为0,与'\n' 换行之类的不可见控制符的理解角度是一样的。注意,不可以用双引号方式对char 进行赋值,那是字符串的表示方式。在代码中直接出现的没有任何上下文的0 和0.0 分别默认为int 和double 类型,可以使用JDK10 的类型推断证明:var a=0; Long b=a; 代码编译出错,因为在自动装箱时,0 默认是int 类型,自动装箱为Integer,无法转化为Long 类型。
表2-4 基本数据类型
所有数值类型都是有符号的,最大值与最小值如表2-4 所示。因为浮点数无法表示零值,所以表示范围分为两个区间:正数区间和负数区间。表2-4 中的float 和double 的最小值与最大值均指正数区间,它们对应的包装类并没有缓存任何数值。
引用分成两种数据类型:引用变量本身和引用指向的对象。为了强化这两个概念的区分,本书把引用变量(Reference Variable)称为refVar,而把引用指向的实际对象(Referred Object)简称为refObject。
refVar 是基本的数据类型,它的默认值是null,存储refObject 的首地址,可以直接使用双等号== 进行等值判断。而平时使用refVar.hashCode() 返回的值,只是对象的某种哈希计算,可能与地址有关,与refVar 本身存储的内存单元地址是两回事。作为一个引用变量,不管它是指向包装类、集合类、字符串类还是自定义类,refVar 均占用4B空间。注意它与真正对象refObject 之间的区别。无论refObject 是多么小的对象,最小占用的存储空间是12B(用于存储基本信息,称为对象头),但由于存储空间分配必须是8B 的倍数,所以初始分配的空间至少是16B。
一个refVar 至多存储一个refObject 的首地址,一个refObject 可以被多个refVar存储下它的首地址,即一个堆内对象可以被多个refVar 引用指向。如果refObject 没有被任何refVar 指向,那么它迟早会被垃圾回收。而refVar 的内存释放,与其他基本数据类型类似。
基本数据类型int 占用4 个字节,而对应的包装类Integer 实例对象占用16 个字节。这里可能会有人问:Integer 里边的代码就只占用16B ?这是因为字段属性除成员属性int value 外,其他的如MAX_VALUE、MIN_VALUE 等都是静态成员变量,在类加载时就分配了内存,与实例对象容量无关。此外,类定义中的方法代码不占用实例对象的任何空间。IntegerCache 是Integer 的静态内部类,容量占用也与实例对象无关。由于refObject 对象的基础大小是12B,再加上int 是4B,所以Integer 实例对象占用16B,按此推算Double 对象占用的存储容量是24B,示例代码如下:
class RefObjDemo {
// 对象头最小占用空间12 个字节(第1 处)
// 下方4 个byte 类型分配后,对象占用大小是16 个字节
byte b1;
byte b2;
byte b3;
byte b4;
// 下方每个引用变量占用是4 个字节,共20 个字节
Object obj1;
Object obj2;
Object obj3;
Object obj4;
Object obj5;
// RefObjOther 实例占用空间并不计算在本对象内,依然只计算引用变量大小4个字节
RefObjOther o1 = new RefObjOther();
RefObjOther o2 = new RefObjOther();
// 综上,RefObjDemo 对象占用:12B + (1B×4) + (4B×5) + (4B×2) = 44 个字节
// 取8 的倍数为48 个字节
}
class RefObjOther {
// double 类型占用8 个字节,但此处是数组引用变量
// 所以对象头12B + 4B = 16B,并非是8012 个字节
// 这个数组引用的是double[] 类型,指向实际分配的数组空间首地址
// 在new 对象时,已经实际分配空间
double[] d = new double[1000];
}
在上述示例代码中,第1 处提到的对象头最小占用空间为12 个字节,其内部存储的是什么信息呢?下面来分析其内部结构,如图2-10 所示,对象分为三块存储区域。
图2-10 对象头的内部结构
(1)对象头(Object Header)
对象头占用12 个字节,存储内容包括对象标记(markOop)和类元信息(klassOop)。对象标记存储对象本身运行时的数据,如哈希码、GC 标记、锁信息、线程关联信息等,这部分数据在64 位JVM 上占用8 个字节,称为“Mark Word”。为了存储更多的状态信息,对象标记的存储格式是非固定的(具体与JVM 的实现有关)。类元信息存储的是对象指向它的类元数据(即Klass)的首地址,占用4 个字节,与refVar 开销一致。
(2)实例数据(Instance Data)
存储本类对象的实例成员变量和所有可见的父类成员变量。如Integer 的实例成员只有一个private int value,占用4 个字节,所以加上对象头为16 个字节;再如,上述示例代码的RefObjDemo 对象大小为48 个字节,一个子类RefObjSon 继承RefObjDemo,即使子类内部是空的,new RefObjSon 的对象也是占用48 个字节。
(3)对齐填充(Padding)
对象的存储空间分配单位是8 个字节,如果一个占用大小为16 个字节的对象,增加一个成员变量byte 类型,此时需要占用17 个字节,但是也会分配24 个字节进行对齐填充操作。