Java基本类型与包装类型

一、Java中的数据类型

熟悉Java的朋友都知道Java中的数据类型分为基本类型引用类型,基本类型就是我们日常使用的intlong等,引用类型就是用class定义出来的类型,如StringHashMap等。Java中有8种基本数据类型,它们同时都对应一个引用数据类型(又称包装类型),具体内容如下:

基本类型 占用空间 取值范围 包装类型
byte 1B $-2^{7}$ ~ $2^{7}-1$ java.lang.Byte
char 2B (unicode) $0$ ~ $2^{16}-1$ java.lang.Character
short 2B $-2^{15}$ ~ $2^{15}-1$ java.lang.Short
int 4B $-2^{31}$ ~ ${2^{31}-1}$ java.lang.Integer
long 8B $-2^{63}$ ~ $2^{63}-1$ java.lang.Long
float 4B 参见IEEE754 java.lang.Float
double 8B 参见IEEE754 java.lang.Double
boolean 与jvm有关 ${True, False}$ java.lang.Boolean

boolean的大小jvm规范未定义,在Oracle Hotspot中实测占1字节。
以下是对Boolean的测试代码(使用JOL工具测试):

1
2
3
4
5
6
7
8
9
10
public class BooleanTest {
public static void main(String[] args) throws Exception {
System.out.println(ClassLayout.parseInstance(new boolean[]{true, false, true}).toPrintable());
System.out.println(ClassLayout.parseInstance(new BooleanTest()).toPrintable());
}

boolean aBoolean;
int anInt;
char aChar;
}

boolean测试结果

二、自动装箱与拆箱

Java是一个纯面向对象的语言,为了让基本数据类型也拥有引用类型的特征(比如取null值),Java工程师特意为他们设计了包装类型。包装类型简单来说就是用一个class声明的类将基本类型包装了一下,并附带了很多常用工具,这样我们就能按面向对象的方式去使用基本数据类型了。下面是Integer类的部分源码:

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
41
42
43
44
45
46
47
48
49
50
51
public final class Integer extends Number implements Comparable<Integer> {
public static final int MIN_VALUE = -2147483648;
public static final int MAX_VALUE = 2147483647;
...
//核心内容
private final int value;
...
//构造方法
public Integer(int var1) {
this.value = var1;
}
//装箱方法
public static Integer valueOf(int var0) {
return var0 >= -128 && var0 <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[var0 + 128] : new Integer(var0);
}
//拆箱方法
public int intValue() {
return this.value;
}
//Integer缓存
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
}

我们从源码中可以看到包装类型中保存数据的核心成员变量就是value,所有对包装类型的操作最终都反映到value字段上。包装类型中的valueOf方法(有多种重载)就是常说的装箱方法,使用该方法可以将一个基本类型变量转变为包装类型,如Integer a = Integer.valueOf(2);。同样,intValue方法(有多种变形,如longValuecharValue等)就是拆箱方法,它可以将包装类型转变为基本类型,如int b = a.intValue(); 。早期的Java中基本类型与包装类型的转换是需要开发人员手工去调用方法实现的,自JDK 5.0开发,Java引入了自动装箱和拆箱机制,极大的方便了开发人员,自动装箱与拆箱简单说就是JVM自动去调用valueOfintValue方法,于是我们的代码就可以这样写了int a = new Integer(1); Integer b = 4;

问题拓展

我们知道在C/C++中写一个可以交换两个int变量值的函数,如下所示,那在Java中怎么实现这样一个方法呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
//C(通过指针交换)
void swap(int* a, int* b){
int c = *a;
*a = *b;
*b = c;
}

//C++(通过引用交换)
void swap(int& a, int& b){
int c = a;
a = b;
b = c;
}

Java中没有指针,但有对象引用(与C/C++中的指针概念类似),我们可以利用此概念实现同样的功能:

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
public class Test {
public static void main(String[] args) throws Exception {
Integer a = new Integer(1); //不能用自动装箱
Integer b = new Integer(2); //不能用自动装箱
swap(a, b);
System.out.println("a="+a+", b="+b);
}

public static void swapa(Integer a, Integer b) throws Exception {
int c = a;//暂存a的值(使用自动拆箱)

//通过反射的方式强行更改value
Field field = a.getClass().getDeclaredField("value");
field.setAccessible(true);
field.set(a, b);

//通过反射的方式强行更改value
field = b.getClass().getDeclaredField("value");
field.setAccessible(true);
field.set(b, c);
}
}

/**
* 输出:a=2, b=1
*/

上面的代码可以实现值的交换,但存在一些限制:

  1. 实参和形参都必须是包装类型,因为只有包装类型传参时是传引用(相当与C/C++中的指针)。
  2. 实参不能使用自动装箱生成,如Integer a = 100;
  3. 效率不高。

下面我们看一下为什么不能使用自动装箱,首先看一下自动装箱的源码:

1
2
3
 public static Integer valueOf(int var0) {
return var0 >= -128 && var0 <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[var0 + 128] : new Integer(var0);
}

我们看到如果自动装箱的值在[-128, high]high∈[127, Integer.MAX_VALUE-129]),就会返回Integer.IntegerCache.cache[var0 + 128]这样一个值,其中的Integer.IntegerCache.cache就是我们提到的缓存,在Integer中缓存默认范围是[-128, 127],也就是说这个范围类的数字对应的包装对象在加载Integer类的时候都已经生成好了,以后谁要采用自动装箱获取Integer对象,只要值在缓存范围内,就直接返回缓存对象即可。

