Java のラムダ式のクラスファイルを調べてみた!
Java のラムダ式が、どのようにコンパイルされ実行されているか気になりますよね。 そこで、クラスファイルを分析して、その中身を調べてみました。
なお、今回は OpenJDK 17.0.1 を使って調べています。 バージョンによって挙動が異なる場合があるので、ご注意ください。
外側のローカル変数を参照しない場合
おさらい:匿名クラスのコンパイル結果
本題のラムダ式を調べてみる前に、匿名クラスの場合にどのようにコンパイルされているかを確認しておきましょう。 (ご存じの方は、読み飛ばしてもらって大丈夫です)
今回は、Runnable インタフェースを匿名クラスとして実装しました。
public class Main { public static void main(String[] args) throws InterruptedException { var thread = new Thread(new Runnable() { @Override public void run() { System.out.println("Hello world!"); } }); thread.run(); thread.join(); } }
このクラスをコンパイルすると、2つのクラスファイルが出来上がりました。
Main.class
Main$1.class
後者が匿名クラスのコンパイル結果です。 これを、OpenJDK 付属の javap でリバースアセンブルしてみました。
Classfile /C:/Users/YujiSoftware/Desktop/java/Main$1.class Last modified 2021/12/18; size 534 bytes SHA-256 checksum f6f8ec5118c480c54b31ee06db110837e3459fa50f8d052a7d157b32885fd22d Compiled from "Main.java" class Main$1 implements java.lang.Runnable minor version: 0 major version: 61 flags: (0x0020) ACC_SUPER this_class: #21 // Main$1 super_class: #2 // java/lang/Object interfaces: 1, fields: 0, methods: 2, attributes: 4 { Main$1(); descriptor: ()V flags: (0x0000) Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public void run(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String Hello world! 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 6: 0 line 7: 8 }
この javap の結果を、@YujiSoftware を使って Java のソースコードに変換してみました。 1
class Main$1 implements java.lang.Runnable { Main$1(){ super(); } public void run() { System.out.println("Hello world!"); } }
「外側のクラス名 + $ + 連番」という名前のクラスです。 クラスの中身は、普通の(匿名ではない)クラスとして実装したのと同じです。
なお、Java 言語仕様に基づいて、匿名コンストラクタが追加されています。
:::note info 【 Java言語仕様:15.9.5.1 匿名コンストラクタ 】 匿名クラスは、明示的にコンストラクタを宣言することができない。その代わりに、コンパイラは匿名クラスに対して匿名コンストラクタ(anonymous constructor)を自動的に提供しなければならない。 :::
ラムダ式のコンパイル結果
続いて、ラムダ式の場合を見ていきましょう。 先ほどの匿名クラスを使って書いた部分を、ラムダ式に置き換えました。
public class Main { public static void main(String[] args) throws InterruptedException { var thread = new Thread(() -> System.out.println("Hello world!")); thread.run(); thread.join(); } }
このクラスをコンパイルすると、1つのクラスファイルが出来上がりました。
Main.class
匿名クラスの時のように、コンパイル時にはクラスファイルは作られませんでした。 代わりに、 実行時(より詳しく言えば、ラムダ式を含んだメソッドを初めて実行する際) に、メモリ上にクラスファイルが作られました。
メモリ上…、調べるのがとてもめんどくさいですね。
でも、簡単な方法があります。
システムプロパティ jdk.internal.lambda.dumpProxyClasses
でディレクトリを指定しておくと、その作られたクラスファイルを出力してくれるのです。
今回は、以下のように Java コマンドの引数でシステムプロパティを指定しました。
java -Djdk.internal.lambda.dumpProxyClasses=output Main
こうして実行すると、output ディレクトリにクラスファイルが出来上がりました。
Main$$Lambda$1.class
このクラスファイルを javap して、Java のソースコードに変換してみました。
Main$$Lambda$1.class の javap 結果
Classfile /C:/Users/YujiSoftware/Desktop/java/Main$$Lambda$1.class
Last modified 2021/12/25; size 227 bytes
SHA-256 checksum b1184aa81051852c052de612ab4b9f272f87cfd655717abb39dfa8ee74d8c5f4
final class Main$$Lambda$1 implements java.lang.Runnable
minor version: 0
major version: 59
flags: (0x1030) ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC
this_class: #2 // Main$$Lambda$1
super_class: #4 // java/lang/Object
interfaces: 1, fields: 0, methods: 2, attributes: 0
{
private Main$$Lambda$1();
descriptor: ()V
flags: (0x0002) ACC_PRIVATE
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
public void run();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #16 // Method Main.lambda$main$0:()V
3: return
}
final class Main$$Lambda$1 implements java.lang.Runnable { private Main$$Lambda$1() { } public void run() { Main.lambda$main$0(); } }
ラムダ式の中身である System.out.println("Hello world!");
がありません。
代わりに、 Main
クラスの lambda$main$0()
という static
メソッドを呼び出しています。
このメソッドは何者でしょうか。
それを確認するために、Main.class
を、 javap してみました。
すると、このクラス内に lambda$main$0()
というメソッドが含まれていることが分かりました。
private static void lambda$main$0(); descriptor: ()V flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=2, locals=0, args_size=0 0: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #27 // String Hello world! 5: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0
private static void lambda$main$0() { System.out.println("Hello world!"); }
どういうことかというと…
- コンパイル時に、 ラムダ式の中身が
static
メソッドに変換され、 コンパイルされる - 実行時に、 インタフェース(今回は
Runnable
)の実装クラスが作られ、 コンパイルされる- このクラスは、事前に作られた
static
メソッドを呼び出すだけ
- このクラスは、事前に作られた
匿名クラスとは、処理が大きく異なりますね。
外側のローカル変数を参照する場合
続いて、匿名クラスやラムダ式で その外側にあるローカル変数を参照した場合、 どのようなクラスファイルが出来上がるのかを見ていきましょう。
おさらい:匿名クラスのコンパイル結果
今回は、 main
メソッド内にあるmessage
というローカル変数を匿名クラス内で参照した場合に、どのようなクラスファイルになるかを見ていきます。
public class Main { public static void main(String[] args) throws InterruptedException { var message = "Hello world!"; var thread = new Thread(new Runnable() { @Override public void run() { System.out.println(message); } }); thread.run(); thread.join(); } }
このクラスをコンパイルすると、先ほどと同様に2つのクラスファイルが出来上がりました。
Main.class
Main$1.class
後者のクラスファイルを、javap して、Java のソースコードに変換してみました。
Main$1.class の javap 結果
Classfile /C:/Users/YujiSoftware/Desktop/java/Main$1.class
Last modified 2021/12/25; size 603 bytes
SHA-256 checksum 4b78e2e34c543f25184362a99d8b506dbe2fd4c46e7a662e245e6b96b8908c95
Compiled from "Main.java"
class Main$1 implements java.lang.Runnable
minor version: 0
major version: 61
flags: (0x0020) ACC_SUPER
this_class: #2 // Main$1
super_class: #8 // java/lang/Object
interfaces: 1, fields: 1, methods: 2, attributes: 4
{
final java.lang.String val$message;
descriptor: Ljava/lang/String;
flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC
Main$1();
descriptor: (Ljava/lang/String;)V
flags: (0x0000)
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #1 // Field val$message:Ljava/lang/String;
5: aload_0
6: invokespecial #7 // Method java/lang/Object."<init>":()V
9: return
LineNumberTable:
line 4: 0
Signature: #12 // ()V
public void run();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #1 // Field val$message:Ljava/lang/String;
7: invokevirtual #19 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
LineNumberTable:
line 7: 0
line 8: 10
}
SourceFile: "Main.java"
EnclosingMethod: #34.#36 // Main.main
NestHost: class Main
InnerClasses:
#2; // class Main$1
class Main$1 implements java.lang.Runnable { final String val$message; Main$1(String arg1) { this.val$message = arg1; super(); } public void run() { System.out.println(this.val$message); } }
なんと、コンストラクタに引数が追加されています。 そして、その引数の値をフィールド変数として保持しています。
run()
メソッド内では、そのフィールド変数の値を参照しています。
直接、ローカル変数の値を参照しているわけではないんですね…。
よく見ると、コンストラクタでフィールド変数に代入してから、super()
を呼び出しています。
Java 言語仕様(8.8.7 コンストラクタの本体)としては、最初に this
または super
のコンストラクタを明示的または暗黙的に呼び出さなくてはいけないので、あれ?という感じがしますね。
でも、大丈夫です。この点は、Javaのクラスファイルとしては問題ありません。
:::note 【 Java仮想マシン仕様:4.8.2 構造上の制約 】 クラス Object のコンストラクタから導出されたインスタンス初期化メソッドを除いた各インスタンス初期化メソッドは、該当インスタンス・メンバに対するアクセスの前に、this に対する別のインスタンス初期化メソッドか、その直接のスーパークラス super に対するインスタンス初期化メソッドのいずれかを呼び出さなければならない。しかし、インスタンス初期化メソッドの呼び出しに先立って、カレント・クラスで宣言されている this に対するインスタンスフィールドへの代入を行うことができる。 :::
ラムダ式のコンパイル結果
続いて、ラムダ式の場合を見ていきましょう。 先ほどの匿名クラスを使って書いた部分を、ラムダ式に置き換えました。
public class Main { public static void main(String[] args) throws InterruptedException { var message = "Hello world!"; var thread = new Thread(() -> System.out.println(message)); thread.run(); thread.join(); } }
このクラスをコンパイルすると、1つのクラスファイルが出来上がりました。
Main.class
そして、これをシステムプロパティ jdk.internal.lambda.dumpProxyClasses=output
を付けて実行した結果、output ディレクトリにクラスファイルが出来上がりました。
Main$$Lambda$1.class
このクラスファイルを javap して、Java のソースコードに変換してみました。
Main$$Lambda$1.class の javap 結果
Classfile /C:/Users/YujiSoftware/Desktop/java/Main$$Lambda$1.class
Last modified 2021/12/25; size 307 bytes
SHA-256 checksum 9ba2c22eca9a152b636e2a0b00a4a12cb0039ed659914bcc48103ec6718a0d17
final class Main$$Lambda$1 implements java.lang.Runnable
minor version: 0
major version: 59
flags: (0x1030) ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC
this_class: #2 // Main$$Lambda$1
super_class: #4 // java/lang/Object
interfaces: 1, fields: 1, methods: 2, attributes: 0
{
private final java.lang.String arg$1;
descriptor: Ljava/lang/String;
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
private Main$$Lambda$1(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0002) ACC_PRIVATE
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #13 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #15 // Field arg$1:Ljava/lang/String;
9: return
public void run();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #15 // Field arg$1:Ljava/lang/String;
4: invokestatic #21 // Method Main.lambda$main$0:(Ljava/lang/String;)V
7: return
}
final class Main$$Lambda$1 implements java.lang.Runnable { private final String arg$1; private Main$$Lambda$1(String arg1) { super(); this.arg$1 = ar$1; } public void run() { Main.lambda$main$0(this.arg$1); } }
匿名クラスと同様に、コンストラクタに引数が追加されています。 そして、その引数の値をフィールド変数として保持しています。
run()
メソッド内では、そのフィールド変数の値を参照しています。
あとは、「外側のローカル変数を参照しない場合」と同じですね。
外側のローカル変数をいっぱい参照する場合
ローカル変数は、1メソッド内で 65,535 個まで宣言できます。
また、メソッドの引数には int
型などでは 255 個まで宣言できます。2
(コンストラクタも同様)
ということは、ラムダ式の内部から、外側のローカル変数を256個以上参照した場合はどうなるのでしょうか。 ここまで見てきたように、ローカル変数はコンストラクタの引数で受け渡していますが、メソッドの引数は 255 個までなので 256 個以上は渡せません。
ローカル変数を256個参照した場合
外側のローカル変数を256個参照するラムダ式を、実際にコンパイルしてみました。
public class Main { public static void main(String[] args) throws InterruptedException { var number1 = 1; var number2 = 2; (中略) var number255 = 255; var number256 = 256; var thread = new Thread(() -> { System.out.println(number1); System.out.println(number2); (中略) System.out.println(number255); System.out.println(number256); }); thread.run(); thread.join(); } }
その結果、 コンパイルエラーになりました。
C:\Users\YujiSoftware\Desktop\java>javac *.java Main.java:1: エラー: パラメータが多すぎます エラー1個
特に対処することなく、256個の引数を持つ static メソッドを作ろうとしてエラーになるみたいです。
ちなみに、匿名クラスの場合はコンパイルに成功しました。
ただし、無効なクラスファイルになったため、実行時に ClassFormatError
になりました。
C:\Users\YujiSoftware\Desktop\java>java -Djdk.internal.lambda.dumpProxyClasses=. Main Exception in thread "main" java.lang.ClassFormatError: Too many arguments in method signature in class file Main$1 at java.base/java.lang.ClassLoader.defineClass1(Native Method) at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012) at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150) at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862) at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520) at Main.main(Main.java:260)
ローカル変数を255個参照した場合
ラムダ式で参照する外側のローカル変数を255個に減らしてみました。 すると、コンパイルに成功しました。
しかし、実行してみると BootstrapMethodError になりました。
C:\Users\YujiSoftware\Desktop\java>java -Djdk.internal.lambda.dumpProxyClasses=. Main Exception in thread "main" java.lang.BootstrapMethodError: java.lang.IllegalArgumentException: bad parameter count 256 at Main.main(Main.java:260) Caused by: java.lang.IllegalArgumentException: bad parameter count 256 at java.base/java.lang.invoke.MethodHandleStatics.newIllegalArgumentException(MethodHandleStatics.java:167) at java.base/java.lang.invoke.MethodType.checkSlotCount(MethodType.java:223) at java.base/java.lang.invoke.MethodType.insertParameterTypes(MethodType.java:437) at java.base/java.lang.invoke.MethodType.appendParameterTypes(MethodType.java:461) at java.base/java.lang.invoke.DirectMethodHandle.makePreparedLambdaForm(DirectMethodHandle.java:256) at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:233) at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:218) at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:227) at java.base/java.lang.invoke.DirectMethodHandle.make(DirectMethodHandle.java:108) at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectMethodCommon(MethodHandles.java:4004) at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectMethodNoSecurityManager(MethodHandles.java:3960) at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectMethodForConstant(MethodHandles.java:4204) at java.base/java.lang.invoke.MethodHandles$Lookup.linkMethodHandleConstant(MethodHandles.java:4152) at java.base/java.lang.invoke.MethodHandleNatives.linkMethodHandleConstant(MethodHandleNatives.java:615) ... 1 more
java.lang.IllegalArgumentException: bad parameter count 256
とのことです。
これは、コンストラクタの最初の引数として暗黙的に自身のオブジェクトが追加されているため、引数の数が +1 されて256個になってしまったのが原因のようです。
ローカル変数を254個参照した場合
もう一個減らして、ラムダ式で参照する外側のローカル変数を254個にしてみました。
しかし、これも実行してみると BootstrapMethodError になりました。 (255個の時と微妙にスタックトレースが違います)
C:\Users\YujiSoftware\Desktop\java>java -Djdk.internal.lambda.dumpProxyClasses=. Main Exception in thread "main" java.lang.BootstrapMethodError: bootstrap method initialization exception at java.base/java.lang.invoke.BootstrapMethodInvoker.invoke(BootstrapMethodInvoker.java:188) at java.base/java.lang.invoke.CallSite.makeSite(CallSite.java:315) at java.base/java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:281) at java.base/java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:271) at Main.main(Main.java:259) Caused by: java.lang.IllegalArgumentException: bad parameter count 256 at java.base/java.lang.invoke.MethodHandleStatics.newIllegalArgumentException(MethodHandleStatics.java:167) at java.base/java.lang.invoke.MethodType.checkSlotCount(MethodType.java:223) at java.base/java.lang.invoke.MethodType.insertParameterTypes(MethodType.java:437) at java.base/java.lang.invoke.DirectMethodHandle.makePreparedLambdaForm(DirectMethodHandle.java:259) at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:233) at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:218) at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:227) at java.base/java.lang.invoke.DirectMethodHandle.makeAllocator(DirectMethodHandle.java:142) at java.base/java.lang.invoke.DirectMethodHandle.make(DirectMethodHandle.java:133) at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectConstructorCommon(MethodHandles.java:4122) at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectConstructor(MethodHandles.java:4106) at java.base/java.lang.invoke.MethodHandles$Lookup.findConstructor(MethodHandles.java:2751) at java.base/java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:269) at java.base/java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:341) at java.base/java.lang.invoke.BootstrapMethodInvoker.invoke(BootstrapMethodInvoker.java:134) ... 4 more
これは、原因が分かりませんでした。 ラムダ式ではなく、匿名クラスだと参照するローカル変数が254個でも大丈夫なんですが…。
ちなみに、参照するローカル変数を253個にまで減らせば、ラムダ式でもエラーにならずに実行できました。
最後に
Java Advent Calendar 2021 も、これにて終了です!
みなさん、お疲れ様でした。
メリークリスマス!!!
JJUG CCC 2021 Fall ( #jjug_ccc ) - セッション資料の一覧
JJUG CCC 2021 Fall に参加しました!
JJUG CCC オンラインも3回目ということで、JJUG運営の方も参加者の方もお互い慣れてきた感じがします。 自分も、今回は夕方から用事があったので、時間差でセッションを見ながら電車で移動ということをやりました。ラジオの生放送と Radiko のオンデマンドの組み合わせみたいな感じで、リアルタイムでみんなで一緒にわいわいできるのも、時間と場所にとらわれずに見れるのもどっちもいいなーと思うようになりました。
だから、オフラインで集まりつつ1、今回のように配信でも見れるといいのかな、と。
コストが不安になりますがw
セッションは、最近のお仕事に関連しそうな「エキサイトブログ刷新に向けて序章」など、パフォーマンス改善やリファクタリングの話を中心に聞いていました。 教科書的な話だけでなく、なぜか歴史的経緯で不思議な構成になっているという話に「あるあるw」と共感したり、現状をもとにした工夫を聞いて何か自分の仕事にも取り入れられないかと考えたりと、いつものことながら楽しく役に立つ時間でした。
さて、最後にいつものを。
今回、残念ながら時間がかぶってしまって参加できなかったセッションがいっぱいあったので、あとで読むために現時点で発表者の方が公開されている資料一覧をまとめしました。2
(あとで JJUG CCC 2021 Fall のページにもリンクが載るかもしれませんが、とりあえず自分の方で調べました)
10:00
- A: GraalVM を普通の Java VM として使う ― クラウドベンチマークでの比較 / 石井 佑弥
- B: Applied Domain-Driven Design Blueprints for Jakarta EE(
動画) / Reza Rahman
- C: 勤怠管理サービスでの継続的テストの取り組み(
動画) / yuya yamaguchi
- D: エキサイトブログ刷新に向けて序章 - APIを一つに(
動画) / 中尾正剛
- D: 変わりゆくAPI連携仕様との付き合い方(
動画) / 太田 拓也
11:00
- A: k8sでJavaを見守る新常識(
動画) / 伊藤ちひろ
- B: Azure Spring Cloud の現状と最新 Azure Container Apps のご紹介(
動画) / てらだよしお
- C: Javaアプリケーションのアーティファクト管理とDevSecOps(
動画) / Ayana Yokota
- D: クリーンアーキテクチャは難しい?(
動画) / 石橋 和洋
12:30
- A: Java ジェネリクス入門(
動画) / なぎせゆうき
- B: Spring Boot採用システムを定期Upgradeするお話 / 山縣 京輔
- B: JavaエンジニアがKubernetesでGitOpsに入門する(
動画) / 正野 勇嗣
- C: Goodbye JSR305, Hello JSpecify!(
動画) / Kengo TODA
- C: LogbackからLog4j 2への移行によるアプリケーションのスループット改善(
動画) / 磯田 浩靖
- D: Kotlinを研究する(
動画) / 小山 健太
13:30
- A: 最新 LTS Java 17 でプログラムが書きやすくなるという話(
動画) / 石井 真
- B: Spring Batch でサブスク1億件のクレカ決済処理 / 鈴木 隆志
- C: 無限にスケールできるRDB(NewSQL)でやれること(
動画) / 成臣
- D: LINEでKotlinを活用して新規サービスを作っていく話(
動画) / 岩谷 明
15:00
- A: Head toward Java 17 and Java 18(
動画) / KUBOTA Yuji
- B: Kafka Cluster Design Patterns 2021 Fall(
動画) / 須田 桂伍
- C: Debeziumで変更データキャプチャを学ぼう / yy_yank
- D: 暗算脳トレアプリで学ぶモデリング(
動画) / Abe Asami
- D: 今のうちに!オンラインの英語Javaカンファレンスに慣れて渡航再開とともに現地参加(あまつさえ講演も)できる状態になっておこう!(
動画) / 阪田 浩一
16:00
- A: GitHub Actionsを使って学んだCI/CD環境のあれこれ(
動画) / 大城 夏樹
- B: 400種類以上のAPI連携をどうやって開発・サポートしているのか? データパイプラインツール CData Sync の中身(
動画) / 杉本 和也 / CData Software Japan
- C: TDD: seriously, try it! / Nacho Cougil
- D: 巨大なトランザクションを分解せよ!イベント駆動入門 in Spring Framework(
動画) / 成瀬 允宣
17:00
- A: Getting the Most from Modern Java(
動画) / Simon Ritter
- B: Building for Resiliency, Fault-Tolerance, Scalability and Your User's Expectation(
動画) / Chris Engelbert
- D: Javaとコミュニティの歩み / 日本Javaユーザーグループ 鈴木雄介/杉山貴章
過去記事
- JJUG CCC 2021 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2020 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2019 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2019 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2018 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2018 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2017 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2017 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2016 Fall ( #jjug_ccc ) - セッション資料の一覧へのリンク
- JJUG CCC 2016 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2015 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2015 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2014 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2014 Spring ( #jjug_ccc ) - セッション資料の一覧
Outlook のメールファイル(.msg ファイル)をテキストに展開するツールを用意しました
たまに、Outlook のメールファイル(.msg ファイル)が添付で送られてくるのですが、手元に Windows 版の Outlook がなくて1内容を確認できなくて困ることが多々…。
そこで、.msg ファイルをテキストファイルに展開するツールを用意しました。
(添付ファイルも展開できます)
YujiSoftware/msg2txt: Extract .msg file (Outlook mail file) to a text file.
インストーラ版の方には Java ランタイムが含まれていますので、どの環境でもすぐに使えます。
自分のお気に入りの Java ランタイムを使いたいという場合は、スタンドアローン版をご利用ください! 2
実装について
Apache POI-HSMF(Java API To Access Microsoft Outlook MSG Files)のサンプルプログラムをビルドしただけです。
頑張ったところ
それだけだと面白みがないので、Java 15 で正式リリースされた Packaging Tool を使って、インストーラ版も作ってみました。
… といっても、適切な引数を指定して jpackage コマンドを実行するだけでいい感じのインストーラができあがるので、とても簡単でした。
なお、手元に macOS のビルド環境はないので、インストーラの作成には GitHub Actions を使いました。3
runs-on: macOS-latest
と指定するだけでいい感じの環境でビルドしてくれるので、マニュアル通りに書いていくだけでサクッと使えました。
これ、便利ですね(いまさら感)。
まとめ
というわけで、ぜひご利用ください!
いつから touch コマンドでファイルを作れるようになったのか
答え: 最初から
最初の touch コマンド
FreeBSD の touch のマニュアル曰く、touch コマンドは Version 7 の AT&T UNIX で登場したそうです。
HISTORY A touch utility appeared in Version 7 AT&T UNIX. touch(1) - FreeBSD Manual Pages
このときの man を読むと、すでに「ファイルが存在しなければ作成する」ということが記載されています。
DESCRIPTION
Touch attempts to set the modified date of each file. This is done by reading a character from the file and writing it back. If a file does not exist, an attempt will be made to create it unless the -c option is specified.
Touchは、それぞれのファイルの修正日時の設定を試みます。 これは、ファイルから文字を読み取り、それを書き戻すことによって行われます。 ファイルが存在しない場合、-c オプションが指定されていない限り、ファイルの作成を試みます。
unix-history-repo/touch.1 at Research-V7 · dspinellis/unix-history-repo · GitHub
ソースコード1を読むと、たまたまファイルがなければ作るようになったのではなく、あえてそのように実装していたことが分かりました。
具体的には、まず stat(3)
でファイルの存在を確認して、なければ create(3)
でファイルを作るという実装をしていました。
unix-history-repo/touch.c at Research-V7 · dspinellis/unix-history-repo · GitHub
#include <sys/types.h> #include <sys/stat.h> touch(force, name) int force; char *name; { struct stat stbuff; char junk[1]; int fd; if( stat(name,&stbuff) < 0) if(force) goto create; else { fprintf(stderr, "touch: file %s does not exist.\n", name); return; } if(stbuff.st_size == 0) goto create; if( (fd = open(name, 2)) < 0) goto bad; if( read(fd, junk, 1) < 1) { close(fd); goto bad; } lseek(fd, 0L, 0); if( write(fd, junk, 1) < 1 ) { close(fd); goto bad; } close(fd); return; bad: fprintf(stderr, "Cannot touch %s\n", name); return; create: if( (fd = creat(name, 0666)) < 0) goto bad; close(fd); }
では、なぜファイルが存在しない場合にエラーにするのではなく、ファイルを作るようにしたのでしょうか。
それは、わかりませんでした(ぇ
まとめ
いかがでしたでしょうかw
ソースコードにコメントはなく、なぜこのような仕様にしたのかはこのコードを書いたDennis M. Ritchie氏2しか知る由がなさそうです。ただ、2011年に亡くなられたので、もはや聞くことはできません…。
おそらく、その方が便利だからだとは思うのですが、確証がありません。
もしかしたら本か何かに、その意図が残されているかもしれません。
ご存じの方がいましたら、情報をお寄せいただければと思います。
JJUG CCC 2021 Spring ( #jjug_ccc ) - セッション資料の一覧
JJUG CCC 2021 Spring に参加しました!
前回に引き続き、今回もオンライン開催となりました。
オンラインだと移動が楽というメリットがあって、これはどっちのセッション見ようかなーというときに、途中でさっとほかの動画に移れるのは便利でした。
あとは、家なので適当にお菓子つまんだり休憩時間中にちょっと横になって昼寝したりと、気軽に楽しめました。
(オフラインと違い、コーヒースポンサーがないのが残念w)
今回、一番おもしろかったのはとださんの「Java8〜16におけるバイトコード生成の変化」でした。
コンパイラーが変なクラスファイル吐くけど⇒Java言語仕様に則っているよ!とか、短いながらもへぇ!という内容が満載でした。
JJUG CCC は、こういう Java の奥深い話が聞けるのが個人的に大好です。
次回の秋も、オンライン開催予定とのことです(日程未定)。
2022年の JJUG CCC こそは、オフラインもできるようになっているといいですね…。また懇親会でお話したいです。
さて、最後にいつものを。
今回、残念ながら時間がかぶってしまって参加できなかったセッションがいっぱいあったので、あとで読むために現時点で発表者の方が公開されている資料一覧をまとめしました。1
(あとで JJUG CCC 2021 Spring のページにもリンクが載るかもしれませんが、とりあえず自分の方で調べました)
10:00
- A: GraalVMのJavaネイティブビルド機能でどの程度起動が速くなるのか?(
動画) / Ho Chi To
- B: フロントエンド・バックエンド分離の道のり(
動画) / masahiro kawakami
- C: Spockで学ぶテスト駆動開発のコツ(
動画) / 米久保 剛
- D: eclipse ユーザのためのVSCodeのススメ / ちけみん
11:00
- A: 今どき?のJavaにおける例外処理についての考察(
動画) / 松下正嗣
- B: Java 開発者のための Kubernetes パッケージマネージャー: Helm(
動画) / 三宅 剛史
- C: Jakarta EE 9.1とパッケージ名変更のためのツールEclipse Transformerについて(
動画) / 髙橋 博実
- D: レガシーなシステムにモダンなAPIを導入した話(
動画) / Koji Kishiura
12:00
- A: 次のLTS Java 17にむけてJava 16までをおさらいしよう(
動画) / きしだ なおき
- B: OpenID Connect 1.0 with Spring Security(
動画) / 多田真敏
- C: これから始める単体テスト改善に向けた第一歩 ~JUnit編~(
動画) / 大城 夏樹
- D: あなたの勘と経験は本当に合っていると断言できますか? プロダクション環境での計測のススメ(
動画) / 株式会社リクルート シニアアーキテクト 森廣 隆行 / 株式会社リクルート 與那城 有
13:00
- A: JDK 16 で導入された JEP 396 にご注意!!(
動画) / Yoshiro Tokumasu
- B: サーバーレスAPIをKotlinで開発してみよう!(
動画) / ポール
- C: Jakarta REST in depth(
動画) / 蓮沼 賢志
- D: ソフトウェアアーキテクチャの選び方 / 成瀬 允宣
14:00
- A: Microsoft Build の OpenJDK に関する詳細のご共有と Java on Azure 最新アップデート(
動画) / てらだよしお
- B: Red HatのKafkaとサーバーレスコンピューティングでバッチ処理をニアリアルタイム化しよう(
動画) / 暮林達也
- C: 少しずつ学べば繋がりが見えてくる!DevOpsを支えるツールと最近の技術トレンド(
動画) / よこな (横田紋奈)
- D: オンライン広告入札システムとZGC(
動画) / 磯田 浩靖
15:00
- A: Java8〜16におけるバイトコード生成の変化(
動画) / Kengo TODA
- B: PostgreSQLの行レベルセキュリティとSpringAOPでマルチテナントのユーザー間情報漏洩を防止する(
動画) / 松岡(@little_hand_s)
- C: Fly like a rocket with Helidon(
動画) / Aleksandrov Dmitry
- D: JFR などのツールを用いて FullGC や OOME の原因を特定する流れ(
動画) / 髙市 智章
16:00
- A: AOT or JIT: Faster Startup, Faster Code, or Faster Both?(
動画) / Simon Ritter
- B: モダンな技術をエンタープライズ開発の現場にインストールした話(
動画) / 田口 亮太郎
- C: お名前.comにおける長続きさせるバックエンド側の開発・保守について(
動画) / 小林 隆晴
- D: Kotlin for Databases(
動画) / Victor Durán
17:00
- A: Plug-in Architectures with the Java Module System(
動画) / Andres Almiray
- B: Migrate Spring Boot app to Quarkus. Stage unlocked(
動画) / Jonathan Vila
- C: Framewars: the battle between NoSQL and Java in the cloud a / Otavio Santana
- D: How Should Java Developers Build Front-Ends Today?(
動画) / Karsten Silz
過去記事
- JJUG CCC 2020 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2019 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2019 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2018 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2018 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2017 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2017 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2016 Fall ( #jjug_ccc ) - セッション資料の一覧へのリンク
- JJUG CCC 2016 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2015 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2015 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2014 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2014 Spring ( #jjug_ccc ) - セッション資料の一覧
-
発表者のお名前は敬称略とさせていただきました。↩
「正しい番号が入力されていること」という難しさ
バリデーションの難しさについて、少し考えてみましょう。
(結論だけ読みたい人は、こちら)
例えば、通販サイトの基本設計書で「入力フォームの電話番号の項目に、正しい電話番号が入力されていない場合はエラーとすること」と書かれていたとします。
では、正しい電話番号が入力されているかどうかの判定とは、どのように実装すればいいのでしょうか。
「正しい電話番号」とは?
電話番号とはどのようなものでしょうか。
総務省のホームページに分かりやすくまとめられていました。1
総務省|電気通信番号制度|電話番号に関するQ&A
この中から一般的2に使われている市外局番から始まる電話番号3、または携帯電話4の番号で検討を進めます。
上記のページから関連する部分をピックアップすると、このようになっています。
◆電話番号とは、通話の相手を識別するために使われる、0から9までの数字を組み合わせた番号です。
「0A0から始まる番号(Aは0以外)」 ◆携帯電話とPHSは「070」、「080」又は「090」から始まる11桁の番号です。
「0ABCから始まる番号(A、B、Cは0以外)」 ◆普通の固定電話の電話番号は、次のようになっています。 固定電話の桁数 国内プレフィックス「0」市外局番「1~4桁」市内局番「1~4桁」加入者番号「4桁」 ※市外局番と市内局番は合計5桁 5
これを基にすると、(ハイフンを取り除いて)以下の正規表現にマッチすれば「正しい電話番号」と言えそうです。
((090|080|070)[0-9]{11}|0[0-9]{9})
しかし、この判定には問題があります。
有効な電話番号の判定
例えば、"0998-76-5432" は正しい番号番号と判定します。
しかし、今日時点で 998 という市外局番は存在しません。そのため、この電話番号は存在しない無効なものだと分かります。
つまり、厳密に正しい番号かどうかを判定するには、市外局番が存在するものかどうかも判定しなければいけません。
また、"060-4321-1234" は正しくない電話番号と判定します。
ですが、060 は携帯電話への割り当てが検討されており6、これはいずれ有効になる番号です。これを、正しくないものとしてエラーにしてしまうと、今後改修が必要になってしまいます。
しかも、ここまで考えても、まだ使われていない無効な電話番号という可能性が残されています。
「正しい番号 = 存在する有効な番号」なのかは微妙なところですが世間一般としてはそういう認識のようです。
存在する有効な番号の判定
では、「正しい番号 = 存在する有効な番号」を判定するにはどうすればいいのでしょうか。
解決策は、実際にその電話番号にかけてみるということです。
これならば、存在する有効な番号であることが確認できます。
銀行などは、実際にこの方式をとっています。
ただし、単純に実装すると脆弱性となります。
本の虫: ダイヤルQ2風の電話番号でInstagramやGoogleやMicrosoftから金をむしりとれる脆弱性
国内ではダイヤルQ2が終了しているので、このままの方法は取れません7が、ひたすら電話をかけさせて相手に莫大な電話料金を支払わせる、他人の番号を入力して DDoS させるといったことが考えられます。
このような可能性を考慮して、慎重に実装を行う必要があります。
簡単な判定
しかし、ここまで厳密な判定は必要なのでしょうか。
「存在しうる番号かどうかの判定」や「有効な電話番号の判定」を行うのはとても大変です。実装にも時間もかかります。
一番手っ取り早く、確実な方法は「正しい電話番号が入力されていること」という判定をしないことです。
シンプルに「数字のみが入力されていること」だけ判定すれば、考えなくてはいけないことがぐっと減ります。
その代わり、架空の番号で登録できてしまうことを許容する必要があります。
どこまでを求めるかは、要件次第です。
ちなみに
「正しい電話番号の判定」はこれでも簡単な方です。
なぜなら、電話局という(仮想的な)一つのデータベースに問い合わせすれば、存在する正しい番号かどうかわかるからです。
もし複数のデータベースに分散していて、それぞれに問い合わせる必要がある場合は、とても大変です。
分散しているすべてのデータベースに接続する必要があるためです。もしくは、番号を一つのデータベースに集約するという方法もありますが、これだと更新をどのように行うかを考えなくてはいけません。
例えば、ワクチンの接種番号は全国の市町村(今日時点で 1,7188)が管理しています。東京都区部に限っても23の区がそれぞれ管理しています。 これが正しいかどうかを確認するには、すべての市町村のデータベースと接続するか、番号を集約し常に更新し続ける必要があります。 参考:新型コロナウイルスワクチンに係る接種券等の印刷及び発送について - 総務省
それをやろうと思うと、時間も費用もかかると思います。
-
仕様としては ITU-T の E.164 勧告ですが、これは国別割り当て電話番号「国番号+国内電話番号を合わせた最大15桁」とだけ定められており、国内電話番号の部分(090-XXXX-XXXX、03-XXXX-XXXX など)は各国の方針となっています。↩
-
119 や 110 といった 1XY 特番、フリーダイヤルといった電話サービスの番号は除く↩
-
中継する電話会社を明示する番号を指定しない番号↩
-
衛星携帯電話は、国際電話扱いのようなのでここでは検討しない。衛星電話(インマルサット)あての電話のかけ方を教えてください。【インマルサット】↩
-
市外局番と市内局番の合計が4桁の地域もありましたが、現在はなくなりました。 9桁の電話番号 ‐ 通信用語の基礎知識↩
-
FMC(Fixed-Mobile Convergence)にすでに使われていますが、利用が伸びないので携帯電話にも使用しようという見当が行われています。 携帯電話の「060」がまもなく開放 そもそも「090」「080」「070」の違い、知ってる?|TIME&SPACE by KDDI↩
-
情報料代理徴収サービスの一種である災害募金サービスは残っているので、意図しない募金を行うということは可能です。↩
Javaのバージョン別、テキストファイルを一括で読み込む方法まとめ
前回の記事(Javaのバージョン別、1行ずつファイルを読む方法まとめ)への感想で、このような話がありました。
丸々読み込む方法のまとめもお願いします!
— ユースケ (山本裕介) (@yusuke) May 3, 2021
というわけで、今回はテキストファイルを一括で読み込む方法をまとめました。
(前回と被っている点は省略しているので、まだ読んでない人は先に前回の記事をどうぞ)
Java 1.1, 1.2, 1.3
private static String readString(File file) throws IOException { Reader reader = null; try { reader = new InputStreamReader(new FileInputStream(file), "UTF-8"); StringBuffer sb = new StringBuffer(); int len; char[] buffer = new char[1024 * 8]; while ((len = reader.read(buffer)) != -1) { sb.append(buffer, 0, len); } return sb.toString(); } finally { if (reader != null) { reader.close(); } } }
まだこのころはめんどくさいです。
FileInputStream
でファイルを読み込み、InputStreamReader
で指定した文字コードにデコードするようにしています。
読み込む際は、char[]
でバッファして StringBuffer
に追加していっています。
ちなみに、char[]
でバッファしながら読み込んでいるので、 BufferedReader
でラップする必要はありません。
Java 1.4, 1.5, 6
private static String readString(File file) throws IOException { CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder(); try (FileInputStream stream = new FileInputStream(file)) { FileChannel channel = stream.getChannel(); ByteBuffer bb = channel.map(MapMode.READ_ONLY, 0, channel.size()); CharBuffer cb = decoder.decode(bb); return cb.toString(); } }
ループがなくなって、だいぶ処理がすっきりしました。
このバージョンで追加された Charset
クラスから、CharsetDecoder
というデコーダを取得できます。
これに FileChannel#map(FileChannel.MapMode mode, long position, long size)
で取得した MappedByteBuffer
を渡すことで、ファイルを読み込めます。
MappedByteBuffer
はファイルの内容をメモリにマッピングして扱う(メモリマップドファイル)ので、大きなファイルの時に効率的に処理が行われます。
逆に言うと、小さいファイルを読み込む場合は性能が悪いです。
その場合は、今まで通り InputStream
で読み込みましょう。
ほとんどのオペレーティング・システムでは、ファイルをメモリーにマッピングするほうが、通常のreadメソッドまたはwriteメソッドを使って数十キロバイトのデータの読み込みまたは書込みを行うよりも負荷が大きくなります。 性能を重視するなら、比較的大きめのファイルだけをマッピングすることをお薦めします。 FileChannel (Java SE 15 & JDK 15))
----
ちなみに、この FileChannel
の map
メソッドを使うやり方はひしだまさんの記事を読んで知りました!
Javaバッファークラスメモ(Hishidama's Java Buffer Memo)
Java 7, 8, 9, 10
private static String readString(Path path) throws IOException { byte[] bytes = Files.readAllBytes(path); return new String(bytes, StandardCharsets.UTF_8); }
Files
クラスが追加され、これに一括でファイルを byte[]
として読み込むメソッドが用意されました。
これを使ってファイルを読み込めば、あとは String
クラスを new
するだけです。
ちなみに、このバージョンでファイルからすべての行を読み取る Files#readAllLines(Path, Charset)
も追加されました。
この戻り値は List<String>
です。行ごとに処理をしたい場合はこちらが便利です。
Java 11~
private static String readString(Path path) throws IOException { return Files.readString(path, StandardCharsets.UTF_8); }
ひとつのメソッドでファイルをすべて読み込めるようになりました。 1
このメソッドの実装では StringCoding
というインターナルなクラスを使っているので、new String(bytes, StandardCharsets.UTF_8)
とするよりも効率的な処理になっています。
まとめ
めんどくさかった処理も、Java のバージョンが上がるごとに簡単に書けるようになっています。
なので、積極的に新しい Java を使っていきましょう!