重新认识Java——基本类型和包装类

Java一种静态编程语言,所有变量和表达式是在编译时就确定的。同时,Java又是一种强类型语言,所有的变量和表达式都有具体的类型,并且每种类型是严格定义的。类型限制了变量可以hold什么样的值,表达式最终会产生什么样的值,可以进行哪些操作。在Java中共有8中基本类型数据,同时每种基本类型又有对应的包装类。本文将介绍Java中的基本数据类型及其包装类,主要内容如下:

  1. 介绍Java中的基本类型及其大小
  2. 基本类型之间的转换
  3. 包装类

基本类型

Java共有8种基本数据类型,它们分别如下表所示:

基本数据类型 类型
byte 数值型
short 数值型
int 数值型
long 数值型
float 数值型
double 数值型
char 字符型
boolean 布尔型

占用空间大小

在介绍基本类型时,先将一个概念——字面量值(literal)。所谓字面量值,就是指表面上的值,譬如5、200等值是整型字面量值,如果想要long型字面量值,在整型字面量值后面加上l或者L(推荐L)。整型字面量值可以由多种不同的表达方式,如16进制值(以0X或者0x开头)、10进制、8进制值(以0开头)和2进制值(以0B或者0b开头,JDK1.7以上版本才有)。

byte、short、int、long类型变量都可以赋予整型字面量值,譬如byte a = 10、short b = 0x45都是合法的赋值操作。Java编译器在编译是会检查字面量值所表示的数字大小是否处于变量类型的合法范围内,如果不在,则无法通过编译。如果多种数值类型的数据进行数学运算时,计算结果的类型是其中这些数值中最高等级或者其更高等级类型。

基本类型的占用空间大小

  • byte(字节)用无符号的8位表示,它的取值范围是[-2^7, 2^7-1]。它是最小的整型类型,通常用于网络传输、文件或者其他I/O数据流。默认值是0。
  • short(短整型)用有符号的16位表示,它的取值范围是[-2^15, 2^15-1]。从日常的观察来看,short类型可能是最不常用的类型了。完全可以用int来替代它,因为我们通常不需要过多地担心内存容量问题。默认值是0。
  • int(整型)用有符号的32位表示,它的取值范围是[-2^31, 2^31-1],计算机中用存放的是整型数值的二进制补码。默认值是0。
  • long(长整型)用有符号的64位表示,它的取值范围是[-2^63, 2^63-1]。它的字面量表示以l或者L结束,如 long a = 45454L。默认值是0L。
  • float(单精度浮点型)用32位表示,遵循IEEE 754规范。如果数值精度要求不高时,可以使用这种类型。float类型字面量值通常以f或者F结束。由于整型可以自动转换为float类型,所以,我们也可以将整型字面量值直接赋予float类型变量。默认值是0F。
  • double(双精度浮点型)用64位表示,遵循IEEE 754规范。它能表示比float更高精度的数值。double是Java基本类型中能达到的最高精度,如果还不能满足要求,可以使用Java中的BigDecimal类。默认值是0.0。
  • char(字符)用无符号的16位表示,它的取值范围是[0, 2^16-1]。Java中使用Unicode字符集来表示字符,Unicode将人类语言的所有已知字符映射成16位数字,所以Java中的char是16位的。默认值是\u00000。
  • boolean( 布尔型)只要true和false两个字面量值,可用于逻辑判断。boolean只能表示1位的信息量,但是它的大小并没有精确地定义。

基本类型的运算和相互转换

基本类型运算

boolean类型数据可以进行逻辑运算(&&、||、!),其他的基本类型都可以进行数值计算(+、-、*、/、%等)。逻辑运算比较简单易懂,完全与逻辑数学的规则一致。而数值运算涉及到运算后的结果的类型问题,稍微比较复杂一点。一般来说,运算最终结果的类型与表达式中的最大(占用空间最大)的类型。

1
2
3
4
long l = 1 + 2L; // 与1L的类型一致
int i = 1 + 2L; // 编译不通过
float f = 1 + 2 + 1.2f; // 与1.2f的类型一致
double d = 1 + 2 + 1.2; // 与1.2的类型一致

如果两种相同的类型的数据进行运算,按理来说,运算结果应该还是那个类型。但事实上,bytecharshort等类型是满足这个结论的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 编译不通过,编辑器报:Type mismatch: cannot convert from int to byte 。
byte s1 = 1;
byte s2 = 1;
byte s = s1 + s2;
// 编译不通过,编辑器报:Type mismatch: cannot convert from int to char 。
char s1 = 1;
char s2 = 1;
char s = s1 + s2;
// 编译不通过,编辑器报:Type mismatch: cannot convert from int to short 。
short s1 = 1;
short s2 = 1;
short s = s1 + s2;

从字面上来看,1+1=2绝对没有超过这个类型的范围。下面的例子都可以编译通过,

1
2
3
byte s1 = 1 + 1;
char s2 = 1 + 1;
short s3 = 1 + 1;

这是因为Java中的数值运算最低要求是int类型,如果参与运算的变量类型都没有超过int类型,则它们都会被自动升级为int类型再进行运算,所以它们运算后的结果类型也是int类型。这种方式所得到结果是否超过了对应类型所表示的范围只能在运行时才能确定,在编译时是无法知晓的。而编译器会直接将byte s1 = 1 + 1编译成byte s1 = 2,这个表达式在编译器就可以确定是合法表达式,故可以通过编译。可以通过字节码来进行佐证。

