概述
为什么需要包装类
Java 提供了两个类型系统,基本数据类型
与引用数据类型
。使用基本数据类型效率高,然而当要使用只针对对象设计的 API 或新特性(例如泛型),怎么办呢?例如:
1
2
3
4
5
6
7
8
9
10
|
// 情况 1:方法形参
Object 类的 equals(Object obj)
// 情况 2:方法形参
ArrayList<T> 类的 add(Object obj)
// ArrayList<T> 中没有如下的方法:
add(int number) add(double d) add(boolean b)
// 情况 3:泛型
Set<T> List<T> Cllection<T> Map<K,V>
|
所以为了使得基本数据类型具备引用数据类型的相关特征(如:封装性、继承性和多态性),Java 为八种基本类型都引入了对应的包装类(封装类)。基本数据类型通过包装类有了类的特点,可以调用类中的方法。
其中整形和浮点型的包装类继承自 Number
,而布尔型和字符型的包装类继承自 Object
。
1
2
3
4
5
6
7
|
public class Test1 {
public static void main(String[] args) {
System.out.println(Byte.class.getSuperclass());
System.out.println(Boolean.class.getSuperclass());
System.out.println(Character.class.getSuperclass());
}
}
|
1
2
3
|
class java.lang.Number
class java.lang.Object
class java.lang.Object
|
封装以后的,内存结构对比:
1
2
3
4
5
6
|
public class Test1 {
public static void main(String[] args) {
int num = 520;
Integer obj = new Integer(520);
}
}
|
自定义包装类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class MyInteger {
int value;
public MyInteger() {}
public MyInteger(int value) {
this.value = value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
|
包装类与基本数据类型间的相互转换
在转换之前,我们先考虑一下为什么需要转换。基本数据类型转包装类的重要性在文章的开头已经提到了。那么我们还需要知道为什么包装类要转成基本数据类型,除了效率的原因外,还因为既然包装类是对象,而在 Java 中对象是不能进行加减乘除等算数运算的(String:幸好我的 +
是拼接的意思,不然我就被开除对象籍了😋),为了能够进行这些运算,就需要将包装类的对象转化为基本数据类型。
装箱
装箱的意思是:把基本数据类型转为包装类对象。将基本类型转为包装类的对象,是为了使用专门为对象设计的API 和特性。
装箱就一般来说有两种方法:
- 使用包装类的构造器
- [推荐] 调用包装类的静态工厂方法valueOf()
因为包装类的构造器在 JDK9 遭到废弃,所以不是很推荐用第一种方法来构建包装类了。
1
2
3
4
5
6
7
|
// 使用构造函数函数
Integer obj1 = new Integer(4); // 参数为对应类型变量
Float f = new Float(“4.56”); // 参数为字符串
Long l = new Long(“asdf”); // 抛出异常 NumberFormatException
// 使用包装类中的 valueOf 方法
Integer obj2 = Integer.valueOf(4);
|
说到装箱就不得不提一嘴 Boolean 的装箱了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class Test1 {
public static void main(String[] args) {
Boolean b1 = new Boolean(true); // true
Boolean b2 = new Boolean("true"); // true
Boolean b3 = Boolean.TRUE; // true
Boolean b4 = Boolean.valueOf(true); // true
Boolean b5 = new Boolean("TrUe"); // true 使用字符串构建时会忽略大小写
Boolean b6 = Boolean.valueOf("TRUE");
// 这里会抛出异常吗?
Boolean b7 = new Boolean("1234567");
Boolean b8 = Boolean.valueOf("abcdefg");
// 这里使用了 JDK5 的自动插装箱特性 之后再说
System.out.println("b1 = " + b1);
System.out.println("b2 = " + b2);
System.out.println("b3 = " + b3);
System.out.println("b4 = " + b4);
System.out.println("b5 = " + b5);
System.out.println("b6 = " + b6);
System.out.println("b7 = " + b7);
System.out.println("b8 = " + b8);
}
}
|
你可能以为用 1234567
和 abcdefg
也会抛出异常,但很可惜,它并不会,而是被赋值为false。
1
2
3
4
5
6
7
8
|
b1 = true
b2 = true
b3 = true
b4 = true
b5 = true
b6 = true
b7 = false
b8 = false
|
这是为什么呢?让我们看看源码就知道了
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Deprecated(since="9", forRemoval = true)
public Boolean(String s) {
this(parseBoolean(s));
}
public static Boolean valueOf(String s) {
return parseBoolean(s) ? TRUE : FALSE;
}
// 我们发现它们都调用了 parseBoolean 这个方法,那么奥秘一定在其中
public static boolean parseBoolean(String s) {
return "true".equalsIgnoreCase(s);
}
|
看过源码后我们就发现了,用字符串给Boolean赋值的原理是用传入的字符串与 `true` 做忽略大小写的比较,然后用比较出来的结果来赋值,通过源码我们不仅知道了只要传入的字符串不是 true
及其大小写变形得到的结果就是 false
,还知道了为什么用字符串构建会忽略大小写,原因是调用了字符串的 equalsIgnoreCase
方法。
拆箱
拆箱就是把包装类对象拆为基本数据类型。将包装类对象转为基本数据类型一般是因为需要运算,Java中的大多数运算符是为基本数据类型设计的,如比较、算术等。
拆箱就非常简单了,只需要调用对应包装类的 xxxValue()
方法就可以了。
1
2
3
4
5
6
7
8
9
10
11
|
public class Test1 {
public static void main(String[] args) {
int i = Integer.valueOf(1).intValue();
boolean b = Boolean.valueOf("TUre").booleanValue();
double d = Double.valueOf(114514.0).doubleValue();
System.out.println("i = " + i);
System.out.println("b = " + b);
System.out.println("d = " + d);
}
}
|
1
2
3
|
i = 1
b = false
d = 114514.0
|
自动拆装箱
由于我们经常要做基本类型与包装类之间的转换,从 JDK5.0 开始,基本类型与包装类的装箱、拆箱动作可以自动完成。
1
2
3
4
5
|
Integer i = 4; //自动装箱 相当于 Integer i = Integer.valueOf(4)
// 等号右边: 将 i 对象转成基本数值(自动拆箱),相当于 i.intValue() + 5
// 等号左边: 加法运算完成后,再次装箱,把基本数值转成对象。i = Integer.valueOf(i.intValue() + 5)
i = i + 5;
|
注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。
1
2
|
Integer i = 1;
Double d = 1; //错误的,1 是 int 类型
|
自动装箱和自动拆箱并不是什么高大上的东西,只是隐式帮我们调用了 valueOf()
和 xxxValue()
方法而已。
1
2
3
4
5
6
|
public class Test1 {
public static void main(String[] args) {
Integer ii = 4;
int i = ii;
}
}
|
包装类对象的特点及注意事项
包装类缓存对象
为了引入这个特点,我们先看一个题目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Test2 {
public static void main(String[] args) {
Integer i1 = 1;
Integer i2 = Integer.valueOf(1);
Integer i3 = new Integer(1);
System.out.println(i1 == i2); // true
System.out.println(i1 == i3); // false
Integer i4 = 114514;
Integer i5 = Integer.valueOf(114514);
Integer i6 = new Integer(114514);
System.out.println(i4 == i5); // false
System.out.println(i4 == i6); // false
}
}
|
我们知道对象之间的 ==
是在比较对象的地址值,i1 和 i2 相等,说明 i1 和 i2 其实是同一个对象,而 i1 与 new
出来的 i3 不是同一个对象,结合上文我们说过自动装箱其实是调用了 valueOf()
方法,那是不是可以得出一个结论,通过 valueOf 得到的其实都是同一个对象呢?这个想法其实很接近真正的答案,不过先再让我们看看 i4 和 i5,我们发现 i4 和 i5 这两个valueOf 得到的对象又不是同一个了。为了解决这个问题,让我们来看看 valueOf 的源码。
1
2
3
4
5
6
|
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
|
我们又发现 valueOf 内部借助了一个 IntegerCache 类,那么奥妙肯定就在其中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
private static final class IntegerCache {
static final int low = -128;
static final int high;
@Stable
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
|
对于这份源码我们不需要完全看懂(我才不会告诉你我也没看多少呢😎),把这份代码精简一下大概就是这样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
private static final class IntegerCache {
static final int low = -128;
static final int high;
@Stable
static final Integer[] cache;
static Integer[] archivedCache;
static {
int h = 127;
high = h;
int size = (high - low) + 1; // size = 256
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) { // 循环 size 次
c[i] = new Integer(j++); // c = {low, low + 1, ..., high - 1, high} 即 [-128, 127]
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
|
这份代码是我断章取义的结果,只能用来大概理解,如有错误还请体谅并指正。
总而言之,就是 IntegerCache
类中定义了一个长度为256
的数组,其中存放了 [-128, 127] 对应的 Integer 对象,结合 valueOf()
方法就能得出:如果你通过 valueOf (或者自动包装)得到的 Integer 包装类对象所对应的值在[-128, 127]之间,那么就不会给你创建新对象,而是从 cache 数组中去取来给你用。。这也是为什么包装类要废弃构造函数,推荐使用valueOf 的原因,因为它可以通过 cache 数组来减少对象的创建,提升效率,而通过构造器 new 出来的对象是实打实创建了一个新的出来。
1
2
3
4
5
6
7
8
9
10
|
/**
* It is rarely appropriate to use this constructor. The static factory
* {@link #valueOf(int)} is generally a better choice, as it is
* likely to yield significantly better space and time performance.
* 很少适合使用此构造函数。静态工厂 {@link valueOf(int)} 通常是更好的选择,因为它可能会产生明显更好的空间和时间性能。
*/
@Deprecated(since="9", forRemoval = true)
public Integer(int value) {
this.value = value;
}
|
当然了,不只 Integer 有缓存数组,很多包装类也有:
包装类 |
缓存对象 |
Byte |
-128 ~ 127 |
Short |
-128 ~ 127 |
Integer |
-128 ~ 127 |
Long |
-128 ~ 127 |
Float |
无 |
Double |
无 |
Character |
0 ~ 127 |
Boolean |
true 和 false |
包装类对象不可变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class Test3 {
public static void main(String[] args) {
int i = 1;
Integer j = new Integer(2);
Circle c = new Circle();
change(i, j, c);
System.out.println("i = " + i); // 1
System.out.println("j = " + j); // 2
System.out.println("c.radius = " + c.radius); // 10.0
}
/*
* 方法的参数传递机制:
* (1)基本数据类型:形参的修改完全不影响实参
* (2)引用数据类型:通过形参修改对象的属性值,会影响实参的属性值
* 这类 Integer 等包装类对象是“不可变”对象,即一旦修改,就是新对象,和实参就无关了
*/
public static void change(int a, Integer b, Circle c) {
a += 10;
Integer tmp = Integer.valueOf(b + 10);
b += 10; // 等价于 b = Integer.valueOf(b + 10);
System.out.println(b == tmp); // true
c.radius += 10;
}
}
class Circle {
double radius;
}
|
b 的运算顺序为:先拆箱,把拆箱得到的数据与10相加,把相加得到的数作为 valueOf 的形参创建一个新的 Integer 对象。
包装类与基本数据类型的默认值不同
我们在声明类的成员变量变量的时候,可能会声明包装类对象而不是基本数据类型。这时我们就要注意到,包装类是引用数据类型,所以它的默认值相对于基本数据类型发生了变化。
1
2
3
4
5
6
7
8
9
10
|
public class Test1 {
Integer integer;
int i;
public static void main(String[] args) {
Test1 t = new Test1();
System.out.println(t.i); // 0
System.out.println(t.integer); // null
}
}
|
所以在使用时要注意一下它们类型变化带来的差异。
类型转化问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class Test4 {
public static void main(String[] args) {
Integer i1 = 1000;
double j1 = 1000;
// true 会先将 i 自动拆箱为 int,然后根据基本数据类型“自动类型转换”规则,转为 double 进行比较
System.out.println(i1 == j1);
Integer i2 = 1000;
int j2 = 1000;
System.out.println(i2 == j2); // true 会自动拆箱,按照基本数据类型进行比较
Integer i3 = 1;
Double d3 = 1.0;
System.out.println(i3 == d3); // 编译报错 Integer 与 Double 不能进行比较,其他包装类也是一样的
}
}
|
字符串与基本数据类型的转化
基本数据类型转字符串
1
2
3
4
5
6
7
8
|
// 方法1 调用字符串重载的 valueOf() 方法
int a = 10;
// String str = a; error
String str = String.valueOf(a); // ok
// 方法2 与空字符串进行拼接
int a = 10;
String str = a + ""; // ok
|
字符串转基本类型
- 除了 Character 类之外,其他所有包装类都具有 parseXxx 静态方法可以将字符串参数转换为对应的基本类型。
public static int parseInt(String s)
:将字符串参数转换为对应的 int 基本类型。
public static long parseLong(String s)
:将字符串参数转换为对应的 long 基本类型。
public static double parseDouble(String s
):将字符串参数转换为对应的 double 基本类型。
- 字符串转为包装类,然后可以自动拆箱为基本数据类型。
public static Integer valueOf(String s)
:将字符串参数转换为对应的 Integer 包装类,然后可以自动拆箱为 int 基本类型。
public static Long valueOf(String s)
:将字符串参数转换为对应的 Long 包装类,然后可以自动拆箱为 long 基本类型。
public static Double valueOf(String s)
:将字符串参数转换为对应的 Double 包装类,然后可以自动拆箱为 double 基本类型。
- 注意: 如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出
java.lang.NumberFormatException
异常。
1
2
3
4
5
6
7
8
9
10
|
// 方法1
int a = Integer.parseInt("整数的字符串");
double d = Double.parseDouble("小数的字符串");
boolean b = Boolean.parseBoolean("true 或 false");
// 方法2
int i = new Integer(“12”);
int a = Integer.valueOf("整数的字符串");
double d = Double.valueOf("小数的字符串");
boolean b = Boolean.valueOf("true 或 false");
|