共计 4715 个字符,预计需要花费 12 分钟才能阅读完成。
我们已经知道,Java 的数据类型分两种:
- 基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
; - 引用类型:所有
class
和interface
类型。
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null
:
String s = null;
int n = null; // compile error!
那么,如何把一个基本类型视为对象(引用类型)?
比如,想要把 int
基本类型变成一个引用类型,我们可以定义一个 Integer
类,它只包含一个实例字段 int
,这样,Integer
类就可以视为 int
的包装类(Wrapper Class):
public class Integer {private int value;
public Integer(int value) {this.value = value;
}
public int intValue() {return this.value;
}
}
定义好了 Integer
类,我们就可以把 int
和Integer
互相转换:
Integer n = null;
Integer n2 = new Integer(99);
int n3 = n2.intValue();
实际上,因为包装类型非常有用,Java 核心库为每种基本类型都提供了对应的包装类型:
基本类型 | 对应的引用类型 |
---|---|
boolean | java.lang.Boolean |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
char | java.lang.Character |
我们可以直接使用,并不需要自己去定义:
// Integer:
public class Main {public static void main(String[] args) {int i = 100;
// 通过 new 操作符创建 Integer 实例(不推荐使用, 会有编译警告):
Integer n1 = new Integer(i);
// 通过静态方法 valueOf(int)创建 Integer 实例:
Integer n2 = Integer.valueOf(i);
// 通过静态方法 valueOf(String)创建 Integer 实例:
Integer n3 = Integer.valueOf("100");
System.out.println(n3.intValue());
}
}
Auto Boxing
因为 int
和Integer
可以互相转换:
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();
所以,Java 编译器可以帮助我们自动在 int
和Integer
之间转型:
Integer n = 100; // 编译器自动使用 Integer.valueOf(int)
int x = n; // 编译器自动使用 Integer.intValue()
这种直接把 int
变为 Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把 Integer
变为 int
的赋值写法,称为自动拆箱(Auto Unboxing)。
注意
自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
装箱和拆箱会影响代码的执行效率,因为编译后的 class
代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException
:
// NullPointerException
public class Main {public static void main(String[] args) {Integer n = null;
int i = n;
}
}
不变类
所有的包装类型都是不变类。我们查看 Integer
的源码可知,它的核心代码如下:
public final class Integer {private final int value;
}
因此,一旦创建了 Integer
对象,该对象就是不变的。
对两个 Integer
实例进行比较要特别注意:绝对不能用 ==
比较,因为 Integer
是引用类型,必须使用 equals()
比较:
// == or equals?
public class Main {public static void main(String[] args) {Integer x = 127;
Integer y = 127;
Integer m = 99999;
Integer n = 99999;
System.out.println("x == y:" + (x==y)); // true
System.out.println("m == n:" + (m==n)); // false
System.out.println("x.equals(y):" + x.equals(y)); // true
System.out.println("m.equals(n):" + m.equals(n)); // true
}
}
仔细观察结果的童鞋可以发现,==
比较,较小的两个相同的 Integer
返回 true
,较大的两个相同的Integer
返回 false
,这是因为Integer
是不变类,编译器把 Integer x = 127;
自动变为 Integer x = Integer.valueOf(127);
,为了节省内存,Integer.valueOf()
对于较小的数,始终返回相同的实例,因此,==
比较“恰好”为 true
,但我们 绝不能 因为 Java 标准库的 Integer
内部有缓存优化就用 ==
比较,必须用 equals()
方法比较两个Integer
。
最佳实践
按照语义编程,而不是针对特定的底层实现去“优化”。
因为 Integer.valueOf()
可能始终返回同一个 Integer
实例,因此,在我们自己创建 Integer
的时候,以下两种方法:
- 方法 1:
Integer n = new Integer(100);
- 方法 2:
Integer n = Integer.valueOf(100);
方法 2 更好,因为方法 1 总是创建新的 Integer
实例,方法 2 把内部优化留给 Integer
的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。
我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()
就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
最佳实践
创建新对象时,优先选用静态工厂方法而不是 new 操作符。
如果我们考察 Byte.valueOf()
方法的源码,可以看到,标准库返回的 Byte
实例全部是缓存实例,但调用者并不关心静态工厂方法以何种方式创建新实例还是直接返回缓存的实例。
进制转换
Integer
类本身还提供了大量方法,例如,最常用的静态方法 parseInt()
可以把字符串解析成一个整数:
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256, 因为按 16 进制解析
Integer
还可以把整数格式化为指定进制的字符串:
// Integer:
public class Main {public static void main(String[] args) {System.out.println(Integer.toString(100)); // "100", 表示为 10 进制
System.out.println(Integer.toString(100, 36)); // "2s", 表示为 36 进制
System.out.println(Integer.toHexString(100)); // "64", 表示为 16 进制
System.out.println(Integer.toOctalString(100)); // "144", 表示为 8 进制
System.out.println(Integer.toBinaryString(100)); // "1100100", 表示为 2 进制
}
}
注意:上述方法的输出都是 String
,在计算机内存中,只用二进制表示,不存在十进制或十六进制的表示方法。int n = 100
在内存中总是以 4 字节的二进制表示:
┌────────┬────────┬────────┬────────┐
│00000000│00000000│00000000│01100100│
└────────┴────────┴────────┴────────┘
我们经常使用的 System.out.println(n);
是依靠核心库自动把整数格式化为 10 进制输出并显示在屏幕上,使用 Integer.toHexString(n)
则通过核心库自动把整数格式化为 16 进制。
这里我们注意到程序设计的一个重要原则:数据的存储和显示要分离。
Java 的包装类型还定义了一些有用的静态变量
// boolean 只有两个值 true/false,其包装类型只需要引用 Boolean 提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;
// int 可表示的最大 / 最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
// long 类型占用的 bit 和 byte 数量:
int sizeOfLong = Long.SIZE; // 64 (bits)
int bytesOfLong = Long.BYTES; // 8 (bytes)
最后,所有的整数和浮点数的包装类型都继承自Number
,因此,可以非常方便地直接通过包装类型获取各种基本类型:
// 向上转型为 Number:
Number num = new Integer(999);
// 获取 byte, int, long, float, double:
byte b = num.byteValue();
int n = num.intValue();
long ln = num.longValue();
float f = num.floatValue();
double d = num.doubleValue();
处理无符号整型
在 Java 中,并没有无符号整型(Unsigned)的基本数据类型。byte
、short
、int
和 long
都是带符号整型,最高位是符号位。而 C 语言则提供了 CPU 支持的全部数据类型,包括无符号整型。无符号整型和有符号整型的转换在 Java 中就需要借助包装类型的静态方法完成。
例如,byte 是有符号整型,范围是 -128
~+127
,但如果把byte
看作无符号整型,它的范围就是 0
~255
。我们把一个负的byte
按无符号整型转换为int
:
// Byte
public class Main {public static void main(String[] args) {byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
}
}
因为 byte
的-1
的二进制表示是 11111111
,以无符号整型转换后的int
就是255
。
类似的,可以把一个 short
按 unsigned 转换为 int
,把一个int
按 unsigned 转换为long
。
小结
Java 核心库提供的包装类型可以把基本类型包装为class
;
自动装箱和自动拆箱都是在编译期完成的(JDK>=1.5);
装箱和拆箱会影响执行效率,且拆箱时可能发生NullPointerException
;
包装类型的比较必须使用equals()
;
整数和浮点数的包装类型都继承自Number
;
包装类型提供了大量实用方法。