458
284

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Java 14新機能まとめ

Last updated at Posted at 2020-02-13

Java 14が2020/3/17にリリースされました。
Java SE 14 Platform JSR 389

ダウンロード

OpenJDKサイトからダウンロードできます。
https://jdk.java.net/14/

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

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

LTSではないのでAmazon Correttoではリリースされないようです。
アップデートは4月に14.0.1が、7月に14.0.2がリリースされることになります。

Oracle JDKは開発用途には利用できますが、商用利用にはJava SE Subscriptionを購入する必要があります。

JEP

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

今回は、16ものJEPが取り込まれました。影響が大きいものも多いです。
305: Pattern Matching for instanceof (Preview)
343: Packaging Tool (Incubator)
345: NUMA-Aware Memory Allocation for G1
349: JFR Event Streaming
352: Non-Volatile Mapped Byte Buffers
358: Helpful NullPointerExceptions
359: Records (Preview)
361: Switch Expressions (Standard)
362: Deprecate the Solaris and SPARC Ports
363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
364: ZGC on macOS
365: ZGC on Windows
366: Deprecate the ParallelScavenge + SerialOld GC Combination
367: Remove the Pack200 Tools and API
368: Text Blocks (Second Preview)
370: Foreign-Memory Access API (Incubator)

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

言語仕様

言語仕様にかかわる変更としては次のようなものがあります。
359: Records (Preview)
305: Pattern Matching for instanceof (Preview)
368: Text Blocks (Second Preview)
361: Switch Expressions (Standard)

359: Records (Preview)

データ保持用のクラスとしてrecordがpreview機能として入りました。
Java 15でフィードバックを反映した改善版が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) {
  public Range {
    if (lo > hi) throw IllegalArgumentException();
    // 設定されなかったフィールドはあとで設定される
  }
}

staticではないinner classの中でrecordを定義することはできません。
次のようなコードをコンパイルするとrecord declarations not allowed in inner classesというエラーになります。

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

次のようなコードはコンパイルできます。

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

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という名前を型(クラス・インタフェース・レコード型など)につけることは制限されています。
--enable-previewを付けた状態ではエラーになります。

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

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

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

jshell> record Rec(int record){}

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

ちなみにこの「リリース13から」というのは誤りで、ただしくは14からです。
http://mail.openjdk.java.net/pipermail/amber-dev/2020-February/005623.html

enumと違いキーワードではないので、変数名やフィールド、recordのコンポーネント名にはrecordを使えます。
--enable-previewを付けない状態では、警告が表示されます。

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

jshell> class record{}
|  警告:
|  'record'は将来のリリースで制限された型名になる可能性があり、型の宣言での使用、または配列の要素タイプとしての使用はできなくなる可能性があります
|  class record{}
|        ^
|  次を作成しました: クラス record

今後の改善

当初はSealed Typesと同じJEPでしたが分離されました。
クラスの継承を限られたクラスに制限できる機能です。Java 15でpreviewに入ると思われます。
JEP 360: Sealed Types (Preview)

Preview 2での改善点はこちらで提案されています。
https://mail.openjdk.java.net/pipermail/amber-spec-experts/2020-January/001913.html

  • java.util.Recordはクラスですが、Valhallaのinline classはinterfaceしか継承できないので、これをinterfaceにしたほうがいいかどうか。
  • 必須メンバーへの可視性(private recordのメンバがpublicなのはおかしい?)
  • 現状ではstaticではないinner classではrecordを入れ子にすることができないけど対応したい
  • abstract record
  • パターンマッチでのdeconstructionへの対応
  • recordコンポーネントへの@Deprecated

参考
Stephen Colebourneさんからのフィードバック
https://mail.openjdk.java.net/pipermail/amber-dev/2019-November/005271.html

305: Pattern Matching for instanceof (Preview)

パターンマッチングです。
まずはinstanceofを使ったパターンマッチが14にプレビューとして入ります。
http://openjdk.java.net/jeps/305

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

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

switchで使えるようになれば便利ですが、これは別のJEPで定義されていて、Java 15に持ち越されています。
JEP draft: Pattern matching for switch (Preview)

また、recordを分解する機能(deconstruction)やその入れ子が導入されてPreview 2としてJava 15に入るようです。
http://openjdk.java.net/jeps/8235186

妥当なスケジュールとしては

  • 14 Pattern Matching for instanceof(Preview)
  • 15 Pattern Matching for instanceof(Preview 2)
    Pattern matching for a switch (Preview)
  • 16 Pattern Matching for instanceof(Standard)
    Pattern matching for a switch (Preview 2)
  • 17LTS Pattern matching for a switch(Standard)

