目录
包括:抽象类、接口、静态字段、静态方法、包、作用域、内部类、Jar、JDK、模块等
输出结果都写在了注释中
1. 抽象类(abstract class)
如果父类的方法不需要具体实现,仅用于强制子类进行覆写,就可以将方法声明为抽象方法。
public class demo23 {
public static void main(String[] args){
// Person p = new Person; 编译错误:抽象类不能被实例化
// 尽量引用抽象类型而不是具体子类类型
Person p1 = new Student();
Person p2 = new Teacher();
p1.run();
p2.run();
}
}
// 抽象方法必须定义在抽象类中,因此Person类也必须是抽象类
abstract class Person{
public abstract void run();
}
class Student extends Person{
@Override
public void run(){
System.out.println("Student.run");
}
}
class Teacher extends Person{
@Override
public void run(){
System.out.println("Teacher.run");
}
}
面向抽象编程的本质:
- 上层代码只定义规范(抽象类或接口)
- 下层子类提供实际逻辑
- 上层无需依赖具体子类即可实现功能
2. 接口(interface)
接口是比抽象类更“抽象”的类型。
接口中:
-
所有方法默认是
public abstract
(可省略不写) -
不能定义实例字段
-
可以包含
default
方法(带有默认实现),static
方法,private
方法
abstract class Person {
public abstract void run();
public abstract String getName();
}
如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口:interface。
特性 | 抽象类 (abstract class ) | 接口 (interface ) |
---|---|---|
继承方式 | 只能 extends 一个类 | 可 implements 多个接口 |
字段 | 可以定义实例字段 | 不能定义实例字段 |
抽象方法 | 支持 | 支持(默认即为抽象方法) |
普通方法 | 支持(可访问字段) | 支持 default 方法(不能访问字段) |
多继承支持 | 不支持 | 支持多接口继承 |
// 使用interface可以声明一个接口
interface Person{
String getName();
// 在接口中定义默认实现的方法,使用 default 关键字
// 实现类可以选择是否重写该方法
default void run(){
System.out.println(getName() + "run");
}
}
// 一个接口可以使用extends关键字继承自另一个接口
// 子接口会继承父接口的所有方法签名
interface Hello extends Person{
String getHello();
}
// 当一个具体的class去实现一个interface时,需要使用implements关键字
// 一个类可以实现多个interface
class Student implements Person,Hello{
private String name;
public Student(String name){
this.name = name;
}
@Override
public String getName(){
return this.name;
}
@Override
public String getHello(){
return this.name + "Hello";
}
}
3. 静态字段和静态方法
public class demo25 {
public static void main(String[] args){
Person p1 = new Person("Xiaoming",15);
Person p2 = new Person("Xiaohong",12);
// 静态字段只有一个共享“空间”,所有实例都会共享该字段
p1.number = 88;
System.out.println(p2.number);
// 88
p2.number = 99;
System.out.println(p1.number);
// 99
// 调用静态方法则不需要实例变量,通过类名就可以调用
Person.setNumber(100);
System.out.println(Person.number);
// 100
}
}
class Person{
// 实例字段
// 特点是每个实例都有独立的字段,各个实例的同名字段互不影响
public String name;
public int age;
// 静态字段,用static修饰
public static int number;
public Person(String name,int age){
this.name = name;
this.age = age;
}
// 静态方法,用static修饰
// 静态方法属于class而不属于实例
// 无法访问this变量,也无法访问实例字段,只能访问静态字段
public static void setNumber(int value){
number = value;
}
}
不推荐用实例变量.静态字段
去访问静态字段,因为在Java程序中,实例对象并没有静态字段,推荐用类名来访问静态字段。可以把静态字段理解为描述class
本身的字段。
Person.number = 99;
System.out.println(Person.number);
静态方法经常用于工具类。例如:Arrays.sort()、Math.random()
接口的静态字段
因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型。
public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}
因为interface的字段只能是public static final类型,所以可以把修饰符都去掉。
public interface Person {
// 编译器会自动加上public static final:
int MALE = 1;
int FEMALE = 2;
}
4. 包(package)
在大型项目中,不同开发者可能会创建同名类(如 Person
、Arrays
)。为避免类名冲突,Java使用包机制来区分类的命名空间。
例如:
- 小明的类:
ming.Person
- 小红的类:
hong.Person
- 小军定义的类:
mr.jun.Arrays
- JDK 自带类:
java.util.Arrays
Java虚拟机识别类时依赖完整类名(包名 + 类名) ,因此只要包名不同,类就是不同的。
在 Java 中,使用 package
关键字声明类所属的包,声明必须放在源文件的第一行。
// 文件:ming/Person.java
package ming;
public class Person {
}
包名对应目录结构,编译时和运行时都必须遵守。
project_root
├── src
│ ├── hong
│ │ └── Person.java
│ ├── ming
│ │ └── Person.java
│ └── mr
│ └── jun
│ └── Arrays.java
├── bin // 编译后的.class文件放此目录下
如果没有显式使用 public
、protected
、private
修饰符,则字段或方法具有包访问权限(package-private):只能被同一包中的类访问。
import语法
1.完整类名使用
mr.jun.Arrays arrays = new mr.jun.Arrays();
2.使用 import
导入类
import mr.jun.Arrays;
Arrays arrays = new Arrays();
3.通配符导入整个包下的类(不推荐)
import mr.jun.*;
Arrays arrays = new Arrays();
我们一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays类属于哪个包。
4.import static
导入静态成员
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
}
import static
很少使用。
当引用一个类名时,编译器依次查找:
- 当前包中是否存在该类;
import
导入的包中是否包含该类;java.lang
包是否包含该类。
为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。
org.apache
org.apache.commons.log
com.liaoxuefeng.sample
避免和 java.lang
包中的类重名(如 String
、System
、Runtime
)
避免与常用 JDK 类重名(如 List
、Map
、Format
)
编译和运行
假设目录结构如下:
work/
├── bin/
└── src/
└── com/
└── itranswarp/
├── sample/
│ └── Main.java
└── world/
└── Person.java
其中,bin目录用于存放编译后的class文件,src目录按包结构存放Java源码
1.编译所有源文件(Linux/macOS)
确保当前目录是work
目录,然后,编译src
目录下的所有Java文件。
cd work
javac -d ./bin src/**/*.java
命令行-d
指定输出的class
文件存放bin
目录,后面的参数src/**/*.java
表示src
目录下的所有.java
文件,包括任意深度的子目录。
2.编译所有源文件(Windows PowerShell)
Windows不支持**
这种搜索全部子目录的做法,所以在Windows下编译必须依次列出所有.java
文件。
C:\work> javac -d bin src\com\itranswarp\sample\Main.java src\com\itranswarp\world\Persion.java
可以利用Get-ChildItem
来列出指定目录下的所有.java
文件
cd C:\work
javac -d .\bin (Get-ChildItem -Path .\src -Recurse -Filter *.java).FullName
如果编译无误,可以在bin目录下看到如下class文件:
bin
└── com
└── itranswarp
├── sample
│ └── Main.class
└── world
└── Person.class
运行主类:
java -cp ./bin com.itranswarp.sample.Main
输出:
Hello, world!
5. 作用域
修饰符 | 同类 | 同包 | 子类(不同包) | 其他包 | 适用结构 | 说明 |
---|---|---|---|---|---|---|
public | ✔️ | ✔️ | ✔️ | ✔️ | 类、方法、字段 | 完全公开;.java 文件中最多一个,且文件名须与 public 类名一致。 |
protected | ✔️ | ✔️ | ✔️ | ❌ | 方法、字段 | 对包内可见,且对子类(跨包)可见,用于继承场景。 |
(无修饰符) | ✔️ | ✔️ | ❌ | ❌ | 类、方法、字段 | 包级私有,仅限同包访问。 |
private | ✔️ | ❌ | ❌ | ❌ | 方法、字段 | 仅在本类内部可见;嵌套类可访问外部类 private 成员。 |
局部变量作用域
局部变量定义在方法内部,其作用域从声明开始,到代码块结束。
方法参数也是局部变量。
void hi(String name) { // 参数 name 作用域:1~10
String s = name.toLowerCase(); // s 作用域:2~10
int len = s.length(); // len 作用域:3~10
if (len < 10) {
int p = 10 - len; // p 作用域:5~9
for (int i = 0; i < 10; i++) {
System.out.println(); // i 作用域:6~8
}
}
}
final修饰符
用法 | 作用对象 | 效果 |
---|---|---|
final class | 类 | 类不可被继承 |
final method | 方法 | 方法不可被子类重写 |
final field | 成员字段 | 字段只能赋值一次 |
final 变量/参数 | 局部变量或参数 | 变量/参数值不可修改 |
注意:
如果不确定是否需要public
,就不声明为public
,即尽可能少地暴露对外的字段和方法。
把方法定义为package
权限有助于测试,因为测试类和被测试类只要位于同一个package
,测试代码就可以访问被测试类的package
权限方法。
一个.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同。
6. 内部类(Nested Class)
在 Java 中,我们通常将类组织在不同的包中,同一包下的类彼此独立,没有父子关系。
java.lang
├── Math
├── Runnable
├── String
└── ...
除了普通类,Java 还支持 内部类(Nested Class) ,即定义在另一个类内部的类。内部类分为三种:
- Inner Class(成员内部类)
- Anonymous Class(匿名类)
- Static Nested Class(静态内部类)
6.1 Inner Class(成员内部类)
class Outer {
class Inner {
// 内部类定义
}
}
特点:
- 内部类的实例必须依附于外部类的实例创建。
- 内部类可以访问外部类的私有字段和方法。
- 外部类实例可通过
Outer.Inner inner = outer.new Inner();
创建内部类实例。 - 编译后生成
Outer.class
和Outer$Inner.class
。
示例:
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
class Inner {
void hello() {
System.out.println("Hello, " + Outer.this.name);
}
}
}
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested");
Outer.Inner inner = outer.new Inner();
inner.hello();
}
}
6.2 Anonymous Class(匿名内部类)
用于快速实现接口或继承类的临时对象,通常在方法内部定义,无需命名。
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
特点:
- 本质上是 Inner Class 的一种特殊写法。
- 必须在定义时立即实例化。
- 可访问外部类的私有字段。
- 编译后生成如
Outer$1.class
的匿名类文件。
示例 1:实现接口
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested");
// 启动线程,内部会调用 r.run(),而不是直接调用 run() 方法
outer.asyncHello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
// 用Runnable创建一个线程,并启动它
new Thread(r).start();
}
}
观察asyncHello()
方法,我们在方法内部实例化了一个Runnable
。Runnable
本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable
接口的匿名类,并且通过new
实例化该匿名类,然后转型为Runnable
。
之所以我们要定义匿名类,是因为在这里我们通常不关心类名,比直接定义Inner Class可以少写很多代码。
示例 2:继承类
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, String> map1 = new HashMap<>();
HashMap<String, String> map2 = new HashMap<>() {}; // 匿名类!
HashMap<String, String> map3 = new HashMap<>() {
{
put("A", "1");
put("B", "2");
}
};
System.out.println(map3.get("A"));
// 1
}
}
通过 new HashMap<>() { … }
,我们创建了 HashMap
的一个匿名子类。整个大括号 {}
中的内容,都是这个子类的类体。
map1
是一个普通的HashMap
实例,但map2
是一个匿名类实例,只是该匿名类继承自HashMap
。map3
也是一个继承自HashMap
的匿名类实例,并且添加了static
代码块来初始化数据。
匿名类无法声明命名构造方法,所以如果你想在对象创建时执行一段自定义逻辑,就需要用双层大括号中的内层 { … }
,这就是 Java 的实例初始化块。
- 第一次
{
是匿名类的类体开始。 - 第二次
{
是匿名类的初始化块。
这种写法通常被称为“双括号初始化”,它等价于:
// 定义一个 HashMap 子类
class MyMap extends HashMap<String, String> {
// 实例初始化块
{
put("A", "1");
put("B", "2");
}
}
// 创建子类实例
HashMap<String, String> map = new MyMap();
6.3 Static Nested Class(静态内部类)
class Outer {
static class StaticNested {
// 静态内部类定义
}
}
特点:
- 使用
static
修饰,不依赖外部类实例即可创建。 - 无法访问外部类的非静态成员。
- 可访问外部类的私有静态成员。
- 更类似于顶级类,只是作用域限定在外部类内部。
- 编译后生成
Outer$StaticNested.class
。
示例:
class Outer {
private static String NAME = "OUTER";
static class StaticNested {
void hello() {
System.out.println("Hello, " + NAME);
}
}
}
public class Main {
public static void main(String[] args) {
Outer.StaticNested sn = new Outer.StaticNested();
sn.hello();
}
}
用static
修饰的内部类和Inner Class有很大的不同,它不再依附于Outer
的实例,而是一个完全独立的类,因此无法引用Outer.this
,但它可以访问Outer
的private
静态字段和静态方法。如果把StaticNested
移到Outer
之外,就失去了访问private
的权限。
总结
特性/类型 | Inner Class(成员内部类) | Anonymous Class(匿名类) | Static Nested Class(静态内部类) |
---|---|---|---|
是否依附外部类实例 | 是,需要先创建外部类实例后才能创建内部类实例 | 是,在外部类方法中定义并依附外部类实例 | 否,是静态的,可以脱离外部类实例直接创建 |
是否可以是 static | 否,不能声明为 static | 否,匿名类不能声明为 static | 是,本身就是 static 的类 |
是否有类名 | 有,具名类 | 无,在定义时直接实例化 | 有,具名类 |
是否可以继承类或实现接口 | 可以继承类或实现接口 | 通常用于实现接口或继承一个类 | 可以继承类或实现接口 |
访问外部类的能力 | 可访问外部类的所有成员(包括 private 字段和方法) | 可访问外部类的所有成员(包括 private 字段和方法) | 只能访问外部类的 static 成员(包括 private static 字段和方法) |
是否能定义构造方法 | 可以定义自己的构造方法 | 不可以定义构造方法,只能使用初始化代码块 | 可以定义自己的构造方法 |
用途 | 用于需要多个类共享外部类状态,且逻辑上属于外部类的一部分的场景 | 用于临时实现接口或继承类,且不需要复用类名时;常用于事件回调、线程等 | 用于将某些与外部类逻辑相关但不依赖外部类实例的类组织在一起,如工具类、静态常量类等 |
创建方式 | Outer.Inner inner = outer.new Inner(); | Runnable r = new Runnable() { public void run() {...} }; | Outer.StaticNested nested = new Outer.StaticNested(); |
编译后的文件名 | Outer$Inner.class | Outer$1.class 、Outer$2.class (多个匿名类会依次编号) | Outer$StaticNested.class |
是否支持多层嵌套 | 支持,可以在内部类中再定义内部类 | 支持,可以嵌套定义匿名类 | 支持,可以在静态内部类中定义其他类 |
7. Classpath和Jar
Java 是编译型语言,.java
文件编译后变为 .class
文件,JVM 执行的是 .class
字节码文件。因此,JVM 需要知道从哪里加载这些 .class
文件。
Classpath 是 JVM 的一个环境变量,用于 指示 JVM 如何查找 .class
文件,也就是告诉 JVM 去哪里加载某个类。
7.1 Classpath的搜索过程
Classpath 是一组目录或 JAR 文件的集合,其格式与操作系统有关:
Windows 系统:使用 ;
分隔,带空格的路径用引号包裹
.;C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"
Linux 系统:使用 :
分隔
.:/usr/local/bin:/home/user/bin
.
表示当前目录。
假设 classpath 为:
.;C:\work\project1\bin;C:\shared
要加载类 abc.xyz.Hello
时,JVM 会查找以下路径:
.\abc\xyz\Hello.class
C:\work\project1\bin\abc\xyz\Hello.class
C:\shared\abc\xyz\Hello.class
一旦找到,不再继续搜索;找不到就报错。
7.2 Classpath的设置
classpath的设置方式有两种:
推荐方式:运行时指定 classpath
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
不推荐方式:设置系统环境变量 CLASSPATH
- 容易污染整个系统环境
- 影响其他项目
- 难以维护
如果没有显式设置,JVM 的默认 classpath 是当前目录。
java abc.xyz.Hello
IDE(如 IntelliJ IDEA、Eclipse)在运行 Java 程序时,会自动为你添加 classpath 参数,包括:
- 当前项目的
bin
或out
目录 - 所有依赖的 JAR 包
JVM 不依赖 classpath 加载 Java 核心类库(如 String
、ArrayList
),而是通过自己的机制加载,不需要你手动添加 rt.jar
。
切记:不要把 Java 核心库加入 classpath!
7.3 创建Jar包
Jar 是 Java Archive 的缩写,实质上是一个 ZIP 格式压缩包,方便分发、备份、发布多个 .class
文件。
运行:
java -cp ./hello.jar abc.xyz.Hello
JVM 会在 jar 文件中按包路径查找类。
手动创建Jar包(适用于简单项目):
因为jar包就是zip包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip
改为.jar
,一个jar包就创建成功。
- 保证包路径正确,如:
hong/Person.class
ming/Person.class
mr/jun/Arrays.class
注意:不要把这些文件放在 bin/
目录内打包,否则路径会错误。
- 压缩为 ZIP 文件 → 修改后缀为
.jar
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF
文件,MANIFEST.MF
是纯文本,可以指定Main-Class
和其它信息。JVM会自动读取这个MANIFEST.MF
文件,如果存在Main-Class
,我们就不必在命令行指定启动的类名,而是用更方便的命令:
java -jar hello.jar
在大型项目中,不可能手动编写MANIFEST.MF
文件,再手动创建jar包。Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。
8. JDK与Class版本
在 Java 开发中,不同版本的 JDK(Java Development Kit)会带来不同的 class 文件版本,所谓的 Java 8、Java 11、Java 17,是指 JDK 的版本,准确来说是 java.exe
(即 JVM) 的版本。
每个 JDK 编译器默认生成的 .class
文件有一个固定的版本号:
JDK 版本 | class 文件版本 |
---|---|
Java 8 | 52 |
Java 11 | 55 |
Java 17 | 61 |
可通过以下命令查看当前 JDK 版本:
$ java -version
高版本 JVM 可以运行低版本 class 文件(向下兼容)。
低版本 JVM 无法运行高版本 class 文件,运行时会报错:
java.lang.UnsupportedClassVersionError: Xxx has been compiled by a more recent version of the Java Runtime...
只要看到UnsupportedClassVersionError
就表示当前要加载的class文件版本超过了JVM的能力,必须使用更高版本的JVM才能运行。
指定编译输出的 class 文件版本:
方式一: 使用 --release
参数(推荐)
$ javac --release 11 Main.java
参数--release 11
表示源码兼容Java 11,编译的class输出版本为Java 11兼容,即class版本55。
方式二: 使用 --source
和 --target
$ javac --source 9 --target 11 Main.java
--source
: 源代码版本
--target
: 输出 class 文件的版本
上述命令如果使用Java 17的JDK编译,它会把源码视为Java 9兼容版本,并输出class为Java 11兼容版本。注意--release
参数和--source
--target
参数只能二选一,不能同时设置。
此方式不会验证 API 是否在目标版本中存在,可能出现运行时错误。
public class Hello {
public static void hello(String name) {
System.out.println("hello".indent(4));
}
}
方法 String.indent()
是 Java 12 引入的,如果使用 Java 17 编译(指定 --source 9 --target 11
)但在 Java 11 JVM 上运行,会出现:
NoSuchMethodError: java.lang.String.indent
使用 --release 11
编译时,会在编译阶段就报错,避免了这种情况。
多版本 JDK 可并存,通过 JAVA_HOME
和 PATH
控制当前使用版本
# 临时切换 JDK 版本示例
$ export JAVA_HOME=/path/to/jdk11
$ export PATH=$JAVA_HOME/bin:$PATH
总结:
场景 | 推荐做法 |
---|---|
控制兼容性 | 使用 --release 参数 |
检查 API 是否存在 | 使用 --release 而不是 --source/--target |
编译运行一致性 | 编译版本不高于运行时 JDK |
多版本管理 | 通过 JAVA_HOME 管理多个 JDK |
9. 模块(Module)
模块是带有依赖声明和导出信息的类容器,Java 9 起,JDK 自带模块不再是 rt.jar,而是以 .jmod
存在于 $JAVA_HOME/jmods
目录下,所有模块都依赖 java.base
,它是根模块。
Java 9 之前,程序是由多个 .class
文件组成,通过 jar 打包,这会导致依赖管理混乱:
- 需要手动指定 classpath;
- 若漏写 jar 包路径,运行时报
ClassNotFoundException
; jar
文件只是容器,不具备依赖信息。
模块的目的就是解决依赖关系管理混乱的问题、支持 JRE 按需裁剪(瘦身)、增强访问权限控制。
9.1 创建模块
以oop-module
工程为例,它的目录结构如下:
oop-module
├── bin
├── build.sh
└── src
├── com
│ └── itranswarp
│ └── sample
│ ├── Greeting.java
│ └── Main.java
└── module-info.java
其中,bin
目录存放编译后的class文件,src
目录存放源码,按包名的目录结构存放,仅仅在src
目录下多了一个module-info.java
这个文件,这就是模块的描述文件。
module-info.java
示例:
module hello.world {
requires java.base; // 可不写,任何模块都会自动引入java.base
requires java.xml; // 声明依赖关系
}
其中,module
是关键字,后面的hello.world
是模块的名称,它的命名规范与包一致。花括号的requires xxx;
表示这个模块需要引用的其他模块名。除了java.base
可以被自动引入外,这里我们引入了一个java.xml
的模块。
9.2 编译与打包模块
首先,我们把工作目录切换到oop-module
,在当前目录下编译所有的.java
文件,并存放到bin目录下。
编译命令:
javac -d bin src/module-info.java src/com/itranswarp/sample/*.java
如果编译成功,现在项目结构如下:
oop-module
├── bin
│ ├── com
│ │ └── itranswarp
│ │ └── sample
│ │ ├── Greeting.class
│ │ └── Main.class
│ └── module-info.class
└── src
├── com
│ └── itranswarp
│ └── sample
│ ├── Greeting.java
│ └── Main.java
└── module-info.java
打包成 jar:
把bin
目录下的所有class
文件先打包成jar
,在打包的时候,注意传入--main-class
参数,让这个jar
包能自己定位main
方法所在的类
jar --create --file hello.jar --main-class com.itranswarp.sample.Main -C bin .
现在我们就在当前目录下得到了hello.jar
这个jar包,它和普通jar包并无区别,可以直接使用命令java -jar hello.jar
来运行它。
转换为模块(jmod):
我们的目标是创建模块,所以,继续使用JDK自带的jmod
命令把一个jar包转换成模块
jmod create --class-path hello.jar hello.jmod
9.3 运行模块
使用 jar 运行:
java --module-path hello.jar --module hello.world
注意:不能直接运行 .jmod 文件:.jmod
是编译期使用的格式,运行时需使用 .jar
。
9.4 打包自定义 JRE(使用 jlink)
过去发布一个Java应用程序,要运行它,必须下载一个完整的JRE,再运行jar包。而完整的JRE块头很大,有100多M。
现在,JRE自身的标准库已经分拆成了模块,只需要带上程序用到的模块,其他的模块就可以被裁剪掉。
怎么裁剪JRE呢?并不是说把系统安装的JRE给删掉部分模块,而是“复制”一份JRE,但只带上用到的模块。为此,JDK提供了jlink
命令来干这件事。
裁剪 JRE 命令:
jlink \
--module-path hello.jmod \
--add-modules java.base,java.xml,hello.world \
--output jre/
我们在--module-path
参数指定了我们自己的模块hello.jmod
然后,在--add-modules
参数中指定了我们用到的3个模块java.base
、java.xml
和hello.world
,用,分隔
最后,在--output
参数指定输出目录
运行裁剪后的 JRE:
在当前目录下,我们可以找到jre
目录,这是一个完整的并且带有我们自己hello.jmod
模块的JRE
jre/bin/java --module hello.world
要分发我们自己的Java应用程序,只需要把这个jre
目录打个包给对方发过去,对方直接运行上述命令即可,既不用下载安装JDK,也不用知道如何配置我们自己的模块,极大地方便了分发和部署。
9.5 访问权限控制
class的这些访问权限只在一个模块内有效,模块和模块之间,例如,a模块要访问b模块的某个class,必要条件是b模块明确地导出了可以访问的包。
我们编写的模块hello.world
用到了模块java.xml
的一个类javax.xml.XMLConstants
,我们之所以能直接使用这个类,是因为模块java.xml
的module-info.java
中声明了若干导出:
module java.xml {
exports java.xml;
exports javax.xml.catalog;
exports javax.xml.datatype;
...
}
模块默认不导出任何包,要允许其他模块访问,需在 module-info.java
中使用 exports
:
module hello.world {
exports com.itranswarp.sample;
requires java.xml;
}
因此,模块进一步隔离了代码的访问权限。