1. 概要

Java 15では、多くの機能が導入されました。 この記事では、JEP-371の隠しクラスと呼ばれる新機能の1つについて説明します。 この機能は、JDK以外では推奨されない UnsafeAPIの代替として導入されています。

隠しクラス機能は、動的バイトコードまたはJVM言語を使用するすべての人に役立ちます。

2. 隠しクラスとは何ですか?

動的に生成されたクラスは、低レイテンシのアプリケーションに効率と柔軟性を提供します。 それらは限られた時間だけ必要です。 静的に生成されたクラスの存続期間中それらを保持すると、メモリフットプリントが増加します。 クラスごとのローダーなど、この状況に対する既存のソリューションは、煩雑で非効率的です。

Java 15以降、非表示クラスは動的クラスを生成するための標準的な方法になりました。

非表示クラスは、バイトコードや他のクラスで直接使用できないクラスです。クラスとして言及されていても、非表示クラスまたはインターフェイスのいずれかを意味すると理解する必要があります。 アクセス制御ネストのメンバーとして定義することもでき、他のクラスとは独立してアンロードできます。

3. 隠しクラスのプロパティ

これらの動的に生成されたクラスのプロパティを見てみましょう。

  • 検出不能–隠しクラスは、バイトコードリンケージ中にJVMによっても、クラスローダーを明示的に使用するプログラムによっても検出できません。 リフレクティブメソッドClass:: forName ClassLoader :: findLoadedClass 、および Lookup ::findClassはそれらを検出しません。
  • 非表示のクラスをスーパークラス、フィールドタイプ、リターンタイプ、またはパラメータータイプとして使用することはできません。
  • 非表示のクラスのコードは、クラスオブジェクトに依存することなく、直接使用できます。
  • 非表示クラスで宣言されたfinalフィールドは、アクセス可能なフラグに関係なく変更できません。
  • これは、アクセス制御ネストを検出不可能なクラスで拡張します。
  • 概念的な定義クラスローダーがまだ到達可能であっても、アンロードされる可能性があります。
  • スタックトレースには、デフォルトでは非表示クラスのメソッドや名前は表示されませんが、JVMオプションを微調整すると表示される可能性があります。

4. 隠しクラスの作成

非表示クラスはクラスローダーによって作成されません。ルックアップクラスと同じ定義クラスローダー、ランタイムパッケージ、および保護ドメインがあります。

まず、Lookupオブジェクトを作成しましょう。

MethodHandles.Lookup lookup = MethodHandles.lookup();

Lookup :: defineHiddenClass メソッドは、非表示のクラスを作成します。 このメソッドは、バイトの配列を受け入れます。

簡単にするために、 HiddenClass という名前の単純なクラスを定義します。このクラスには、指定された文字列を大文字に変換するメソッドがあります。

public class HiddenClass {
    public String convertToUpperCase(String s) {
        return s.toUpperCase();
    }
}

クラスのパスを取得して、入力ストリームにロードしましょう。 その後、 IOUtils.toByteArray()を使用してこのクラスをバイトに変換します。

Class<?> clazz = HiddenClass.class;
String className = clazz.getName();
String classAsPath = className.replace('.', '/') + ".class";
InputStream stream = clazz.getClassLoader()
    .getResourceAsStream(classAsPath);
byte[] bytes = IOUtils.toByteArray();

最後に、これらの構築されたバイトを Lookup ::defineHiddenClassに渡します。

Class<?> hiddenClass = lookup.defineHiddenClass(IOUtils.toByteArray(stream),
  true, ClassOption.NESTMATE).lookupClass();

2番目のboolean引数trueはクラスを初期化します。 3番目の引数ClassOption.NESTMATEは、作成された非表示クラスがネストメイトとしてルックアップクラスに追加され、すべてのクラスおよびインターフェイスのprivateメンバーにアクセスできるようにすることを指定します。同じ巣。

隠しクラスをそのクラスローダーClassOption.STRONGで強力にバインドするとします。 つまり、非表示のクラスは、その定義ローダーに到達できない場合にのみアンロードできます。

