2020/03/17

JEPでは語れないJava SE 14

このエントリーをはてなブックマークに追加

Javaのアップデートでしか更新しないようになってしまいましたが、半年ぶりのblog更新ですw

ということで、アメリカ西海岸時間の3月17日にJava SE 14がリリースされます。

今回もJEPに含まれていないAPIの変更をメインにJava 14についてまとめていきます。

とはいうものの、Java 14は16もJEPがあります。特にProject Amberがらみの文法変更や、ZGCがWindowsとOS Xに対応したことなどが注目されてますね。

そして、久しぶりにIncubator ModuleがJEPで提案されました!!

Project Panamaで策定されているForeign Funtion Interfaceに関連するモジュールで、ヒープではないメモリへのアクセスを行うためのAPIです。

JEPに関する変更はまた別の機会に書くとして、本エントリーはJEP以外のAPIの変更についてです。

今回もABC順にならんでいます。同じように、セキュリティ系のAPIはちゃんと理解していないので、省略してます。

 

廃止になったAPI

Java 14ではPack200関連の3つのクラスが廃止されました。

これらは、Java 11で@DeprecatedのforRemovalがtrueになったクラスです。

  • java.util.jar.Pack200
  • java.util.jar.Pack200.Packer
  • java.util.jar.Pack200.Unpacker

クラス3つといっても、実際にはPack200クラスの内部クラスも含んでいます。Pack200はもう役目を終えましたね。

もう1つ、セキュリティのACL (Access Control List)関連のパッケージが廃止されました。

こちらは、Java 10でforRemovalがtrueになったものです。

  • java.security.acl

java.security.aclパッケージに含まれていたインタフェース/例外は以下の通り。

  • java.security.acl.Acl
  • java.security.acl.AclEntry
  • java.security.acl.Group
  • java.security.acl.Owner
  • java.security.acl.Permission
  • java.security.acl.AclNotFoundException
  • java.security.acl.LastOwnerException
  • java.security.acl.NotOwnerException

これらのAPIはjava.security.Policyで置き換えができるはずです。

 

廃止予定のAPI

Java 14で追加された廃止予定APIは以下の通り。

メソッド

  • java.lang.Thread.resume
  • java.lang.Thread.suspend
  • java.lang.ThreadGroup.allowThreadSuspension
  • java.lang.ThreadGroup.resume
  • java.lang.ThreadGroup.suspend

コンストラクタ

  • java.lang.invoke.ConstantBootstraps
  • java.lang.reflect.Modifier
  • javax.tools.ToolProvider 

ThreadクラスとThreadGroupクラスのresumeメソッド、suspendメソッドは使うのははばかられていたので、廃止されてくれた方が分かりやすくていいですね。

ConstantBootstrapsクラスはCONDYのブートストラップのためのユーティリティクラスですが、もともとstaticなクラスメソッドしか定義されていないので、コンストラクタがある方がおかしかったわけです。

Modifierクラスもモディファイアの定数と、クラスメソッドのisXXXメソッド群を定義しているクラスなので、コンストラクタは必要ありません。基本的にはClass.getModifiersメソッドやMember.getModifiersメソッドで返されるint値を引数にして、isXXXメソッドでチェックするという使い方です。

ToolProviderクラスも同じくクラスメソッドしか定義していないので、コンストラクタがあるのがおかしかったわけです。

 

追加されたAPI

Java 14で追加されたAPIはjava.baseモジュール、java.compilerモジュール、java.xmlモジュールの3つです。とはいうものの、ほとんどがjava.baseモジュールです。

java.base/java.ioパッケージ

java.ioパッケージではメソッドの追加が2、アノテーションの追加が1つありました。

PrintStreamクラス

なぜ今になって追加されたのか全然理解できませんが、2つのメソッドが追加されました。

  • write(byte[] buf)
  • writeBytes(byte[] buf)

いずれも、内部ではthis.write(buf, 0, buf.length)をコールしています。唯一の違いは、writeメソッドがIOException例外をスローするのに対し、writeBytesメソッドは例外をスローしないという点です。

Serialアノテーション

