Java自动类型提升与强制类型转换机制详解

在深入学习Java的底层机制的过程中,数据类型的相互转换的结果往往并符合预期。在网上查阅资料和博客时,整型部分的转换还有参考价值,但一旦涉及到浮点类型,许多博文往往一笔带过,或者语焉不详。所以笔者决心写一篇详解,说明数据类型转换时内存中究竟发生了什么,又为何会出现类型转换后与预期不符的情况。

由于文章内容大部分都是笔者自己的理解,或是查阅资料和博客后的总结,多多少少会有错误,请各位dalao斧正。

另外,文中对浮点数的一些基础知识,没有过多的解释,如果对浮点数机制有所疑惑,请参考笔者的另一篇文章:

深入理解Java浮点数机制:https://blog.****.net/Return_head/article/details/88623060

 

正文

Java中,经常可以见到类型转换的场景,数据类型转换在Java编码过程中占据着及其重要的地位。对于Java来说,数据类型转换大致可以分为三种,它们分别是:

1.基本数据类型之间的转换;

2.字符串与基本数据类型的转换;

3.引用数据类型之间的转换。

本文主要探讨基本数据类型转换的原理和底层实现。

 

基本数据类型之间的转换

Java中,共有八种数据类型

byte,short,int,long,char,float,double,boolean

按表示数据类型分类,大致可划分为数值型、字符型和布尔类型。它们具有各自不同的表征范围和底层实现。

如何实现它们的互相转换和正确存储,就成为了一个必须解决的问题。

按照日常编程的习惯,我们可以将基本数据类型之间的转换分为以下三种情况:

1.低级类型到高级类型的自动类型提升;

2.高级类型到低级类型的强制类型转换;

3.包装类的拆装箱。

本文中先抛开包装类的拆装箱问题。主要讨论基本数据类型的自动类型提升和强制类型转换。

在开始探讨类型转换之前,我们首先需要认识一个词语——符号扩展Sign Extension

 

符号扩展

在编程或者笔试题目中,常常可能遇到这样一种情况。对于一个int类型的数,将其转换为byte类型后,得到的究竟一个正数还是一个负数呢?反之,将一个byte的负数转换为int类型,我们又怎样从底层去判断它最终的符号呢?要理解这两点,我们首先需要了解计算机中数的表示,以及Java中数据的转换方式。

 

计算机中数的表示

计算机中的数都是以补码的形式存储的,最高位单独作为符号位。正数的补码、反码都等于其二进制原码。而负数的补码等于其原码除去符号位外,按位取反再加1。知道这一点后,我们便能很清楚Java中各种类型的整型数据的范围是怎么得到的了。

举个栗子。我们都知道,Java中byte类型占一字节,8比特位。那么它的取值范围为什么是-128~127呢?

首先,对于byte类型,首位需要作为符号位,若符号位为1,就代表整个数为负数,符号位为0,就代表整个数为正数。其余7位。各能表示0B000_0000~0B111_1111(0~127)这个范围内的128个数。但这就带来了一个问题:当符号位为1或0的时候,各有一个0,即+0和-0。对于数学运算来说,若0的表示有两种,就会使得运算规则复杂化。所以我们就将负0(补码1000 0000)作为一个新的负数,其表示的值,是在原来该类型所能表示的最小负数的基础上再-1所得到的值。对于byte来说,这个值为-128。

至此,我们就得到了byte的取值范围—— -128~127。(以上的过程,适用于所有整型数的范围计算,由于浮点型数据采用了另一种标准,所以其表示范围会有很大差别。)

 

有了以上的表示后,另一个问题接踵而至:如何在进行类型扩展的时候,保持括号和数字的值不变呢?

 

Java中的符号扩展(Sign Extension)

什么是符号扩展?

符号扩展,用于在数值类型扩展时扩展二进制的长度,以保证转换后的数值和原数值的符号、以及数值大小相同,一般用于较窄的类型向较宽的类型转换。扩展二进制的长度是指,在原数值的二进制位左边补齐若干个符号位(正数补0,负数补1)。

再举个栗子。

byte a = 15;

a == 0B0000_0111

若将其扩展为int类型,那么需要在其高8位补上0。

最后得到int a = 0B0000_0000_0000_0111

对于负数

byte b = -15;

b == 0B1111_1001(补码)

