天天看點

java import 導入包時,我們需要注意什麼呢?

java import 導入包時,我們需要注意什麼呢?

你好,我是看山。

這篇文章起因是 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 源碼中,大部分使用了單類型導入。