古のJavaではオブジェクトをシリアライズ/デシリアライズする時にSerializableインタフェースを実装する必要がありました。しかし、Serializableインタフェースはマーカーインタフェースでメソッドは定義されていません。もし、シリアライズ/デシリアライズする時に特別な処理が必要な場合はwriteObject/readObjectメソッドなどを定義する必要があります。

@Serialアノテーションはそれを補うため、シリアライズ/デシリアライズに関連したメソッド/定数を修飾するために使われます。

@Serialアノテーションを使用すべきメソッドは下記の5種類です。

  • private void writeObject(java.io.ObjectOutputStream stream) throws IOException
  • private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException
  • private void readObjectNoData() throws ObjectStreamException
  • ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException
  • ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException

定数は次の2種類。

  • private static final ObjectStreamField[] serialPersistentFields
  • private static final long serialVersionUID

Javadocにはここであげたメソッド/定数以外に@Serialアノテーションを使うことはセマンティックエラーだと書いてあるのですが、今のところコンパイル時にチェックすることはやっていないような気が... 今後のバージョンではコンパイル時のチェックに使われるのでしょうか?

 

java.base/java.langパッケージ

Java 14では新しいRecords型がプレビュー機能で導入されますが、それに伴ってAPIにも変更がありました。

Classクラス

後述しますが、Records型をリフレクションで扱うためのクラスが導入されており、Classクラスはそれに対応したメソッドが追加されています。

  • RecordComponent[] getRecordComponents()
  • boolean isRecord()

Records型とはいっていますが、実際にはちょっと特殊なクラスというだけなので、Classクラスで扱うことができます。isRecordメソッドはクラスがRecords型かどうかをチェックするためのメソッドですね。

もう1つのgetRecordComponentsメソッドはRecords型で定義されたフィールドを取り出すためのメソッドです。名前から分かると思いますが、RecordComponentクラスがRecords型で定義されたフィールドの情報を保持するクラスです。

jshell> record Data(int x, int y) {}
|  次を作成しました: レコード Data

jshell> var clzz = Data.class
clzz ==> class Data

jshell> var comps = clzz.getRecordComponents()
comps ==> RecordComponent[2] { int x, int y }

jshell> Stream.of(comps).forEach(System.out::println)
int x
int y

jshell>

Records型のDataはint xとint yを定義しているので、getRecordComponentsメソッドの返り値は要素が2つの配列になります。

そのまま出力してみると、フィールドの型と名前が表示されました。

RecordComponentクラスに関しては後ほどもうちょっと説明を加えます。

 

Recordクラス

ClassクラスのところでRecords型はちょっと特殊なクラスと言いましたが、Records型のスーパークラスとなるのがRecordクラスです。列挙型のスーパークラスがEnumクラスだというのと同じようなものですね。

たとえば、Records型のDataで試してみます。

C:\sample>cat Data.java
public record Data(int x, int y) {}

C:\sample>javac --enable-preview --release 14 Data.java
注意:Data.javaはプレビュー言語機能を使用します。
注意:詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。

C:\sample>

Records型はまだプレビュー機能なので、コンパイルに--enable-previewオプションが必要です。また、Java 14以前では使えないので、--release 14を指定しておく必要があります。

これでData.classファイルが生成されたので、javapしてみましょう。

C:\sample>C:\sample>javap -p Data
Compiled from "Data.java"
public final class Data extends java.lang.Record {
  private final int x;
  private final int y;
  public Data(int, int);
  public java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public int x();
  public int y();
}

C:\sample>

DataクラスがRecordクラスを派生させたクラスであることが分かります。また、xとyがフィールドとして宣言され、アクセッサ―メソッドも生成されています。

後は、おなじみのコンストラクタ、equalメソッド、hashCodeメソッド、toStringメソッドが作られていることも分かります。

ついでなので、もうちょっと見てみましょう。

C:\sample>javap -p -c Data
Compiled from "Data.java"
public final class Data extends java.lang.Record {
  private final int x;

  private final int y;

  public Data(int, int);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Record."<init>":()V
       4: aload_0
       5: iload_1
       6: putfield      #7                  // Field x:I
       9: aload_0
      10: iload_2
      11: putfield      #13                 // Field y:I
      14: return

  public java.lang.String toString();
    Code:
       0: aload_0
       1: invokedynamic #18,  0             // InvokeDynamic #0:toString:(LData;)Ljava/lang/String;
       6: areturn

