JDK

Java 15の新機能(2)ーHiddenクラス

Hiddenクラスは、かなり特殊な用途でしか使われないと思われる。Javaバイトコードが直接アクセスできないクラスを動的に作り出すことを可能にする機能である。フレームワークなどが動的に作り出すクラスが、リフレクションによって検出されてしまうのを避ける意図があるという。

 このクラスはその特性ゆえに、実験するには少々手間がかかる。java. lang. invoke. MethodHandles. Lookup # defineHiddenClass()メソッドにbyte[]型でクラス定義を与えること以外にはインスタンスを作成する方法がないようだ。そこで、下準備として、次のようなHello Worldプログラムを作成して、コンパイルしclassファイルを用意しておく。このclassファイルを読み込んで利用することにする。

public class Hello
{
    public final String message = "Hello World!";
    public static void main(String[] args)
    {
        Hello instance = new Hello();
        instance.greet();
    }
    public void greet()
    {
        System.out.println(message);
    }
}

続いて、これをコンパイルしたバイトコードを使って、Hiddenクラスのインスタンスを使用するプログラムを作成する。FilesクラスのstaticメソッドreadAllBytes()メソッドを使って、上のHelloクラスをコンパイルしたHello.classファイルをbyte[]型の変数bytesに読み込む。続いて、MethodHandles.lookup()によって、ルックアップオブジェクトlookupを取得する。lookupのdefineHiddenClass()メソッドに、Helloクラスのバイトコードを読み込んだ配列bytesを渡して、Hiddenクラスを定義する。

lookupのfindClass()メソッドを使って、Class<?>オブジェクトを取得する。このオブジェクトを取得できれば、あとは従来のようなリフレクションを使ってインスタンスを作成したり、メソッドを呼び出したりできる。

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.*;
import java.io.*;
import java.nio.file.*;

public class HiddenExample
{
    public static void main(final String[] args) throws Exception
    {
        byte[] bytes = Files.readAllBytes(Paths.get("Hello.class"));
        Lookup lookup = MethodHandles.lookup();
        lookup.defineHiddenClass(bytes, false);

        Class<?> clazz = lookup.findClass("Hello");

        Constructor constuctor = clazz.getDeclaredConstructor();
        Object hiddenClassInstance = constuctor.newInstance();
        
        Method method = clazz.getDeclaredMethod("greet");
        method.invoke(hiddenClassInstance);
        try
        {
            Field field = clazz.getField("message");
            field.set(hiddenClassInstance, "Goodbye!");
        }
        catch(IllegalAccessException e)
        {
            System.out.println("    finalフィールドは変更できません "+ e.getMessage());
        }
    }
}

このプログラムをコンパイルして実行すると、次のような出力が得られる。Hello World!と出力されていて、Helloクラスのgreet()メソッドが実行されていることがわかる。また、finalフィールドの値を変更しようとして、例外も発生している。

Hello World!
    finalフィールドは変更できません Can not set final java.lang.String field Hello.message to java.lang.String

普通にクラスを定義して、それを利用することを考えると、面倒なことこの上ない。それでも、フレームワークを作っている人には役に立つのであろう。