跳转至

4 面向对象2

包括:抽象类、接口、静态字段、静态方法、包、作用域、内部类、Jar、JDK、模块等

输出结果都写在了注释中

1. 抽象类(abstract class

如果父类的方法不需要具体实现,仅用于强制子类进行覆写,就可以将方法声明为抽象方法。

Java
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");
    }
}

面向抽象编程的本质:

  1. 上层代码只定义规范(抽象类或接口)
  2. 下层子类提供实际逻辑
  3. 上层无需依赖具体子类即可实现功能

2. 接口(interface

接口是比抽象类更“抽象”的类型。

接口中:

  1. 所有方法默认是 public abstract(可省略不写)

  2. 不能定义实例字段

  3. 可以包含 default 方法(带有默认实现), static 方法, private 方法

Java
abstract class Person {
    public abstract void run();
    public abstract String getName();
}

如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口:interface。

特性 抽象类 (abstract class) 接口 (interface)
继承方式 只能 extends 一个类 implements 多个接口
字段 可以定义实例字段 不能定义实例字段
抽象方法 支持 支持(默认即为抽象方法)
普通方法 支持(可访问字段) 支持 default 方法(不能访问字段)
多继承支持 不支持 支持多接口继承
Java
// 使用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. 静态字段和静态方法

Java
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本身的字段。

Java
Person.number = 99;
System.out.println(Person.number);

静态方法经常用于工具类。例如:Arrays.sort()、Math.random()

接口的静态字段

因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型。

Java
public interface Person {
    public static final int MALE = 1;
    public static final int FEMALE = 2;
}

因为interface的字段只能是public static final类型,所以可以把修饰符都去掉。

Java
public interface Person {
    // 编译器会自动加上public static final:
    int MALE = 1;
    int FEMALE = 2;
}

4. 包(package)

在大型项目中,不同开发者可能会创建同名类(如 PersonArrays)。为避免类名冲突,Java使用包机制来区分类的命名空间。

例如:

  • 小明的类:ming.Person
  • 小红的类:hong.Person
  • 小军定义的类:mr.jun.Arrays
  • JDK 自带类:java.util.Arrays

Java虚拟机识别类时依赖完整类名(包名 + 类名) ,因此只要包名不同,类就是不同的。

在 Java 中,使用 package 关键字声明类所属的包,声明必须放在源文件的第一行。

Java
// 文件:ming/Person.java
package ming;

public class Person {

}

包名对应目录结构,编译时和运行时都必须遵守。

CSS
project_root
├── src
   ├── hong
      └── Person.java
   ├── ming
      └── Person.java
   └── mr
       └── jun
           └── Arrays.java
├── bin  // 编译后的.class文件放此目录下

如果没有显式使用 publicprotectedprivate 修饰符,则字段或方法具有包访问权限(package-private):只能被同一包中的类访问

import语法

1.完整类名使用

Java
mr.jun.Arrays arrays = new mr.jun.Arrays();

2.使用 import 导入类

Java
import mr.jun.Arrays;

Arrays arrays = new Arrays();

3.通配符导入整个包下的类(不推荐)

Java
import mr.jun.*;

Arrays arrays = new Arrays();

我们一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays类属于哪个包。

4.import static 导入静态成员

Java
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;

public class Main {
    public static void main(String[] args) {
        // 相当于调用System.out.println(…)
        out.println("Hello, world!");
    }
}

import static很少使用。

当引用一个类名时,编译器依次查找:

  1. 当前包中是否存在该类;
  2. import 导入的包中是否包含该类;
  3. java.lang 包是否包含该类。

为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。

Java
org.apache
org.apache.commons.log
com.liaoxuefeng.sample

避免和 java.lang 包中的类重名(如 StringSystemRuntime

避免与常用 JDK 类重名(如 ListMapFormat


编译和运行

假设目录结构如下:

CSS
work/
├── bin/
└── src/
    └── com/
        └── itranswarp/
            ├── sample/
               └── Main.java
            └── world/
                └── Person.java

其中,bin目录用于存放编译后的class文件,src目录按包结构存放Java源码

1.编译所有源文件(Linux/macOS)

确保当前目录是work目录,然后,编译src目录下的所有Java文件。

Bash
cd work
javac -d ./bin src/**/*.java

命令行-d指定输出的class文件存放bin目录,后面的参数src/**/*.java表示src目录下的所有.java文件,包括任意深度的子目录。

2.编译所有源文件(Windows PowerShell)

Windows不支持**这种搜索全部子目录的做法,所以在Windows下编译必须依次列出所有.java文件。

Bash
C:\work> javac -d bin src\com\itranswarp\sample\Main.java src\com\itranswarp\world\Persion.java

可以利用Get-ChildItem来列出指定目录下的所有.java文件

Bash
cd C:\work
javac -d .\bin (Get-ChildItem -Path .\src -Recurse -Filter *.java).FullName

如果编译无误,可以在bin目录下看到如下class文件:

CSS
bin
└── com
    └── itranswarp
        ├── sample
           └── Main.class
        └── world
            └── Person.class

运行主类:

Bash
java -cp ./bin com.itranswarp.sample.Main

输出:

Bash
Hello, world!

5. 作用域

修饰符 同类 同包 子类(不同包) 其他包 适用结构 说明
public ✔️ ✔️ ✔️ ✔️ 类、方法、字段 完全公开;.java 文件中最多一个,且文件名须与 public 类名一致。
protected ✔️ ✔️ ✔️ 方法、字段 对包内可见,且对子类(跨包)可见,用于继承场景。
(无修饰符) ✔️ ✔️ 类、方法、字段 包级私有,仅限同包访问。
private ✔️ 方法、字段 仅在本类内部可见;嵌套类可访问外部类 private 成员。

局部变量作用域

局部变量定义在方法内部,其作用域从声明开始,到代码块结束。

方法参数也是局部变量。

Java
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 中,我们通常将类组织在不同的包中,同一包下的类彼此独立,没有父子关系。

CSS
java.lang
├── Math
├── Runnable
├── String
└── ...

除了普通类,Java 还支持 内部类(Nested Class) ,即定义在另一个类内部的类。内部类分为三种:

  • Inner Class(成员内部类)
  • Anonymous Class(匿名类)
  • Static Nested Class(静态内部类)

6.1 Inner Class(成员内部类)

Java
class Outer {
    class Inner {
        // 内部类定义
    }
}

特点:

  • 内部类的实例必须依附于外部类的实例创建。
  • 内部类可以访问外部类的私有字段和方法。
  • 外部类实例可通过 Outer.Inner inner = outer.new Inner(); 创建内部类实例。
  • 编译后生成 Outer.classOuter$Inner.class

示例:

Java
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(匿名内部类)

用于快速实现接口或继承类的临时对象,通常在方法内部定义,无需命名。

Java
Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

特点:

  • 本质上是 Inner Class 的一种特殊写法。
  • 必须在定义时立即实例化。
  • 可访问外部类的私有字段。
  • 编译后生成如 Outer$1.class 的匿名类文件。

示例 1:实现接口

Java
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()方法,我们在方法内部实例化了一个RunnableRunnable本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable接口的匿名类,并且通过new实例化该匿名类,然后转型为Runnable

之所以我们要定义匿名类,是因为在这里我们通常不关心类名,比直接定义Inner Class可以少写很多代码。

示例 2:继承类

Java
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是一个匿名类实例,只是该匿名类继承自HashMapmap3也是一个继承自HashMap的匿名类实例,并且添加了static代码块来初始化数据。

匿名类无法声明命名构造方法,所以如果你想在对象创建时执行一段自定义逻辑,就需要用双层大括号中的内层 { … },这就是 Java 的实例初始化块

  • 第一次 { 是匿名类的类体开始。
  • 第二次 { 是匿名类的初始化块。

这种写法通常被称为“双括号初始化”,它等价于:

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(静态内部类)

Java
class Outer {
    static class StaticNested {
        // 静态内部类定义
    }
}

特点:

  • 使用 static 修饰,不依赖外部类实例即可创建。
  • 无法访问外部类的非静态成员。
  • 可访问外部类的私有静态成员。
  • 更类似于顶级类,只是作用域限定在外部类内部。
  • 编译后生成 Outer$StaticNested.class

示例:

Java
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,但它可以访问Outerprivate静态字段和静态方法。如果把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.classOuter$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 系统:使用 ; 分隔,带空格的路径用引号包裹

Mathematica
.;C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"

Linux 系统:使用 : 分隔

Ruby
.:/usr/local/bin:/home/user/bin

. 表示当前目录。

假设 classpath 为:

Mathematica
.;C:\work\project1\bin;C:\shared

要加载类 abc.xyz.Hello 时,JVM 会查找以下路径:

  1. .\abc\xyz\Hello.class
  2. C:\work\project1\bin\abc\xyz\Hello.class
  3. C:\shared\abc\xyz\Hello.class

一旦找到,不再继续搜索;找不到就报错。


7.2 Classpath的设置

classpath的设置方式有两种:

推荐方式:运行时指定 classpath

Bash
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello

不推荐方式:设置系统环境变量 CLASSPATH

  • 容易污染整个系统环境
  • 影响其他项目
  • 难以维护

如果没有显式设置,JVM 的默认 classpath 是当前目录。

Bash
java abc.xyz.Hello

IDE(如 IntelliJ IDEA、Eclipse)在运行 Java 程序时,会自动为你添加 classpath 参数,包括:

  • 当前项目的 binout 目录
  • 所有依赖的 JAR 包

JVM 不依赖 classpath 加载 Java 核心类库(如 StringArrayList),而是通过自己的机制加载,不需要你手动添加 rt.jar

切记:不要把 Java 核心库加入 classpath!


7.3 创建Jar包

Jar 是 Java Archive 的缩写,实质上是一个 ZIP 格式压缩包,方便分发、备份、发布多个 .class 文件。

运行:

Bash
java -cp ./hello.jar abc.xyz.Hello

JVM 会在 jar 文件中按包路径查找类。

手动创建Jar包(适用于简单项目):

因为jar包就是zip包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip改为.jar,一个jar包就创建成功。

  1. 保证包路径正确,如:
CSS
hong/Person.class
ming/Person.class
mr/jun/Arrays.class

注意:不要把这些文件放在 bin/ 目录内打包,否则路径会错误。

  1. 压缩为 ZIP 文件 → 修改后缀为 .jar

jar包还可以包含一个特殊的/META-INF/MANIFEST.MF文件,MANIFEST.MF是纯文本,可以指定Main-Class和其它信息。JVM会自动读取这个MANIFEST.MF文件,如果存在Main-Class,我们就不必在命令行指定启动的类名,而是用更方便的命令:

Java
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 版本:

Bash
$ java -version

高版本 JVM 可以运行低版本 class 文件(向下兼容)。

低版本 JVM 无法运行高版本 class 文件,运行时会报错:

Bash
java.lang.UnsupportedClassVersionError: Xxx has been compiled by a more recent version of the Java Runtime...

只要看到UnsupportedClassVersionError就表示当前要加载的class文件版本超过了JVM的能力,必须使用更高版本的JVM才能运行。


指定编译输出的 class 文件版本:

方式一: 使用 --release 参数(推荐)

Bash
$ javac --release 11 Main.java

参数--release 11表示源码兼容Java 11,编译的class输出版本为Java 11兼容,即class版本55。

方式二: 使用 --source--target

Bash
$ javac --source 9 --target 11 Main.java

--source: 源代码版本

--target: 输出 class 文件的版本

上述命令如果使用Java 17的JDK编译,它会把源码视为Java 9兼容版本,并输出class为Java 11兼容版本。注意--release参数和--source --target参数只能二选一,不能同时设置。

此方式不会验证 API 是否在目标版本中存在,可能出现运行时错误。

Java
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 上运行,会出现:

Java
NoSuchMethodError: java.lang.String.indent

使用 --release 11 编译时,会在编译阶段就报错,避免了这种情况。


多版本 JDK 可并存,通过 JAVA_HOMEPATH 控制当前使用版本

Bash
# 临时切换 JDK 版本示例
<div markdown="1" style="margin-top: -30px; font-size: 0.75em; opacity: 0.7;">
:material-circle-edit-outline:  4871 个字 :fontawesome-solid-code: 374 行代码 :material-clock-time-two-outline: 预计阅读时间 29 分钟
</div>
$ 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工程为例,它的目录结构如下:

CSS
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 示例:

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目录下。

编译命令:

Bash
javac -d bin src/module-info.java src/com/itranswarp/sample/*.java

如果编译成功,现在项目结构如下:

CSS
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方法所在的类

Bash
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包转换成模块

Bash
jmod create --class-path hello.jar hello.jmod

9.3 运行模块

使用 jar 运行:

Java
java --module-path hello.jar --module hello.world

注意:不能直接运行 .jmod 文件.jmod 是编译期使用的格式,运行时需使用 .jar


过去发布一个Java应用程序,要运行它,必须下载一个完整的JRE,再运行jar包。而完整的JRE块头很大,有100多M。

现在,JRE自身的标准库已经分拆成了模块,只需要带上程序用到的模块,其他的模块就可以被裁剪掉。

怎么裁剪JRE呢?并不是说把系统安装的JRE给删掉部分模块,而是“复制”一份JRE,但只带上用到的模块。为此,JDK提供了jlink命令来干这件事。

裁剪 JRE 命令:

Bash
jlink \
  --module-path hello.jmod \
  --add-modules java.base,java.xml,hello.world \
  --output jre/

我们在--module-path参数指定了我们自己的模块hello.jmod

然后,在--add-modules参数中指定了我们用到的3个模块java.basejava.xmlhello.world,用,分隔

最后,在--output参数指定输出目录

运行裁剪后的 JRE:

在当前目录下,我们可以找到jre目录,这是一个完整的并且带有我们自己hello.jmod模块的JRE

Bash
jre/bin/java --module hello.world

要分发我们自己的Java应用程序,只需要把这个jre目录打个包给对方发过去,对方直接运行上述命令即可,既不用下载安装JDK,也不用知道如何配置我们自己的模块,极大地方便了分发和部署。


9.5 访问权限控制

class的这些访问权限只在一个模块内有效,模块和模块之间,例如,a模块要访问b模块的某个class,必要条件是b模块明确地导出了可以访问的包。

我们编写的模块hello.world用到了模块java.xml的一个类javax.xml.XMLConstants,我们之所以能直接使用这个类,是因为模块java.xmlmodule-info.java中声明了若干导出:

Java
module java.xml {
    exports java.xml;
    exports javax.xml.catalog;
    exports javax.xml.datatype;
    ...
}

模块默认不导出任何包,要允许其他模块访问,需在 module-info.java 中使用 exports

Java
module hello.world {
    exports com.itranswarp.sample;

    requires java.xml;
}

因此,模块进一步隔离了代码的访问权限。