となるか

  • 14 Pattern Matching for instanceof(Preview)
  • 15 Pattern Matching for instanceof(Preview 2)
    Pattern matching for a switch (Preview)
  • 16 Pattern Matching for instanceof(Preview 3)
    Pattern matching for a switch (Preview 2)
  • 17LTS Pattern Matching for instanceof(Standard)
    Pattern matching for a switch(Standard)

となるかですが、17LTSにはパターンマッチングの機能がひととおり標準機能として入る目途がついてきました。

368: Text Blocks (Second Preview)

改行などを含んだ文字列を定義できます。"""で囲みます。
JDK13にPreviewとして入ったものにJDK14では改行のエスケープなど少し仕様変更が入りました。JDK15でstandardになる予定。

// same as "You can write\ntwo line string.\n"
var str = """
  You can write
  two line string.
  """;

開始の"""のあとには文字列を続けれません。また、インデントは"""や内部の文字列で一番浅いところが基準になります。

var str = """
..You can write
..two line string.
  """;
var str = """
..  You can write
..  two line string.
  """;

改行をエスケープすることもできます。

var str = """
  You can write \
  two line string, \
  but this is single.
  """;

これは"You can write two line string, but this is single."になります。

行末のスペースは削除されます。
そこで、行末にスペースが必要なときは\sを入れてスペースが必要なことを示します。

var str = """
  test\s
  test \s
  """;

これは"test_\ntest__\n"になります。(Qiitaでは複数スペースをいれてもスペースひとつになってしまう)

文字列への変数の埋め込みはできません。その代わりにformattedメソッドがインスタンスメソッドとして用意されて、次のように書けるようになりました。

var str = """
  こんにちは、%sさん。
  今日はいい天気ですね。
  """.formatted("きしだ");

361: Switch Expressions (Standard)

いままでステートメントであったswitchを式として使えるようになります。Java 12でプレビューとして導入され、Java 13で仕様変更、そしてJava 14で正式機能として導入されます。
https://openjdk.java.net/jeps/361

switchはステートメントでしたが、多くのswitchで同一の変数に値を割り当てたりすべてのcaseでreturnしたりといった使いかたがされていたため、効率的に書けるように式としても使えるようになります。

つまり、こう。

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY -> 7;
    case THURSDAY, SATURDAY -> 8;
    case WEDNESDAY -> 9;
};

yieldで値を返すということもできます。

int result = switch (s) {
    case "Foo":
        yield 1;
    case "Bar":
        yield 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        yield 3;
}

基本的な形はyieldで値を返すものです。

case LABEL: yield expression;

そのシンタックスシュガーとしてラムダっぽく書けるということのようです。

case LABEL -> expression;

また、caseに複数の値を指定できるようにもなります。

switch (day) {
    case MONDAY, FRIDAY, SUNDAY: 
        numLetters = 6;
        break;
    ...
};

このようなcaseの拡張は、既存のswitchステートメントでも有効です。

JVM

JVMの挙動変更としては、NullPointerExceptionでの詳細メッセージがあります。
358: Helpful NullPointerExceptions

358: Helpful NullPointerExceptions

JavaプログラマのみなさんはNullPointerExceptionが大好きだと思います。
しかし、メッセージがそっけないという悩みがありました。
Java 14では、NullPointerExceptionについて詳細なメッセージを表示できるようになりました。

例えば次のようなコードがあるとします。

public class Sample {
  static String s;
  public static void main(String... args) {
    s.length();
  }
}

Java 14でも普通に実行すると以前のバージョンと同様に特別なメッセージは表示されません。

$ java Sample.java
Exception in thread "main" java.lang.NullPointerException
        at Sample.main(mysample.java:4)

-XX:+ShowCodeDetailsInExceptionMessagesオプションをつけて実行すると次のように何に対して何を呼び出したときにエラーになったかということが表示されます。

$ java -XX:+ShowCodeDetailsInExceptionMessages Sample.java
Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke "String.length()" because "Sample.s" is null
        at Sample.main(mysample.java:4)

このメッセージ構築はメッセージ取得時に行われるので、パフォーマンスへの影響はほとんどないようです。

JShellの場合は-R-XX:+ShowCodeDetailsInExceptionMessagesをつけます。

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

jshell> String[] strs = {null}
strs ==> String[1] { null }

jshell> strs[0].toUpperCase()
|  例外java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "REPL.$JShell$11.strs[0]" is null
|        at (#2:1)

API

APIに関する変更は次の3つのJEPです。
349: JFR Event Streaming
370: Foreign-Memory Access API (Incubator)
352: Non-Volatile Mapped Byte Buffers
この他にCompactNumberFormatでの複数形対応とStrictMathでの便利メソッドの追加がありました。
また、あとでツールの項で取り上げるPackagingツール関連のAPIがIncubatorとして追加されたのと、Pac200関連のAPIが削除されています。

APIの相違はこちらでまとまっています。
DRAFT: API Differences Between Java SE 13 (build 33) & Java SE 14 (build 36)

349: JFR Event Streaming

Flight Recorderはメトリクス収集ツールで、障害発生時の解析に役立ちますが、ダンプファイルの解析が必要で、モニタリング用途には不便でした。
JFR Event Streamingでは、イベント登録ができるなど、モニタリングに使いやすい機能が追加されています。

JEPのサンプルではイベントの登録の例が載っていますが、これにGCログの出力をつけくわえてみました。

import java.io.IOException;
import java.time.Duration;
import jdk.jfr.consumer.RecordingStream;
public class JFRStreamTest {
     
    public static void main(String[] args) throws IOException  {
         
        try (var rs = new RecordingStream()) {
            rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
            rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));
            rs.onEvent("jdk.CPULoad", event -> {
                System.out.println(event.getFloat("machineTotal"));
            });
            rs.onEvent("jdk.JavaMonitorEnter", event -> {
                System.out.println(event.getClass("monitorClass"));
            });
            rs.onEvent("jdk.GarbageCollection", System.out::println);
            rs.start();
        }
    }
}

-XX:StartFilghtRecordingをつけて実行すると、Flight Recorderが起動して、定期的にメトリクスが表示されます。

$ java -XX:StartFlightRecording JFRStreamTest.java
Started recording 1. No limit specified, using maxsize=250MB as default.

Use jcmd 79660 JFR.dump name=1 filename=FILEPATH to copy recording data to file.
[1.715s][warning][os] Unable to resolve PDH index: (230)
[1.716s][warning][os] Please check the registry if this performance object/counter is disabled
{
  classLoader = null  name = "jdk/jfr/internal/PlatformRecorder"
  package = {
    name = "jdk/jfr/internal"
    module = {
      name = "jdk.jfr"
      version = "14-ea"
      location = "jrt:/jdk.jfr"
      classLoader = null    }
    exported = true
  }
  modifiers = 49
}

jdk.GarbageCollection {
  startTime = 13:51:48.973
  duration = 12.5 ms
  gcId = 1
  name = "G1New"
  cause = "G1 Evacuation Pause"
  sumOfPauses = 12.5 ms
  longestPause = 12.5 ms
}

JDK Mission Control(JMC)で見ることができます。
JFRを動かしておくと、イベントブラウザにイベントが記録されているのがわかります。
image.png

370: Foreign-Memory Access API (Incubator)

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

ということで、ヒープ外のメモリを直接扱うAPIが導入されたわけです。
次のようなコードになります。

VarHandle intHandle = MemoryHandles.varHandle(int.class);

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

352: Non-Volatile Mapped Byte Buffers

ByteBufferを不揮発メモリに対応します。
jdk.nio.mapmodeといモジュールが導入されて、同名のパッケージにExtendedMapModeクラスが用意され、新しいMapModeとして

  • READ_ONLY_SYNC
  • READ_WRITE_SYNC

が追加されます。

var fc = FileChannel.open(path);
fc.map(ExtendedMapMode.READ_WRITE_SYNC, 0, 1024);

などとする感じです。デバイスが不揮発メモリではない場合はUnsupportedOperationExceptionが投げられます。

また、BufferPoolMXBeanに永続MappedByteBufferの統計情報がmapped - 'non-volatile memory'という名前で取れるようになるようです。

@PreviewFeature

String.formattedのようなプレビュー機能のために導入APIには、13ではDeprecatedがついていました。
14ではinternalなアノテーションとして@PreviewFeatureが導入されて、プレビュー機能用のAPIであることが明示されました。
13では--enable-previewがなくても使えましたが、14では--enable-previewの指定がないときにこれらのAPIを使うと

formatted(java.lang.Object...)はプレビュー機能の一部であるAPIです

のようなエラーが出ます。

シリアライズ関連メソッド・フィールドを@Serialで明示

シリアライズできるクラスには関連フィールドやメソッドを定義しますが、シグネチャが決まっているもののコンパイル時にチェックされず、正しく定義されているかどうか確認できませんでした。

private static final long serialVersionUID

https://download.java.net/java/GA/jdk14/docs/api/java.base/java/io/Serial.html
https://bugs.openjdk.java.net/browse/JDK-8217698

CompactNumberFormatの複数形対応

JEPになってない変更のひとつ。
ドイツ語とかイタリア語の場合に100万と200万でMillionかMillionenみたいな変形があって、それに対応したということらしい。

jshell> import java.text.*

jshell> var cnf = CompactNumberFormat.getCompactNumberInstance(Locale.GERMAN, NumberFormat.Style.LONG)
cnf ==> java.text.CompactNumberFormat@5088aaba

jshell> cnf.format(1_000_000)
$3 ==> "1 Million"

jshell> cnf.format(2_000_000)
$4 ==> "2 Millionen"

UnicodeのLanguage Plural Rulesに従って独自のルールを設定することができるコンストラクタも用意されています。

StrictMathへの便利メソッド追加

StrictMathに便利メソッドが追加されています。メソッド参照が使いやすくなることを狙ったんでしょうか。

  • incrementExact(int)
  • incrementExact(long)
  • decrementExact(int)
  • decrementExact(long)
  • negateExact(int)
  • negateExact(long)

インクリメント、デクリメント、符号反転ですが、オーバーフローしたときに
エラーがでます。

jshell> StrictMath.incrementExact(3)
$5 ==> 4

jshell> StrictMath.incrementExact(Integer.MAX_VALUE)
|  例外java.lang.ArithmeticException: integer overflow
|        at Math.incrementExact (Math.java:968)
|        at StrictMath.incrementExact (StrictMath.java:849)
|        at (#4:1)

jshell> StrictMath.negateExact(Integer.MAX_VALUE)
$6 ==> -2147483647

jshell> StrictMath.negateExact(Integer.MIN_VALUE)
|  例外java.lang.ArithmeticException: integer overflow
|        at Math.negateExact (Math.java:1044)
|        at StrictMath.negateExact (StrictMath.java:909)
|        at (#7:1)

通常の演算子ではオーバーフローがおきます。

jshell> Integer.MAX_VALUE+1
$8 ==> -2147483648

jshell> Integer.MIN_VALUE
$9 ==> -2147483648

jshell> -Integer.MIN_VALUE
$10 ==> -2147483648

ツール

ツールの変更としては、jpackageの追加が大きいですね。
343: Packaging Tool (Incubator)
367: Remove the Pack200 Tools and API

343: 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メニューにアプリが追加されるようにするほうがいいでしょう。

367: Remove the Pack200 Tools and API

Pack200はJarファイルを効率的に圧縮するツールでしたが、主なユースケースであるAppletが使われなくなり、役目を終わったということで削除されます。
java.util.jar配下の関連APIも削除されます。

GC

GCに関する変更のJEPは5つありました。
363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
364: ZGC on macOS
365: ZGC on Windows
366: Deprecate the ParallelScavenge + SerialOld GC Combination
345: NUMA-Aware Memory Allocation for G1

363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector

Concurrent Mark & Sweep GCがソースコードから削除されます。
Java 9のJEP 291でDeprecatedになっていましたが、メンテナンスを引き継ぐような人も現れなかったので削除するということです。
そうすることで、GCのメンテナンスコストが下がり、他のGCの開発が速くなることが期待されています。

364: ZGC on macOS / 365: ZGC on Windows

いままではLinuxしかサポートしてなかったZGCがmacOSとWindowsをサポートするようになりました。
当初は、ZGCは本番のLinuxサーバーでだけ使われて、macOSやWindowsは開発用途に使うだけだという想定でしたが、IDEをZGCで動かしたいなどの要望もあったことからmacOSやWindowsもサポートすることになったようです。
WindowsはWindows 10 ver 1803以降に対応しています。
ZGCは仮想アドレスを使って物理メモリを複数のアドレスでアクセスする仕組みを使っていますが、Windowsでは1803からページングファイルメモリがサポートされたということでWindowsでもZGCが動かせるようになったようです。

366: Deprecate the ParallelScavenge + SerialOld GC Combination

Young領域のGCをPrallelでOld領域のGCをSerialでという、ほとんど使われてない割にメンテナンスが大変な組み合わせが非推奨になりました。

345: NUMA-Aware Memory Allocation for G1

最近は各コアからのメモリアクセスが均等というわけではないNUMA(Non-Uniform Memory Access)アーキテクチャが広まっています。
パラレルGCではNUMAに対応していましたが、G1は対応していませんでした。
このJEPによってG1もNUMAに対応しています。ただし対応OSはLinuxのみです。

JDK

JDK自体に関する変更というのは、リリース形態やビルド方針などに関するものです。
Java 14ではSolarisやSPARCへの対応がDeprecatedになりました。
362: Deprecate the Solaris and SPARC Ports

362: Deprecate the Solaris and SPARC Ports

Solaris/SPARCやSolaris/x64、Linux/SPARCへの対応を外していくというものです。
CMSの削除と同様に、もし、いっぱい需要があるということを示せれば、取り下げられます。
また、これはオープンソースとしてのOpenJDKの開発の話で、Oracle JDKなど商用ディストリビューションでの対応継続は、それぞれに続く可能性があります。Solaris/SPARCを導入してるところだとJavaのライセンス代もそれほど問題にならなそうだし。

458
284
6

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
458
284

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?