Java 中有八种内置的基本数据类型,他们分别是 byte、short、int、long、float、double、char 和 boolean,其中,byte、short、int 和 long 都是用来表示整数,float 和 double 是用来表示浮点数的,那它们之间有什么区别和联系呢?除了这八种基本类型(primitive type)外,其余的类型都是引用类型(reference type)。但是包装类型(wrapper class) Integer 和 Long 也可以用来表示整数,那它们与 int 和 long 又有什么区别和联系呢?这八种基本类型之间的运算关系又是怎样的呢?
一、基本类型
基本类型的表示范围
八种基本类型中,char 类型是用来表示字符的,boolean 类型是用来表示布尔值的,除此之外的 6 种都是用来表示数字的。byte、short、int 和 long 是用来表示整数的,float 和 double 是用来表示浮点数的。下面是每一种类型能表示的范围:
定义变量时超过对应数据类型最大的表示范围的话将报错。位数越大,单个该数据类型的变量所占用的内存就越多。
每一种类型的最大最小值都保存在了其对应包装类型的属性中,如 Integer.MAX_VALUE 的值为 2147483647,关于包装类型,下面会做详细说明。
类型的默认值
每一种类型都有一个默认值,除基本类型外,其他的类型的默认值都是 null,因为它们都是引用类型。整数默认为 int 类型,浮点数默认为 double 类型。
当我们定义静态的基本类型变量时,它们将采用默认值:
public class Test {
static byte b;
static short s;
static int i;
static long l;
static float f;
static double d;
static char c;
static boolean bool;
static String str;
public static void main(String[] args) {
System.out.println("byte " + b); // byte 0
System.out.println("short " + s); // short 0
System.out.println("long " + l); // long 0
System.out.println("int " + i); // int 0
System.out.println("float " + f); // float 0.0
System.out.println("double " + d); // double 0.0
System.out.println("char " + c); // char '\u0000' (这个显示不出来)
System.out.println("boolean " + bool); // boolean false
System.out.println("String " + str); // String null
}
}
类型之间的转换
类型之间的转换有自动类型转换和强制类型转换两种。自动类型转换也称隐式转换,是编译器自动进行的,强制类型转换称为显式转换,是写代码时显式地完成的。
自动类型转换(隐式转换)
在八种基本数据类型中,除开 boolean 类型,其他的七种都是可以互相进行运算的(如加减法运算)。不同的类型在进行运算的时候,低位的数据类型会向高位的转化,最后的计算结果将是整个运算过程中数据类型位数最大的,这就是自动类型转换。
int a = 1;
long b = 2;
int c = a + b; // Error
long c = a + b; // Right
但要注意一点,比 int 类型数据小的数据类型在做运算的时候,会转换成 int 类型然后再做运算,尽管计算结果能被 byte 或 short 类型表示:
short c = 1, d = 2;
short e = c + d; // Error
int e = c + d; // Right
自动类型转换的规则可以总结为如下:
若有一个变量是 double 类型,则另一个也会被隐式转换为 double 类型;否则,若有一个变量是 float 类型,则另一个也会被隐式转换为 float 类型;否则,若有一个变量是 long 类型,则另一个也会被隐式转换为 long 类型;否则,若有一个变量是 int 类型,则另一个也会被隐式转换为 int 类型。特别的,boolean 类型无法转换(包括自动类型转换和强制类型转换)
即,自动类型转换优先级为:double > float > long > int > short = char = byte (boolean 不可转换)
注意:在 short、byte 和 char 之间不会发生自动类型转换,它们运算时都会先转换成 int 类型,结果最终也是 int 类型,所以前面说,比 int 还小的类型在运算时会被自动转换为 int 类型。
强制类型转换(显式转换)
强制类型转换就是在程序中显式地将一个变量从某一类型强制转换成另一类型。从小位类型到大位类型的转换是向上转换,一般这是没有问题的,但反过来,从大位类型转换到小位类型,即向下转换,是有可能发生数据溢出或者精度损失的。
int a = 32768;
float b = 3.1415926f;
System.out.println((long)a); // Right
// Output: 1
System.out.println((short)a); // overflow
// Output: -32768
System.out.println((int)b); // loss of accuracy
// Output: 3
数据溢出和精度损失
这里关于数据溢出和精度损失做一些简单的说明。
数据溢出
数据溢出之后变量也还是有值的,程序也不会报错,那溢出后的值是如何计算出来的?这实际上和数据存储的规则有关,涉及到原码、反码和补码等知识,这里不会详细说明这个规则,我们只需要一张图便能知道溢出后的值是如何计算得到的。
举个例子,int 类型变量值为 131073,在强制类型转换为 short 类型后值就变为了 1。我们可以这样理解,上面的图是 short 类型变量所有的取值,从 0 开始,131073 一个一个去对应,到 32768 时发生数据溢出,变成了 -32768,继续顺时针旋转去对应每个数,最会对应两圈多两个数,然后 131073 对应到第三圈的第二个数字 1,因此,强制类型转换数据溢出后的结果为 1。
注意:131073 = 65536 * 2 + 1
int a = 131073;
System.out.println((short)a); // Output: 1
精度损失
在数据存储长度比较长的数据转换为数据存储长度较短的类型时,就可能会发生精度损失,因为要把长数据变成短数据只能将数据后面多余的部分截断才行。因为转换方式采用的是截断多余的,因此我们可以认为当浮点型数据转换为整型数据时是做了一个向零取整的操作(直接舍去小数)。
float a = 3.14f, b = -3.14f;
System.out.println((int)a); // Output: 3
System.out.println((int)b); // Output: -3
隐含强制类型转换
除了前面提到的两种,还有一种隐含的强制类型转换,一个整数数字(int 表示范围之内的)会被隐含地转换成 int 类型,一个浮点数字会被隐含地转换成 double 类型。这也解释了为什么前面说整数默认是 int 类型,浮点数的默认类型是 double 类型了。所以,以后在用 float 类型时一定要在后面加上字母 f 或 F,long 类型后面要加上字母 l 或者 L。
二、包装类型
基本数据类型并不是对象,但为了编程的方便,Java 还是引入了它们,也就是前面提到的八个基本类型,但要将其作为对象来操作的话,还是要用这八个基本类型对应的包装类型(wrapper class)。这个包装类型就是 Java 对基本类型的封装,使其能作为对象使用,它们与其他的类型并没有什么太多的区别,都是引用类型(reference type)。
在 Java5 的时候引入了自动装箱/拆箱机制,使得基本类型和对应的包装类型之间可以相互转换。
基本类型和包装类型的区别
基本类型和包装类型在使用时所表示的值是一样的,表示的范围也是对应的,当 int 类型在转换为 Integer 类型的时候,实际上是调用了 Integer 的 valueOf 方法:
Integer a = 1;
Integer a = Integer.valueOf(1);
// the two are equivalent
下面我们列出基本类型和包装类型的一些区别:
- 基本类型不是对象,而包装类型是对象(有属性和方法);
- 基本类型不需要实例化,而包装类型需要实例化;
- 基本类型是直接存储的值,而包装类型是对实例对象的引用;
- 基本类型有特殊的默认值(如 int 的默认值是 0),而包装类型都是 null;
- 基本类型所占用的内存比较少,包装类型占用内存较大;
- 同一基本类型相同值的不同变量的内存地址一样,但实例化得到的同一包装类型相同值的不同变量的内存地址不一样;
- 基本类型的运算是直接进行的,而包装类型的运算要先拆箱为基本类型才能进行运算。
对象有属性和方法,前面提到的 Integer.MAX_VALUE 就是包装类型的一个属性,上面提到的 Integer.valueOf() 就是一个方法,而基本类型 int 是没有这些的。
基本类型的值都是直接储存的,所以同一个基本类型、相同值的不同变量的地址是一样的:
int a = 1, b = 1;
System.out.println(a == b); // Output: true
包装类型都是对象实例化的引用,一般每个对象都拥有不同的内存空间,它们的内存地址也就不一样:
Integer a = new Integer(100);
Integer b = new Integer(100);
System.out.println(a == b); // Output: false
valueOf 方法的缓存机制
如果一个 Integer 实例对象是由基本类型转换而来的,值处于 -128 ~ 127 之间时,之前的代码输出结果就为 true 。是因为转换时调用的 valueOf 方法有一个缓存机制。 这个判断范围 -128 ~ 127 是默认的,可以在 JVM 提供的配置 (-XX:AutoBoxCacheMax) 进行修改。
为什么要有这个缓存机制?这是因为处于这个区间的值比较小,而且又比较常用,因此编译器就提前将其缓存了,需要时就直接从缓存里拿,既能提高速度,又能在同样值的包装类型较多的时候节省内存。所以,两个值在 -128 ~ 127 之间,且由基本类型 int 转换而来的 Integer 就是同一个对象。这对于包装类型 Byte、Short 和 Long 也是一样的。
Integer a = 100, b = 100;
System.out.println(a == b); // Output: true
Integer c = 128, d = 128;
System.out.println(c == d); // Output: false
与此类似的还有包装类型 Boolean,它一共就只有两种可能,true 和 false,因此也被提前缓存了。包装类型 Character 在 '\u0000' ~ '\u007F' 之间的值(0 ~ 127)也是被提前缓存了。不过包装类型 Float 和 Double 的 valueOf 方法就没有这个缓存机制了。
基本类型和包装类型的联系
为什么包装类型叫做“包装类型”呢?顾名思义,就是对基本类型的一个封装,从名字里我们就能知道,基本类型和包装类型之间的实际上有个装箱与拆箱的关系。
直接由基本类型 int 转化而来的 Integer 对象是对 int 类型的装箱操作,而由 Integer 对象转换来的 int 类型变量就是对 Integer 对象的拆箱操作。
Integer a = 1; // 装箱
Integer a = Integer.valueOf(1); // 等价形式
int b = a; // 拆箱
int b = a.intValue(); // 等价形式
实例化生成的 Integer 类型变量与基本类型转化而来的 integer 类型变量尽管值相同,但内存地址并不相同(非 new 生成的 Integer 对象指向 Java 常量池中的对象),不是同一个对象,所以在比较的时候输出 false。但是当二者与基本类型相同值的变量去比较时却都输出 true。
Integer a = 1;
Integer b = new Integer(1);
System.out.println(a == b); // Output: false
int c = 1;
System.out.println(c == a); // Output: true
System.out.println(c == b); // Output: true
这是因为包装类型 Integer 与基本类型在比较的时候会自动拆箱为 int 类型,然后再去比较,这样所得到的结果就是一样的,因此输出 true。