在Java 11中运行单文件程序而无需编译



让源文件HelloUniverse.java包含一个类定义和一个静态main方法,该方法将单行文本输出到终端:

 public class HelloUniverse{ public static void main(String[] args) { System.out.println("Hello InfoQ Universe"); } } 

通常,要运行此类,必须首先使用Java编译器(javac)对其进行编译,该Java编译器将创建HelloUniverse.class文件:

 mohamed_taman$ javac HelloUniverse.java 

然后,您需要使用Java虚拟机命令(解释器)运行结果文件:

 mohamed_taman$ java HelloUniverse Hello InfoQ Universe 

然后virtualka将首先启动,这将加载该类并执行代码。

如果您需要快速检查一段代码? 还是您不熟悉Java( 在本例中是关键点 )并想尝试该语言? 所描述的两个步骤会使事情复杂化。

在Java SE 11中,您可以直接运行单个源文件,而无需中间编译。

对于想使用简单程序的初学者来说,此功能特别有用。 与jshell结合使用,您将获得大量用于培训初学者的工具。

专业人士可以使用这些工具来学习该语言的创新或测试不熟悉的API。 我们认为,最好将许多任务自动化,例如以脚本形式编写Java程序并随后从OS Shell执行。 结果,我们可以灵活地使用shell脚本并使用Java的所有功能。 让我们在文章的第二部分中对此进行更详细的讨论。

Java 11的这一强大功能使您无需编译即可直接执行单个源文件。 让我们讨论一下。

你需要什么


要运行本文中提供的代码,您需要Java版本不少于11。在撰写本文时,当前版本是Java SE Development Kit 12.0.1-最终版本在这里 ,只需接受许可条款,然后单击您操作系统的链接即可。 如果您想试验最新功能,可以下载 JDK 13早期访问。

请注意,现在还提供了各种OpenJDK供应商的发行版,包括AdoptOpenJDK

在本文中,我们将使用纯文本编辑器而不是Java IDE来避免IDE的所有魔力,并直接在终端中使用Java命令行。

使用Java运行.java


JDK 11中出现了JEP 330函数(使用源代码运行单文件程序),它使您可以直接使用Java源代码执行源文件,而无需使用解释器。 源代码在内存中编译,然后由解释器执行,而无需在磁盘上创建.class文件。

但是,此功能仅限于存储在单个文件中的代码。 您不能一次执行多个源文件。

要解决此限制,必须在单个文件中定义所有类。 对它们的数量没有限制。 另外,当它们在同一文件中时,它们是公共的还是私有的都没有关系。

文件中定义的第一个类将被视为主要类,并且必须将main方法放入其中。 也就是说,顺序很重要。

第一个例子


让我们从最简单的经典示例开始-Hello Universe!

我们将通过各种示例来演示所描述的功能,以便您了解如何在日常编程中使用它。

使用本文开头的代码创建一个HelloUniverse.java文件,编译并运行生成的类文件。 然后将其删除,现在您将了解原因:

 mohamed_taman$ rm HelloUniverse.class 

如果现在使用Java解释器,则无需编译即可运行类文件:

 mohamed_taman$ java HelloUniverse.java Hello InfoQ Universe 

您将看到相同的结果:文件将被执行。

这意味着现在您可以只执行java HelloUniverse.java 。 我们传输源代码本身,而不是类文件:内部的系统对其进行编译,启动并在控制台中显示一条消息。

也就是说,编译仍在后台进行。 如果发生她的错误,我们将收到通知。 您可以检查目录结构,并确保未生成类文件,而是在内存中执行编译。

现在,让我们弄清楚它们是如何工作的。

Java解释器如何执行HelloUniverse程序


在JDK 10中,Java启动器可以在三种模式下运行:

  1. 执行类文件。
  2. 从JAR文件执行主类。
  3. 执行模块的主类。

在Java 11中,出现了第四个模式:

  1. 执行在源文件中声明的类。

在这种模式下,源文件被编译在内存中,然后执行该文件中的第一类。

系统根据两个条件确定您打算输入源文件:

  1. 命令行的第一项既不是选项也不是选项的一部分。
  2. 该行可能包含--source <vrsion>选项。

