BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Java 11でコンパイルせずに単一ファイルプログラムを実行する

Java 11でコンパイルせずに単一ファイルプログラムを実行する

原文(投稿日:2019/07/24)へのリンク

なぜこれが必要か

Java SE 11 (JDK 11)以前を思い出し、クラス定義と、次のようなターミナルに1行のテキストとして出力するstatic mainメソッドを含むHelloUniverse.javaソースファイルがあるとします。

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の学習を始めたばかりの場合(これが鍵です)、言語を試してみたい場合はどうでしょう。そのプロセスにおけるこれら2つのステップは、少し手間がかかるように思えるかもしれません。
Java SE 11では、中間コンパイルなしで、単一のソースコードファイルを直接起動するオプションがあります。

この機能は、簡単なプログラムを試してみたい言語を初めて使用する人にとって特に便利です。この機能をjshellと組み合わせると、優れた初心者向けの学習ツールセットが得られます。

新しいJshell 10+の詳細については、ビデオコース「 Hands-on Java 10 Programming with JShellをご覧ください。

専門家は、これらのツールを使用して、言語の変更を調べたり、未知のAPIを試すこともできます。私の意見では、Javaプログラムをスクリプトとして作成し、オペレーティングシステムシェルから実行するような多くのタスクを自動化できると、大きな力が生まれます。この組み合わせにより、Java言語のパワーと共に、シェルスクリプトの柔軟性が得られます。これについては、記事の後半で詳しく説明します。

この優れたJava 11機能により、コンパイルせずにJavaの単一ファイルのソースコードを直接実行できます。それでは、詳細と興味深い関連トピックをさらに掘り下げてみましょう。

あなたが守るべきこと

この記事で提供されるすべてのデモを実行するには、最新バージョンのJavaを使用する必要があります。Java 11以降である必要があります。現在の機能リリースはJava SE Development Kit 12.0.1です。最終バージョンはこのリンクから入手できます。ライセンスに同意し、オペレーティングシステムに関連するリンクをクリックするだけです。最新の機能を調べたい場合は、最新のJDK 13 early accessが最新であり、このリンクからダウンロードできます。

また、OracleおよびAdoptOpenJDKを含む他のベンダーから、OpenJDKリリースも利用できるようになりました。

この記事では、IDEマジックを避け、ターミナルを通してJavaコマンドラインを直接使用するため、Java IDEではなくプレーンテキストエディタを使用します。

Javaで.javaを実行する

JEP 330単一ファイルのソースコードプログラムの起動は、JDK 11リリースで導入された新機能の1つです。この機能により、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ランチャーは3つのモードで動作します。

  1. クラスファイルの実行
  2. JARファイルのメインクラスの実行
  3. モジュールのメインクラスの実行

Java 11では、新しい4番目のモードが追加されました。

  1. ソースファイルで宣言されたクラスの実行/li>

ソースファイルモードでは、ソースファイルがメモリにコンパイルされ、ソースファイルで見つかった最初のクラスが実行されるような効果があります。

ソースファイルモードに入るかどうかは、コマンドラインにおいて、次の2つの項目によって決定されます。

  1. コマンドラインの最初の項目がオプションでもオプションの一部でもない
  2. code> --source <version>オプションが存在する

1つ目については、Javaコマンドは、オプションでもオプションの一部でもないコマンドラインの最初の項目を調べます。.javaで終わるファイル名の場合、コンパイルおよび実行されるJavaソースファイルとして扱われます。ソースファイル名の前にJavaコマンドにオプションを提供できます。たとえば、ソースファイルが外部の依存関係を使用するときにクラスパスを設定する場合です

2つ目については、ソースファイルモードが選択され、最初のオプションでないコマンドライン項目は、コンパイルおよび実行されるソースファイルとして扱われます。

ファイルに.java拡張子が付いていない場合は、 --sourceオプションは、ソースファイルモードを強制するために使われます。
これは、ソースファイルが実行される"script"で、ソースファイルの名前がJavaソースファイルの通常の命名規則に従わない場合に必要です。

--sourceオプションを使用して、ソースコードの言語バージョンを指定できます。これについては後で詳しく説明します。

コマンドライン引数を渡すことはできるか

Hello Universeプログラムを強化して、InfoQユニバースにアクセスするすべての人にパーソナライズされた挨拶を作成しましょう。

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ファイルは生成されません。Eagle-eyedの読者は、実行するファイルの名前の後にコードに引数を渡す方法にも気付いているかもしれません。つまり、ファイル名の後にコマンドラインに表示される引数が、この明白な方法で標準のmainメソッドに渡されます。

