作者:白色蜗牛
阅读本文你将收获:
类库与 JAR 文件
什么是类库
我们知道,在面向对象的程序设计里,一个类是可以调用另外一个类的方法,只要把被调用的那个类引入到 classpath
下就可以。
一个类当然好搞,但如果是很多类,都会被其他类重复使用到,并且可能有多个工程,其他开发者也需要,那么按类这个维度去加入 classpath
显然会很麻烦。
这种情况就需要把这些可以被重复使用的类打包,统一提供给使用方。这种打包好的类,就是类库(Class Library)。
类库是类的集合,可以被重复使用。
什么是 JAR 文件
类库只是一种概念,不同程序设计语言,表现形式不同。在 Java 中,一般以 JAR 文件的方式提供类库。
什么是 JAR 文件呢?
JAR (Java ARchive,Java 归档)是一种软件包文件格式,通常会聚合大量的 Java 类文件、相关的元数据和资源(文本、图片等)文件到一个文件,以便分发到 Java 平台应用软件或库。
JAR 文件就是 JAR 这种格式下的归档文件,以 ZIP 格式构建,以 .jar
为文件扩展名。用户可以使用 JDK 自带的 jar 命令创建或提取 JAR 文件。
JAR 文件创建和提取
我们演示下 JAR 文件的创建和提取过程。
假设你 Java 环境已经安装好,我们创建一个演示目录并进入:
mkdir jartest
cd jartest
创建一个 Java 文件,命名为 A.java:
vi A.java
编辑 Java 文件:
public class A {
}
再创建一个 Java 文件,命名为 B.java:
vi B.java
编辑 Java 文件:
public class B {
}
编译 Java 代码:
javac A.java B.java
我们发现已经生成了 .class 文件:
使用 JDK 自带的 jar 命令创建 JAR 文件:
jar cvf ab.jar A.class B.class
JAR 文件生成成功!
我们去一个新的目录提取下 JAR 文件!
mkdir xvf
cd xvf
jar xvf ../ab.jar
解压会得到以下内容:
我们看下 MANIFEST.MF
文件内容:
$ cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: 1.8.0_281 (Oracle Corporation)
Manifest 文件看着是创建 JAR 文件自动生成的,它起什么作用呢?
在 Java 平台中, Manifest 文件是 JAR 归档中所包含的特殊文件。Manifest 文件被用来定义扩展或文件打包相关数据。Manifest 文件是一个元数据文件,它包含键 - 值对(英语:Attribute–value pair)数据。如果一个 JAR 文件被当成可执行文件,那么其中的 Manifest 文件需要指出该程序的主类文件。通常 Manifest 文件的文件名为 MANIFEST.MF
。
JAR 文件的使用
假设 ab.jar 功能强大,即将提供给其他开发者使用,那怎么接入呢?
我们新建个目录,新建个使用类,尝试使用下 A 这个类:
mkdir usejar
cd usejar
vi UseJarDemo.java
编辑 Java 文件,我们 new
一个 A,表明要使用它:
public class UseJarDemo {
public static void main(String[] args) {
A a = new A();
}
}
保存退出,尝试编译:
javac UseJarDemo.java
我们看到编译没通过,它找不到 A 这个类,为什么呢?
因为我们没有把 A 这个类加入到 CLASSPATH
里!
既然我们学习了 JAR,我们就不像以前把单个类往类路径里加了,我们以 JAR 的形式添加,看下能不能正常使用!
打开环境配置文件(我的是 macOS 系统),使用命令vi ~/.bash_profile
在 CLASSPATH
后边追加 ab.jar
所在的路径,然后 source ~/.bash_profile
生效一下配置文件,
追加前
$ cat ~/.bash_profile
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$JAVA_HOME/bin:$PATH
追加后
$ cat ~/.bash_profile
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:/Users/woniu/jartest/ab.jar
export PATH=$JAVA_HOME/bin:$PATH
此时我们再编译一下 UseJarDemo.java
:
javac UseJarDemo.java
非常顺利,没有报错!说明我们成功用到了 JAR 中的 A!
$ ls
UseJarDemo.class UseJarDemo.java
Java 核心类库
上一小节我们把自己创建的 JAR 文件添加到 CLASSPATH
路径下的时候,眼尖的你可能发现我们的通用配置CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
里有两个 jar 非常醒目。
它们就是 dt.jar
和 tools.jar
,这俩都在 $JAVA_HOME/lib
下。这是 Java 官方把很多功能强大的类打包,做成类库,随着 JDK 一起发布。
dt.jar
dt 是 DesignTime 的缩写,翻译过来就是设计时。
为什么叫设计时呢?
因为 dt.jar
是面向图形用户界面(GUI)场景的,使用它你可以在开发环境通过添加控件、设置控件或窗体属性来设计你的图形化的应用程序。
dt.jar
是设计时环境的类库,主要是 swing
包。因此如果你的开发场景不涉及 GUI,是可以不引入这个包的。
tools.jar
tools.jar
是工具类库,运用在编译和运行以及其他场景。我们经常用的 javac、java 命令文件都很小,一般几十上百 KB。这是因为它们实际上只是一层代码的封装,这些工具的实现所要用到的类库都在 tools.jar 中。
观察 tools.jar
,我们会发现很多文件和 bin 目录下的可执行文件是有相对性的。除此以外,也提供了像 Applet 和 RMI 这样的文件,支持其他的一些功能。
rt.jar
rt.jar 不同于 dt.jar
和 tools.jar
的位置,它位于 $JAVA_HOME/jre/lib
下。
rt 是 RunTime 的缩写,翻译过来就是运行时。
为什么叫运行时呢?
因为它包含了所有已编译的类文件,包括引导类以及来自核心 Java API 的所有类,是 Java 运行时环境中所有核心 Java 类的集合。
rt.jar
是运行时环境的类库,当你编写程序用到很多系统类,比如 String
,System
这些类,其实都来自这个类库。JVM 在运行时从 rt.jar 访问所有这些类。如果类路径中没有包含 rt.jar,就无法访问 java.lang.String,java.util.ArrayList 和 Java API 中的所有其他类。
由于 rt.jar
中的所有类都是 JVM 已知,当 JVM 加载这些类时,会用单独的引导类加载器(Bootstrap ClassLoader)进行加载。这样做的好处就是,不需要做太多的基本安全检查,提升了性能。
有朋友可能会问,我自己定义的类路径和 rt.jar
冲突会发生什么呢?会不会加载我自己定义的类呢?比如我也定义了一个类 String,路径是 java.lang.String。
答案是不会!这就要提到 Java 虚拟机的类加载机制了。
Java 有四种类加载器,分别是
- 引导类加载器 Bootstrap ClassLoader,负责加载
$JAVA_HOME/jre/lib/rt.jar
下的类。 - 扩展类加载器 Extension ClassLoader,负责加载下
$JAVA_HOME/jre/lib/ext/*.jar
的类。 - 系统类加载器 System ClassLoader,负责加载
$CLASSPATH
下的类。 - 用户自定义加载器 User Defined ClassLoader。
Java 虚拟机对类文件的加载,采用的是双亲委派模式。双亲委派模式要求除了顶层的引导类加载器外,其余的类加载器都应当有自己的父类加载器。
但这里请注意,叫法上虽然是父类,实际上他们之间并非继承关系,而是采用组合关系来复用父类加载器的相关代码。
双亲委派的工作原理是,如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,一次递归,请求最终将到达顶层的引导类加载器。
如果父类加载器可以完成类加载任务,就成功返回,如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
你会发现,Java 类随着它的类加载器具备了带有优先级的层次关系,通过这种层级关系,可以避免类的重复加载,当父类已经加载了该类,就没必要子加载器再加载一次。
因此像 java.lang.String 这种 Java 核心 API,即便你同名,JVM 也会优先加载 rt.jar 里的,因为引导类加载器是最顶级的加载器。这样也避免了 Java 核心 API 被随意替换,保证了安全。
常用的 Java 类库 API
什么是 API
我们前边多次提到 Java API,那么什么是 API 呢?
API 的全称是 Application Programming Interface,翻译过来就是 应用程序接口。
假如我写了一个类,可以对输入文本进行翻译,这个类非常稳定且功能好用,如果你的项目中也需要这么一个功能。那你就不需要自己编写代码,直接把我的类拿来用就可以。但我又不想让别人看到内部实现,想要保护版权,怎么办呢?
这时候我可以将我的类编译,并附带一个文档,告诉你我的类怎么使用,有哪些方法,你只要按照文档说明来调用就可以。既节省了你编码实现的时间,也保护了我的版权。比如文本翻译的方法:
String translate(String text, String language)
像这种描述类的使用方法,就叫做 API。
Java API 也有说明文档,比如 Java SE 8:https://docs.oracle.com/javase/8/docs/api/index.html
常用的 API
介绍 Java 核心类库中常用的 API。
包名 | 包说明 | API | API 说明 |
java.lang | java 核心包,覆盖 Java 编程的基础类,JVM 自动导入无需手动导包。 | Object#equals(Object obj) | 判断其他某个对象是否与此对象“相等” |
Object#hashCode() | 获取调用对象的哈希码值 | ||
Object#toString() | 获取调用对象的字符串形式 | ||
String#length() | 获取字符串的长度 | ||
java.util | java 工具包,覆盖集合类和工具类。 | Date() | 根据当前系统时间来构造对象。 |
Collection#add(E e) | 向集合里添加对象 | ||
List#get(int index) | 从集合中获取指定位置元素 | ||
Queue#offer(E e) | 将一个对象添加至队尾 | ||
Set#iterator() | 用于获取当前集合中的迭代器对象,可以取出每个元素 | ||
Map#put(K key,V value); | 将key-value对存入Map,若集合中已经包含该key,则替换该Key所对应的Value,返回值为该Key原来所对应的Value,若没有则返回null(增加和修改) | ||
java.io | java 输入输出包,通过文件系统、数据流和序列化提供系统的输入和输出。 | File(String Pathname) | 根据参数指定的路径来构造对象 |
File#createNewFile() | 用于创建新的空文件 | ||
java.net | java 网络包,覆盖网络编程类。 | Socket#close() | 关闭 Socket |
java.sql | java 数据 API 包,覆盖操作数据库的所有类和接口。 | Connection#createStatement() | 创建向数据库发送 SQL 的语句 |
总结
本文介绍了类库的概念以及 JAR 文件的使用,重点讲解了 Java 中三个常见的 JAR 的概念,由来和作用,同时提到了 Java 的类加载机制是双亲委派模式,最后介绍了 API 的概念以及 Java 类库中常用到的一些 API。看完这篇文章,想必你对 Java 核心类库有了更深的了解。