5. 隠しクラスの使用

非表示のクラスは、実行時にクラスを生成し、リフレクションを介して間接的に使用するフレームワークによって使用されます。

前のセクションでは、非表示のクラスの作成について説明しました。 このセクションでは、それを使用してインスタンスを作成する方法を説明します。

Lookup.defineHiddenClass から取得したクラスをキャストすることは他のクラスオブジェクトでは不可能であるため、Objectを使用して非表示のクラスインスタンスを格納します。 非表示のクラスをキャストする場合は、インターフェイスを定義し、インターフェイスを実装する非表示のクラスを作成できます。

Object hiddenClassObject = hiddenClass.getConstructor().newInstance();

それでは、隠しクラスからメソッドを取得しましょう。 メソッドを取得したら、他の標準メソッドと同じように呼び出します。

Method method = hiddenClassObject.getClass()
    .getDeclaredMethod("convertToUpperCase", String.class);
Assertions.assertEquals("HELLO", method.invoke(hiddenClassObject, "Hello"));

これで、いくつかのメソッドを呼び出すことで、非表示のクラスのいくつかのプロパティを確認できます。

メソッドisHidden()は、このクラスに対してtrueを返します。

Assertions.assertEquals(true, hiddenClass.isHidden());

また、非表示クラスの実際の名前はないため、その正規名はnullになります。

Assertions.assertEquals(null, hiddenClass.getCanonicalName());

非表示のクラスには、ルックアップを実行するクラスと同じ定義ローダーがあります。 ルックアップは同じクラスで行われるため、次のアサーションが成功します。

Assertions.assertEquals(this.getClass()
    .getClassLoader(), hiddenClass.getClassLoader());

メソッドを介して非表示のクラスにアクセスしようとすると、ClassNotFoundExceptionがスローされます。 非表示のクラス名は他のクラスに表示されるほど珍しく、修飾されていないため、これは明らかです。 いくつかのアサーションをチェックして、非表示のクラスが検出できないことを証明しましょう。

Assertions.assertThrows(ClassNotFoundException.class, () -> Class.forName(hiddenClass.getName()));
Assertions.assertThrows(ClassNotFoundException.class, () -> lookup.findClass(hiddenClass.getName()));

他のクラスが非表示のクラスを使用できる唯一の方法は、そのClassオブジェクトを使用することです。

6. 匿名クラス対。 隠しクラス

前のセクションで非表示のクラスを作成し、そのプロパティのいくつかで遊んだ。 それでは、匿名クラス(明示的な名前のない内部クラス)と非表示クラスの違いについて詳しく説明しましょう。

  • 匿名クラスの名前は動的に生成され、その間に$がありますが、com.baeldung.reflection.hiddenclass.HiddenClassから派生した非表示クラスはcom.baeldung.reflection.hiddenclass.HiddenClass/になります。 1234.
  • 匿名クラスは、非推奨の Unsafe :: defineAnonymousClass を使用してインスタンス化されますが、 Lookup ::defineHiddenClassは非表示のクラスをインスタンス化します。
  • 非表示のクラスは、定数プールのパッチ適用をサポートしていません。 これは、定数プールエントリがすでに具体的な値に解決されている匿名クラスを定義するのに役立ちます。
  • 非表示のクラスとは異なり、匿名クラスは、サブクラスではなく別のパッケージにある場合でも、ホストクラスの保護されたメンバーにアクセスできます。
  • 匿名クラスは他のクラスを囲んでそのメンバーにアクセスできますが、非表示のクラスは他のクラスを囲むことはできません。

非表示クラスは匿名クラスの代わりにはなりませんが、JDKでの匿名クラスの使用法の一部を置き換えています。 Java 15以降、ラムダ式は非表示クラスを使用します

7. 結論

この記事では、隠しクラスと呼ばれる新しい言語機能について詳しく説明しました。 いつものように、コードはGitHubから入手できます。