JDK核心JAVA源碼解析(8) - 自動封箱拆箱與效率的思考

想寫這個系列很久了,對自己也是個總結與提高。原來在學JAVA時,那些JAVA入門書籍會告訴你一些規律還有法則,但是用的時候我們一般很難想起來,因爲我們用的少並且不知道爲什麼。知其所以然方能印象深刻並學以致用。

本文基於 Java 14

在JDK1.5引入自動裝箱/拆箱,讓開發更高效。自動裝箱時編譯器調用valueOf()將原始類型值轉換成對象,同時自動拆箱時,編譯器通過調用類似intValue(),doubleValue()這類的方法將對象轉換成原始類型值。
自動裝箱是將 boolean 值轉換成 Boolean 對象,byte 值轉換成 Byte 對象,char 轉換成 Character 對象,float 值轉換成 Float 對象,int 轉換成 Integer,long 轉換成 Long,short 轉換成 Short,自動拆箱則是相反的操作。

public static void main(String[] args) {
        Long v = 6L;
        long a = v;
        if (a > v) {
            
        }
    }

通過javap -c命令查看:

 0 ldc2_w #68 <6>
 3 invokestatic #66 <java/lang/Long.valueOf>
 6 astore_1
 7 aload_1
 8 invokevirtual #70 <java/lang/Long.longValue>
11 lstore_2
12 lload_2
13 aload_1
14 invokevirtual #70 <java/lang/Long.longValue>
17 lcmp
18 ifle 21 (+3)
21 return

可以看到,調用Long.valueOf自動封箱,調用Long.longValue自動拆箱。

自動裝箱拆箱時機

1.賦值還有比較運算時,類型不一致,會自動裝箱拆箱

public static void main(String[] args) {
       Long v = 6L;
       long a = v;
}

通過javap -c命令查看:

 0 ldc2_w #68 <6>
 3 invokestatic #66 <java/lang/Long.valueOf>
 6 astore_1
 7 aload_1
 8 invokevirtual #70 <java/lang/Long.longValue>
11 lstore_2
12 return

可以看到,調用Long.valueOf自動封箱,調用Long.longValue自動拆箱。

2.方法調用時,類型不一致,會自動裝箱拆箱

public static void main(String[] args) {
    Long v = 6L;
    test1(v);
    long a = v;
    test2(a);
}
private static void test1(long v) {

}
private static void test2(Long v) {

}

通過javap -c命令查看main方法:

 0 ldc2_w #68 <6>
 3 invokestatic #66 <java/lang/Long.valueOf>
 6 astore_1
 7 aload_1
 8 invokevirtual #70 <java/lang/Long.longValue>
11 invokestatic #71 <com/hopegaming/order/revo/controller/backend/CustomerAnalysisController.test1>
14 aload_1
15 invokevirtual #70 <java/lang/Long.longValue>
18 lstore_2
19 lload_2
20 invokestatic #66 <java/lang/Long.valueOf>
23 invokestatic #72 <com/hopegaming/order/revo/controller/backend/CustomerAnalysisController.test2>
26 return

調用方法前,發生了自動拆箱與自動裝箱。

3. 對於同時有封裝類型和原始類型兩種參數的重載,不會發生自動封箱拆箱

public static void main(String[] args) {
    Long v = 6L;
    test1(v);
    long a = v;
    test1(a);
}
private static void test1(long v) {

}
private static void test1(Long v) {

}

通過javap -c命令查看main方法:

 0 ldc2_w #68 <6>
 3 invokestatic #66 <java/lang/Long.valueOf>
 6 astore_1
 7 aload_1
 8 invokestatic #70 <com/hopegaming/order/revo/controller/backend/CustomerAnalysisController.test1>
11 aload_1
12 invokevirtual #71 <java/lang/Long.longValue>
15 lstore_2
16 lload_2
17 invokestatic #72 <com/hopegaming/order/revo/controller/backend/CustomerAnalysisController.test1>
20 return

這次調用方法前,並沒有發生自動拆箱與自動裝箱。

自動封箱拆箱性能問題

由於自動封箱拆箱需要額外的操作,運算必須轉化爲原始類型,所以在**運算過程中,使用原始類型。存儲數據的時候,用封裝類型,**因爲原始類型有默認值,我們有時候想用null代表這個數據不存在。

例如下面的代碼,這個封箱就是沒有必要的,會浪費性能:

Long l = 0L;
for(int i = 0; i < 50000; i++) {
    l += 1L;
}

三目運算符與性能問題

對於三目運算符,比如冒號表達式,如果有原始類型,則會發生自動拆箱。

public static void main(String[] args) {
    Long v = null;
    Long a = new Random().nextBoolean() ? v : 0L;
}

通過javap -c命令查看main方法:

 0 aconst_null
 1 astore_1
 2 new #68 <java/util/Random>
 5 dup
 6 invokespecial #69 <java/util/Random.<init>>
 9 invokevirtual #70 <java/util/Random.nextBoolean>
12 ifeq 22 (+10)
15 aload_1
16 invokevirtual #71 <java/lang/Long.longValue>
19 goto 23 (+4)
22 lconst_0
23 invokestatic #66 <java/lang/Long.valueOf>
26 astore_2
27 return

可以看出,冒號表達式其實變成了:

Long a = Long.valueOf(new Random().nextBoolean() ? v.longValue() : 0L);

這樣的話,如果 Random 隨機的是 true,則會拋出NullPointerException

三目運算符判斷null返回默認值效率問題

有時候,我們需要null轉換成默認值,一般像common-langObjectUtils裏面的defaultIfNull這麼寫:

Long a = 6L;
a = a != null ? a : 0L;

這樣,雖然沒錯,但是會多出一步自動拆箱,再封箱,查看字節碼:

 0 ldc2_w #68 <6>
 3 invokestatic #66 <java/lang/Long.valueOf>
 6 astore_1
 7 aload_1
 8 ifnull 18 (+10)
11 aload_1
12 invokevirtual #70 <java/lang/Long.longValue>
15 goto 19 (+4)
18 lconst_0
19 invokestatic #66 <java/lang/Long.valueOf>
22 astore_1
23 return

相當於:

a = Long.valueOf(a != null ? a.longValue() : 0L);

這樣的話,無論 a 是不是 null,都會多出來這些拆箱封箱,效率不好。

對於這種場景,考慮到效率,還是老老實實,寫 if-else 不要用三目運算符了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章