在第一种情况下,Java将首先找出命令的第一个元素是选项还是选项的一部分。 如果这是一个以.java结尾的文件名,则系统会将其视为需要编译和运行的源代码。 您还可以在源文件名称之前向Java命令添加选项。 例如,如果要在源文件使用外部依赖项时设置类路径。

在第二种情况下,将选择使用源文件的模式,并且命令行上的第一个元素(不是选项)被视为需要编译和运行的源文件。

如果文件不具有.java扩展名,则需要使用--source选项强制其进入使用源文件的模式。

这在源文件是需要执行的“脚本”并且文件名不符合使用Java代码命名源文件的常规约定的情况下非常重要。

使用--source选项,可以确定源语言的版本。 我们将在下面讨论。

我可以在命令行中传递参数吗?


让我们扩展Hello Universe程序,以便它向访问InfoQ Universe的任何用户显示个人问候:

 public class HelloUniverse2{ public static void main(String[] args){ if ( args == null || args.length< 1 ){ System.err.println("Name required"); System.exit(1); } var name = 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!! 

如您所见,类和文件名不匹配根本不重要。 细心的读者可能还会注意到,我们在处理文件名后将参数传递给了代码。 这意味着文件名后的命令行上的任何参数都将传递给标准main方法。

使用--source选项确定源代码级别


使用--source选项有两种方案:

  1. 确定源代码的级别。
  2. 强制Java运行时进入源模式。

在第一种情况下,如果您未指定源代码级别,则将使用JDK的当前版本。 在第二种情况下,扩展名为.java以外的文件可以进行动态编译和执行。

让我们首先看第二种情况。 仅将Greater.java重命名为Greater而不进行扩展,然后尝试执行:

 mohamed_taman$ java greater "Mo. Taman" Error: Could not find or load main class greater Caused by: java.lang.ClassNotFoundException: greater 

在没有.java扩展名的情况下,命令解释器通过作为参数传递的名称搜索编译的类-这是Java启动器的第一种操作模式。 为防止这种情况发生,请使用--source选项强制切换到源文件模式:

 mohamed_taman$ java --source 11 greater "Mo. Taman" Hello, Mo. Taman to InfoQ universe!! 

现在让我们继续第一种情况。 Greater.java类与JDK 10兼容,因为它包含var关键字,但与JDK 9不兼容。将source更改为10

 mohamed_taman$ java --source 10 Greater.java "Mo. Taman" Hello Mo. Taman to InfoQ universe!! 

再次运行前面的命令,但是这次传递--source 9而不是10

 mohamed_taman$ java --source 9 Greater.java "Mo. Taman" Greater.java:8: warning: as of release 10, 'var' is a restricted local variable type and cannot be used for type declarations or as the element type of an array var name = args[0]; ^ Greater.java:8: error: cannot find symbol var name = args[0]; ^ symbol: class var location: class HelloWorld 1 error 1 warning error: compilation failed 

注意:编译器警告var在JDK 10中已成为受限类型名称。 但是由于我们拥有10级语言,因此编译仍在继续。 但是,由于源文件没有名为var的类型,所以发生崩溃。

一切都很简单。 现在考虑使用几个类。

这种方法是否适用于多个类?


是的,确实如此。

考虑一个具有两个类的示例。 该代码检查以查看给定的字符串值是否是回文

这是保存在PalindromeChecker.java文件中的代码:

 import static java.lang.System.*; public class PalindromeChecker { public static void main(String[] args) { if ( args == null || args.length< 1 ){ err.println("String is required!!"); exit(1); } out.printf("The string {%s} is a Palindrome!! %b %n", args[0], StringUtils .isPalindrome(args[0])); } } public class StringUtils { public static Boolean isPalindrome(String word) { return (new StringBuilder(word)) .reverse() .toString() .equalsIgnoreCase(word); } } 

运行文件:

 mohamed_taman:code$ java PalindromeChecker.java RediVidEr The string {RediVidEr} is a Palindrome!! True 

再次运行它,用“ RaceCar”代替“ MadAm”:

 mohamed_taman:code$ java PalindromeChecker.java RaceCar The string {RaceCar} is a Palindrome!! True 

现在用“穆罕默德”代替“ RaceCar”:

 mohamed_taman:code$ java PalindromeChecker.java Taman The string {Taman} is a Palindrome!! false 

如您所见,您可以在一个源文件中添加任意多的公共类。 确保首先定义了main方法。 解释器将在编译了内存中的代码之后将第一类用作启动程序的起点。

我可以使用模块吗?


是的,没有限制。 内存编译代码通过--add-modules=ALL-DEFAULT选项作为未命名模块的一部分运行,该选项可访问JDK随附的所有模块。

也就是说,代码可以使用不同的模块,而无需使用module-info.java显式定义依赖项。

让我们看一下使用JDK 11中引入的新HTTP Client API进行HTTP调用的代码。请注意,这些API是作为实验功能在Java SE 9中引入的,但是现在它们具有java.net.http模块的完整功能状态。 。

在此示例中,我们将使用GET方法调用一个简单的REST API以获取用户列表。 我们转向公共服务reqres.in/api/users?page=2 。 我们将代码保存在名为UsersHttpClient.java的文件中:

 import static java.lang.System.*; import java.net.http.*; import java.net.http.HttpResponse.BodyHandlers; import java.net.*; import java.io.IOException; public class UsersHttpClient{ public static void main(String[] args) throws Exception{ var client = HttpClient.newBuilder().build(); var request = HttpRequest.newBuilder() .GET() .uri(URI.create("https://reqres.in/api/users?page=2")) .build(); var response = client.send(request, BodyHandlers.ofString()); out.printf("Response code is: %d %n",response.statusCode()); out.printf("The response body is:%n %s %n", response.body()); } } 

运行程序并获得结果:

 mohamed_taman:code$ java UsersHttpClient.java Response code is: 200 The response body is: {"page":2,"per_page":3,"total":12,"total_pages":4,"data":[{"id":4,"first_name":"Eve","last_name":"Holt","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg"},{"id":5,"first_name":"Charles","last_name":"Morris","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/stephenmoon/128.jpg"},{"id":6,"first_name":"Tracey","last_name":"Ramos","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/bigmancho/128.jpg"}]} 

现在,您可以快速测试不同模块提供的新功能,而无需创建自己的模块。

为什么脚本在Java中很重要?


首先,让我们回顾一下什么是脚本:

脚本是为特定的运行时环境编写的程序,该程序可以自动执行一个人可以依次执行的任务或命令。

从此一般定义中,我们可以得出脚本语言的简单定义-它是一种使用高级构造一次解释和执行一个(或多个)命令的编程语言。

脚本语言使用一系列写在文件中的命令。 通常,这些语言会被解释(而不是编译)并遵循过程编程风格(尽管某些脚本语言也具有面向对象语言的属性)。

通常,与诸如Java,C和C ++的结构化编译语言相比,脚本语言更易于学习和键入。 服务器端脚本语言包括Perl,PHP和Python,以及客户端 JavaScript。

长期以来,Java被认为是一种结构良好的高类型编译语言,可以由虚拟机解释为可在任何计算体系结构上运行。 但是,与其他脚本语言相比,Java并不是那么容易学习和原型化。

尽管如此,Java已经有24年历史了,它被全世界大约1000万开发人员使用。 最新版本添加了许多新功能,以使年轻程序员更轻松地学习该语言,以及无需编译和IDE即可使用该语言和API的功能。 例如,Java SE 9引入了JShell工具(REPL),该工具支持交互式编程。

随着JDK 11的发布,该语言已经能够支持脚本,因为现在您可以通过简单地调用java命令来执行代码!

在Java 11中使用脚本的主要方法有两种:

  1. 直接调用java命令。
  2. 在命令行中使用* nix脚本,类似于Bash脚本。

我们已经考虑了第一种选择,现在我们将处理第二种选择。 它为我们打开了许多可能性。

Shebang文件:将Java作为shell脚本运行


因此,在Java SE 11中,出现了对脚本的支持,包括来自* nix世界的传统shebang文件。 为了支持它们,不需要语言规范。

在shebang文件中,前两个字节必须为0x23和0x21。 这是ASCII字符编码#!..基于此平台上的默认编码系统读取文件中的所有后续字节。

因此,为了使用OS内置的shebang机制执行文件,只有一个要求:第一行以#!开头。这意味着当显式使用Java启动器时,我们不需要任何特殊的第一行就像HelloUniverse.java一样,从源文件运行代码。

在运行macOS Mojave 10.14.5的终端上运行以下示例。 但是首先,我们将定义创建shebang文件时要遵循的重要规则:

  • 不要将Java代码与OS Shell脚本的脚本语言代码混合使用。
  • 如果需要添加虚拟机选项,则必须在shebang文件中的可执行文件名之后指定--source一个选项。 虚拟机选项包括: --class-path --patch-module --upgrade-module-path--upgrade-module-path--upgrade-module-path --add-exports ,-- --add-modules--limit-modules --patch-module --limit-modules ,-- --patch-module--upgrade-module-path --patch-module --upgrade-module-path ,及其任何变体。 此列表中还包括新选项--enable-preview ,如JEP 12中所述
  • 您必须指定源文件中使用的Java版本。
  • 文件的第一行应以shebang字符(#!)开头。 例如:
    #!/path/to/java --source <vrsion>
  • 对于Java源文件,请勿使用shebang机制来执行符合标准命名约定的文件(以.java结尾)
  • 您必须使用以下命令将文件标记为可执行文件:
    chmod +x <Filname>.<Extnsion>

让我们创建一个shebang文件(脚本程序),该文件将列出目录的内容,其名称将作为参数传递。 如果未传递任何参数,则默认为当前目录。

 #!/usr/bin/java --source 11 import java.nio.file.*; import static java.lang.System.*; public class DirectoryLister { public static void main(String[] args) throws Exception { vardirName = "."; if ( args == null || args.length< 1 ){ err.println("Will list the current directory"); } else { dirName = args[0]; } Files .walk(Paths.get(dirName)) .forEach(out::println); } } 

将代码保存到不带扩展名的dirlist文件中,然后将其标记为可执行文件: mohamed_taman:code$ chmod +x dirlist

运行文件:

 mohamed_taman:code$ ./dirlist Will list the current directory . ./PalindromeChecker.java ./greater ./UsersHttpClient.java ./HelloWorld.java ./Greater.java ./dirlist 

使用传递父目录的命令再次运行它,然后检查结果。

 mohamed_taman:code$ ./dirlist ../ 

注意:在评估源代码时,解释器将忽略shebang行(第一行)。 因此,可以使用启动器显式调用shebang文件,例如,带有其他选项:

 $ java -Dtrace=true --source 11 dirlist 

还应该注意:如果脚本文件在当前目录中,则可以像这样执行它:

 $ ./dirlist 

如果脚本位于用户PATH中指定了路径的目录中,则可以像这样执行它:

 $ dirlist 

最后,我将为您提供一些使用脚本时要记住的提示。

小费


  1. 您将传递给javac的某些选项可能不会传递(或无法识别)给java ,例如-processor-Werror选项。
  2. 如果类路径中有.class和.java文件,则启动器将强制您使用该类文件。

     mohamed_taman:code$ javac HelloUniverse.java mohamed_taman:code$ java HelloUniverse.java error: class found on application class path: HelloUniverse 

  3. 注意类名和包名之间可能发生冲突。 看一下这个目录结构:

     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 

    那么哪个文件将首先执行,第二个将执行? 启动器不再引用HelloUniverse包中的类文件。 相反,它将加载并执行原始的HelloUniverse.java文件,即该文件将在当前目录中启动。

Shebang文件为使用Java工具创建脚本以自动化各种任务提供了许多可能性。

总结


从Java SE 11开始,这是编程历史上的第一次,您可以直接使用Java代码执行脚本,而无需编译。 这使您可以编写Java脚本并从* nix命令行执行它们。

试用此功能,并与他人分享您的知识。

有用的资料


Source: https://habr.com/ru/post/zh-CN483888/


All Articles