  public final int hashCode();
    Code:
       0: aload_0
       1: invokedynamic #22,  0             // InvokeDynamic #0:hashCode:(LData;)I
       6: ireturn

  public final boolean equals(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: invokedynamic #26,  0             // InvokeDynamic #0:equals:(LData;Ljava/lang/Object;)Z
       7: ireturn

  public int x();
    Code:
       0: aload_0
       1: getfield      #16                 // Field x:I
       4: ireturn

  public int y();
    Code:
       0: aload_0
       1: getfield      #17                 // Field y:I
       4: ireturn
}

C:\sample>

バイトコードを眺めていると、コンストラクタとアクセッサ―メソッドはフィールドに対してのアクセスを行っているだけということが分かります。

おもしろいのが、equalsメソッドなどがINDYで実装されていることです。後述しますが、これに関連したクラスが追加されています。

ちなみに、Recordクラスを派生させたクラスを自作することはできません。

jshell> class Data extends Record {}
|  エラー:
|  クラスは直接java.lang.Recordを拡張できません
|  class Data extends Record {}
|  ^--------------------------^

jshell>

 

StrictMathクラス

StrictMathクラスには6つのメソッドが追加されました。

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

decrementExtractメソッドは引数の値を1減算するためのメソッド、incrementExactメソッドは1加算、negateExactメソッドが符号の反転を行うメソッドです。もし、int/longをオーバーフローする場合はArithmeticException例外をスローします。

jshell> int x = 0
x ==> 0

jshell> StrictMath.decrementExact(x)
$10 ==> -1

jshell> int y = Integer.MIN_VALUE
y ==> -2147483648

jshell> StrictMath.decrementExact(y)
|  例外java.lang.ArithmeticException: integer overflow
|        at Math.decrementExact (Math.java:1006)
|        at StrictMath.decrementExact (StrictMath.java:879)
|        at (#12:1)

jshell>

内部的にはMathクラスのdecrementExactメソッドをコールしているだけです。逆にいうと、Mathクラスに定義してあるのにStrictMathクラスにはなかったメソッドを追加したという感じでしょうか。

NullPointerException例外

これまでThrowable例外で定義されていたメソッドがオーバーロードされました。

  • String getMessage()

「なぜ今になってgetMessageメソッドが」と思うかもしれません。その理由はJEP 358 Helpful NullPointerExceptionsにあります。試してみれば、今までよりは分かりやすいメッセージになっているはずですよ。

 

java.base/java.lang.annotationパッケージ

ElementType列挙型

Records型が導入されたので、ElementType列挙型の定数も追加されています。

  • RECORD_COMPONENT

ElementType列挙型はTargetアノテーションの引数に使用します。Targetアノテーションはアノテーションを自作する時に使い、自作するアノテーションが何を修飾するものなのかを示します。

Records型のコンポーネントを修飾するアノテーションを作るときに、この定数を使うわけですね。

 

java.base/java.lang.invokeパッケージ

MethodHandles.Lookupクラス

普通に使う人には、どう考えても使わないクラスだと思いますが...

INDYで動的にコールするメソッドを定義する時に使用されるのがMethodHandleクラスです。そのMethodHandleオブジェクトをルックアップする時に使われるのが、MethodHandles.Lookupクラスです。ラムダ式などで使われています。

このLookupクラスに追加されたのが、以下の2メソッドです。
  • boolean hasFullPrivilegeAccess()
  • Class<?> previousLookupClass()

Lookupオブジェクトにはアクセス権が設定されています。この中で、privateアクセスされるかどうかをチェックするのがhasPrivateAccessメソッドです。

このhasPrivateAccessメソッドはJava 14でDeprecatedになりました(forRemovalは設定されていません)。

その代わりに追加されたのが、hasFullPrivilegeAccessメソッドです。

hasFullPrivilegeAccessメソッドはprivateアクセスとmoduleアクセスされるかどうかをチェックするメソッドです。

一方のhasPrivateAccessメソッドも動作が変更になり、privateアクセスとmoduleアクセスの両方をチェックするメソッドになりました(内部ではhasFullPrivilegeAccessメソッドをコールしています)。

さて、もう1つの方のpreviousLookupClassメソッドですが、Java 14でLookupクラスはクロスモジュールルックアップがサポートされたことに関連しています。

クロスモジュールルックアップは複数のモジュールに分かれている時に使われます。Javadocに記載されている例を見てもらうのが一番分かりやすいでしょうか。

    Lookup lookup = MethodHandles.lookup();   // in class C
    Lookup lookup2 = lookup.in(D.class);
    MethodHandle mh = lookup2.findStatic(E.class, "m", MT);

このlookup2変数が呼び出し元のlookup変数を返すために使われるのがpreviousLookupClassメソッドです。まぁ、そんなもんかと思っていただければいいかなw

 

java.base/java.lang.reflectパッケージ

RecordComponentクラス

前述したようにRecords型の導入に伴ってリフレクションでRecords型を扱うためのクラスがRecordComponentクラスです。Records型自身はClassクラスで扱えるので、Records型で定義するコンポーネントにリフレクションでアクセスするためのクラスです。

RecordComponentクラスは、他のリフレクション用のクラスと同様にAnnotatedElementインタフェースの実装クラスです。

定義されているメソッドは以下の通り。

  • Method getAccessor()
  • AnnotatedType getAnnotatedType()
  • <T extends Annotation> T getAnnotation(Class<T> annotationClass)
  • Annotation[] getAnnotations()
  • Annotation[] getDeclaredAnnotations()
  • Class<?> getDeclaringRecord()
  • String getGenericSignature()
  • Type getGenericType()
  • String getName()
  • Class<?> getType()
  • String toString()

アノテーションに関するメソッドはAnnotatedElementインタフェースで定義されているメソッドです。

getNameメソッドやgetTypeメソッドはその名のとおりですね。おもしろいのが、自動生成されるアクセッサ―メソッドを取得するgetAccessorメソッドや、コンポーネントを定義しているRecords型を取得するgetDeclaringRecordメソッドが定義されているところなどですね。

getGenericSignatureメソッドはジェネリクス化されたコンポーネントの場合、そのシグネチャを文字列で返します。ジェネリクス化されていないとnullが返るようです。

getGenericTypeメソッドはコンポーネントの型を返します。ジェネリクスなコンポーネントであれば型パラメータも一緒に取得できます。ジェネリクスでなければgetTypeと同じ結果になります。

jshell> record Data(String text) {}
|  次を作成しました: レコード Data

jshell> var clz = Data.class
clz ==> class Data

jshell> var component = clz.getRecordComponents()[0]
component ==> java.lang.String text

jshell> System.out.println(component.getDeclaringRecord())
class REPL.$JShell$11$Data

jshell> System.out.println(component.getName())
text

jshell> System.out.println(component.getType())
class java.lang.String

jshell> System.out.println(component.getGenericSignature())
null

jshell> System.out.println(component.getGenericType())
class java.lang.String

jshell> record Data2(List<String> texts) {}
|  次を作成しました: レコード Data2

jshell> var clz2 = Data2.class
clz2 ==> class Data2

jshell> var comp2 = clz2.getRecordComponents()[0]
comp2 ==> java.util.List texts

jshell> System.out.println(comp2.getGenericSignature())
Ljava/util/List<Ljava/lang/String;>;

jshell> System.out.println(comp2.getGenericType())
java.util.List<java.lang.String>

jshell> System.out.println(comp2.getType())
interface java.util.List

jshell>

Stringクラスのコンポーネントの場合、getGenericSignatureメソッドはnullを返しています。また、getGenericTypeメソッドとgetTypeメソッドは両方ともjava.lang.Stringを返していることが分かります。

一方、List<String>クラスのコンポーネントだと、getGenericSignatureメソッドがLjava/util/List<Ljava/lang/String;>;になっています。シグネチャの読み方ですが、Lは参照型を表していて、そのクラスがLと;に挟まれた部分になっています。なので、これはList<String>を表しています。

これはgetGenericTypeメソッドで返る値と同じです。

一方でgetTypeメソッドは型パラメータがないListになっていることが分かります。

 

java.base/java.lang.runtimeパッケージ

なんとパッケージが1つ追加されていました。今のところ定義されているクラスは1つです。

ObjectMethodsクラス

ObjectMethodsクラスが定義しているメソッドは1つ。このメソッドを見ると、このクラスが何のためのクラスか分かりますw

  • static Object bootstrap(MethodHandles.Lookup lookup, String methodName, TypeDescriptor type, Class<?> recordClass, String names, MethodHandle... getters)

bootstrapメソッドといえばINDYです。

Javadocを見ると、Object.equals/hashCode/toStringの3メソッドを生成するとあります。この3つのメソッドというと、先ほどのRecords型で生成されたメソッドがINDYを使っていましたね。どうやら、Records型のクラスを定義すると、このbootstrapメソッドで実行時にこれらのメソッドを生成するようです。ちゃんと追っていないので、もしかしたら違うかもしれませんが 😅

 

java.base/java.textパッケージ

CompactNumberFormatクラス

CompactNumberFormatクラスにコンストラクタが1つ追加されました。

  • CompactNumberFormat(String decimalPattern, DecimalFormatSymbols symbols, String[] compactPatterns, String pluralRules)

これまでのコンストラクタと比べると、最後のpluralRulesが追加されています。Plural RuleはUnicodeで決められているLanguage Plural Rulesのことです。記述のしかたもUnicodeのドキュメントを見てください。

とはいうものの、CompactNumberFormatオブジェクトを自分でコンストラクタを使って生成することは、ほぼないと思うんですよね。

 

java.base/java.util.concurrent.locksパッケージ

LockSupportクラス

ロックのサポートのためのLockSupportクラスですが、スレッドのスケジューリングを行うためカレントスレッドを無効にするparkメソッドがあります。

parkメソッドには引数でブロッカーを指定することもできるのですが、引数なしのオーバーロードもあります。

引数なしのparkメソッドをコールする前に、ブロッカーを設定するためのメソッドが追加されました。

  • statci void setCurrentBlocker(Object blocker)

 

java.compiler/javax.lang.modelパッケージ

SourceVersion列挙型

毎度のことですが、定数が追加されています。

  • RELEASE_14

 

java.compiler/javax.lang.model.elementパッケージ

ElementKind列挙型

パターンマッチングとRecords型に対応するため定数が追加になっています。

  • BINDING_VARIABLE
  • RECORD
  • RECORD_COMPONENT

BINDING_VARIABLEがinstanceofを使ったパターンマッチングに対応しています。

 

RecordComponentElementインタフェース

こちらもRecords型に対応するために追加されたインタフェースです。定義されているメソッドは3つ。

  • ExecutableElement getAccessor()
  • Element getEnclosingElement()
  • Name getSimpleName()

getAccessorメソッドがコンポーネントのアクセッサ―メソッドに対する要素を返します。getEnclosingElementメソッドはコンポーネントを定義しているRecord型の要素を返します。

 

TypeElementインタフェース

こちらもRecords型に対応するためにメソッドが追加されました。

  • List<? extends RecordComponentElement> getRecordComponents()

 

java.compiler/javax.lang.model.utilパッケージ

javax.lang.model.utilパッケージではコンパイル時に使用するプログラム要素の扱うためのビジターなどをいろいろと定義しているパッケージです。こちらもRecords型導入にともなってビジターなどが追加されています。

追加されたクラスだけを列挙しておきます。

  • AbstractElementVisitor14
  • AbstractTypeVisitor14
  • ElementKindVisitor14
  • ElementScanner14
  • SimpleAnnotationValueVisitor14
  • SimpleElementVisitor14
  • SimpleTypeVisitor14
  • TypeKindVisitor14

 

java.xml/org.xml.saxパッケージ

ContentHandlerインタフェース

XMLのSAXパーサーで使用するContentHandlerインタフェースにメソッドが追加されました。。

  • void declaration(String version, String encoding, String standalone) throws SAXException)

逆に、今までなぜdeclarationを受け取るメソッドがなかったのかの方が不思議ですね。

ちなみに、このメソッドはdefaultメソッドで定義されていますが、defaultメソッドでは何も行っていません。

 

ということで、Java 14のAPIの変更をまとめてみました。Records型導入による追加が大きいですね。逆にいうと、それ以外のAPI追加はほとんどないのがちょっと寂しい...