1
short s = 1 + 1;

上面伪代码所对应的字节码如下,iconst_2表示直接生成常量,然后赋值给s变量。

1
2
3
4
5
Code:
stack=1, locals=2, args_size=1
0: iconst_2
1: istore_1
2: return

类型转换

Java中除了boolean类型之外,其他7中类型相互之间可以进行转换。转换分为自动转换和强制转换。对于自动转换(隐式),无需任何操作,而强制类型转换需要显式转换,即使用转换操作符(type)。7种类型按照其占用空间大小进行排序:

byte <(short=char)< int < long < float < double

类型转换的总则是:小可直接转大、大转小会失去精度。这句话的意思是较小的类型直接转换成较大的类型,没有任何印象;而较大的类型也可以转换为较小的类型,但是会失去精度。他们之间的转换都不会抛出任何运行时异常。小转大是Java帮我们自动进行转换的,与正常的赋值操作完全一样;大转小需要进行强制转换操作,其语法是target-type var =(target-type) value

1
2
3
4
5
6
7
8
// 自动转换
long l = 10;
double d = 10;
float = 10;
// 强制转换
int a = (int) 1.0;
char c = (char) a;

值得注意是,大转小是一个很不安全的动作,可能导致莫名其妙的错误。譬如在下面的代码中,1111111111111L强转成int类型后,其值(-1285418553)与转换前的值相差巨大。这是由于在进行强制转换时,在二进制层面上直接截断,导致结果“面目全非”。

包装类

Java中每一种基本类型都会对应一个唯一的包装类,基本类型与其包装类都可以通过包装类中的静态或者成员方法进行转换。每种基本类型及其包装类的对应关系如下,值得注意的是,所有的包装类都是final修饰的,也就是它们都是无法被继承和重写的。

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

包装类与基本类型的转换

从源代码的角度来看,基础类型和包装类型都可以通过赋值语法赋值给对立的变量类型,如下面的代码所示。

1
2
Integer a = 1;
int a = new Integer(1);

这种语法是可以通过编译的。但是,Java作为一种强类型的语言,对象直接赋值给引用类型变量,而基础数据只能赋值给基本类型变量,这个是毫无异议的。那么基本类型和包装类型为什么可以直接相互赋值呢?这其实是Java中的一种“语法糖”。“语法糖”是指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会(来自百度百科)。换句话说,这其实是一种障眼法,那么实际上是怎么样的呢?下面是Integer a = 1;语句编译的字节码。

1
2
3
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1

首先,生成一个常量1,然后调用Integer.valueOf(int)方法返回Integer对象,最后将对象的地址(引用)赋值给变量a。Integer a = 1;其实相当于Integer a = Integer.valueOf(1);

其他的包装类都是类似的,下表是所有包装类中的类型转换方法。

包装类 包装类转基本类型 基本类型转包装类
Byte Byte.valueOf(byte) byteInstance.byteValue()
Short Short.valueOf(short) shortInstance.shortValue()
Integer Integer.valueOf(int) integerInstance.intValue()
Long Long.valueOf(long) longInstance.longValue()
Float Float.valueOf(float) floatInstance.floatValue()
Double Double.valueOf(double) doubleInstance.doubleValue()
Character Character.valueOf(char) charInstance.charValue()
boolean Boolean.valueOf(booleann) booleanInstance.booleanValue()

“神奇”的包装类

如果不了解包装类中的一些机制,我们有时会碰到一些莫名其妙的问题,丈二和尚——摸不着头脑。

“莫名其妙”的NullPointException

在笔者开发经历中,碰到过不少因为请求参数或者接口定义字段设置为int(或者其他基本类型)而导致NullPointException。代码大致地运行步骤如下所示,当然不会跟这个完全一样。

1
2
3
Integer a = null;
...
int b = a; // 抛出NullPointException

上面的代码可以编译通过,但是会抛出空指针异常(NullPointException)。前面已经说过了,int b = a实际上是int b = a.intValue(),由于a的引用值为null,在空对象上调用方法就会抛出NullPointException。

两个包装类引用相等性

在Java中,“==”符号判断的内存地址所对应的值得相等性,具体来说,基本类型判断值是否相等,引用类型判断其指向的地址是否相等。看看下面的代码,两种类似的代码逻辑,但是得到截然不用的结果。

1
2
3
4
5
6
7
Integer a1 = 1;
Integer a2 = 1;
System.out.println(a1 == a2); // true
Integer b1 = 222;
Integer b2 = 222;
System.out.println(b1 == b2); // false

这个必须从源代码中才能找到答案。Integer类中的valueOf()方法的源代码如下:

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) // 判断实参是否在可缓存范围内,默认为[-128, 127]
return IntegerCache.cache[i + (-IntegerCache.low)]; // 如果在,则取出初始化的Integer对象
return new Integer(i); // 如果不在,则创建一个新的Integer对象
}

由于1属于[-128, 127]集合范围内,所以valueOf()每次都会取出同一个Integer对象,故第一个“==”判断结果为true;而222不属于[-128, 127]集合范围内,所以valueOf()每次都会创建一个新的Integer对象,由于两个新创建的对象的地址不一样,故第一个“==”判断结果为false。


本文由xialei原创,转载请说明出处http://hinylover.space/2016/06/16/relearn-java-base-type-and-wrapper/