Java基础 - 包装器类型
一、基本类型
类型的作用是修饰变量,不同的类型修饰变量表示这个变量代表不同的数据含义。
Java是一门面向对象编程的语言,就像《Java编程思想》中说到的那样: "一切都是对象"。但是在程序设计中经常用到一系列的类型,它们需要特殊对待。可以把它们想象成"基本"类型。之所以特殊对待,是因为new将对象存储在"堆"里,故用new创建一个对象——特别是小的、简单的变量,往往不是很有效。因此,对于这些类型,Java采取与C和C++相同的方法。就是说,不用new来创建变量,而是创建一个并非是引用的"自动"变量。这个变量直接存储"值",并置于堆栈中,因此更加高效。
可以这样理解吧,像下面这样:
int a = 123;
就可以理解为在内存中开辟了一个空间,这个空间存储值123,用命名"a"代表这个空间,访问a即表示访问这个空间。
可以比对一下C中的指针,在C语言中可以一个指针,如下:
int *a;
这个与上面的不同,这个命名a代表的内存空间存储的就不是直接值,而是代表另外一个内存空间的地址,比如这样:
int a = 123;
int *b = &a;
总之就这样理解:基本类型修饰的变量代表它所表示的这段内存空间存储的是直接值。
Java中定义的基本类型
Java中定义了8种基本类型,分别是数字类型和布尔类型。
数字类型又可以分为整数类型和浮点类型。
整数类型包括byte、short、int和long,它们的值分别是8位、16位、32位和64位有符号二进制补码表示的整数;char也是一种整数类型,它的值是16位无符号整数,表示,UTF-16码元。
浮点数类型包括float和double,前者的值包括32位IEEE 754浮点数,而后者的值包括64位IEEE 754浮点数。
布尔类型只有两个值:true和false。
二、包装器类型
上面说了,一切都是对象。对于基本类型,Java都定义了相对应的包装器类,使得可以在堆中创建一个非基本对象,用来表示对应的基本类型。
有人会问,有了基本类型为什么还要定义包装类呢?
以对象的方式来操作基本类型的数据,解决一些特定的问题,这是引入包装类的原因。
比如说我们定义了一个学生类Student,其中有一个属性表示该学生某学科的考试成绩,成绩我们用整型的数据表示。那么这个数据我们用什么数据类型修饰呢?不提及到包装类的情况下,我们用int类型来修饰这个属性,但是你会发现这样不能解决一个特殊的问题,就是你无法表示这个学生该学科没有成绩(规定不用用其他属性标记表示没有成绩)。如果我们用包装类Integer来表示,空值就可以表示没有成绩。
0就是表示成绩为0分,不能表示没有成绩。
上面只是举了某个需要包装类的需求,这种需求的场景还有很多,就不一一举例。
Java定义的包装器类型
基本类型对应的包装类如下表所示:
基本类型 | 包装器类型 |
---|---|
boolean | Boolean |
byte | Byte |
char | Character |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
三、装箱和拆箱
上面我们已经说到了基本类型都有相对于的包装器类型,那么这两种类型可以相互转换吗?
答案是肯定的,Java提供了装箱和拆箱来实现基本类型与对应包装器类型的转换。
1. 装箱
装箱是指把基本类型转换为包装类类型。
看下面的代码:
int i = 10;
这里我们定义了一个int的变量i,现在要转换到其对应的包装器类型Integer,我们怎么做,在JDK1.5之前我们可以这样做:
Integer ii = new Integer(i);
或者这样做:
Integer ii = Integer.valueOf(i);
JDK1.5中引入了自动装箱的功能来,具体如下:
init i = 10;
Integer ii = i;
这里可以看到,直接将基本类型的变量赋值给包装器类型,这种类型相比我们上面写的那两种,体现了"自动"这一概念。
那么"自动装箱"是怎么做到的呢?
看下面代码:
public class E1 {
public static void main(String[] args) {
int i = 10;
Integer ii = i;
}
}
我们通过javap指令来看一下生成的字节码(片段):
{
public E1();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
7: astore_2
8: return
LineNumberTable:
line 4: 0
line 5: 3
line 6: 8
}
注意下面红色方框标记:
自动装箱.png
从上面可以看到,int类型转换到Integer,直接赋值方式其实还是通过Integer的valueOf方法来操作,只是说这个是编译器为我们处理的,所以相比人为转换这个就叫做"自动"。
2. 拆箱
拆箱是指把包装类型转换为基本类型。
看下面的代码:
Integer i = 10;
这里我们定义了一个Integer的变量i,现在要转换到其对应的基本类型,我们怎么做,在JDK1.5之前我们可以这样做:
int ii = i.intValue();
对于其他基本类型的包装类类型,我们会发现都会有相对应的xxValue的方法来实现到基本类型的装换。
JDK1.5中引入了自动拆箱的功能来,具体如下:
Integer i = 10;
init ii = i;
和上面自动装箱一下,体现了自动装换这个概念。
那么"自动拆箱"是怎么做到的呢?
看下面代码:
public class E2 {
public static void main(String[] args) {
Integer i = 10;
int ii = i;
}
}
我们通过javap指令来看一下生成的字节码(片段):
{
public E2();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: bipush 10
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: aload_1
7: invokevirtual #3 // Method java/lang/Integer.intValue:()I
10: istore_2
11: return
LineNumberTable:
line 4: 0
line 5: 6
line 6: 11
}
注意下面红色方框标记:
自动拆箱.png
从上面可以看到,Integer类型转换到int,直接赋值方式其实还是通过Integer的intValue方法来操作,只是说这个是编译器为我们处理的,所以相比人为转换这个就叫做"自动"。
三、包装类缓存池
先来看一道面试题:
public class E3 {
public static void main(String[] args) {
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2);
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);
}
}
输出结果如下:
rockydeMacBook-Pro:Desktop rocky$ java E3
true
false
我们知道"=="比较两个对象是否是同一个对象,从上面的结果我们可以看到i1和i2是同一个对象,i3和i4不是同一个对象。
为什么结果是这样的?
上面我们知道基本类型数据赋值给包装类型是通过自动装箱实现的,调用的是相对应的包装类型的valueOf方法,我们来看一下Integer的valueOf方法,源代码如下:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
我们看到用到了IntegerCache这个类,只从名字上应该也可以知道这个是做缓存用的。
JDK做了这样的处理:
为Byte、Short、Integer和Long缓存了数值范围为[-128,127](闭区间)的对象;
为Character缓存了数值范围为[0,127](闭区间)的对象。
包装类与相对应的缓存类如下:
包装类 | 包装类对象缓存类 |
---|---|
Byte | ByteCache |
Short | ShortCache |
Integer | IntegerCache |
Long | LongCache |
Character | CharacterCache |
有了这些知识就可以分析上面的代码:
Integer i1 = 10;
将数值为10的Integer对象放入了缓存中。
Integer i2 = 10;
10在缓存数值区间类,直接从缓存数组中取出对象。
而128超出了缓存的数值范围,所以:
Integer i4 = 128;
创建的是一个新对象。
JDK中没有为Float和Double实现缓存池是因为某个范围内的整型数值的个数是有限的,而浮点数却不是。
1. 为什么使用缓存池
这是Flyweight pattern的一种实现:尽量与其他对象共享更多的数据以减少内存的占用。
同时,是缓存嘛,减少内存占用,同时提升性能。
2. 可以改变缓存的数值范围吗?
答案是:一些新版本的Java6和Java7开始可以在启动的时候配置-XX:AutoBoxCacheMax参数来设置Integer最大缓存的数值。这个参数配置的值会设置到java.lang.Integer.IntegerCache.high属性中,从而修改Integer最大的缓存数值。
其他有缓存的包装类缓存的数值范围都是硬编码的,无法修改。
使用JDK7我们来测试一下这个设置。
代码如下:
public class E4 {
public static void main(String[] args) {
Integer i1 = 128;
Integer i2= 128;
System.out.println(i1 == i2);
Integer i3 = 1000;
Integer i4= 1000;
System.out.println(i3 == i4);
Integer i5 = 1001;
Integer i6= 1001;
System.out.println(i5 == i6);
}
}
使用下面命令运行:
java -XX:AutoBoxCacheMax=1000 E4
输出结果如下:
rockydeMacBook-Pro:Desktop rocky$ java -XX:AutoBoxCacheMax=1000 E4
true
true
false