Java理论与实践:运行单文件源代码程序而无需编译 为什么需要此功能

349 阅读13分钟

如果你想起Java SE 11(JDK 11)之前的过去,可以说你有一个HelloWorld.java源文件,其中包含类定义和静态main方法,该方法以单行文本的形式输出到终端:

显示更多 通常,要运行此类,首先需要使用Java编译器(javac)对其进行编译,这将生成一个HelloWorld.class文件。
然后,你将使用java解释器命令来运行生成的类文件:
这将启动JVM,加载类并执行代码。

本系列中的更多

内容该内容是“ Java理论与实践 ”系列的一部分。

但是,如果你想快速测试一段代码,或者刚开始学习Java并想尝试该语言,该怎么办?在此过程中的这两个步骤似乎有些繁琐。

在Java SE 11中,你可以选择直接启动单个源代码文件,而无需编译。

对于刚尝试该语言的新手来说,此功能特别有用。当将此功能与结合使用时jshell,你将获得出色的初学者工具集。 专业人士还可以使用这些工具来探索新的语言更改或尝试未知的API。你还可以自动化任务,例如编写Java脚本,然后在操作系统级别将其作为脚本执行。

有关新Jshell 10+的更多信息,请参阅本系列中有关此主题的以下两个教程:

“ Java理论与实践:具有JShell 12,第1部分的交互式Java编程(REPL) ”(IBM Developer,2019年4月)

“ Java理论与实践:具有JShell 12,第2部分的交互式Java编程(REPL) ”(IBM Developer,2019年4月)

你需要遵循的内容

要运行本教程中提供的所有演示,你将需要下载Java SE 11 JDK。在本教程中,我使用纯文本编辑器而不是Java IDE,因为我想避免任何IDE魔术,而是直接在整个终端中使用Java命令行。

跑 .java和Java JEP 330,启动单文件源代码程序,是JDK 11发行版中引入的令人兴奋的功能之一。此功能使你可以直接使用java解释器执行Java源代码文件。源代码在内存中编译,然后由解释器执行。

功能限制

但是,此功能仅限于驻留在单个源文件中的代码。你不能添加要在同一运行中进行编译的其他源文件。 要解决此限制,必须在同一文件中定义所有类,但是文件中的类数没有限制。

功能最简单的例子

现在,让我们以最简单的示例:Hello World!开始学习新事物时,我们始终会做的事情-完全是你所猜到的。 我不会深入研究该功能的实现细节,但会通过尝试不同的示例来集中精力演示如何使用此功能,以便你了解如何在编码中使用该功能。

如果尚未创建HelloWorld.java文件,则对其进行编译,然后运行生成的类文件。 现在,我希望你删除类文件;你很快就会明白为什么:

现在,如果仅使用java无编译运行类,如下所示
你现在可以说javaHelloWorld.java。Java用完了,然后你看到我们只传入源代码文件而不是类文件,它在内部编译该源文件,然后运行生成的编译后的代码,最后将结果显示在控制台中。

因此,正在进行编译,如果有编译错误,你将收到错误通知。另外,你可以检查目录结构,看看没有生成类文件。这是一个内存中的编译过程。

现在让我们看看这是怎么发生的。

Java解释器如何运行HelloWorld

从JDK 10开始,Java启动器以三种模式运行:

1.运行一个类文件

2.运行JAR文件的主类

3.运行模块的主类

现在,我们添加一种新的第四种模式:运行在源文件中声明的类。

源文件模式由命令行上的两个项目确定:

1.命令行中的第一项既不是选项也不是选项的一部分

2.该--source 选项(如果存在)

对于第一项,Java命令会将命令行中的第一项(既不是选项也不是选项的一部分)视为要编译和运行的Java源文件。你仍然可以在源文件名之前为Java命令提供选项。例如,如果你希望在源文件使用外部依赖项时设置类路径。

对于第二项,如果filename标识具有.java扩展名的现有文件,则将选择源文件模式,然后将编译并运行该文件。该--source选项可用于指定源代码的源版本。稍后我将详细讨论。