--sourceオプションでコードファイルのレベルを指定する

--sourceオプションを活用する2つのシナリオがあります。

  1. コードファイルのソースレベルを指定する
  2. Javaランタイムをソース実行モードに強制する

最初のケースでは、ソースレベルを省略すると、現在のJDKバージョンであるとされます。2番目のケースでは、.java以外の拡張子を持つファイル名を渡して、その場でコンパイル、実行できます。

最初に2番目のシナリオを検証してみましょう。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クラスにはvarキーワードが含まれているためJDK 10と互換性がありますが、JDK 9とは互換性がありません。ソースを10に変更して何が起こるかを確認します。

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

今度は、前のコマンドを再度実行しますが、 --sourceオプションにJDK 10ではなくJDK 9を渡します。

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という型が見つからないため、コンパイルの試行は失敗します。

とても簡単ですね。では、複数のクラスを使う場合を見てみましょう。

複数のクラスで動作するか

答えはイエスです。そうなんです。

2つのクラスを含むサンプルコードを調べて、この機能が複数のクラスで動作することを示します。コードは、指定された文字列が回文であるかどうかを確認します。回文とは、「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

「MadAm」を「RaceCar」を置き換えて、もう一度やってみましょう。

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

最後に、「RaceCar」の代わりに「Mohamed」を試してみましょう。

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

ご覧のとおり、単一のソースファイルに必要な数のパブリッククラスを追加できます。重要なのは、mainメソッドをソースファイルの最初のクラスで定義することだけです。インタープリタ(Javaコマンド)は、メモリ内のコードをコンパイルした後、プログラムを起動するためのエントリポイントとして最初のクラスを使用します。

モジュールは使えるか

はい、モジュールの使用は完全に許可されています。メモリ内でコンパイルされたコードは、JDKに同梱されているすべてのモジュールにアクセスできるオプション --add-modules=ALL-DEFAULTを使用して、名前のないモジュールの一部として実行されます。

これにより、module-info.java.を使用して依存関係を明示的に宣言する必要なく、コードで異なるモジュールを使用できます。

JDK 11に付属する新しいHTTPクライアントAPIを使用してHTTP呼び出しを行うコードを見てみましょう。これらのAPIは、インキュベーター機能としてJava SE 9で導入されましたが、 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)の詳細については、ビデオコース「Getting Started with Clean Code Java SE 9」を見てみてください。

Javaでスクリプトが重要な理由

最初に、Javaプログラミング言語で利用できることが重要である理由を理解するために、スクリプトとは何かを思い出してみましょう。

スクリプトは次のように定義できます。
「スクリプトは、特定の実行時環境用に記述されたプログラムであり、タスクまたはコマンドの実行を自動化します。タスクまたはコマンドは、人が1つずつ実行することもできます。」

この一般的な定義から、スクリプト言語の簡単な定義を導き出すことができます。スクリプト言語は、一度に1つのコマンドを解釈および実行するために高レベルの構造を使用するプログラミング言語です。

スクリプト言語は、ファイル内の一連のコマンドを使用するプログラミング言語です。一般に、スクリプト言語はしばしば(コンパイルではなく)インタープリットされ、プログラミングの手続き型スタイルに向かう傾向があります(ただし、一部のスクリプト言語にはオブジェクト指向機能もあります)

一般に、スクリプト言語は、Java、CC++などのより構造化されたコンパイル言語よりも、学習しやすく、コーディングも高速です。サーバサイドスクリプト言語の例としてPerl、PHP、Pythonなどがあり、クライアントサイドではJavaScriptがあります。

長い間、Javaは適切に構造化され、厳密に型指定されたコンパイル言語として分類され、任意のコンピューターアーキテクチャで実行するために、JVMによってインタープリットされます。ただし、Javaに関する不満の1つは、一般的なスクリプト言語と比較すると、学習やプロトタイプの作成がそれほど速くないことです。

ただし、Javaは現在24歳の言語であり、世界中で約940万人の開発者が使用しています。若い世代のプログラマーがコンパイルとIDEの儀式を必要とせずにJavaを学び、その機能とAPIを簡単に試せるようにするため、最近のリリースではいくつかの機能が追加されました。Java SE 9以降、対話型プログラミングをサポートするJShell (REPL)ツールが追加されました。これは、Javaのプログラミングと学習を容易にすることを目的としています。

