- 该特性可以在无需编译的情况下,直接运行 Java 单文件源代码,避免了以前只运行一个简单的 Hello World 程序所涉及的繁琐步骤。
- 这个特性对于那些想尝试简单程序或特性的 Java 新手来说特别有用;当我们将这个特性与jshell结合起来使用时,将会得到一个很棒的初学者学习工具集。
- 该特性还能带来一些高级功能,我们可以通过命令行参数来控制它,处理更多的类,甚至可以在一次运行中向当前应用程序添加模块。
- 将此特性与 Shebang 文件结合,通常,我们可以像使用命令行运行 *nix bash 脚本那样, 将 Java 作为 shell 脚本运行。
- 本文探讨了 Java 11+ 启动单文件源代码程序的新特性,并提供了基于 JShell 的示例,这些示例分别展示了正确的和错误的用法及技巧。
如果我们回想一下JavaSE 11之前的日子,假设我们有一个 HelloUniverse.java 源文件,它包含一个类定义和一个静态的 main 方法,该方法打印一行文本到终端中,代码如下所示:
复制代码
publicclassHelloUniverse{publicstaticvoidmain(String[] args){ System.out.println("Hello InfoQ Universe"); }}复制代码
mohamed_taman$ javac HelloUniverse.java然后,需要使用一条 java 虚拟机(解释器)命令来运行生成的字节码类文件:
复制代码
mohamed_taman$ java HelloUniverseHello InfoQ Universe它将启动 JVM、加载类并执行代码。
但是,如果我们想快速测试一段代码,或者我们刚开始学习 Java(这里的关键词是 Java)并想实践这种语言,应该怎么办呢?上述过程中的两个步骤实践起来似乎还是有点难度。
在 Java SE 11 中,我们可以在无需任何中间编译的情况下,直接启动单个源代码文件。
这一特性对于那些想尝试简单程序的 Java 新手来说特别有用;当我们将这个特性与 jshell 结合起来使用时,我们将会得到一个很棒的初学者学习工具集。
更多关于 **Jshell 10+** 的新信息,请查看视频教程“ Hands-on Java 10 Programming with JShell ”。
专业人员也可以利用这些工具来探索新的语言变化或尝试未知的 API。在我看来,当我们可以自动化地执行很多任务时,比如,将 Java 程序编写为脚本,然后在操作系统 shell 中执行这些脚本,它将会产生更强大的功能。这种组合不仅为我们提供了 shell 脚本的灵活性,同时也提供了 Java 语言的强大功能。我们将在本文的第二部分更详细地探讨这个问题。
该 Java 11 特性的伟大之处在于,它使我们可以无需任何编译即可直接运行 Java 单文件源代码。现在让我们深入地了解它的更多细节和其他有趣的相关主题。
我们还应该注意到,现在也可以从Oracle 和其他供应商处获取 OpenJDK 版本。
JEP 330启动单文件源代码程序(Launch Single-File Source-Code Programs),是 JDK11 发行版本中引入的新特性之一。该特性允许我们直接使用 Java 解释器来执行 Java 源代码文件。源代码在内存中编译,然后由解释器执行,而不需要在磁盘上生成.class 文件了。
但是,该特性仅限于保存在单个源文件中的代码。不能在同一个运行编译中添加其他源文件。
为了满足这个限制,所有的类都必须在同一个文件中定义,不过它对文件中类的数量没有限制,并且类既可声明为公共类,也可以不是,因为只要它们在同一个源文件中就没关系。
源文件中声明的第一个类将被提取出来作为主类,我们应该将 main 方法放在第一个类中。所以类的顺序很重要。
现在,让我们以学习新东西时的一贯做法开始我们的学习吧,是的,你没有猜错,以一个最简单的“Hello Universe!” 示例开始。
我们将集中精力通过尝试不同的示例来演示如何使用该特性,以便你了解如何在日常编码中使用该特性。
如果还没有准备好,请先创建本文顶部列出的 HelloUniverse.java 文件,编译它,并运行生成的字节码类文件。
现在,我希望你删除编译生成的类文件;你马上就会明白为什么:
复制代码
mohamed_taman$ rm HelloUniverse.class现在,如果不编译,只使用 Java 解释器运行该类,操作如下:
复制代码
mohamed_taman$ java HelloUniverse.javaHello InfoQ Universe我们会看到它运行了,并返回和之前编译时相同的结果。
对于 java HelloUniverse.java 来说,我们传入的是源代码而不是字节码类文件,这就意味着,它在内部编译源代码,然后运行编译后的代码,最后将消息输出到控制台。
所以,它仍然需要进行一个编译过程,如果有编译错误,我们仍然会收到一个错误通知。此外,我们还可以检查目录结构,会发现并未生成字节码类文件;这是一个内存编译过程。
现在,让我们看看这个魔法是如何发生的。
在 JDK 10 中,Java 启动程序会以如下三种模式运行:
- 运行字节码类文件
- 运行 JAR 文件中的 main 类
- 运行模块中的 main 类
现在,在 Java 11 中,又添加了一个新的第四模式:
- 运行源文件中声明的类
在源文件模式下,运行效果就像是,将源文件编译到内存中,并执行可以在源文件中找到的第一个类。
是否进入源文件模式由命令行上的如下两项来决定:
- 在命令行中既不是选项也不是选项一部分的第一项。
- 如果存在选项的话,它将是–source选项。
对于第一种情况,Java 命令将查看命令行上的第一项,它既不是选项也不是选项的一部分。如果它有一个以.java 结尾的文件名,那么它将会被当作是一个要编译和运行的 Java 源文件。我们也可以在源文件名之前为 Java 命令提供选项。比如,如果我们希望在源文件中通过设置类路径来使用外部依赖项时。
对于第二种情况,选择源文件模式,并将第一个非选项命令行项视为要编译和运行的源文件。
如果文件没有.java 扩展名,则必须使用–source 选项来强制执行源文件模式。
当源文件是要执行的“脚本”,或者源文件的名称不遵循 Java 源文件的常规命名约定时,–source 选项是必要的。
–source 选项还可用于指定源代码的语言版本。稍后我会详细讨论。
让我们丰富下“Hello Universe”程序,为访问 InfoQ Universe 的任何人创建一个个性化的问候:
复制代码
publicclassHelloUniverse2{publicstaticvoidmain(String[] args){if( args ==null|| args.length1){System.err.println("Name required");System.exit(1); }varname = args[0]; System.out.printf("Hello, %s to InfoQ Universe!! %n", name); }}我们将代码保存在一个名为 Greater.java 的文件中。请注意,该文件的命名违反了 Java 编程规范,它的名称和公共类的名称不匹配。
运行如下代码,看看将会发生什么:
复制代码
mohamed_taman$ java Greater.java"Mo. Taman"Hello, Mo. Taman to InfoQ universe!!我们可以看到的,类名是否与文件名匹配并不重要;它是在内存中编译的,并且没有生成 .class 文件。敏锐的读者可能还注意到了,我们是如何在要执行的文件名之后将参数传递给代码的。这意味着在命令行上文件名之后出现的任何参数都会以这种显式的方式传递给标准的 main 方法。
有两种使用 --source 选项的场景:
- 指定代码文件的语言版本
- 强制 Java 运行时进入源文件执行模式
在第一种情况下,当我们缺省代码语言版本时,则假定它是当前的 JDK 版本。在第二种情况下,我们可以对除 .java 之外的扩展名文件进行编译并立即运行。
我们先研究一下第二个场景,将 Greater.java 重命名为没有任何扩展名的 greater,然后使用相同的方法,尝试再次执行它:
复制代码
mohamed_taman$ java greater"Mo. Taman"Error: Could not find or load mainclassgreaterCausedby:java.lang.ClassNotFoundException: greater正如我们所看到的那样,在没有 .java 扩展名的情况下,Java 命令解释器将以模式 1 的形式启动 Java 程序,它会根据参数中提供的文件名寻找编译后的字节码类。为了防止这种情况的发生,我们需要使用 --source 选项来强制指定源文件模式:
复制代码
mohamed_taman$java --source11 greater"Mo. Taman"Hello, Mo. Taman to InfoQ universe!!现在,让我们回到第一个场景。Greater.java 类与 JDK 10 兼容的,因为它包含 var 关键字,但与 JDK 9 不兼容。将源版本更改为 10,看看会发生什么:
复制代码
mohamed_taman$java --source10 Greater.java"Mo. Taman"Hello Mo. Taman to InfoQ universe!!现在再次运行前面的命令,但传递到 --source 选项的是 JDK 9 而不是 JDK 10:
复制代码
mohamed_taman$ java --source9Greater.java"Mo. Taman"Greater.java:8: warning:asof release10,'var' is a restricted local variabletypeandcannot be usedfortypedeclarationsorasthe elementtypeofan arrayvar name = args[0]; ^Greater.java:8: error: cannot find symbolvar name = args[0]; ^ symbol: class var location: class HelloWorld1 error1 warningerror: compilation failed请注意错误消息的形式,编译器警告说,在 JDK 10 中 var 会成为一个受限制的类型名,但是由于当前是 Java 语言 9 版本,所以编译仍会继续进行。但是,由于在源文件中找不到名为 var 的类型,所以编译失败。
很简单,对吧?现在让我们看看如何使用多个类。
答案是肯定的。
让我们测试一段包含两个类的示例代码,以演示该特性可以适用于多个类。该代码的功能是检验给定的字符串是否为回文。回文可以是一个单词、短语、数字或其他字符序列,但它们从两个方向读取时,都能得到相同的字符序列,例如“redivider”或“reviver”。
如下是保存在名为 PalindromeChecker.java 文件中的代码:
复制代码
importstaticjava.lang.System.*;publicclassPalindromeChecker{publicstaticvoidmain{if( args == null || args.length1){ err.println("String is required!!");exit(1); } out.printf("The string {%s} is a Palindrome!! %b %n", args[0], StringUtils .isPalindrome(args[0])); }}publicclassStringUtils{publicstaticBooleanisPalindrome{return(newStringBuilder) .reverse .toString .equalsIgnoreCase; }}现在,我们运行一下这个文件:
复制代码
mohamed_taman:code$ java PalindromeChecker.java RediVidErThestring{RediVidEr}isa Palindrome!! True使用“RaceCar”代替“RediVidEr”后,再运行一次:
复制代码
mohamed_taman:code$ java PalindromeChecker.java RaceCarThestring{RaceCar}isa Palindrome!! True最后,再使用“Taman”来代替“RaceCar”:
复制代码
mohamed_taman:code$ java PalindromeChecker.java TamanThestring{Taman}isa Palindrome!!false正如我们看到的那样,我们可以在单个源文件中添加任意多个的公共类。唯一的要点是,main 方法应该在源文件的第一个类中定义。解释器将使用第一个类作为入口,在内存中编译代码并启动程序。
是的,完全允许使用模块。内存中编译的代码作为未命名模块的一部分运行,该未命名模块带有 --add-modules=ALL-DEFAULT 选项,该选项允许访问 JDK 附带的所有模块。
这使得代码可以使用不同的模块,而无需使用 module-info.java 显式声明依赖项。
复制代码
运行程序,将产生如下的输出结果:
复制代码
这允许我们快速测试不同模块提供的新功能,而无需创建自己的模块。
更多关于新的 Java 平台模块系统(JPMS)的信息,请查看视频教程“ Getting Started with Clean Code Java SE 9 ”。
首先,让我们回顾一下脚本是什么,以便于理解为什么在 Java 编程语言中使用脚本如此重要。
我们可以给脚本作如下的定义:
“脚本是为特定的运行时环境编写的程序,它可以自动执行任务或命令,这些任务或命令也可以由操作人员逐个执行。”
在这个通用定义中,我们可以推导出脚本语言的一个简单定义;脚本语言是一种编程语言,它使用高级构造器每次解释并执行一个命令。
长期以来,Java 被归类成一种结构良好的、强类型的编译语言,经 JVM 解释运行于任何计算机体系结构上。然而,对于 Java 的一个抱怨是,与普通脚本语言相比,它的学习及原型开发速度不够快。
然而,现在,Java 已经成为一门历经 24 年的语言,全世界大约有 940 万的开发人员在使用它。为了让年轻一代的程序员更容易地学习 Java,并在不需要编译和 IDE 的情况下尝试其特性和 API,Java 最近发布了一些特性。从 Java SE 9 开始,添加了一个支持交互式编程的JShell (REPL)工具集,其目的就是使 Java 更易于编程和学习。
现在,使用 JDK 11,Java 逐步成为一种支持脚本的编程语言,因为我们可以简单地通过调用 Java 命令来运行代码了!
在 Java 11 中,有两种基本的脚本编写方法:
- 直接使用 java 命令工具。
- 使用 *nix命令行脚本,它类似于 bash 脚本
我们已经探讨过了第一种方法了,所以现在是时候看一下第二种方法,这是一个可以打开许多可能性大门的特性。
如前所述,Java SE 11 引入了对脚本的支持,包括支持传统的 *nix,即所谓的 Shebang 文件。无需修改JLS(JavaLanguageSpecification,Java 语言规范)就可以支持该特性。
在一般的 Shebang 文件中,前两个字节必须是0x23和0x21,这是"#!"两个字符的 ASCII 编码。然后,才能有效地使用默认平台字符编码读取文件所有后续字节。
因此,当希望使用操作系统的 Shebang 机制执行文件时,文件的第一行需要以#! 开始。这意味着,当显式使用 Java 启动程序运行源文件代码时,无需任何特殊的第一行,比如上面的 HelloUniverse.java 示例。
让我们在 macOS Mojave 10.14.5 的终端中运行下一个示例。但是首先,我们需要列出一些创建 Shebang 文件时,应该遵循的重要规则:
- 不要混合使用 Java 代码与操作系统的 shell 脚本语言。
- 如果需要包含VM(虚拟机)选项,则必须将 --source 指定为 Shebang 文件可执行的文件名后面的第一个选项。这些选项包括:–class-path、–module-path、–add-exports、–add-modules、–limit-modules、–patch-module、upgrade-module-path ,以及这些选项的任何变体形式。它还可以包括 JEP 12 引入的新的–enable-preview 选项。
- 必须为文件中的源代码指定 Java 语言版本。
- Shebang 字符(#!)必须在文件的第一行,它应该是这样的:
复制代码
#!/path/to/java--source version- 不允许使用Shebang 机制来执行遵循标准命名约定的 Java 源文件。
- 最后,必须使用以下命令将文件标记为可执行文件:
复制代码
chmod +xFilename.Extension.在我们的示例中,我们创建一个 Shebang 文件,它将列出作为参数传递的目录内容。如果没有传递任何参数,则默认列出当前目录。
复制代码
#!/usr/bin/java --source11importjava.nio.file.*;importstaticjava.lang.System.*;publicclassDirectoryLister {publicstaticvoidmain(Stringargs) throws Exception { vardirName =".";if( args ==null|| args.length1){err.println("Will list the current directory"); }else{ dirName = args[0]; } Files .walk) .forEach; }}将此代码保存在一个名为 dirlist 文件中,它不带任何扩展名,然后将其标记为可执行文件:
复制代码
mohamed_taman:code$ chmod +x dirlist按以下方式运行:
复制代码
mohamed_taman:code$ ./dirlistWilllistthe current directory../PalindromeChecker.java./greater./UsersHttpClient.java./HelloWorld.java./Greater.java./dirlist通过传递父目录,按照如下命令再次运行程序 ,并检查它输出。
复制代码
mohamed_taman:code$ ./dirlist ../注意:在计算源代码时,解释器会忽略 Shebang 行。因此,启动程序也可以显式地调用 Shebang 文件,可能需要使用如下附加选项:
复制代码
$java -Dtrace=true--source11 dirlist另外,值得注意的是,如果脚本文件在当前目录中,还可以按以下方式执行:
复制代码
$ ./dirlist或者,如果脚本在用户路径的目录中,也可以这样执行:
复制代码
$ dirlist最后,我们通过展示一些使用该特性时需要注意的用法和技巧来结束本文。
- 可以传递给 javac 的一些选项可能不会被 Java 工具所传递 (或识别),比如, -processor 和 -Werror 选项。
- 如果类路径中同时存在.class 和.java 文件,启动程序将强制使用字节码类文件。
复制代码
mohamed_taman:code$ javac HelloUniverse.javamohamed_taman:code$ java HelloUniverse.javaerror:classfoundonapplicationclasspath:HelloUniverse请记住类和包存在命名冲突的可能性。请看如下的目录结构:
复制代码
mohamed_taman:code$ tree.├── Greater.java├── HelloUniverse│ ├── java.class│ └──java.java├──HelloUniverse.java├──PalindromeChecker.java├──UsersHttpClient.java├──dirlist└──greater注意:HelloUniverse 包下的两个 java.java 文件和当前目录中的 HelloUniverse.java 文件。当我们试图运行如下命令时,会发生什么呢?
复制代码
mohamed_taman:code$ java HelloUniverse.java运行哪个文件,第一个还是第二个?Java 启动程序不再引用 HelloUniverse 包中的类文件。相反,它将通过源代码模式加载并运行 HelloUniverse.java 文件,以便运行当前目录中的文件。
我喜欢使用 Shebang 特性,因为它为利用 Java 语言的强大功能来创造脚本自动化完成大量工作提供了可能性。
从 Java SE 11 开始,在这款编程语言的历史上,首次可以在无需编译的情况下,直接运行包含 Java 代码的脚本。Java 11 源文件执行特性使得使用 Java 编写脚本并直接使用 *inx命令行执行脚本成为可能。