java程序编译运行
重要要点
- 此功能提供了直接运行Java单文件源代码而无需进行任何编译的功能,从而避免了以前仅运行简单的hello world程序所涉及的繁琐步骤。
- 对于想尝试简单程序或功能的语言新手来说,此功能特别有用。 当将此功能与jshell结合使用时 ,您将获得出色的初学者学习工具集。
- 此功能还带您进入高级级别,您可以在其中传递命令行参数来控制它,使用更多类,甚至可以在一次运行中将模块添加到当前应用程序。
- 将此功能与Shebang文件(#!)结合使用时,通常可以从命令行运行* nix bash脚本,从而将Java作为shell脚本运行。
- 本文探讨了新的Java 11+ Launch Single-File源代码程序(JEP 330),并提供了基于JShell的示例以及正确和不正确用法的提示和技巧。
为什么你需要这个
如果您想起Java SE 11(JDK 11)之前的过去,请说您有一个HelloUniverse.java
源文件,其中包含类定义和静态main方法,该方法以单行文本的形式输出到终端,如下所示: :
public class HelloUniverse{
public static void main(String[] args) {
System.out.println("Hello InfoQ Universe");
}
}
通常,要运行该类,首先,您需要使用Java编译器( javac
)对其进行编译,这将产生HelloUniverse.class
文件:
mohamed_taman$ javac HelloUniverse.java
然后,您将使用java
虚拟机(解释器)命令来运行生成的类文件:
mohamed_taman$ java HelloUniverse
Hello InfoQ Universe
这将启动JVM,加载类并执行代码。
但是,如果您想快速测试一段代码,或者刚开始学习Java( 这是这里的关键 )并且想尝试该语言,该怎么办? 在此过程中的这两个步骤似乎有些繁重。
在Java SE 11中,您可以选择直接启动一个源代码文件,而无需中间编译。
对于刚尝试该语言的新手来说,此功能特别有用。 当将此功能与jshell
结合使用jshell
,您将获得出色的初学者学习工具集。
有关新的Jshell 10+的更多信息,请查看我的视频课程“ 使用JShell进行Java 10编程实践 ” 。
专业人士还可以使用这些工具来探索新的语言更改或尝试未知的API。 在我看来,当我们可以自动化许多任务(例如将Java程序编写为脚本,然后从操作系统外壳执行这些任务)时,强大的力量就会来临。 这种组合为我们提供了Shell脚本的灵活性,但具有Java语言的强大功能。 我们将在本文的下半部分对此进行更详细的探讨。
强大的Java 11功能使您可以直接运行Java单文件源代码,而无需进行任何编译。 因此,现在让我们深入研究更多细节和有趣的相关主题。
您需要遵循的内容
要运行本文提供的所有演示,您将需要使用Java的最新版本。 它应该是Java 11或更高版本。 当前的功能版本是Java SE Development Kit 12.0.1-此链接提供了最终版本,只需接受许可,然后单击与您的操作系统相关的链接即可。 如果您想探索更多最新功能,则最新的JDK 13抢先体验是最新的,您可以从此链接下载。
您还应该注意,Oracle以及包括AdoptOpenJDK在内的其他供应商现在也提供了OpenJDK版本。
在本文中,我们使用纯文本编辑器而不是Java IDE,因为我们想避免任何IDE魔术,而是直接在整个终端中使用Java命令行。
使用Java运行.java
JEP 330是“ 启动单文件源代码程序” ,是JDK 11发行版中引入的新功能之一。 此功能使您可以直接使用java
解释器执行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启动器以三种模式运行:
- 运行一个类文件
- 运行JAR文件的主类
- 运行模块的主类
现在在Java 11中,添加了新的第四种模式
- 运行在源文件中声明的类。
在源文件模式下,效果就像将源文件编译到内存中并执行在源文件中找到的第一类。
进入源文件模式的决定由命令行上的两个项目决定:
- 命令行上的第一项既不是选项也不是选项的一部分。
-
--source
<version
>选项(如果存在)。
对于第一种情况,Java命令将查看命令行中的第一项,它既不是选项也不是选项的一部分。 如果文件名以.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!!
如您所见,类名是否与文件名匹配无关紧要; 它在内存中编译,并且没有生成.class
文件。 鹰眼的读者也许已经注意到我们如何在要执行的文件名之后将参数传递给代码。 这意味着文件名之后出现在命令行上的所有参数都以这种显而易见的方式传递给标准main
方法。
使用--source选项指定代码文件的级别
使用--source
选项有两种情况:
- 指定代码文件的源级别
- 强制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命令解释器正在通过作为参数提供的名称(Java启动器的方式1)寻找一个已编译的类。 为了防止这种情况,我们需要使用--source
选项来强制源文件模式:
mohamed_taman$ java --source 11 greater "Mo. Taman"
Hello, Mo. Taman to InfoQ universe!!
现在让我们回到第一种情况。 Greater.java
类与JDK 10兼容,因为它包含var关键字,但与JDK 9不兼容。将源更改为10,然后看看会发生什么:
mohamed_taman$ java --source 10 Greater.java "Mo. Taman"
Hello Mo. Taman to InfoQ universe!!
现在再次运行前面的命令,但是将--source
选项传递给JDK 9而不是JDK 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中成为受限类型名称,但是由于这是语言级别9,因此编译仍在继续。 但是,编译尝试失败,因为在源文件中找不到名为var的类型。
很简单,对吧? 现在让我们看一下使用多个类。
它适用于多个类吗?
答案是肯定的。
让我们检查一段包含两个类的示例代码,以演示该功能可用于多个类。 该代码检查以确定给定的字符串是否是回文。 回文是从两个方向读取相同内容的单词,短语,数字或其他字符序列,例如“ redivider”或“ reviver” 。
这是保存在名为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
最后,让我们尝试用“ Mohamed”代替“ RaceCar”:
mohamed_taman:code$ java PalindromeChecker.java Taman
The string {Taman} is a Palindrome!! false
如您所见,您可以在单个源文件中添加所需数量的公共类。 唯一重要的是应在源文件的第一类中定义main方法。 解释器(Java命令)在编译内存中的代码之后将使用第一类作为入口点来启动程序。
是否允许使用模块?
是的,完全允许使用模块。 使用选项--add-modules=ALL-DEFAULT
,内存中已编译的代码作为未命名模块的一部分运行,该选项可访问JDK随附的所有模块。
这使代码可以使用不同的模块,而无需使用module-info.java.
显式声明依赖关系module-info.java.
让我们看一下一些代码,这些代码使用JDK 11附带的新HTTP客户端API进行HTTP调用。请注意,这些API是Java SE 9中的一个孵化器功能,但是现在它们已逐渐升级为java.net.http
的完整功能java.net.http
模块。
在此示例中,我们将通过GET方法调用一个简单的REST API以获取一些用户。 我们将调用公共端点服务https://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平台模块系统( JPMS )的更多信息,请看我的视频课程“ Clean Code Java SE 9入门 ”。
为什么脚本对Java很重要?
首先,让我们提醒自己什么是脚本编制,以便了解使用Java编程语言提供脚本的重要性。
我们可以将脚本定义如下:
“脚本是为特定的运行时环境编写的程序,它可以自动执行任务或命令的执行,而这些任务或命令可以由人一个接一个地执行。”
从这个通用定义中,我们可以得出脚本语言的简单定义。 脚本语言是一种编程语言,它采用高级构造来一次解释和执行一个或多个命令。
脚本语言是一种在文件中使用一系列命令的编程语言。 通常,脚本语言通常是解释性的 (而不是编译式的 ),并且倾向于编程的程序风格(尽管某些脚本语言也具有面向对象的功能)
一般而言,脚本语言比Java,C和C ++等结构化的编译语言更易于学习且编写代码更快。 服务器端脚本语言的示例,包括Perl,PHP和Python,以及客户端 JavaScript。
长期以来,Java被归类为一种结构良好的强类型编译语言,由JVM解释为可在任何计算机体系结构上运行。 但是,关于Java的一个抱怨是,与普通的脚本语言相比,它的学习或原型开发速度并不快。
但是,Java现在已经有24年的历史了,全世界约有940万开发人员使用Java。 为了使年轻一代的程序员更轻松地学习Java并尝试其功能和API,而无需进行编译和IDE的仪式,最近的发行版中增加了一些功能。 从Java SE 9开始,已添加了一个支持交互式编程的JShell(REPL)工具,目的是使Java更易于编程和学习。
现在,随着JDK 11的出现,Java逐渐成为一种可以支持脚本的编程语言,因为您只需调用java
命令就可以运行代码!
Java 11中有两种基本的脚本编写方法:
- 直接使用
java
命令工具。 - 使用* nix命令行脚本-类似于bash脚本。
我们已经探讨了第一个选项,因此现在是时候看看第二个选项了,该功能为很多可能性打开了大门。
Shebang文件:将Java作为shell脚本运行。
如前所述,Java SE 11引入了对脚本的支持-包括传统的* nix所谓的shebang文件。 到JLS( 的J ava 大号 anguage 小号 pecification)无需更改以支持该特征。
在一般的shebang文件中,前两个字节必须为0x23和0x21 ,这是两个字符“ #!”的ASCII编码。 。 然后使用有效的默认平台字符编码读取文件的所有后续字节。
因此,一线开始#! 仅在需要使用操作系统的shebang机制执行文件时才需要 。 这意味着当显式使用Java启动器从源文件运行代码时,不需要任何特殊的第一行,如上面的HelloUniverse.java
示例中所示。
让我们在macOS Mojave 10.14.5上运行的终端中运行下一个示例。 但是首先,我们应该列出创建shebang文件时要遵循的一些重要规则:
- 不要将 Java代码与操作系统的Shell脚本语言混合使用。
- 如果需要包括VM (虚拟机)选项,则必须在shebang文件中的可执行文件名后面指定
--source
作为第一个选项。 这些选项包括:--class-path, --module-path, --add-exports, --add-modules, --limit-modules, --patch-module, --upgrade-module-path
,--class-path, --module-path, --add-exports, --add-modules, --limit-modules, --patch-module, --upgrade-module-path
,--class-path, --module-path, --add-exports, --add-modules, --limit-modules, --patch-module, --upgrade-module-path
以及任何这些选项的变体形式。 它还可以包括新的--enable-preview选项,如JEP 12中所述 。 - 您必须在文件中指定用于源代码的Java语言版本。
- shebang字符( #! )必须是文件的第一行,并且应如下所示:
#!/path/to/java --source <version>
- 使用该家当机制来执行遵循标准的命名约定(文件结尾的文件
.java
Java源文件)是不允许的。 - 最后,您必须使用以下命令将文件标记为可执行文件:
chmod +x <Filename>.<Extension>.
对于我们的示例,让我们创建一个shebang文件( script utility program
),该文件将列出目录的内容,该目录的名称作为参数传递。 如果未传递任何参数,则默认情况下将列出当前目录。
#!/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
最后,让我们通过使用此功能时需要注意的一些提示和技巧结束。
技巧和窍门
- 可以传递给
javac
某些选项可能不会被java工具传递(或为此识别),例如-processor
或-Werror
选项。 - 如果类路径中同时存在.class和.java文件,则启动器会强制您使用该类文件。
mohamed_taman:code$ javac HelloUniverse.java
mohamed_taman:code$ java HelloUniverse.java
error: class found on application class path: 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命令行执行脚本成为可能。
立即开始尝试使用此新功能,并开始编写愉快的代码。 请通过传播这个词来表示感谢,只为了与您的其他怪才分享。
资源资源
java程序编译运行