今やJDK 11では、Javaはjavaコマンドを呼び出すだけでコードを実行できるため、スクリプトをサポートできるプログラミング言語になりつつあります!

Java 11でスクリプトを作成するには、2つの基本的な方法があります。

  1. javaコマンドツールを直接使用する。
  2. *nixコマンドラインスクリプトの使用する(bashスクリプトに似ている)。

最初のオプションについては既に検討したので、今度は2番目のオプションを見てみましょう。これは、多くの可能性への扉を開く機能です。

Shebangファイル:Javaをシェルスクリプトとして実行する

前述のように、Java SE 11ではスクリプトのサポートが導入されました。これには、従来の*nix、いわゆるshebangファイルが含まれます。この機能をサポートするために、JLS (Java Language Specification)を変更する必要はありません。

一般的なshebangファイルでは、最初の2バイトは0x230x21である必要があります。これは、2つの文字「#!」のASCIIエンコーディングです。その後、ファイルの後続のすべてのバイトは、デフォルトのプラットフォーム文字エンコーディングで読み取られます。

したがって、#!で始まる最初の行は、オペレーティングシステムのshebangメカニズムでファイルを実行する場合にのみ必要です。 つまり、上記のHelloUniverse.javaの例のように、ソースファイルからコードを明示的に実行するためにJavaランチャーを明示的に使用する場合、特別な最初の行は必要ありません。

次の例を、macOS Mojave 10.14.5のターミナルで実行しましょう。ただし、最初に、shebangファイルを作成する際に従うべき重要なルールをいくつか列挙します。

  • Javaコードをオペレーティングシステムのシェルスクリプト言語とミックスしない。
  • VM(仮想マシン)オプションを含める必要がある場合、shebangファイルの名前に続いて、--sourceを最初のオプションとして指定する必要がある。これらのオプションには、--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ソースファイルの標準命名規則( .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

最後に、この機能を使用する際に知っておくべきいくつかのヒントとコツを示して終わりにしましょう。

ヒントとコツ

  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
  1. クラスとパッケージの名前の競合の可能性に留意してください。 次のディレクトリ構造を見てください。

    mohamed_taman:code$ tree
    .
    ├── Greater.java
    ├── HelloUniverse
    │   ├── java.class
    │   └── java.java
    ├── HelloUniverse.java
    ├── PalindromeChecker.java
    ├── UsersHttpClient.java
    ├── dirlist
    └── greater

    HelloUniverseパッケージの下の2つのファイルjava.javaと、現在のディレクトリのファイルHelloUniverse.javaに注目してください。 実行しようとするとどうなるでしょうか。

    mohamed_taman:code$ java HelloUniverse.java

    最初のファイルと2番目のファイルのどちらを実行するでしょうか。javaランチャーは、HelloUniverseパッケージ内のクラスファイルを参照しません。代わりに、ソースからHelloUniverse.javaファイルをロードして実行し、現在のディレクトリのファイルが実行されます。

このshebang機能は、Java言語の力を使用して多くの作業を自動化するスクリプトを作成する可能性の世界を開くので、私はこのshebang機能を使うのが大好きです。

まとめ

Java SE 11以降、プログラミング言語の歴史の中で初めて、コンパイルせずにJavaコードを含むスクリプトを直接実行できるようになりました。Java 11ソース実行機能により、Javaでスクリプトを記述し、*inxコマンドラインから直接実行できます。

今日、この新機能の実験を開始し、コーディングを楽しんでください。仲間のオタクと共有するために、言葉を広めてくれるとうれしいです。

参考

著者について

Mohamed Taman氏はDevTech d.o.oのシニアエンタープライズアーキテクト、Javaチャンピオン、Oracle Groundbreaker Ambassador, Adopts Java SE.next()、JakartaEE.next()、JCPメンバである。JCP Executive Committeeメンバ、JSR 354、363、373 Expert Groupメンバ、EGJUGリーダ、Oracle Egypt Architects Clubボードメンバであった。Javaを話し、Mobile、Big Data、Cloud、Blockchain、DevOpsに興味がある。国際的なスピーカーで、「JavaFX essentials」、「Getting Started with Clean Code, Java SE 9」、「Hands-On Java 10 Programming with JShell」の著者。新しい著書として「Secrets of a Java Champions」がある。Duke’s choice 2015, 2014アワードとJCP outstanding adopt-a-jar participant 2013アワードを受賞。

この記事に星をつける

おすすめ度
スタイル

BT