若将其扩展为int类型,那么需要在其高8位上补上1。

最后得到int a = 0B1111_1111_1111_1001(补码)

将其转化为原码后得

a = 0B1000_0000_0000_0111(原码)

 

由此可见,符号扩展前后数值的大小和符号都保持不变

 

自动类型提升

当数据范围小的(低级的)数据向数据范围大的(高级的)数据转换时,无需进行特殊转换。编译器将会进行自动类型提升。

在本文中我们将其分为四个部分进行介绍。

1.整型数据的自动类型提升;

2.字符型数据向整型数据的自动类型提升;

3.整型数据向浮点型数据的自动类型提升。

4.浮点型数据的自动类型提升;

 

首先我们先来介绍整型数据相互之间的自动类型提升

例一:低位正整数提升为高位正整数

byte b = 64; //二进制表示:0100 0000

short s = b; //二进制表示:0000 0000 0100 0000

从该例可以看出,低位正数转向高位正数时,s的高地址补0。

 

例二:低位负整数提升为高位负整数

byte b = -64; //二进制表示:1100 0000

short s = b; //二进制表示:1000 0000 0100 0000

注意。这里并不说明低位负数转向高位负数时,s的高地址补1。

实际上,所有的二进制整数在计算机底层都是以补码储存的。所以不管是正整数的自动类型提升还是负整数的自动类型提升,都是在对应补码前补0。由于正数的正、反、补码都一致,所以可以直接在高位补0。而负数需要转化为原码才能被我们识别。

以例二为例。

byte b = -64; //二进制原码表示:1100 0000

byte b = -64; //二进制反码表示:1011 1111 (原码除符号位外按位取反)

byte b = -64; //二进制补码表示:1100 0000 (反码+1)

short s = b; //二进制补码表示:1111 1111 1100 0000 (符号位不变高位补1)

short s = b; //二进制反码表示:1111 1111 1011 1111 (补码-1)

short s = b; //二进制反码表示:1000 0000 0100 0000 (反码除符号位外按位取反)

 

综上,byte向short提升,byte/short向int提升,byte/short/int向long提升,都遵循如下规则。

正负整型类型数据,进行自动类型提升时,对补码进行符号扩展

 

例三:char类型向高位整型提升

char c = 32767; //二进制:0111 1111 1111 1111

int a = c; //二进制:0000 0000 0000 0000 0111 1111 1111 1111

在计算机中,所有的字符在底层都存储为整型,每个具体数字通过一张码表映射为具体的字符。

在Java中,char类型字符集为unicode编码,char占 2个字节16位,可以表示0~65535这个范围中的65536个字符。

所以char是无符号的,16位都用来表示数值大小。

char类型无论被提升为int还是long,都进行零扩展(即高位补0)。

 

介绍完了整型和字符型的自动类型提升。我们再来看看整型向浮点型数据进行自动类型提升的原理。

要理解整型向浮点型的转换。首先我们需要了解浮点数在计算机中的存储方式。由于篇幅限制,这里不再赘述,不清楚的同学可以跳转我之前的《深入理解Java浮点数机制》一文简单了解。

 

例四:int类型向float进行自动类型提升

int a = 2147483647;//二进制:0111 1111 1111 1111 1111 1111 1111 1111

float f = a; //二进制:0 10011110 0000000000000000000

(注:32位浮点数float的底层存储:第1位是数符S(表示底数的符号),2~9位为阶码E,最后23位为尾数M。)

要将a存放在浮点型数据f中,我们只需要将a对应的二进制数进行正规化处理,得到

+1.1111 1111 1111 1111 1111 1111 1111 111 * 2^30

由于尾数部分超出float类型的范围,需要先依据浮点数的舍入规则进行舍入

由于第24位为1,且其后数值不全为0,故需要向第23位进位。

10.0000 0000 0000 0000 000 * 2^30

规范化得

1.0000 0000 0000 0000 000 * 2^31

然后分别得到其符号位、阶码和尾数

符号位S = 0

阶码E = 阶数 + 移码 = 31 + 127 = 158 =10011110

尾数M = 底数去除整数部分1 = 0000000000000000000

得到f的二进制表示

0 10011110 0000000000000000000

实际应该得到2.147483648E9

但由于float型只能精确表示8位数字,故在Java程序中实际输出的值为2.14748365E9

Java自动类型提升与强制类型转换机制详解

 

若是再将这个数强转为long输出,将会得到2147483648,这验证了本文上述的计算。

Java自动类型提升与强制类型转换机制详解

 

(注意不要强转为int,在同一个空间进行自动类型提升和强制类型转换,会因为编译期优化而输出原来int类型存放的数字,而不是实际存储在float中的值。)

Java自动类型提升与强制类型转换机制详解

 

其他类型的整数向浮点数的转换也同上述过程。但需要说明的是,byte/short/char转换为浮点型时,是通过int为跳板进行转换,即先转换为对应的int类型,再进一步转化为目标浮点类型。

 

通过例五我们完成了整型数据向浮点类型的转换过程的探讨。现在我们继续介绍浮点类型进行自动类型提升的过程。

 

例五:低位浮点类型向高位浮点类型的自动类型提升

float f = 1.6; //二进制:1.1001100110011001100110011001...

double d = f;

首先说明一下float向double的自动类型提升规则:

符号位 S :直接复制

阶码 E:直接复制阶数,但注意移码有所不同。应该原阶码-127+1023才是正确的阶码

尾数 M:直接复制原尾数,但由于double类型尾数为52位,所以需要在原尾数后补上29个0

 

首先我们对1.6进行正规化

f = 1.6 = 1.1001100110011001100110011001...(按浮点数舍入规则进行舍入)

=1.10011001100110011001101 * 2^23

符号位 S = 0

移码 E = 23 + 127 = 150 = 10010110

尾数 M = 10011001100110011001101

得f的二进制表示

0 10010110 10011001100110011001101

所以实际f保存的值应该是约1.6000000238418579

但由于float的精度限制,精确到小数点后七位,故实际输出1.6

当f赋值给double类型数d时,根据前文的规则,得

符号位 S = 0

移码 E = 150 - 127 +1023 = 10000010110

尾数 M = 1001100110011001100110100000000000000000000000000000

又由于double类型可以精确的表示小数点后15位,故实际输出1.600000023841858

下面我们结合程序验证上述计算是否正确。

Java自动类型提升与强制类型转换机制详解

由此可见,前文对浮点数自动类型提升的计算和推论是正确的。

 

通过以上五个例子,我们详细的介绍了自动类型提升时数据的底层转变。下面给出一张自动类型提升的方向图。自动类型提升只能严格按照以下方向进行提升。否则将会出现编译错误。

Java自动类型提升与强制类型转换机制详解

需要注意的是,boolean类型不能与任何数值型以及字符型数据类型相互转换。

学习完了自动类型提升的底层原理。我们继续研究强制类型转换时底层产生的变化。

 

强制类型转换

当数据范围大的(高级的)数据向数据范围小的(低级的)数据进行转换时,常常会出现丢失精度或者溢出的问题。如果直接进行转换,例如将一个long型的数赋值给int类型的变量,将会出现编译错误。

所以,如果我们需要进行类似这样的操作,就需要用到强制类型转换。

 

在Java中,存在两种不同类型的强制类型转换。

1.显式的强制类型转换。

2.隐式的强制类型转换。

 

显式的强制类型转换

显式的强制类型转换遵循以下格式。

TYPE a = (TYPE) B;

根据平时编程和使用的经验,笔者将显式的类型转换大概划分为三种情况。

1.高位整型、字符型向低位整型、字符型的强制类型转换;

2.浮点型向整型的强制类型转换;

3.高位浮点型向低位浮点型的强制类型转换。

 

首先我们来探讨整型与字符之间的强制类型转换。

例六:

long l = 12345678910; //二进制:0000 0000 0000 0000 0000 0000 0000 0010 1101 1111 1101 1100 0001 1100 0011 1110

int i = (int)l; //二进制:1101 1111 1101 1100 0001 1100 0011 1110 —— -539222978

char c = (char)l; //二进制:0001 1100 0011 1110 —— 7230

short s = (short)l; //二进制:0001 1100 0011 1110 —— 7230

byte b = (byte)l; //二进制:0011 1110 —— 62

通过具体代码实现,我们验证了猜测。

Java自动类型提升与强制类型转换机制详解

通过上述转换不难发现,整型向字符型的转换规则是