换句话说,Java将寻找第一个.java。请注意,我说的是文件名,而不是类名。

如果文件没有.java扩展名,则--source必须使用该选项来强制源文件模式。

对于源文件是要执行的脚本并且源文件的名称不遵循Java源文件的常规命名约定的情况,这是必需的。(当我shebang在本教程稍后讨论文件时,将对此有更多信息。)

在源文件模式下,效果就像将源文件编译到内存中并执行在源文件中找到的第一个类。 你可以传递命令行参数吗?

让我们增强Hello World程序,为访问IBM Developer World的任何人创建个性化的问候:

让我们将代码保存在名为的文件中Greater.java。请注意,文件名与违反Java语言规范规则的公共类的名称不匹配。

运行以下代码,看看会发生什么:

如你所见,类名是否与文件名匹配无关紧要;它被编译在内存中,并且没有.class文件生成。

而且我知道你已经用鹰眼注意到了我是如何在要执行的文件名之后将参数传递给代码的。

因此,文件名之后出现在命令行上的所有参数都以main这种显而易见的方式传递给标准方法。

使用–source选项指定代码文件的级别

有两种情况可以使用该--source选项:

1.你可以使用source选项指定代码文件的源级别

2.你可以强制Java运行时进入源执行模式

在第一个选项中,当你省略源代码级别时,假定它是当前的JDK版本,在本例中为11。启用第二个选项后,你现在还可以传递具有其他Java扩展名的文件,以进行编译和即时运行。

现在让我们首先检查第二种情况,并重命名Greater.java为greater没有任何扩展名,然后尝试使用相同的方法再次执行它:

如你所见,在没有.java扩展名的情况下,Java命令解释器正在通过提供作为参数的名称来查找编译的类。在这种情况下,我们需要使用该--source选项来强制源文件模式:
现在让我们回到第一种情况。该Greater.java班是JDK 10,因为它包含了兼容的var关键字,但不兼容JDK 9源切换到10,看看会发生什么:
现在再次运行前面的命令,但将其传递给--source选项JDK 9而不是JDK 10:

很简单,对吧?现在让我们看一下使用多个类。

你可以参加多个课程吗?

答案是可以的。

检查一段包含两个类的示例代码,以表明该代码将要检查以确定特定给定的字符串是否是回文。回文是从两个方向读取相同内容的单词,词组,数字或其他字符序列,例如“女士”或“赛车”。 这是保存在文件名中的代码PalindromeChecker.java:

现在,让我们如下运行文件:
同样,将“ RaceCar”替换为“ MadAm”:
最后,让我们尝试用“ Mohamed”代替“ RaceCar”:
如你所见,你可以在单个源文件中添加所需数量的公共类。唯一重要的是应该在源文件的第一类中定义main方法。解释器(Java命令)在编译内存中的代码之后将使用第一类来启动执行。

是否允许使用模块?

是的,完全允许使用模块。内存中已编译的代码作为带有选项–的未命名模块的一部分运行-add-modules=ALL-DEFAULT。这使代码可以使用不同的模块,而无需使用显式声明依赖项module-info.java。

让我们看一下使用JDK 11随附的新HTTP客户端API进行HTTP调用的代码。这些API是JavaSE9中的孵化器功能,但已逐步引入该java.net.http模块。

在此示例中,我将调用一个简单的REST API GET方法来通过调用端点服务来获取一些用户https://reqres.in/api/users?page=2。示例代码位于名称为的文件中UsersHttpClient.java:

如下运行:
这使你可以快速测试不同模块中的任何新功能,而无需创建模块。你也可以module-info为该模块定义文件,依此类推。

脚本,为什么需要它,为什么它很重要

首先,让我们了解什么是脚本编制,以了解为什么它是Java编程语言的重要组成部分。

我们可以将脚本定义如下:

“这是为特殊的运行时环境编写的程序,该程序可以自动执行任务,也可以由人一一执行。”

从这个通用定义中,我们可以得出脚本语言的简单定义:脚本语言是一种编程语言,它使用高级构造来一次解释和执行一个命令:

脚本使用文件中的一系列命令,这些命令无需编译即可执行-通常,脚本语言通常被解释而不是编译 与更结构化和编译的语言(例如Java,C和C ++)相比,它们更易于学习,并且你可以使用它们更快地编码 服务器端脚本语言的很好的例子包括Perl,PHP和Python。在客户端是JavaScript。

长期以来,Java被归类为结构良好,强类型化和兼容的语言,只能由JVM解释为可在任何计算机体系结构上运行。这是对Java的主要抱怨。学习或使用其他脚本语言进行原型制作的速度并不快。

但是Java现在是一种成熟的语言,全世界大约有九十万个开发人员使用Java。为了使学习起来更加容易,并且无需参与复杂的编译过程即可试用其功能和API,Java SE 9的发布增加了JShell REPL,该工具支持交互式编程。

JShell REPL使Java易于学习并可以快速进行实验。如果你不相信我,请跳到本系列中有关使用JShell的两个动手教程,并阅读它们。

那么,如何在Java 11中编写脚本?你可以通过java两种基本方式通过调用命令来运行代码: java直接使用命令工具 使用* nix终端行脚本作为bash脚本 我们已经探讨了第一个选项,因此现在让我们探索第二个选项。

使用Java编写Shell脚本:Shebang文件 请记住,Java SE 11引入了对使用传统* nix shebang文件进行脚本编写的支持。支持此功能不需要更改Java语言规范。

在一般的Shebang文件中,前两个字节必须为“ 0x23”和“ 0x21”,即两个字符的ASCII编码“#!”。使用有效的默认平台字符编码读取所有后续字节。

第一行以#!仅在要使用操作系统的shebang机制执行文件时才需要。像HelloWorld.java我们前面的示例一样,当显式使用Java启动器在源文件中运行代码时,你不需要特殊的第一行。

创建shebang文件时,你还需要了解一些其他重要规则。

创建shebang文件的规则 不要将 Java代码与所需的操作系统外壳程序脚本语言混合使用。

如果需要包括虚拟机选项,则必须--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 。 不允许使用shebang机制.java为Java源文件执行遵循标准命名约定的文件(以结尾的文件)。

最后,你必须使用将文件标记为可执行文件chmod +x .。

好戏开始; 让我们创建一个shebang文件(脚本实用程序),该文件会列出传递的目录的内容作为参数。如果目录未通过,则默认情况下将列出当前目录。该示例在使用MacOS Mojave 10.14.3的终端上运行。 源代码是:

将此代码保存在一个dirlist没有扩展名的文件中,然后将其标记为可执行文件:

mohamed_taman:code$chmod +x dirlist

如下运行:

通过传递父目录的以下命令再次运行它,然后自己检查输出。

mohamed_taman:code$ ./dirlist ../

注意,在评估源代码时,解释器会忽略shebang行(第一行)。shebang文件也可以由启动程​​序显式调用,也许还有其他选项,例如:$ java -Dtrace=true --source 11 dirlist。 另外,如果脚本文件在当前目录中,则可以这样执行:

$ ./dirlist

或者,如果脚本位于用户PATH的目录中,则可以这样执行:

$ dirlist

最后,我想向你展示使用此功能时要注意的一些技巧和陷阱。 提示,技巧和陷阱 使用此功能时,可能会遇到一些棘手的情况。 选项可能无法通过或无法识别 你可以传递给某些选项javac的Java工具可能不会传递(或对此识别),例如-processor或-Werror选项。 当你被迫使用类文件时 如果.class和都.java存在于类路径中,则必须使用类文件。

注意类和包的命名冲突

请记住类和包的命名冲突。检查以下目录结构:

现在注意这两个文件;java.java在HelloWorld软件包和HelloWorld.java当前目录中的文件下。看到问题了吗?真讨厌 尝试运行时会发生什么:
哪个文件将运行,第一个还是第二个?

java启动器不再引用HelloWorld包中的类文件。相反,它将HelloWorld.java从源加载文件。结果是当前目录中的文件将运。

我喜欢这个shebang功能,因为它为创建脚本提供了无限的可能性,这些脚本可以使用Java语言的强大功能自动执行很多工。