本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记
使用任何语言进行编程都有一个类似的问题,那就是如何组织代码。具体来说,是如何避免命名冲突?如何合理组织各种源文件?如何使用第三方库?各种代码和依赖库如何编译链接为一个完整的程序?本节讨论Java
中的解决机制,具体包括包、jar
包、程序的编译与链接等。
使用任何语言进行编程都需要解决一个相同的问题,那就是如何解决命名冲突。实际开发中,程序一般不全是一个人写的,会调用系统提供的代码、第三方库中的代码、项目中其他人写的代码等,不同的人就不同的目的可能定义同样的类名或者接口名,Java
中解决这个问题的主要方法就是包。Java
中组织类和接口的方式也是包。
包是一个比较容易理解的概念,类似于计算机中的文件夹,正如我们在计算机中管理文件,文件放在文件夹中一样,类和接口放在包中,为便于组织,文件夹一般是一个层次结构,包也类似。包有包名,这个名称以点号(.
)分隔表示层次结构。比如,我们经常使用的String
类就位于包java.lang
下,其中java
是上层包名,lang
是下层包名。带完整包名的类名称为其完全限定名,比如String
类的完全限定名为java.lang.String
。Java API
中所有的类和接口都位于包Java
或javax
下,java
是标准包,javax
是扩展包。
1、声明类所在的包
定义类的时候,应该先使用关键字package
声明其包名,如下所示:
package com.ieening;
public class Hello {
//类的定义
}
以上声明类Hello
的包名为com.ieening
,包声明语句应该位于源代码的最前面,前面不能有注释外的其他语句。包名和文件目录结构必须匹配,如果源文件的根目录为E:\src\
,则上面的Hello
类对应的文件Hello.java
,其全路径就应该是E:\src\com\ieening\Hello.java
。如果不匹配,Java
会提示编译错误。为避免命名冲突,Java
中命名包名的一个惯例是使用域名作为前缀,因为域名是唯一的,一般按照域名的反序来定义包名,比如,域名是apache.org
,包名就以org.apache
开头。没有域名的也没关系,使用一个其他代码不太会用的包名即可。
除了避免命名冲突,包也是一种方便组织代码的机制。一般而言,同一个项目下的所有代码都有一个相同的包前缀,这个前缀是唯一的,不会与其他代码重名,在项目内部,根据不同目的再细分为子包,子包可能又会分为下一级子包,形成层次结构,内部实现一般位于比较底层的包。包可以方便模块化开发,不同功能可以位于不同包内,不同开发人员负责不同的包。包也可以方便封装,供外部使用的类可以放在包的上层,而内部的实现细节则可以放在比较底层的子包内。
2、通过包使用类
同一个包下的类之间互相引用是不需要包名的,可以直接使用。但如果类不在同一个包内,则必须要知道其所在的包。使用有两种方式:一种是通过类的完全限定名;另外一种是将用到的类引入当前类。但也有一个例外,java.lang
包下的类可以直接使用,不需要引入,也不需要使用完全限定名,比如String
类、System
类,其他包内的类则不行。看个例子,使用Arrays
类中的sort
方法,通过完全限定名可以这样使用:
int[] arr = new int[]{1,4,2,3};
java.util.Arrays.sort(arr);
System.out.println(java.util.Arrays.toString(arr));
显然,这样比较烦琐,另外一种就是将该类引入当前类。引入的关键字是import
,import
需要放在package
定义之后,类定义之前,如下所示:
package com.ieening;
import java.util.Arrays;
public class Hello {
public static void main(String[] args) {
int[] arr = new int[]{1,4,2,3};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
做import
操作时,可以一次将某个包下的所有类引入,语法是使用.*
,比如,将java.util
包下的所有类引入,语法是:import java.util.*
。需要注意的是,这个引入不能递归,它只会引入java.util
包下的直接类,而不会引入java.util
下嵌套包内的类,比如,不会引入包java.util.zip
下面的类。试图嵌套引入的形式也是无效的,如import java.util.*.*
。
在一个类内,对其他类的引用必须是唯一确定的,不能有重名的类,如果有,则通过import
只能引入其中的一个类,其他同名的类则必须要使用完全限定名。
从JDK 5
以后,增加了一种静态导入的新语法,它有一个static
关键字,可以直接导入类的公开静态方法和成员。看个例子:
import java.util.Arrays;
import static java.util.Arrays.*; //静态导入Arrays中的所有静态方法
import static java.lang.System.out; //导入静态变量out
public class Hello {
public static void main(String[] args) {
int[] arr = new int[]{1,4,2,3};
sort(arr); //可以直接使用Arrays中的sort方法
out.println(Arrays.toString(arr)); //可以直接使用out变量
}
}
3、包范围可见性
前面章节我们介绍过,对于类、变量和方法,都可以有一个可见性修饰符public/private
,我们还提到,可以不写修饰符。如果什么修饰符都不写,它的可见性范围就是同一个包内,同一个包内的其他类可以访问,而其他包内的类则不可以访问。需要说明的是,同一个包指的是同一个直接包,子包下的类并不能访问。比如,类com.ieening.Hello
和com.ieening.inner.Test
,其所在的包com.ieening
和com.ieening.inner
是两个完全独立的包,并没有逻辑上的联系,Hello
类和Test
类不能互相访问对方的包可见性方法和属性。除了public
和private
修饰符,还有一个与继承有关的修饰符protected
。protected
可见性包括包可见性,也就是说,声明为protected
不仅表明子类可以访问,还表明同一个包内的其他类可以访问,即使这些类不是子类也可以。总结来说,可见性范围从小到大是:private < default(package) < protected < public
。
4、常用包介绍
基础的常用包有以下几个。
java.lang
:包含Java
的核心的基础类型,如String
、Math
、Integer
、System
类等。java.net
:包含执行与网络相关的API
。java.io
:包含能表示文件和目录的File
类及各种数据读/写相关的功能类。java.util
:包含实用工具类,如集合框架类、数组工具类、旧版时间日期API等。java.time
:包含新版日期时间API
。java.text
:包含Java
文本格式化相关的API
。java.lang.reflect
:包含一些与反射相关的API
。java.sql
:包含Java
进行JDBC
数据库编程的相关API
。java.util.function
:是Java 8
新增的包,包含与函数式编程相关的接口。java.util.stream
:也是Java 8
新增的包,包含与Stream
相关的API
。为方便使用第三方代码,也为了方便我们写的代码给其他人使用,各种程序语言大多有打包的概念,打包的一般不是源代码,而是编译后的代码。打包将多个编译后的文件打包为一个文件,方便其他程序调用。
在Java
中,编译后的一个或多个包的Java class
文件可以打包为一个文件,Java
中打包命令为jar
,打包后的文件扩展名为.jar
,一般称之为jar
包。可以使用如下方式打包,首先到编译后的java class
文件根目录,然后运行如下命令:
jar -cvf <包名>.jar <最上层包名>
比如,对前面介绍的类打包,如果Hello.class
位于E:\bin\com\ieening\Hello.class
,则可以到目录E:\bin
下,然后运行:
jar -cvf hello.jar com
hello.jar
就是jar
包,jar
包其实就是一个压缩文件,可以使用解压缩工具打开。Java
类库、第三方类库都是以jar
包形式提供的。如何使用jar
包呢?将其加入类路径(classpath
)中即可。类路径是什么呢?我们下面来看。
从Java
源代码到运行的程序,有编译和链接两个步骤。编译是将源代码文件变成扩展名是.class
的一种字节码,这个工作一般是由javac
命令完成的。链接是在运行时动态执行的,.class
文件不能直接运行,运行的是Java
虚拟机,虚拟机听起来比较抽象,执行的就是Java
命令,这个命令解析.class
文件,转换为机器能识别的二进制代码,然后运行。所谓链接就是根据引用到的类加载相应的字节码并执行。
Java
编译和运行时,都需要以参数指定一个classpath
,即类路径。类路径可以有多个,对于直接的class
文件,路径是class
文件的根目录;对于jar
包,路径是jar
包的完整名称(包括路径和jar
包名)。在Windows
系统中,多个路径用分号;
分隔;在其他系统中,以冒号:
分隔。
在Java
源代码编译时,Java
编译器会确定引用的每个类的完全限定名,确定的方式是根据import
语句和classpath
。如果导入的是完全限定类名,则可以直接比较并确定。如果是模糊导入(import
带.*
),则根据classpath
找对应父包,再在父包下寻找是否有对应的类。如果多个模糊导入的包下都有同样的类名,则Java
会提示编译错误,此时应该明确指定导入哪个类。
Java
运行时,会根据类的完全限定名寻找并加载类,寻找的方式就是在类路径中寻找,如果是class
文件的根目录,则直接查看是否有对应的子目录及文件,如果是jar
文件,则首先在内存中解压文件,然后再查看是否有对应的类。总结来说,import
是编译时概念,用于确定完全限定名,在运行时,只根据完全限定名寻找并加载类,编译和运行时都依赖类路径,类路径中的jar
文件会被解压缩用于寻找和加载类。
马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎
尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