若目标类型的长度小于源类型的长度,则在源类型低位直接截取目标类型的长度的数据。

但在实际运算时需要注意以下两点:

1.注意计算机里存储的数据都是补码存储的,需要变为原码才能转换为十进制。

如例六中的变量i,截取l的低32位,得到了一个符号位为1的数,负数需要-1再按位取反才能得到正确的原码。

实际上i的原码应该为1010 0000 0010 0011 1110 0011 1100 0010,也即-539222978。

2.整型转化为char类型时需要特别注意,因为char类型是无符号的,所以不可能出现强制类型转换后输出为负的情况。所以同一个数强制类型转换为short和char时,可能会出现结果不同的情况(因为一个是有符号的,一个是无符号的)。

 

说完了整型、字符型的强制类型转换,下面我们探讨一下浮点型向整型的强制类型转换规则。

例七:

float f = 12345678910111213141516;

long l = (long)f;

int i = (int)f;

char c = (char)f;

short s = (short)f;

byte b = (byte)f;

由于浮点数转换为整型的规则并非计算得到,所以我们这里直接给出结果

Java自动类型提升与强制类型转换机制详解

这样的结果可能乍一看难以理解,但我们将其转为二进制,就能看出一些端倪了。

l //二进制:0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111

i //二进制:0111 1111 1111 1111 1111 1111 1111 1111

c //二进制:1111 1111 1111 1111

s //二进制:1111 1111 1111 1111

b //二进制:1111 1111

实际上浮点数强制类型转换为整型的规则就是

若不超过long或int的最大值,就省略小数部分直接赋值。

若超过long或int的最大值,那么强制类型转换的结果就是long或int对应的最大值。

 

另外,浮点型转换为byte/char/short时,需要先以int为跳板,即先转换为int。再按整型的强制类型转换规则,按目标类型长度截取低位数值。

这里我们通过例八来演示一下不超过目标整型类型最大值时的浮点型向整型的强制类型转换。

例八:

float f = 12345.125;

int i = (int)f;

首先需要将f进行正规化

f = 891011.125

= +1101 1001 1000 1000 0011 . 001

= +1.10110011000100000110010 * 2^22

符号位 S = 0

阶码 E = 10010101

尾数 M = 1011001100010000011001

f的二进制为 0 10010101 1011001100010000 110010

若要强转为int类型

则先进行对阶,得到f的整数部分

11011001100010000011

然后从低位至高位取截取32位,不够则进行符号扩展。

得到32位int类型二进制数

0000 0000 0000 1101 1001 1000 1000 0011

即891011

下面结合具体程序验证推算结果

Java自动类型提升与强制类型转换机制详解

由此可知,浮点型强制转换为整型时,若没有超出对应整型的范围,则直接截取浮点数的整数部分进行赋值。

 

最后,我们继续研究double向float的强制转换

这里分为两种情况

1.double数值在float允许范围之内

2.double数值超出float允许范围

由于float和double所能表示的范围太大,这里不再进行编码实验。直接说明这两种情况的结果。

 

对于第一种情况。float将会直接获取符号位、阶数(这里需要将原阶码-1023再+127,因为float和double的移码不相同),并根据浮点数的舍入规则,将double类型52位的尾数舍入为float类型的23位尾数。

 

对于第二种情况。首先需要说明的是,出现第二种情况往往是因为阶码部分超出表示范围,尾数只决定精度,阶码才决定范围。若出现这种情况,最后的结果将会输出Infinity或-Infinity,也即正无穷和负无穷。但这并不是数据上的无穷,只是因为浮点数存储机制带来的一种结果。

 

以上,是笔者对显式的强制类型转换机制的一些认识。接下来我们简单介绍一下Java中的隐式强制类型转换。

 

隐式的强制类型转换

隐式的强制类型转换常发生在复制表达式和有返回值的函数调用表达式中。

在赋值表达式中,如果赋值符左右两侧的操作数类型不同,则将赋值符号右操作数强制转换为赋值符号左操作数的类型,然后进行赋值。

在函数调用时,如果return后面的表达式的类型与函数返回值类型不同,则在返回值时将return后的表达式的数值强制转换为函数返回值所定义的类型,再将返回值进行返回。

需要注意的是,上述的强制转换只能当两个类型可转时才能进行。

具体转换原理同显式强制转换对应内容。