一、背景
JAVA基本数据类型几乎每天都会用到,但是你真的了解其在计算机中是如何表示的吗?每一种基本数据类型的取值范围究竟是如何计算得到的?为什么float型所占字节数为4,但比同样占4个字节的int型的取值范围更为广阔?为什么float型和double型变量会有精度丢失的问题?这些问题都值得我们去细细探究一下。
二、JAVA基本数据类型
基本数据类型 |
最小值 |
最大值 |
所占字节 |
byte |
2^7=-128 |
2^7-1=127 |
1 |
short |
2^15=-32768 |
2^15-1=32767 |
2 |
int |
2^31=0x80000000 |
2^31-1=0x7fffffff |
4 |
long |
2^63 |
2^63-1 |
8 |
float |
|
|
4 |
double |
|
|
8 |
char |
0 |
2^16-1=65535 |
2 |
boolean |
|
|
1 |
这八种基本类型可以分为以下几类:
1. boolean型:boolean
2. 数值型:
A. 整数:按取值范围由小到大排列,byte,short,int,long
B. 小数:按取值范围由小到大排列,float,double
三、数据在计算机中的存储结构
任何数据,在计算机中都是按照二进制进行存储的。基于此,我们可以根据不同基本类型所占字节数的多少,来计算得出其取值范围。
此外,数据有原码、反码、补码三种形式。
原码顾名思义,不再赘述。
反码就是原码按位取反,例如,原码为10101010,反码为01010101。
补码为原码取反码+1,例如,原码为10101010,补码为01010110。
说了这么多,本质上是为了说明,正数在计算机中用原码表示,而负数用其补码表示(第一位符号位为1,标识负数)。
这也就能解释为什么占用4个字节的int型变量的取值范围是-2^31 -- 2^31-1。这里先卖个关子,下边章节中进行具体计算。
四、基本数据类型的取值范围计算
4.1 boolean型
boolean型就不加以赘述了,只有true和false两种取值。
4.2 数值型
数值型又分为整数型和小数型两大类。
4.2.1 整数型
我们以int型为例。int型占用4个字节,共32位。第一位为符号位,0-正数,1-负数。
因此,最大的正数为0x7fffffff,即除了高位为0外,其他位均为1,对应十进制数为2^31-1;
最小的负数为0x80000000,其最高位为1,绝对值为0x80000000的补码即80000000。对应十进制数为2^31。
同理,可以得到short型和long型的取值范围,所不同的均为所占用字节数不同。
4.2.2 小数型:
小数型的数据存储方式与整数型不同,因此不能完全套用上文中提到的整数型的方式来进行取值范围计算。
小数型数据的存储结构分为符号位+指数位+尾数位。
符号位决定该小数是正数还是负数,指数位决定了其取值范围,而尾数位决定了其精度,即保留小数点后多少位。
float型的存储方式如下:
符号位 1位 |
指数位 8位 |
尾数位 23位 |
double型的存储方式:
符号位 1位 |
指数位 11位 |
尾数位 52位 |
从二者的尾数位的不同上看,你就可以理解为什么说double是双精度浮点数,其精度为什么要高于float。
由于8个指数位决定了其取值范围,且表征的是以2为底的指数,因此其取值范围要远远大于同样是8个位的整数。
五、基本数据类型的包装类及自动装拆箱
上文中说到了8种基本数据类型,在JAVA中,还有8种与之对应的包装类。具体关联关系如下表:
基本数据类型 |
包装类 |
byte |
Byte |
char |
Character |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
boolean |
Boolean |
那么很多人看到这里要问,既然JAVA有了这些基本类型,为什么还要引入包装类的概念呢?为什么要引入自动拆装箱呢?原因很简单,那就是JAVA是面向对象的语言,很多地方要用到的不是基本数据类型,而是其包装类。如果没有自动拆装箱,势必涉及从基本数据类型创建其包装类的实例,代码不够简洁。
例如,如果我们不使用自动拆装箱,那么就不可避免地引入:Integer i = new Integer(1);
但是如果我们借助于自动拆装箱,可以使用:Integer i = 1;相比较而言,简洁了很多。
自动拆装箱是由JAVA编译器提供的功能,以int与Integer的互转为例,可以通过调用Integer.valueOf实现int到Integer的转换,实现装箱;通过调用Integer.intValue实现Integer到int的转换,即拆箱。这一切都是由JAVA编译器提供的,对开发者完全透明。
六、类型间的转换
1. 将取值范围小的类型向取值范围大的类型转换,必须使用强制类型转换,有可能会损失精度或数据溢出
例如将double型向float型转换,极有可能会损失精度。
2. 浮点数到整数的转换其实是舍弃小数部分,而不是四舍五入;
3. 包装类有帮助进行类型转换的方法,例如Integer.longValue, shortValue,floatValue,doubleValue。
七、==与equals方法
基本数据类型比较是否相等时,常用==进行比较。
比如:
int i = 0;
int j = 0;
System.out.println(i == j); // true
但是基本数据类型的包装类的实例其实是个对象,对象间==的语义是两者是否是同一个引用。因此不能简单用==来进行比较。
Double i = 1.0;
Double j = 1.0;
System.out.println(i==j); // false
我们之所以没有用Integer来举例,是因为Integer.valueOf方法中对-128--127之间的数进行了cache,因此valueOf函数的返回值是同一个引用。
而equals方法的出现就是用来解决上述问题的,即两个同等类型的包装类的对象是否在取值上相等。
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
首先判断是否为同类型,如果不同,则直接就是不相等。如果是同类型,则取intValue,看是否在取值上相等。
个人认为equals方法算是JAVA中比较重要的方法,其最常见的使用场景是在HashMap.get(Object key)中。通过key的hash值找到HashMap中对应该hash值的链表,然后遍历,通过key.equals(链表中取到的key),来最终定位到期望的key,之后取出对应的value。
最容易出错的场景就是在很多初学者非常容易忘记equals方法的实现,尤其是实现中首先要判断是否为同类型。即使语义上的取值相同,但类型不同,也会被equals方法判定为false,从而在HashMap中获取不到期望的key-value。