返回
Featured image of post 包装类

包装类

”Java中的包装类的使用和细节“

目录

概述

为什么需要包装类

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);
    }
}

你可能以为用 1234567abcdefg 也会抛出异常,但很可惜,它并不会,而是被赋值为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");

Licensed under CC BY-NC-SA 4.0
鹅掌草の森已经茁壮生长了
发表了8篇文章 · 总计50.10k字 · 共 0 次浏览
记录任何我想记录的事情。若无特殊说明,则本博客文章均为原创,复制转载请保留出处。
使用 Hugo 构建
主题 StackJimmy 设计