7

java import 导入包时,我们需要注意什么呢?

 3 years ago
source link: https://xie.infoq.cn/article/e432cd576a77f38f55e64dd69
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

你好,我是看山。



这篇文章起因是 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.* :通过通配符 * 定义导入方式,但是并不是直接导入这个包下的所有类,而是可以导入所有类。也就是说,如果需要就导入,不需要就不导入。



有如下属性:


  1. java 以这样两种方式导入包中的任何一个 public 的类和接口(只有 public 类和接口才能被导入)

  2. 上面说到导入声明仅导入声明目录下面的类而不导入子包,这也是为什么称它们为类型导入声明的原因。

  3. 导入的类或接口的简名(simple name)具有编译单元作用域。这表示该类型简名可以在导入语句所在的编译单元的任何地方使用。这并不意味着你可以使用该类型所有成员的简名,而只能使用类型自身的简名。例如:java.lang 包中的 public 类都是自动导入的,包括 Math System 类。但是,你不能使用它们的成员的简名 PI() gc() , 而必须使用 Math.PI() System.gc() . 你不需要键入的是 java.lang.Math.PI() java.lang.System.gc()

  4. 程序员有时会导入当前包或 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



除了命名冲突,还有一些不太明显的缺点:


  1. 编译速度:因为按需导入机制的特性,需要在 CLASSPATH 下找到所有符合包名的类,在编译时会消耗性能。在小项目中,这个速度可以忽略。如果在大项目中,就会有明细差异。

  2. 可读性:在使用 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."<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:
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."<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:
line17:0
line18:8
line19:17
}
SourceFile:"Main.java"


从 class 文件内容可以看出:


  1. 按需类型导入方式在 class 文件中的表现形式,与按类型导入一样,也会找到需要的类导入,不会导入包中的所有类。

  2. 不需要的类导入声明,最终都会被优化掉,不会出现在 class 文件中。

  3. 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 声明,可以看出,使用了单类型导入声明,所以,在没有其他要求的情况下,我们尽量还是使用单类型导入。



文末思考


  1. java 的 import 是类导入声明,不会将文件写入到编译后的 class 文件中

  2. java 的 import 有两种导入方式:单类型导入、按需类型导入

  3. 按需类型导入只会在编译过程中有性能损失,在运行期与单类型导入无差别

  4. JDK 源码中,大部分使用了单类型导入。



你好,我是看山,公众号:看山的小屋,10 年老后端,Apache Storm、WxJava、Cynomys 开源贡献者。主业:程序猿,兼职:架构师。游于码界,戏享人生。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK