LoginSignup
204
125

More than 1 year has passed since last update.

Java 16新機能まとめ

Last updated at Posted at 2021-03-16

Java 16が2021/3/16にリリースされました。

多くのプログラマに関係ありそうな変更は、record、instanceofパターンマッチの正式化、Stream.toListです。またTLS1.0と1.1が無効になっているので古いMySQLなどへの接続でハマることがあるかもしれません。

詳細はこちら
Java SE 16 Platform JSR 391
JDK 16 GA Release

APIドキュメントはこちら
Overview (Java SE 16)

MacやLinuxでのインストールにはSDKMAN!をお勧めします

Oracle OpenJDK以外に無償で商用利用できるディストリビューションとしては、次のようなものがあります。

アップデートは4月に16.0.1が、翌年7月に16.0.2がリリースされることになります。

Oracle JDKは開発用途には無償利用できますが、商用利用にはJava SE Subscriptionを購入するなどのライセンスが必要になります。

JEP

大きめの変更はJEPでまとまっています。
http://openjdk.java.net/projects/jdk/16/

今回は17個のJEPが取り込まれました。すでにプレビューなどで出ていたものが5つあり、新たに取り込まれたものは12です。
JEP 338: Vector API (Incubator)
JEP 347: Enable C++14 Language Features
JEP 357: Migrate from Mercurial to Git
JEP 369: Migrate to GitHub
JEP 376: ZGC: Concurrent Thread-Stack Processing
JEP 380: Unix-Domain Socket Channels
JEP 386: Alpine Linux Port
JEP 387: Elastic Metaspace
JEP 388: Windows/AArch64 Port
JEP 389: Foreign Linker API (Incubator)
JEP 390: Warnings for Value-Based Classes
JEP 392: Packaging Tool
JEP 393: Foreign-Memory Access API (Third Incubator)
JEP 394: Pattern Matching for instanceof
JEP 395: Records
JEP 396: Strongly Encapsulate JDK Internals by Default
JEP 397: Sealed Classes (Second Preview)

分野ごとにまとめていきます。

言語機能

言語仕様にかかわる変更としては次のようなものがあります。
Pattern Matching for instanceofとRecordsは正式化されました。
プレビューで残ってるのはSealed Classesだけですね。これも17LTSでは正式化されるはずで、プレビュー機能を残さないようにしているのだと思います。
JEP 394: Pattern Matching for instanceof
JEP 395: Records
JEP 397: Sealed Classes (Second Preview)
JEP 390: Warnings for Value-Based Classes

JEP 394: Pattern Matching for instanceof

パターンマッチングです。
instanceofを使ったパターンマッチが14でプレビューとして入り、そのまま変更なしにJava 15でセカンドプレビューになりました。
Java 16では、パターン変数が変更可能になりました。これはローカル変数との非対称性を減らすためということです。
また、いままでString型のsに対してs instanceof Object oのような常に真となるマッチも可能でしたが、Java 16からはコンパイルエラーになります。

jshell> var s = ""
s ==> ""

jshell> s instanceof Object o
|  エラー:
|  パターン・タイプjava.lang.Objectは式タイプjava.lang.Stringのサブタイプです
|  s instanceof Object o
|  ^--

値 instanceof パターンで、値をマッチさせることができます。
パターンは、定数か変数定義です。変数定義の場合には、型が一致していた場合にtrueになりその変数に値が割り当てられます。

if (x instanceof Integer i) {
    // can use i here
}

switchで使えるようになれば便利ですが、これは別のJEPで定義されていて、Java 17以降に持ち越されています。17LTSにはプレビュー機能は入らないと思うので、来年3月のJava 18ですかね。
JEP draft: Pattern matching for switch (Preview)

今後は、Recordsと組み合わせてrecord Point(int x, int y){}があるときにp instanceof(var x, var y)のような分解のパターンも使えるようにするようです。
また、Optional.ofのようなビルダーに対して同様にパターンを使えることも考えてるようです。つまりos instanceof Optional.of(String s)のようなパターンが書けるようにするということです。
楽しみですね。
https://github.com/openjdk/amber-docs/blob/master/site/design-notes/pattern-match-object-model.md

分解のパターンについてはこちら。
http://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html

JEP 395: Records

レコードが標準化される
Java 14でJEP 359としてPreviewになりJava 15でJEP 384として少し変更を加えてSecond Previewになっていました。
Java 16では、インナークラス内でも定義可能にして標準化されました。

定義

recordとして定義します。

record Foo(int x, int y) {}

他のクラスは継承できません。

record レコード名(状態) { 定義 }

これは次のようなクラスになります。

class Foo extends Record {
  // 状態がprivate finalとして定義
  private final int x;
  private final int y;
  // 追加のインスタンスフィールドは定義できない

  // 状態をフィールドに設定するコンストラクタが定義される
  public Foo(int x, int y) {
    this.x = x;
    this.y = y;
  }

  // 状態と同じ名前のメソッドが定義される
  public int x() {
    return x;
  }
  public int y() {
    return y;
  }

  // 状態を反映するhashCodeが定義される
  public int hashCode() { ... }
  // 状態を比較するequals が定義される
  public boolean equals() { ... }
  // 状態を表示するtoStringが定義される
  public String toString() { ... }
}

取得用メソッドは定義されますが、値設定用のメソッドは定義されません。つまり、イミュータブルなオブジェクトとなります。また、get/setではないことからJava Beanとの互換性もありません。
hashCode()メソッドやequals()メソッドなどは実際にはinvokeDynamicで実行時に実装コードが生成されます。

次のように、状態の検査や正規化にコンストラクタが定義できます。コンストラクタの引数リストは省略できます。

record Range(int lo, int hi) {
  Range {
    if (lo < 0) lo = 0; // 正規化
    if (lo > hi) throw IllegalArgumentException();
    // 設定されなかったフィールドはあとで設定される
  }
}

また、コンストラクタの引数リストを省略した場合、コンポーネントフィールドに値を割り当てるとコンパイルエラーになります。

record Range(int lo, int hi) {
  Range {
    this.lo = 3; // コンパイルエラー
  }
}

Java 15までではstaticではないinner classの中でrecordを定義することはできませんでしたが、この制約が今回のJava 16で外れました。

public class NestedRecord {
  class Foo {
      record Bar(int x){}
  }
}

また、Java 16ではコンポーネント定義にC形式の配列宣言が使えなくなっています。

jshell> record T(int a[]){}
|  エラー:
|  旧式の配列表記法はレコード・コンポーネントでは使用できません
|  record T(int a[]){}

APIの拡張

recordはjava.lang.Recordを継承したクラスになります。

次のようにRecordを継承するコードを書こうとするとrecords cannot directly extend java.lang.Recordというエラーになります。

class Foo extends Record {
}

Classクラスにはレコード関連のメソッドが追加されています。
isRecordメソッドで型がrecordかどうか判定できます。またgetRecordComponentsでrecordで定義されたコンポーネントを取得することができます。ただし、値の取得はRecordComponent経由では行えないので、Field経由で行うようです。

jshell> Foo.class.isRecord()
$9 ==> true

jshell> Foo.class.getRecordComponents()
$10 ==> RecordComponent[2] { int x, int y }

jshell> String.class.isRecord()
$11 ==> false

jshell> String.class.getRecordComponents()
$12 ==> null

型名としてのrecordの制限

recordという名前を型(クラス・インタフェース・レコード型など)につけることは制限されています。

$ jshell
|  JShellへようこそ -- バージョン16
|  概要については、次を入力してください: /help intro

jshell> class record{}
|  エラー:
|  ここでは'record'は許可されません
|    リリース14から'record'は制限された型名であり、型の宣言に使用できません
|  class record{}
|        ^

jshell> String record=""
record ==> ""

jshell> record Rec(int record){}

jshell> Rec record=new Rec(3)
record ==> Rec[record=3]

enumと違いキーワードではないので、変数名やフィールド、recordのコンポーネント名にはrecordを使えます。

JEP 397: Sealed Classes (Second Preview)

Sealedクラスは、継承できるクラスを限定する機能です。仕様の名前は「クラス」 となってますが、interfaceでも使えます。
クラス名のあとに permitsで継承クラスを指定していくという感じになります。

public abstract sealed class Shape
    permits Circle, Rectangle, Square {...}

そうするとShapeクラスを継承できるのはCircle、Rectangle、Squareに限定されます。Circle、Rectangle、Squareは必ず定義する必要があります。
これらのクラスは、モジュール化しているのであれば同じモジュール内、モジュール化していないのであれば同じパッケージ内である必要があります。
また、permitsされるクラスが同一のソースだけにあるのであれば、permitsは省略できます。
permitsするクラスは定義のときにsealedかnon-sealedかfinalをつける必要があります。recordは暗黙的にfinalになるのでなにもつける必要はありません。

現状ではほとんど使いどころがないです。
例えば、Shapeの継承先は確定しているので、次のコードはコンパイル通ってほしいところですが、エラーになります。

String getName(Shape s) {
  if (s instanceof Circle) {
    return "円";
  } else if (s instanceof Rectangle) {
    return "四角";
  } else if (s instanceof Square) {
    return "正方形";
  }
}

まだドラフトですがPattern matching for switch (Preview)が導入されてswitch式/文でパターンマッチが使えるようになれば、次のようにdefaultなしで書ける予定です。

String getName(Shape s) {
  switch (s) {
    case Circle c -> return "円";
    case Rectangle r -> return "四角";
    case Square q -> return "正方形";
  }
}

もちろん、ここでswitch式を使うこともできますね。

String getName(Shape s) {
  return switch (s) {
    case Circle c -> "円";
    case Rectangle r -> "四角";
    case Square q -> "正方形";
  }
}

ということで、現時点でのSealedクラスは今後の拡張のための下準備というところです。
背景にある考え方は、次の記事で言語設計者であるBrian Goetzが解説しています。
Java 注目の機能:Sealed クラス

JEP 390: Warnings for Value-Based Classes

Integerなどvalue-based classをsynchronizedのロックに使っているとコンパイル時・実行時に警告がでる

jshell> synchronized (LocalDateTime.now()){}
|  警告:
|  値ベース・クラスのインスタンスで同期しようとしました
|  synchronized (LocalDateTime.now()){}
|  ^----------------------------------^

Inner ClassでStaticメンバーの定義

RecordがInner Classで定義できなかったので、その原因になるstaticメンバーの制限が緩和されています。

public class Outer {
  class Inner {
    static final String MESSAGE = "hoge";
    static String message() {
      return MESSAGE;
    }
  }
}

API

APIの変更に関するJEPは次のようなものがあります。
JEP 380: Unix-Domain Socket Channels
JEP 338: Vector API (Incubator)
JEP 389: Foreign Linker API (Incubator)
JEP 393: Foreign-Memory Access API (Third Incubator)
JEP 396: Strongly Encapsulate JDK Internals by Default
[JDK-8180352] Add Stream.toList() method - Java Bug System
Stream.mapMulti
[JDK-8254876] (fs) NullPointerException not thrown when first argument to Path.of or Paths.get is null - Java Bug System

APIの変更はこちらにまとまっています。
https://cr.openjdk.java.net/~iris/se/16/latestSpec/apidiffs/overview-summary.html

JEP 380: Unix-Domain Socket Channels

SocketChannelやServerSocketChannelでUNIXドメインに対応する。

同じシステム内でのプロセス間通信でTCP/IPよりも効率がよく、システムをまたがって通信しないのであればセキュアになる。
同一マシン上のコンテナ間でも共有ボリューム経由で効率的な通信ができる。

JEP 338: Vector API (Incubator)

AVX命令のような、複数のデータに対する計算を同時に行う命令をJavaから利用できるようになります。
Project Panamaのひとつとして開発されていました。
今回インキュベータとしてJava 16に導入されました。

使うためには実行時やコンパイル時に--add-modules jdk.incubator.vectorをつける必要があります。

import jdk.incubator.vector.*;
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;

void vectorComputation(float[] a, float[] b, float[] c) {

    for (int i = 0; i < a.length; i += SPECIES.length()) { // SPECIES.length() = 256bit / 32bit -> 8
        VectorMask<Float> m = SPECIES.indexInRange(i, a.length); // 端数がマスクされる
                                                                 // a.lengthが11でiが8のとき最初の3つしか要素がないので [TTT.....]
        // FloatVector va, vb, vc;
        FloatVector va = FloatVector.fromArray(SPECIES, a, i, m);
        FloatVector vb = FloatVector.fromArray(SPECIES, b, i, m);
        FloatVector vc = va.mul(va).
                    add(vb.mul(vb)).
                    neg();
        vc.intoArray(c, i, m);
    }
}

利用できるのは次の6つの型です。それぞれに対応するVector型があって、これが基本になります。

bit幅 Vector
byte 8 ByteVector
short 16 ShortVector
int 32 IntVector
long 64 LongVector
float 32 FloatVector
double 64 DoubleVector

ただ、利用するにはVectorSpeciesが必要です。利用したいVectorにSPECIES_*という定数が用意されているので、それを使います。*は一度に計算するbit数ですね。

jshell> FloatVector.SP
SPECIES_128         SPECIES_256         SPECIES_512         SPECIES_64          SPECIES_MAX         SPECIES_PREFERRED   

MAXではそのハードウェアで使える最大、PREFERREDは推奨ビット数だけど、同じになるんじゃないのかな。ここでは256bitが推奨されて、floatが8個同時に計算できるようになっていますね。

jshell> FloatVector.SPECIES_PREFERRED
$11 ==> Species[float, 8, S_256_BIT]

ハードウェアで使えるbit数は搭載CPUに依存しますが、普通のIntel/AMDであれば256、XEONとか つよつよCPUなら512かな。M1は128でした。ハードウェアでサポートされないbit数を使おうとするとソフトウェア処理になるので遅くなります。

実際のVectorはfrom*というメソッドで取得します。fromArray、fromByteArray、fromByteBufferが用意されています。インキュベータに入る前はfromValuesがあったのですが、なくなってますね。

Vectorを得られたら、用意されたメソッドで計算します。ひととおりの算術命令はあります。

jshell> va.
abs()                   add(                    addIndex(               bitSize()               blend(                  
broadcast(              byteSize()              compare(                div(                    elementSize()           
elementType()           eq(                     equals(                 fma(                    getClass()              
hashCode()              intoArray(              intoByteArray(          intoByteBuffer(         lane(                   
lanewise(               length()                lt(                     max(                    min(                    
mul(                    neg()                   notify()                notifyAll()             pow(                    
rearrange(              reduceLanes(            reduceLanesToLong(      reinterpretAsBytes()    reinterpretShape(       
selectFrom(             shape()                 slice(                  sqrt()                  sub(                    
test(                   toArray()               toDoubleArray()         toIntArray()            toLongArray()           
toShuffle()             toString()              unslice(                viewAsFloatingLanes()   viewAsIntegralLanes()   
wait(                   withLane(      

ところで、こういったメソッド呼び出しの内部でAVX命令などを呼び出すのでは遅くなるんではという気がしますが、実際にはJVM intrinsicsという仕組みでJITコンパイラがこれらのメソッド呼び出しをネイティブ関数呼び出しに置き換えます。

JEP 393: Foreign-Memory Access API (Third Incubator)

変更点
MemorySegmentとMemoryAddressの明確な分離
MemoryAccessインタフェースの導入。VarHandle APIの利用を最小限にする
共有セグメントのサポート
Cleanerつきセグメント

ヒープ外のメモリをアクセスする方法としては、ByteBufferを使う方法やUnsafeを使う方法、JNIを使う方法がありますが、それぞれ一長一短があります。
ByteBufferでdirect bufferを使う場合、intで扱える範囲の2GBまでに制限されたり、メモリの解放がGCに依存したりします。
Unsafeの場合は、性能もいいのですが、名前が示すとおり安全ではなく、解放済みのメモリにアクセスすればJVMがクラッシュします。
JNIを使うとCコードを書く必要があり、性能もよくないです。

ということで、ヒープ外のメモリを直接扱うAPIがJava 14でインキュベータモジュールとして導入されたわけです。そしてJava 15でセカンドインキュベータになっています。
次のようなコードになります。

import jdk.incubator.foreign.*;
import java.nio.ByteOrder;

VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   for (int i = 0 ; i < 25 ; i++) {
        intHandle.set(seg, i * 4, i);
   }
}

コンパイル、実行では--add-modules jdk.incubator.foreignを付けてモジュールを読み込む必要があります。
パッケージもモジュール名と同様、jdk.incubator.foreignになっています。

MemoryHandlesからwithOffsetとwithStrideが消えましたね。

jshell> import jdk.incubator.foreign.*

jshell> MemoryHandles.
asAddressVarHandle(   asUnsigned(           class                 collectCoordinates(   dropCoordinates(
filterCoordinates(    filterValue(          insertCoordinates(    permuteCoordinates(   varHandle(

Java 15ではMemoryHandlesからVarHandleを得るメソッドが拡充してました。

jshell> MemoryHandles.
asAddressVarHandle(   asUnsigned(           class                 collectCoordinates(   dropCoordinates(
filterCoordinates(    filterValue(          insertCoordinates(    permuteCoordinates(   varHandle(
withOffset(           withStride(

Java 14では次の3つのメソッドしかありませんでした。

jshell> MemoryHandles.
class         varHandle(    withOffset(   withStride(

Java 16ではMemoryAccessというユーティリティが用意されて、VarHandleを使わずにシンプルにかけるようになっています。

import jdk.incubator.foreign.*;

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   for (int i = 0 ; i < 25 ; i++) {
        MemoryAccess.setIntAtOffset(i * 4, i);
   }
}

JEP 389: Foreign Linker API (Incubator)

ネイティブライブラリの呼び出しを行う
外部メモリのアクセスにはForeign Memory Access APIを使う
JNIの代替

JEP 396: Strongly Encapsulate JDK Internals by Default

sun.misc.Unsafeなど以外の内部APIの隠蔽をデフォルトにします。
内部APIから標準APIへの移行がやりやすいようにします。

Java 9で内部APIを隠蔽する仕組みが導入されていましたが、デフォルトでは無効になっていました。それが今回有効化されました。

次のオプションを利用します。15まではpermitがデフォルトでしたが、denyがデフォルトになりました。

オプション APIの利用 警告 スタックトレース デフォルト
--illegal-access=permit 9から15まで
--illegal-access=warn 出力
--illegal-access=debug 出力 出力
--illegal-access=deny 不可 16でデフォルト

制限される内部APIの一覧はこちらです。
https://cr.openjdk.java.net/~mr/jigsaw/jdk8-packages-denied-by-default

jshell> A.class.getClassLoader().loadClass("com.sun.beans.finder.ClassFinder").getMethod("findClass", String.class).invoke("test")
|  例外java.lang.IllegalAccessException: class REPL.$JShell$15 cannot access class com.sun.beans.finder.ClassFinder (in module java.desktop) because 
module java.desktop does not export com.sun.beans.finder to unnamed module @77556fd
|        at Reflection.newIllegalAccessException (Reflection.java:385)
|        at AccessibleObject.checkAccess (AccessibleObject.java:687)
|        at Method.invoke (Method.java:559)
|        at (#5:1)

[JDK-8180352] Add Stream.toList() method - Java Bug System

JavaDoc: Stream.toList
collect(Collectors.toList)がめんどくさすぎたので、toListが導入されました。
変更不能なListになってますね。

C:\Users\naoki>java\jdk\jdk-16-ea31\bin\jshell
|  JShellへようこそ -- バージョン16-ea
|  概要については、次を入力してください: /help intro

jshell> Stream.of("a","b").map(String::toUpperCase).toList()
$1 ==> [A, B]

jshell> $1.getClass()
$2 ==> class java.util.ImmutableCollections$ListN

いままでStreamとCollectionフレームワークを分離したいという意向で、Collectors経由でのtoListを行っていたのですが、なにをきっかけに気がかわったのか、突然とりこまれました。
BTSは2017年からありますね。

http://mail.openjdk.java.net/pipermail/core-libs-dev/2020-November/070782.html
https://github.com/openjdk/jdk/pull/1026

Stream.mapMulti

値を次々に生成するような処理をStream中にはさみたい場合に使います。

Stream.of("a", "b")
  .mapMulti((s, sink) -> sink.accept(s.toUpperCase()))
  .toList()

ここで、sにはStreamを流れる値で、sinkに対してacceptしたものが次に渡されていきます。
http://mail.openjdk.java.net/pipermail/core-libs-dev/2020-July/067560.html

[JDK-8254876] (fs) NullPointerException not thrown when first argument to Path.of or Paths.get is null - Java Bug System

Path.ofに複数の引数わたしたとき、最初の引数がnullだと変なオブジェクトが生成されていました。このPathを使うところでぬるぽが発生していたはず。

C:\Users\naoki>jshell
|  JShellへようこそ -- バージョン15
|  概要については、次を入力してください: /help intro

jshell> Path.of(null, "r")
$1 ==> null\r

Java 16では最初の引数がnullだったらぬるぽ出るようになっています。

C:\Users\naoki>java\jdk\jdk-16\bin\jshell
|  JShellへようこそ -- バージョン16
|  概要については、次を入力してください: /help intro

jshell> Path.of(null, "r")
|  例外java.lang.NullPointerException
|        at Objects.requireNonNull (Objects.java:208)
|        at WindowsFileSystem.getPath (WindowsFileSystem.java:214)
|        at Path.of (Path.java:147)
|        at (#1:1)

JVM

JEP 376: ZGC: Concurrent Thread-Stack Processing
JEP 387: Elastic Metaspace
[JDK-8255616] Removal of experimental features AOT and Graal JIT - Java Bug System
[JDK-8202343] Disable TLS 1.0 and 1.1 - Java Bug System

JEP 376: ZGC: Concurrent Thread-Stack Processing

ZGCではPause Mark StartフェーズとPause Relocate Startフェーズでスレッドスタックのスキャンを行っていた。このふたつのフェーズはstop-the-worldを行う。
つまりアプリケーションのルートセットサイズがこのふたつのフェーズの停止時間を決めていた。
アプリケーションスレッドと並列にすることでSTWではスレッドスタックがスキャンされなくなる。つまりSTWがアプリケーションのルートセットのサイズへの依存が少なくなる。
16TBでも1ms以下の停止時間でZGCを使える

スレッドスタックの処理をsafepointからconcurrentフェーズに移動する。
スタック処理を遅延させて並列でインクリメンタルにする
他のスレッドごとのルート処理をZGC safepointから削除する
他のHotSpotのサブシステムがスタックを遅延実行できるような仕組を提供する

典型的なマシンで1ミリ秒以下でsafepointの処理が行えるようにする

大量にスレッドがあるとスレッドごとのルート処理が問題になる

JEP 387: Elastic Metaspace

クラスのデータなど、JVMが利用するデータはMetaspaceに保存されますが、このようなデータが使用済みになったときに返却するようにして、効率的にMetaspaceが利用できるようにします。

[JDK-8255616] Removal of experimental features AOT and Graal JIT - Java Bug System

Experimentalとして入っていたAOTコンパイラやGraal JITコンパイラがOracleのOpenJDKビルドからは削除されているようです。
つまり、java.netでダウンロードしたバイナリには含まれなくなっています。他のディストリビューションがどうなっているか確認していないけど、恐らく無効にするんじゃなかろうか。GraalVMはデフォルトでGraal JITを使うので残りますね。
ソースコードには残っているようなので、自分でビルドすれば試すことができます。

[JDK-8202343] Disable TLS 1.0 and 1.1 - Java Bug System

TLS 1.0と1.1が無効化されています。
そのため、古いバージョンのMySQLへの接続などがエラーになることがあるようです。

ツール

JEP 392: Packaging Tool

JEP 392: Packaging Tool

Incubatorとして入っていたパッケージングツールが標準化する。

Javaアプリケーションのインストーラを作るツールが入りました。
関連APIも追加されていますが、Incubatorモジュールになっています。
Windowsではlight.exeとcandle.exeが必要なのでhttps://wixtoolset.org からダウンロードしてPATHに追加する必要があります。

試しに次のアプリケーションのインストーラを作ってみます。

import javax.swing.*;
public class App {
    public static void main(String[] args) {
        var f = new JFrame("My App");
        var t = new JTextArea();
        f.add(t);
        var b = new JButton("Hello");
        b.addActionListener(al -> t.append("Hello!\n"));
        f.add("North", b);
        f.setSize(500, 400);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setVisible(true);
    }
}

アプリケーションはJarファイルになっている必要があります。

$ javac App.java
$ mkdir target
$ jar cf target/app.jar App.class
$ jpackage --name myapp --input target --main-jar app.jar --main-class App

そうするとWindowsの場合はmyapp.exeという名前で45MBのインストーラが作成されます。
モジュラーJARの場合は自動的に必要なモジュールのみのJavaランタイムがインストールされるようになりますが、ここではモジュール対応しないJarファイルを作ったので、jlinkを使って最低限のJavaランタイムを作ると小さなインストーラが作れるようになります。

$ jdeps --list-deps App.class
java.base
java.desktop
$ jlink --add-modules java.base,java.desktop --output jre
$ jpackage --name myapp --input target --main-jar app.jar --main-class App --runtime-image jre

そうすると27MBまでインストーラのサイズが削減されました。
インストール後のアプリケーションサイズも124MBから74MBに減ります。
Windowsの場合は--win-menuをつけてWindowsメニューにアプリが追加されるようにするほうがいいでしょう。

OpenJDK

JEP 347: Enable C++14 Language Features
JEP 357: Migrate from Mercurial to Git
JEP 369: Migrate to GitHub
JEP 386: Alpine Linux Port
JEP 388: Windows/AArch64 Port

JEP 347: Enable C++14 Language Features

C++14の機能をJDK開発に利用できるようにします。

JEP 357: Migrate from Mercurial to Git

Mercurialで管理されていたOpenJDKのソースコードをGitでの管理に変更します。
プロジェクトSkaraとして進んでいました。
OpenJDK: Skara

JEP 369: Migrate to GitHub

こちらもプロジェクトSkaraとして進んでいましたが、開発管理をGitHubベースにするものです。

JEP 386: Alpine Linux Port

Alpine Linuxを始めとして、Cライブラリにmuslを使うようなLinuxシステムに対応します。
各ディストリビューションでもAlpine Linux対応バイナリを提供するようになっています。
jlinkを使って実行環境をつくりなおすと、Spring Bootを含めて77MBのイメージを作ることができました。
image.png

ところで、Alpineでビルドするとsrc/hotspot/os/linux/os_linux.cppでエラーが出るので、とりあえずこんな感じで対処しておく必要がありました。

672c672
<   alloca(((pid ^ counter++) & 7) * 128);
---
>   int* dummy = (int*)alloca(((pid ^ counter++) & 7) * 128);

って言ってたらパッチあててもらったのでJava 17やJava 16.0.1では対処されるんじゃないかと思います

8263718: unused-result warning happens at os_linux.cpp by YaSuenag · Pull Request #3042 · openjdk/jdk

JEP 388: Windows/AArch64 Port

Arm Windows対応。

Arm Mac対応のJEPもあるけど、間に合わなかった。Java 17になるようです。
JEP 391: macOS/AArch64 Port

204
125
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
204
125