前言
如果有了解过面向对象编程语言发展史的话,知道有一个语言叫Smalltalk.它的影响力很大,以至于后面的面向对象编程语言,或多或少的都借鉴了它的设计思想和实现。
image.png在Smalltalk中,所有的值都是对象。所以,很多人认为它是一门纯粹的面向对象语言。
Java则不同,它引入了8个基本类型,来支持数值计算。Java这么做的原因主要是基于工程上考虑,因为使用基本类型可以在执行效率及内存占用两个方面提供软件性能。
下面,看一下基本类型在Java中的实现:
/**
* @Author: 王琦
*/
public class JavaBasicTypesTest {
public static void main(String[] args) {
boolean 帅吗 = 2;
if(帅吗){
System.out.println("帅!");
}
if (true == 帅吗){
System.out.println("真的帅!");
}
}
}
上面这段代码我将一个boolean类型的变量赋值为2。假如这个变量名被定义为“帅吗”。
赋值语句的后面,紧跟着我定义了两个看似一样的if语句。第一个语句就是直接判断“帅吗”,当条件满足的情况下会打印“帅!”。
第二个if语句也就是判断“帅吗”与true是否相等,相等则打印“真的帅!”。
当然直接编译这段代码编译器会报错。所以我绕开编译器,采用字节码的汇编工具(AsmTools或者ASM等都可以直接修改字节码的Java库),直接对字节码进行更改。
那么问题来了:当一个boolean类型的变量值为2时,它究竟是True还是False?
但实际情况是,问虚拟机"帅吗",它会回答“帅!”。而问虚拟机 真 == 帅吗,它不会回答“真的帅!”。
那么虚拟机到底帅不帅?我们来分析下背后的细节。
Java虚拟机的boolean
首先,我们来看看Java语言规范及Java虚拟机规范是怎么定义boolean的?
在Java语言规范中,boolean类型的值只有两种表示方式:true/false。显然这两个符号是不能直接被虚拟机使用的。
在Java虚拟机中boolean类型则被直接映射为int类型。具体来说,“true”被映射为整数1,“false”被映射为整数0.这个规范约束了Java字节码的具体实现。
举个例子,对于存储boolean类型的数组,Java虚拟机需保证存入的值是整数1,或者整数0.
Java虚拟机规范同时也要求Java 编译器遵守这个编码规则,并且用整数相关的字节码来实现逻辑运算,以及基于boolean类型的条件跳转。这样一来,在编译后的class文件中,出来字段和传入参数外,基本看不成boolean类型的痕迹。
# JavaBasicTypesTest .main 编译后的字节码
0: iconst_2 // 我们用 AsmTools 更改了这一指令
1: istore_1
2: iload_1
3: ifeq 14 // 第一个 if 语句,即操作数栈上数值为 0 时跳转
6: getstatic java.lang.System.out
9: ldc " 帅!"
11: invokevirtual java.io.PrintStream.println
14: iload_1
15: iconst_1
16: if_icmpne 27 // 第二个 if 语句,即操作数栈上两个数值不相同时跳转
19: getstatic java.lang.System.out
22: ldc " 真的帅!"
24: invokevirtual java.io.PrintStream.println
27: return
在上述的例子中,第一个if语句会被编译成条件跳转字节码ifeq(也就是第3行对应的字节码),翻译为我们能理解的:如果局部变量“帅吗”对应的值为0,那么跳过打印“帅!”的语句。
而第2个if语句会被编译成条件跳转字节码if_icmpne (对应第16行的字节码指令)。意思是如果局部变量“帅吗”的值与整数1不相等,则跳过打印“真的帅!”语句。可以看出,Java编译器的确遵守了相同的编码规则。
对于JVM来说,它看到的boolean类型对应的值早已被映射为整数类型。因此,原本声明为boolean类型的局部变量,值除了0,1之外的整数值,在JVM看来是“合法”的(这里说的是类型上是合法的)。
Java的基本类型
除了上面的boolean类型外,我们都知道Java的基本类型还包括:byte、char、short、int、long以及浮点类型float和double。
image.pngJava的基本类型都有对应的值域和默认值。可以看到byte、short、int、long、float、double他们对应的值域一次在扩大。而且前面的值域被后面的值域所包含。因此从前面的基本类型转换为后面的类型时,无需强制类型转化。 另外一点需要注意的是,尽管他们的默认值看上去都长的不一样,但是他们在内存中的值都是0.
在这些基本类型中,boolean和char是仅有的两个无符号类型。在不违反规约的情况下boolean类型的取值是0或1,char类型的取值是[0, 65535]。通常我们认为char的类型的值为非负数,这个特性非常有用,比如说作为数组的索引等。
Java的浮点类型采用了IEEE 754的浮点数格式。以float类型为例,浮点类型通常有两个0:+0.0f 及 -0.0f。前者在java里面为0,后者为符号数为1,其他位均为0的浮点数,在内存中等同于16进制整数 0x8000000(及 -0.0f可通过Float.intBitsToFloat(0x8000000)求得),尽管他们的内存数值不同,但是在java中 if(+0.0f == -0.0f)会返回真。
有了+0.0f 和 -0.0f这两个定义后,我们就可以定义浮点数中的正无穷及负无穷。负无穷就是任意整浮点数除以+0.0f得到的值。负无穷则为任意正浮点数除以-0.0f得到的值。在Java里面正无穷和负无穷有确切的值,在内存中分别等同于十六进制整数0x7F800000 和 0xFF800000。
问题又来了:既然0x7F800000已经表示正无穷了,那0x7F800001又是什么玩意呢?0x7F800002呢……
0x7F800001这个整数对应的浮点数为NaN(Not-a-Number)
不仅如此,[0x7F800001, 0x7FFFFFFF]和 [0xFF800001, 0xFFFFFFFF] 对应的都是NaN。当然一般我们计算得出的NaN,在内存中对应的应该是0x7F800000,这个值被称为标准的NaN,其他值称为非标准的NaN。
NaN有个非常有趣的特性,出来“!=”会返回true,其他比较始终返回false。因此,我们在程序中作浮点数比较的时候,需要考虑这个特性。
Java基本类型的大小
上篇笔记中有提到,在Java虚拟机中每调用一个Java方法,便会创建一个栈帧。为了方便理解,这里只以供解释器使用的解释栈帧来看这个问题。
这种栈帧由两部分组成:局部变量区 及 字节码的操作数栈。这里的局部变量是广义上的,除了普遍意义下的局部变量之外,它还包括实例方法的“this”指针以及方法所接受的参数。
在JVM规范中,局部变量区等价于一个数组,并且可以用正整数来索引。除了long和double类型的值需要两个数组单元来存储之外,其他基本类型以及引用类型的值只需要一个数组存储就够了。也就是说,byte、short、char、boolean在栈上占用的空间跟int是一样的。因此在32为的HotSpot中这些类型在栈上各占用4个字节,而在64为的HotSpot中个占用8个字节。
当然,这种情况仅限于局部变量,并不会出现在堆上的字段及数组元素上。即byte、short、char还是个占用1个字节、2个字节、2个字节,也就是说跟类型的值域相对应。
boolean类型和boolean类型的数组比较特殊。在HotSpot中boolean类型占用1个字节,boolean数组则直接用byte数组来实现。为了保证堆中的boolean值是合法的,HotSpot在存储是显示的进行掩码操作,也就是说,只取最后一位的值存入boolean字段或者数组中。
网友评论