在上面的swap示例中,如果传入的实参是采用自动装箱生成的,且数值范围在缓存范围内,就会将缓存的引用传入swap方法中,然后用反射修改的就是这些缓存的值。最终,我们最初的问题是解决了,但接着会带来更大的麻烦,示例如下:

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 Test {
public static void main(String[] args) throws Exception {
Integer a = 1;
Integer b = 2;

swap(a, b);
System.out.println("a="+a+", b="+b);

//!!! Big trouble !!!
System.out.println("Integer.valueOf(1) = " + Integer.valueOf(1)); //缓存被修改了
System.out.println("(Integer)2 = " + (Integer)2);//缓存被修改了
}

public static void swap(Integer a, Integer b) throws Exception {
Integer c = new Integer(a); //原始值进行深度拷贝

Field field = a.getClass().getDeclaredField("value");
field.setAccessible(true);
field.set(a, b);

field = b.getClass().getDeclaredField("value");
field.setAccessible(true);
field.set(b, c);
}
}

/** 输出:
* a=2, b=1
* Integer.valueOf(1) = 2
* (Integer)2 = 1
*/

☠ 我们看到由于直接修改了12缓存,后续所有用到这两个缓存的值将全部出错,这是一种很危险的行为。

忠告:在java中上诉两种交换值的方式请都不要使用,如果有此类需求,可以考虑以下方案:

  • 不使用方法,直接就地解决。
  • 使用方法,把原始两个参数合并成list或数组传入swap方法
  • 使用方法,把原始数据包装进一个对象中传入swap方法

三、Java基本数据类型缓存

在Java5中,为节省内存提高性能,除Float和Double之外,所有基本类型的包装类型都引入了缓存,一般缓存[-128, 127](Charactor是[0, 127])之间的所有包装类对象。这些缓存在对应类加载的时候都完成了初始化,后续使用时如果用到自动装箱且数值在缓存范围内,则直接返回缓存对象。

Integer的缓存范围上界是可调整的(也是唯一可调范围的类型),在JVM启动参数中加入 -XX:AutoBoxCacheMax=<size>调整Integer缓存范围。其中size的可取范围是[127, 2147483518],如果不在此范围就取该范围的边界值。

示例:

1
2
3
4
5
Integer a = 100; //use cache
Integer b = Integer.valueOf(-128); //use cache
Integer c = Integer.valueOf("23"); //use cache
Integer d = new Integer(23);// not use cache
Integer e = 7367; //not use cache by default

四、延申思考

4.1 方法重载优先级

问题如下:

1
2
3
4
5
6
7
8
9
public void swap(int a, int b){} //m1
public void swap(Integer a, int b){} //m2
public void swap(int a, Integer b){} //m3
public void swap(Integer a, Integer b){} //m4

//调用
swap(2, 3); //调用m1
swap((Integer)2, 3); //调用m2
swap((Integer)2, (Integer)3); //调用m4

java在重载的时候优先按相同类型精确匹配,当精确匹配失败时就尝试自动装箱/拆箱匹配,如果此时匹配到多个方法,就会导致编译失败。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void swap(int a, Integer b){} //m3
public static void swap(Integer a, Integer b){} //m4

swap(2, 3); // compile error,精确匹配失败后尝试自动装箱,但无法确定唯一的匹配方法
/**
* java: 对swap的引用不明确
* com.zjee.test.AutoBoxTest 中的方法 swap(int,java.lang.Integer) 和
* com.zjee.test.AutoBoxTest 中的方法 swap(java.lang.Integer,java.lang.Integer) 都匹配
*/
swap((Integer)2, 3); // compile error,精确匹配失败后,尝试自动装箱/拆箱,但无法确定唯一的匹配方法
/**
* java: 对swap的引用不明确
* com.zjee.test.AutoBoxTest 中的方法 swap(int,java.lang.Integer) 和
* com.zjee.test.AutoBoxTest 中的方法 swap(java.lang.Integer,java.lang.Integer) 都匹配
*/
swap((Integer)2, (Integer)3); //ok,精确匹配m4
swap(3, (Integer)4);//ok,精确匹配m3

4.2泛型

java中泛型不支持基本数据类型,即int,float这样的,但数组是个例外,数据在java中是个对象,不过这个对象没有明确的class,我们可像操作对象一样操作数组,所以泛型是可以接收数组的,示例如下:

1
2
3
List<Integer> list1 = new ArrayList<>(); // ok
List<int[]> list2 = new ArrayList<>(); // also ok
List<int> list3 = new ArrayList<>(); // compile error

我们日常开发中有一个数组转List的工具用的特别多,它就是Arrays.asList(T... a),这个方法接收可变参数,我们知道可变参数可以传多个参数或一个数组,当传数组的时候需要注意,泛型会将int[]视为一个整体,也就是说这里传new Integer[]{1,2,3}new int[]{1,2,3}是不一样的。对于前者来说,相当于传入3个Integer参数,最后接收方收到的参数长度为3。而后者相当于传入一个类型为int[]的参数,接收方收到的参数长度为1。因此,我们在使用Arrays.asList的时候要特别注意用包装类型而不是基本类型。

文章作者: Jack.Charles
文章链接: https://blog.zjee.me/2019/11/19/java-primitive-and-wrapper-type/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 江影不沉浮