你好,我是看山。
這篇文章起因是 code review 時和同僚關于 import 導入聲明的分歧。
用過 IDEA 的都知道,預設情況下,通過 import 導入類時,當數量達到設定數量(類 5 個、靜态變量 3 個),就會改為按需導入方式,也就是使用使用*号折疊導入。
同僚建議不要采用按需導入,要使用單類型導入 (single-type-import)。而我是覺得既然 IDEA 作為宇宙級的 IDE,不會在這種地方出現纰漏,是以想繼續按照 IDEA 預設配置來。
是以總結一下這兩種方式的差異。如果對 java import 不熟悉,可以從 這裡 看看。
import 的兩種導入聲明
在 java 中,通過 import 導入類的方式有兩種:
單類型導入(single-type-import),例如 import java.io.File:這種方式比較容易了解,而且大部分時候我們用的都是這種方式。通過明确指明類和接口路徑,将他們導入進來。
按需類型導入(type-import-on-demand),例如 import java.io.*:通過通配符*定義導入方式,但是并不是直接導入這個包下的所有類,而是可以導入所有類。也就是說,如果需要就導入,不需要就不導入。
有如下屬性:
java 以這樣兩種方式導入包中的任何一個public的類和接口(隻有 public 類和接口才能被導入)
上面說到導入聲明僅導入聲明目錄下面的類而不導入子包,這也是為什麼稱它們為類型導入聲明的原因。
導入的類或接口的簡名(simple name)具有編譯單元作用域。這表示該類型簡名可以在導入語句所在的編譯單元的任何地方使用。這并不意味着你可以使用該類型所有成員的簡名,而隻能使用類型自身的簡名。例如:java.lang 包中的 public 類都是自動導入的,包括Math和System類。但是,你不能使用它們的成員的簡名PI()和gc(), 而必須使用Math.PI()和System.gc(). 你不需要鍵入的是java.lang.Math.PI()和java.lang.System.gc()。
程式員有時會導入目前包或java.lang包,這是不需要的,因為目前包的成員本身就在作用域内,而java.lang包是自動導入的。java 編譯器會忽略這些備援導入聲明 (redundant import declarations)。
按需導入機制
按需類型導入在大部分情況用起來更加友善,一個通配符可以導入包下的所有類,就不用費勁寫一堆導入了。
但是,根據能量守恒,在敲代碼時節省下來的能量,必然會在其他地方消耗。
比如,Date類,如果完全使用按需類型導入,可以寫做import java.util.*。當這個類恰好需要,PrepareStatement時,又需要加上import java.sql.*導入,這個時候,編譯器不知道Date類是要用java.util包裡的還是java.sql裡面的了,就會報出Reference to 'Date' is ambiguous, both 'java.util.Date' and 'java.sql.Date' match異常,也就是所說的命名沖突。
解決辦法就是指明Date類的全路徑,也就是使用單類型導入:import java.util.Date。
除了命名沖突,還有一些不太明顯的缺點:
編譯速度:因為按需導入機制的特性,需要在 CLASSPATH 下找到所有符合包名的類,在編譯時會消耗性能。在小項目中,這個速度可以忽略。如果在大項目中,就會有明細差異。
可讀性:在使用 IDE 開發過程中,我們很少會在import中檢視類的路徑。但是如果需要我們在其他環境編輯檔案,比如 vim,從import檢視類的路徑就很便捷了。
導入不需要的類會發生什麼呢
從理性講,java 編譯器一定會在這裡做優化,不會把不需要的導入聲明加入到 class 檔案中,但是之前沒有看到哪裡有說明,是以動手做一下實驗:
先定義 java 類:
package cn.howardliu;
// 需要用到的單類型導入
import java.util.Date;
// 需要用到的按需類型導入
import java.math.*;
// 不需要用到的單類型導入
import java.sql.PreparedStatement;
// 不需要用到的按需類型導入
import java.awt.*;
public class Main {
private Date date1;
private BigDecimal num1;
public void test(){
Date date2 = new Date();
BigDecimal num2 = new BigDecimal(0);
}
}
通過指令javac Main.java編譯,然後通過javap -verbose Main.class檢視編譯結果:
Classfile /path/to/Main.class
Last modified 2021-1-31; size 439 bytes
MD5 checksum 81e13559f738197b4875c2c2afd6fc41
Compiled from "Main.java"
public class cn.howardliu.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#19 // java/lang/Object."<init>":()V
#2 = Class #20 // java/util/Date
#3 = Methodref #2.#19 // java/util/Date."<init>":()V
#4 = Class #21 // java/math/BigDecimal
#5 = Methodref #4.#22 // java/math/BigDecimal."<init>":(I)V
#6 = Class #23 // cn/howardliu/Main
#7 = Class #24 // java/lang/Object
#8 = Utf8 date1
#9 = Utf8 Ljava/util/Date;
#10 = Utf8 num1
#11 = Utf8 Ljava/math/BigDecimal;
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 test
#17 = Utf8 SourceFile
#18 = Utf8 Main.java
#19 = NameAndType #12:#13 // "<init>":()V
#20 = Utf8 java/util/Date
#21 = Utf8 java/math/BigDecimal
#22 = NameAndType #12:#25 // "<init>":(I)V
#23 = Utf8 cn/howardliu/Main
#24 = Utf8 java/lang/Object
#25 = Utf8 (I)V
{
public cn.howardliu.Main();
descriptor: ()V
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 12: 0
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: new #2 // class java/util/Date
3: dup
4: invokespecial #3 // Method java/util/Date."<init>":()V
7: astore_1
8: new #4 // class java/math/BigDecimal
11: dup
12: iconst_0
13: invokespecial #5 // Method java/math/BigDecimal."<init>":(I)V
16: astore_2
17: return
LineNumberTable:
line 17: 0
line 18: 8
line 19: 17
}
SourceFile: "Main.java"
從 class 檔案内容可以看出:
按需類型導入方式在 class 檔案中的表現形式,與按類型導入一樣,也會找到需要的類導入,不會導入包中的所有類。
不需要的類導入聲明,最終都會被優化掉,不會出現在 class 檔案中。
java 中的import與 C 語言中的include不同,不會将導入聲明的類寫入到 class 檔案中,各自還是獨立的 class 檔案。
JDK 推薦哪種方式
JDK 絕對是 java 程式設計的标杆,我們很多都可以從 JDK 中學習:
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.io.OutputStreamWriter;
import java.io.BufferedWriter;
import java.security.AccessController;
import java.security.PrivilegedAction;
import sun.util.spi.XmlPropertiesProvider;
這是java.util.Properties中的 import 聲明,可以看出,使用了單類型導入聲明,是以,在沒有其他要求的情況下,我們盡量還是使用單類型導入。
文末思考
java 的import是類導入聲明,不會将檔案寫入到編譯後的 class 檔案中
java 的import有兩種導入方式:單類型導入、按需類型導入
按需類型導入隻會在編譯過程中有性能損失,在運作期與單類型導入無差别
JDK 源碼中,大部分使用了單類型導入。