JEP 458:启动多文件源代码程序
概括
增强java
应用程序启动器,使其能够运行作为多个 Java 源代码文件提供的程序。这将使从小程序到大程序的过渡更加渐进,使开发人员能够选择是否以及何时配置构建工具。
非目标
-
通过“shebang”机制启动多文件源代码程序并不是目标。只有单文件源代码程序才能通过该机制启动。
-
目的不是简化源代码程序中外部库依赖项的使用。这可能是未来 JEP 的主题。
动机
Java 编程语言擅长编写由大型团队多年开发和维护的大型复杂应用程序。尽管如此,即 使是大型程序也是从小处开始的。在早期阶段,开发人员修补和探索,并不关心可交付的工件;项目的结构可能还不存在,一旦出现,就会经常发生变化。快速迭代和彻底改变是当今的常态。近年来,JDK 中添加了一些有助于修补和探索的功能,包括JShell(用于处理代码片段的交互式 shell)和简单的 Web 服务器(用于快速构建 Web 应用程序原型)。
在 JDK 11 中,JEP 330增强了java
应用程序启动器,使其能够.java
直接运行源文件,而无需显式编译步骤。例如,假设该文件Prog.java
声明两个类:
class Prog {
public static void main(String[] args) { Helper.run(); }
}
class Helper {
static void run() { System.out.println("Hello!"); }
}
然后运行
$ java Prog.java
在内存中编译这两个类并执行main
该文件中声明的第一个类的方法。
这种运行程序的简单方法有一个主要限制:程序的所有源代码必须放在一个.java
文件中。要使用多个.java
文件,开发人员必须返回显式编译源文件。对于经验丰富的开发人员来说,这通常需要为构建工具创建项目配置,但是当试图让想法和实验顺利进行时,从无定形的修补到正式的项目结构的转变是令人厌烦的。对于初学者来说,从单个.java
文件过渡到两个或多个文件需要更严峻的阶段性转变:他们必须暂停语言学习并学习操作javac
,或者学习第三方构建工具,或者学习依赖IDE 的魔力。
如果开发人员能够推迟项目设置阶段,直到他们更多地了解项目的形状,或者甚至在快速破解然后丢弃原型时完全避免设置,那就更好了。一些简单的程序可能永远保留源代码形式。这促使增强java
启动器能够运行超出单个.java
文件的程序,但无需强制执行显式编译步骤。传统的编辑/构建/运行周期变得简单的编辑/运行。开发人员可以自行决定何时建立构建流程,而不是由于工具的限制而被迫这样做。
描述
我们增强了java
启动器的源文件模式,使其能够运行作为多个 Java 源代码文件提供的程序。
例如,假设一个目录包含两个文件Prog.java
和Helper.java
,其中每个文件声明一个类:
// Prog.java
class Prog {
public static void main(String[] args) { Helper.run(); }
}
// Helper.java
class Helper {
static void run() { System.out.println("Hello!"); }
}
运行会在内存中java Prog.java
编译该类并调用其方法。因为此类中的代码引用了 class ,所以启动器在文件系统中查找该文件并在内存中编译其类。如果类中的代码引用了其他类,例如,启动器也会找到并编译它。Prog``main``Helper``Helper.java``Helper``HelperAux``HelperAux.java
当不同文件中的类.java
相互引用时,java
启动器不保证文件编译的任何特定顺序或时间.java
。例如,启动器可以Helper.java
在Prog.java
.某些代码可能会在程序开始执行之前进行编译,而其他代码可能会在运行中延迟编译。 (下面详细介绍源文件程序的编译和执行过程。)
仅.java
编译其类被程序引用的文件。这使得开发人员可以使用新版本的代码,而不必担心旧版本会被意外编译。例如,假设该目录还包含OldProg.java
,其旧版本的类Prog
期望该类Helper
具有名为 的方法go
,而不是run
。运行时,的存在OldProg.java
及其潜在错误并不重要Prog.java
。
多个类可以在一个.java
文件中声明,并且全部一起编译。在一个文件中共同声明的类.java
优先于在其他.java
文件中声明的类。例如,假设Prog.java
上面的文件被扩展以声明一个类Helper
,尽管已经在 中声明了该名称的类Helper.java
。当代码Prog.java
引用时,使用Helper
共同声明的类;Prog.java
启动器将不会搜索该文件Helper.java
。
源代码程序中禁止出现重复的类。也就是说,不允许在同一.java
文件中或在构成程序一部分的不同文件中对具有相同名称的类进行两次声明。.java
假设经过一些编辑后,Prog.java
结果Helper.java
如下所示,并且Aux
在两者中意外声明了该类:
// Prog.java
class Prog {
public static void main(String[] args) { Helper.run(); Aux.cleanup(); }
}
class Aux {
static void cleanup() { ... }
}
// Helper.java
class Helper {
static void run() { ... }
}
class Aux {
static void cleanup() { ... }
}
运行java Prog.java
编译中的Prog
和Aux
类Prog.java
,调用main
的方法Prog
,然后——由于main
的引用Helper
——查找Helper.java
并编译它的类Helper
和Aux
。不允许重复声明Aux
in ,因此程序会停止并且启动器会报告错误。Helper.java
启动器的源文件模式java
是通过向其传递单个文件的名称来触发的.java
。如果给出了额外的文件名,它们将成为其main
方法的参数。例如,java Prog.java Helper.java
生成一个包含字符串的数组,该字符串"Helper.java"
作为参数传递给main
该类的方法Prog
。
使用预编译的类
依赖于类路径或模块路径上的库的程序也可以从源文件启动。例如,假设一个目录包含两个小程序和一个帮助程序类,以及一些库 JAR 文件:
Prog1.java
Prog2.java
Helper.java
library1.jar
library2.jar
您可以通过传递--class-path '*'
给java
启动器来快速运行这些程序:
$ java --class-path '*' Prog1.java
$ java --class-path '*' Prog2.java
这里'*
选项的 ' 参数--class-path
将所有 JAR 文件放入类路径的目录中;星号被引用是为了避免 shell 的扩展。
当您继续试验时,您可能会发现将 JAR 文件放在单独的libs
目录中更方便,在这种情况下--class-path 'libs/*'
将使它们可用。您可以开始考虑生成打包的可交付成果,可能是在构建工具的帮助下,直到项目成型之后。
启动器如何查找源文件
启动java
器要求将多文件源代码程序的源文件排列在通常的目录层次结构中,其中目录结构遵循包结构,从按如下所述计算的根目录开始。这意味着:
- 根目录中的源文件必须声明未命名包中的类,并且
- 根目录下的目录中的源文件
foo/bar
必须声明指定包中的类foo.bar
。
例如,假设一个目录包含Prog.java
,它在未命名的包中声明类,还有一个子目录pkg
,其中在包中Helper.java
声明类:Helper``pkg
// Prog.java
class Prog {
public static void main(String[] args) { pkg.Helper.run(); }
}
// pkg/Helper.java
package pkg;
class Helper {
static void run() { System.out.println("Hello!"); }
}
运行java Prog.java
会导致Helper.java
在子目录中找到pkg
并在内存中进行编译,从而产生pkg.Helper
class 中的代码所需的类Prog
。
如果Prog.java
在命名包中声明类,或者Helper.java
在除 之外的包中声明类pkg
,java Prog.java
则将失败。
启动器根据初始文件的包名称和文件系统位置java
计算源树的_根_.java
。对于java Prog.java
,初始文件是Prog.java
,它在未命名的包中声明了一个类,因此源代码树的根目录是包含Prog.java
.另一方面,如果Prog.java
在指定包中声明一个类a.b.c
,则必须将其放置在层次结构中的相应目录中:
dir/
a/
b/
c/
Prog.java
它还必须通过运行来启动java dir/a/b/c/Prog.java
。在这种情况下,源树的根是dir
。
如果Prog.java
将其包声明为b.c
,那么源树的根将是dir/a
;如果它声明了包c
,那么根就是dir/a/b
,如果它没有声明包,那么根就是dir/a/b/c
。如果Prog.java
声明了一些其他包(例如p
,与文件系统中文件路径的后缀不对应),程序将无法启动。
一个微小但不兼容的更改
在上面的示例中,如果Prog.java
在不同的命名包中声明类,java a/b/c/Prog.java
则会失败。这是java
启动器源文件模式行为的变化。
在过去的版本中,启动器的源文件模式允许在.java
给定位置的文件中声明哪个包(如果有):java a/b/c/Prog.java
只要Prog.java
在 中找到,就会成功a/b/c
,无论package
文件中是否有任何声明。文件在命名包中声明类而不将该文件驻留在层次结构中的相应目录中是不常见的.java
,因此此更改的影响可能是有限的。如果包名称不重要,则修复方法是package
从文件中删除声明。