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 类:
packagecn.howardliu; // 需要用到的单类型导入 importjava.util.Date; // 需要用到的按需类型导入 importjava.math.*; // 不需要用到的单类型导入 importjava.sql.PreparedStatement; // 不需要用到的按需类型导入 importjava.awt.*; publicclassMain{ privateDate date1; privateBigDecimal num1; publicvoidtest(){ Date date2 =newDate(); BigDecimal num2 =newBigDecimal(0); } }
通过命令
javac Main.java
编译,然后通过
javap -verbose Main.class
查看编译结果:
Classfile /path/to/Main.class Last modified2021-1-31; size439bytes MD5 checksum 81e13559f738197b4875c2c2afd6fc41 Compiled from"Main.java" publicclasscn.howardliu.Main minorversion:0 majorversion:52 flags:ACC_PUBLIC, ACC_SUPER Constantpool: #1 = Methodref #7.#19 // java/lang/Object."":()V #2 = Class #20 // java/util/Date #3 = Methodref #2.#19 // java/util/Date."":()V #4 = Class #21 // java/math/BigDecimal #5 = Methodref #4.#22 // java/math/BigDecimal."":(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 #13 = Utf8 ()V #14 = Utf8 Code #15 = Utf8 LineNumberTable #16 = Utf8 test #17 = Utf8 SourceFile #18 = Utf8 Main.java #19 = NameAndType #12:#13 // "":()V #20 = Utf8 java/util/Date #21 = Utf8 java/math/BigDecimal #22 = NameAndType #12:#25 // "":(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."":()V 4:return LineNumberTable: line12: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."":()V 7: astore_1 8: new#4 // class java/math/BigDecimal 11: dup 12: iconst_0 13: invokespecial#5 // Method java/math/BigDecimal."":(I)V 16: astore_2 17:return LineNumberTable: line17:0 line18:8 line19:17 } SourceFile:"Main.java"
从 class 文件内容可以看出:
- 按需类型导入方式在 class 文件中的表现形式,与按类型导入一样,也会找到需要的类导入,不会导入包中的所有类。
- 不需要的类导入声明,最终都会被优化掉,不会出现在 class 文件中。
-
java 中的
import
与 C 语言中的
include
不同,不会将导入声明的类写入到 class 文件中,各自还是独立的 class 文件。
JDK 推荐哪种方式
JDK 绝对是 java 编程的标杆,我们很多都可以从 JDK 中学习:
importjava.io.IOException; importjava.io.PrintStream; importjava.io.PrintWriter; importjava.io.InputStream; importjava.io.OutputStream; importjava.io.Reader; importjava.io.Writer; importjava.io.OutputStreamWriter; importjava.io.BufferedWriter; importjava.security.AccessController; importjava.security.PrivilegedAction; importsun.util.spi.XmlPropertiesProvider;
这是
java.util.Properties
中的 import 声明,可以看出,使用了单类型导入声明,所以,在没有其他要求的情况下,我们尽量还是使用单类型导入。
文末思考
-
java 的
import
是类导入声明,不会将文件写入到编译后的 class 文件中
-
java 的
import
有两种导入方式:单类型导入、按需类型导入
- 按需类型导入只会在编译过程中有性能损失,在运行期与单类型导入无差别
- JDK 源码中,大部分使用了单类型导入。
你好,我是看山,公众号:看山的小屋,10 年老后端,Apache Storm、WxJava、Cynomys 开源贡献者。主业:程序猿,兼职:架构师。游于码界,戏享人生。