Java基础 | 基本数据类型

967 阅读8分钟

Java 有八大基本数据类型,这些数据类型的创建并不是通过 new 来产生的。通常 new 出来的对象都保存在堆内存中,以此来创建小而简单的变量往往是不划算的。所以 Java 使用了和 C/C++ 一样的策略,就是变量直接存储值,并置于栈内存中,这样更加高效。

基本数据类型的分类

我们知道 Java 中的数据类型可以分为基本数据类型(primitive data type)和引用数据类型(reference data type)。基本数据类型有如下几种:

yuque_diagram

存储大小与取值范围

Java 确定了每种基本类型的内存占用大小。这些大小不会像其他一些语言那样随着机器环境的变化而变化,这就是 Java 更具有移植性的一个原因。

数据类型存储大小取值范围初始化默认值包装类型
byte1字节(8位)-128(-27)~127(27-1)0Byte
short2字节(16位)-32768(-215)~32767(215-1)0Short
int4字节(32位)-22147483648(-231)~22147483647(231-1)0Integer
long8字节(64位)-263~263-10Long
float4字节(32位)符合IEEE754标准的浮点数0.0fFloat
double8字节(64位)符合IEEE754标准的浮点数0.0dDouble
char2字节(16bits)Unicode 0 ~ Unicode 216 -1(本质也是数值)nullCharacter
boolean1字节(8位)/ 4字节(32位)true/falsefalseBoolean

类型转换

Java 会在适当的时候将一种数据类型自动转换成另一种,这就是“类型转换”(Casting)。

自动转型的规则是:byte -> short/char -> int -> long -> float -> double。

举个栗子,如果按自动转型规则进行类型转换就没问题,但是我们把 long 的值赋予一个 int(即高位转低位) 就会提示错误:

image-20220819174718063

但我们强制转换会发生什么呢?如下代码:

 public static void main(String[] args) {
        int a = 1000;  
        short b = (short) a; 
        byte c = (byte) a; 
        
        System.out.println("b:" + b);
        System.out.println("c:" + c);
    }

由于我们知道 short 的取值范围是 -32768 ~32767,所以预测 b 是 1000,但 byte 的取值范围是 -128 ~127,很明显超出了范围,那 c 是多少呢?

执行结果为:

b:1000
c:-24

WHAT?int 强转 byte,1000 变 -24?要搞懂这个问题,我们需要知道计算机二进制的一些知识。

在计算机中,字节(bit)是表示空间大小的基本单位,存储数字都是以二进制补码的形式,现实生活中数字的正负用 “+”、“-” 符号表示,而计算机语言里面是不能识别 “+”、“-” 符号,所以使用最高位 bit 存储数值的正负(最高位 bit 0 代表正数,1 代表负数)。

原码:符号位 + 数值部分,最高位为 0 表示为正数,最高位为 1 表示为负数。其规律是:正数的原码就是正数本身,负数的原码就是在绝对值的原码基础上,最高位符号位为 1。

反码:对于正数,正数的反码等于正数的原码。对于负数,负数的反码为原码符号位不变,原码剩下数值取反(取反的意思是 0 变成 1,1 变成 0)。其规律是:正数的反码 = 正数的原码,负数的反码 = 负数原码符号位不变,剩下数值部分取反(0 变成 1,1 变成 0)。

补码:对于正数,正数的补码就是正数的原码;对于负数,负数的补码 = 负数的反码 + 1。其规律是:正数的原码 = 正数的反码 = 正数的补码;负数的原码 = 正数的原码基础上,符号位为1;负数的反码 = 负数原码取反(符号位不变);负数的补码 = 负数的反码(符号位不变) + 1。

int 类型占 32 位,1000 转化为二进制原码: 0(+)000 0000 0000 0000 0000 0011 1110 1000

short 类型占 16 位,所以从 int 转 short 存在精度丢失(高位转低位存在精度丢失),其转化为二进制原码: 0(+)000 0011 1110 1000。由于正数的补码等于正数的原码,即补码:0(+)000 0011 1110 1000,所以转为十进制仍为 1000。

byte 类型占 8 位,所以从 int 转 byte 存在精度丢失,其转化为二进制原码:1(-)110 1000。由于负数的反码等于负数原码取反符号位不变,即反码为:1(-)001 0111。负数的补码等于负数的反码(符号位不变) + 1,即补码为:1(-)001 1000,转为十进制为 -24。

再看以下代码:

public static void main(String[] args) {
        int d = Integer.MAX_VALUE;
        int e = Integer.MAX_VALUE;
        int f = d + e;
        long g = d + e;
        System.out.println("f:" + f);
        System.out.println("g:" + g);
}

运行结果都为 -2,说明同数据类型并不会在计算时超出范围自动转型。

f:-2
g:-2

那 -2 是怎么来的呢?我们可以通过再次原码推补码的方式求得。如下:

Integer.MAX_VALUE 转化为二进制为:0(+)111 1111 1111 1111 1111 1111 1111 1111

所以两个 Integer.MAX_VALUE 相加得到原码为:1(-)111 1111 1111 1111 1111 1111 1111 1110

其对应的反码为:1(-)000 0000 0000 0000 0000 0000 0000 0001

求得补码为:1(-)000 0000 0000 0000 0000 0000 0000 0010,即 -2。

在程序中有些计算会导致超出表示范围,发生了溢出,溢出的时候并不会抛异常,也没有任何提示。所以在同类型数据计算的时候一定要注意数据溢出的问题。

再看以下代码:

public static void main(String[] args) {
        int h = Integer.MAX_VALUE;
        long i = Integer.MAX_VALUE;
        System.out.println("h*i=" + (h * i));
}

结果为:

h*i=4611686014132420609

说明在不同数据类型计算时,会向高位自动转型。

自动装箱和拆箱

什么是装箱?什么是拆箱?

  • 装箱:将基本类型用他们的引用类型包装起来。
  • 拆箱:将包装类型转换为基本数据类型。

在 JavaSE 5 中新增自动装箱的特性,从此代码有了如下转变:

JavaSE 5之前:

Integer a = new Integer(10); // 装箱

Java5及之后:

Integer a = 10; // 自动装箱
int b = a;  // 自动拆箱

以上代码查看字节码如下:

image-20220825181831258

可以发现装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 intValue()方法(其他包装类同理)。

布尔的存储

我们看到上面的 boolean类型存储大小有两个:1 字节(8 位)和 4 字节(32 位)。为什么呢?

每次 JDK 出新版本的时候,Java 官方都会发布一个对应版本的《 Java 虚拟机规范》。在《 Java 虚拟机规范》中,对 boolean 类型的存储有专门的解释:

Although the Java Virtual Machine defines a boolean type, it only provides very limited support for it. There are no Java Virtual Machine instructions solely dedicated to operations on boolean values. Instead, expressions in the Java programming language that operate on boolean values are compiled to use values of the Java Virtual Machine int data type. The Java Virtual Machine does directly support boolean arrays. Its newarray instruction (§newarray) enables creation of boolean arrays. Arrays of type boolean are accessed and modified using the byte array instructions baload and bastore (§baload, §bastore). In Oracle’s Java Virtual Machine implementation, boolean arrays in the Java programming language are encoded as Java Virtual Machine byte arrays, using 8 bits per boolean element. The Java Virtual Machine encodes boolean array components using 1 to represent true and 0 to represent false. Where Java programming language boolean values are mapped by compilers to values of Java Virtual Machine type int, the compilers must use the same encoding。

尽管 Java 虚拟机定义了一个布尔类型,但它只提供非常有限的支持。没有专门用于布尔值操作的 Java 虚拟机指令。相反,Java 编程语言中对布尔值进行操作的表达式被编译为使用 Java 虚拟机 int 数据类型的值。

Java虚拟机确实直接支持布尔数组。其 newarray 指令(§newarray)支持创建布尔数组。使用字节数组指令 baload 和bastore(§baload,§bastore)访问和修改布尔类型的数组。

在 Oracle 的 Java 虚拟机实现中,Java 编程语言中的布尔数组被编码为 Java 虚拟机字节数组,每个布尔元素使用 8 位。

Java 虚拟机使用 1 表示真,0 表示假,对布尔数组组件进行编码。当 Java 编程语言布尔值由编译器映射到 Java 虚拟机类型 int 的值时,编译器必须使用相同的编码。

从上我们可以得知,boolean 类型在单独使用时占用 4 个字节的空间,而存储到 boolean 数组中时,每个 boolean 元素占用 1个字节的空间。

为什么一个 boolean 类型数据单独使用要用 4 个字节的空间来存储呢?当下大部分 CPU 处理器都是 32 位的,把boolean 型数据存储成 4 个字节(即 32 位),存取效率是最高的。

不过以上是 Oracle 的虚拟机规范,其他虚拟机厂商可能综合运算性能与存储空间考虑,有其他不同的实现。

其他

PS:主要分享一些有意思的点,如果觉得对你有帮助,点个赞呗~