seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

ExcelのRTDサーバをC#で作る方法

Excelでリアルタイムデータを取得するRTDサーバをC#で作る方法

概要

Excelには動的にリアルタイムデータをセルに取得・表示するための仕組みが2つある

どちらも、ほぼ似たようなワークシート関数で、使い方もほぼ同じ、結果もほぼ同じである。

=DDE("APPLICATION", "TOPIC", "ITEM")
=RTD("PROGRAM_ID", "SERVER", "TOPIC1"....)

Excelでリアルタイムデータを取得できるようになると、刻々と変わる値から動的に計算させたり、状態に応じて表示色を変えたりできるので、多数の計測機器から出力される計測ファイルや、ネット上で刻々と変化する無数の値からリアルタイムに分析・俯瞰するのに使えるようになる。

今回は、このちうちのRTDとして機能するRTDサーバーをC#で作成してみることにする。

DDEの特性

DDEはWindows3.1の16ビット全盛時代によく使われていたレガシーな技術で、データ交換にウィンドウメッセージを用いているところに特徴がある。1

このDDE関数で指定する「APPLICATION」はトップレベルウィンドウの「ウィンドウクラス名」そのものである。 つまり、ExcelのDDE関数は起動している別アプリのウィンドウとの間でメッセージを交換することでリアルタイムにデータを取得している。

数あるウィンドウメッセージのうち、DDEまわりを制御するライブラリはDDEML(Dynamic Data Exchange Management Library)としてまとめられている。

C++で実装する分にはddeml.hをインクルードすれば現在でも問題なく実装できるのであるが、C#(WindowsFormやWPF)では難しい。

DotNETではウィンドウクラスを独自に設定することが想定されておらず、DDEMLに相当するウィンドウメッセージのハンドリングも標準では全くサポートされていないためである。2

RTDの特性

RTD関数はExcel 2002からサポートされており、ダイナミックデータの取得方法としてウィンドウメッセージではなく、COMオートメーションサーバーを用いているところに特徴がある。

要するに、データの送り元としては、別アプリとしてウィンドウを用意する必要はなく、COMのDDLないしEXEを作成すれば良い、ということである。

EXE、またはプロキシ/スタブをもつDLLの場合は適切に設定すればリモートPC上のCOMを扱える(DCOM)ようになる、という利点もある。3

そのため、ExcelのRTD関数の第2引数ではコンピュータ名が指定できるようになっている。(ローカルPCのCOMを指定するのであれば、ここは省略して良い。)

また、DDEではTOPIC, ITEMの2種類のデータしか持つことが出来なかったが、RTDの場合は1つ以上、最大で253個のパラメータをもつことができる。

単純なCOMサーバーを作るのであればDotNETで容易につくることができる。

ということで、今回はC#で簡単なRTDサーバーを作ってみる。

※ 欠点としてはCOMであるため、基本的にレジストリへの登録が必須となる点である。(DDEの場合は現在存在するトップレベルウィンドウのウィンドウクラス名を検索するだけなので、レジストリ等への事前登録が必要ない。)

IRtdServer, IRTDUpdateEvent インターフェイス

RTDはCOMなので当然インターフェイスとして定義されており、RTDサーバーは、このインターフェイスを実装することで実現する。

これらのインターフェイスExcelのタイプライブラリをインポートすることで利用可能になるが、インターフェイスの型やGUID等々は明確になっているので、わざわざExcelからタイプライブラリをインポートせずとも、同じ定義を自分で用意しても何ら問題ない。(そうすればExcelのないビルド環境でもビルドできるようになる。)

また、RTDに必要なインターフェイスは2つだけである。

IRtdServerインターフェイス

IRtdServerが実装すべきRTDサーバーのインターフェイスである。

IRTDUpdateEventインターフェイス

IRTDUpdateEventは、データが更新されたことをExcelに通知するためのコールバック用のインターフェイスである。

サーバー開始時にExcelからIRTDUpdateEventが実装されたCOMオブジェクトが渡されてくるので、これに対してコールバックを行う。

定義例

以下は https://weblogs.asp.net/kennykerr/Rtd7 などから借用した。(DISPIDなどは別のソースコードから検索してつけてみた。)4

↑↑↑ RTDまわりのことは、この人の記事だけで、ほとんど網羅できる。

// Excelを参照せず、IRTDServer, IRTDUpdateEventのインターフェイスの定義を行う.
// Excel RTD Servers: C# without the Excel Assembly Reference
// https://weblogs.asp.net/kennykerr/Rtd7

/// <summary>
/// RTDのイベントのコールバック用インターフェイス.
/// </summary>
[ComImport]
[TypeLibType(TypeLibTypeFlags.FDual | TypeLibTypeFlags.FDispatchable)]
[Guid("A43788C1-D91B-11D3-8F39-00C04F3651B8")]
public interface IRTDUpdateEvent
{
    [DispId(10)]
    void UpdateNotify();

    [DispId(11)]
    int HeartbeatInterval
    {
        get;
        set;
    }

    [DispId(12)]
    void Disconnect();
}

/// <summary>
/// Excelから呼び出されるRTDインターフェイス
/// </summary>
[ComImport]
[TypeLibType(TypeLibTypeFlags.FDual | TypeLibTypeFlags.FDispatchable)]
[Guid("EC0E6191-DB51-11D3-8F3E-00C04F3651B8")]
public interface IRtdServer
{
    [DispId(10)]
    int ServerStart(IRTDUpdateEvent callback);

    [DispId(11)]
    object ConnectData(int topicId,
                        [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)] ref Array strings,
                        ref bool newValues);

    [DispId(12)]
    [return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)]
    Array RefreshData(ref int topicCount);

    [DispId(13)]
    void DisconnectData(int topicId);

    [DispId(14)]
    int Heartbeat();

    [DispId(15)]
    void ServerTerminate();
}

VB6的なAutomation型らしさの色濃いSafeArray配列を多用したり、ref引数を使ったりとしていて、なかなかめんどくさそうな型定義になっている。

IRTDServerの実装

以下、IRTDServerの実装方法を示す。

プロジェクトの作成

まずC#のクラスライブラリプロジェクトを作成する。

ここでは「SeraRTDServerExample」というプロジェクトを作成するものとする。

これは、ソリューションエクスプローラのプロジェクト名を右クリックしてプロパティを開き、アプリケーションタブのアセンブリ情報から「アセンブリをCOM参照可にする」をチェック状態にしておく。

これでビルドされたDLLはCOMとして登録可能になる。

IRTDServer実装コード

IRTDServerは以下のクラスとして実装してみた。

今回はRTDサーバを作ることが目的なので、

  • 引数としてファイル名とフォーマットを渡すと...
    • 日付またはファイルサイズのいずれかを動的に返す
    • もしくはフォルダ名を指定してファイル一覧を文字列として返す

だけの単純なものとした。

Visual Studio 2017 Express for Desktopでビルド可能としている。

ソース一式はgist:51b509592e0b20fc77612673d490f247にも置いている。

/// <summary>
/// シンプルなRTDサーバーの実装例
/// </summary>
[Guid("5B6AA03F-F280-4B06-AFA3-519C9215D6BD")]
[ProgId("SeraRtdSvr")]
[TypeLibType(TypeLibTypeFlags.FDual | TypeLibTypeFlags.FDispatchable)]
[ComVisible(true)]
public class SeraphyRTDServer : IRtdServer
{
    /// <summary>
    /// TopicIdごとの値を保持し更新の有無を判定する値ホルダー
    /// </summary>
    class ValueHolder
    {
        /// <summary>
        /// 値のリゾルバ
        /// </summary>
        private Func<object> resolver;

        /// <summary>
        /// 現在の値
        /// </summary>
        public object Current { get; set; }

        /// <summary>
        /// 更新の有無
        /// </summary>
        public bool Modified { set; get; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="resolver"></param>
        internal ValueHolder(Func<object> resolver)
        {
            this.resolver = resolver;
        }

        /// <summary>
        /// 更新する.
        /// 変更がある場合は更新の有無が設定される.
        /// </summary>
        public void Refresh()
        {
            object value;
            try
            {
                value = resolver();
            }
            catch (Exception ex)
            {
                Debug.Print("▼exception {0}", ex.Message);
                // https://groups.google.com/forum/#!topic/exceldna/Z6mmxJ4LSbM
                // ErrDiv0  = -2146826281 
                // ErrNA = -2146826246
                // ErrName = -2146826259
                // ErrNull = -2146826288
                // ErrNum = -2146826252
                // ErrRef = -2146826265
                // ErrValue = -2146826273
                value = new ErrorWrapper(-2146826246); // N/A
            }
            if (!Object.Equals(value, Current))
            {
                Modified = true; // ModifiedはOffにはしない
                Debug.Print("◇ modified old={0}, new={1}", Current, value);
            }
            Current = value;
        }
    }

    /// <summary>
    /// タスクを終了させるためのキャンセラレーショントークンソース
    /// </summary>
    private CancellationTokenSource cancelTokenSource;

    /// <summary>
    /// 定期的にRTDにイベントを通知するための無限ループタスク
    /// </summary>
    private Task task;

    /// <summary>
    /// RTDイベントコールバック
    /// </summary>
    private IRTDUpdateEvent callback;

    /// <summary>
    /// RTDイベントコールバックを使う場合の同期コンテキスト
    /// </summary>
    private SynchronizationContext synchronizationContext;

    /// <summary>
    /// TopicIdごとのValueHolderを保持するマップ.
    /// </summary>
    private Dictionary<int, ValueHolder> TopicMap = new Dictionary<int, ValueHolder>();

    /// <summary>
    /// ファイル名に対する更新日付を取得する。例外の場合は例外メッセージを返す.
    /// </summary>
    /// <param name="name">ファイル名</param>
    /// <returns>更新日付</returns>
    private object GetLastWriteTime(string name)
    {
        return System.IO.File.GetLastWriteTime(name);
    }

    /// <summary>
    /// ファイル名に対するファイルサイズを取得する。例外の場合は例外メッセージを返す.
    /// </summary>
    /// <param name="name">ファイル名</param>
    /// <returns>更新日付</returns>
    private object GetFileSize(string name)
    {
        return new System.IO.FileInfo(name).Length;
    }

    /// <summary>
    /// ファイルまたはディレクトリに対するタブ区切りファイル一覧を返す.
    /// </summary>
    /// <remarks>
    /// 本当は配列を返したいのだが、方法がなさそうなので受け取り側で分割する.
    /// </remarks>
    /// <param name="name">ファイル名</param>
    /// <returns>更新日付</returns>
    private object GetFileList(string name)
    {
        var attr = File.GetAttributes(name);
        if (attr.HasFlag(FileAttributes.Directory))
        {
            var dir = new DirectoryInfo(name);
            return String.Join("\t", dir.GetFiles().Select(f => f.FullName).ToArray());
        }
        return name;
    }

    /// <summary>
    /// リアルタイム データ サーバーから新しいトピックを追加します。
    /// ConnectData メソッドは、リアルタイム データ関数を含むファイルが開かれたとき、
    /// またはユーザーが RTD 関数を含む新しい数式を入力したときに呼び出されます。
    /// </summary>
    /// <param name="topicId">必ず指定します。整数型 (Integer) の値を指定します。
    /// トピックを識別する一意の値を指定します。この値は、Excel によって割り当てられます。</param>
    /// <param name="strings">必ず指定します。オブジェクト型 (Object) の値を指定します。
    /// トピックを識別する文字列の 1 次元配列を指定します。(RTD関数の引数)</param>
    /// <param name="newValues">必ず指定します。ブール型 (Boolean) の値を指定します。
    /// 入力値がFALSEの場合はRTDセルを含むブックをロードしたなどでセル上に既存の値があることを示します。
    /// TRUE(非0)の場合はRTDセルを新規に入力したことを示します。
    /// 返却値は、新規の値を使用するべきかを示し、FALSEの場合は既存のセルの値を変更しないことを示します。</param>
    /// <returns></returns>
    public object ConnectData(
        int topicId,
        [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)] ref Array strings,
        ref bool newValues)
    {
        PrintMethodInfo();
        try
        {
            // RTD関数の引数(トピック)の取得
            // トピックは1個以上、最大253個までありえる。
            // (なお、同一のパラメータは同一のtopicidが割り当てられるので
            // セルのコピー等を行った場合は本メソッドが重ねて呼び出されることはない。)
            string fname = strings.GetValue(0).ToString();
            string fmt = "time";
            if (strings.Length >= 2) // 2個目以降は省略可
            {
                fmt = strings.GetValue(1).ToString();
            }
            Debug.Print("○ topicId={0} fname={1}, format={2}", topicId, fname, fmt);

            Func<object> func;
            switch (fmt)
            {
                case "time":
                default:
                    func = () => this.GetLastWriteTime(fname);
                    break;

                case "size":
                    func = () => this.GetFileSize(fname);
                    break;

                case "list":
                    func = () => this.GetFileList(fname);
                    break;
            }

            // 割り当てられたTOPICIDと関連づけて保存する
            var valueHolder = new ValueHolder(func);
            TopicMap[topicId] = valueHolder;

            // セルを更新することを示す
            newValues = true;

            // データを取得し返却する.
            valueHolder.Refresh();
            valueHolder.Modified = false;
            return valueHolder.Current;
        }
        catch (Exception ex)
        {
            return ex.ToString();
        }
    }

    /// <summary>
    /// RTD (リアルタイム データ) サーバー アプリケーションに、トピックが使用されなくなったことを通知します。
    /// </summary>
    /// <param name="topicId">必ず指定します。整数型 (Integer) の値を指定します。
    /// トピックに割り当てる一意の値を指定します。この値は、Excel によって割り当てられます。</param>
    public void DisconnectData(int topicId)
    {
        PrintMethodInfo();
        TopicMap.Remove(topicId);
    }

    /// <summary>
    /// リアルタイム データ サーバーがアクティブかどうかを判別します。
    /// </summary>
    /// <returns>ゼロまたは負の値は失敗を示し、正の値は、サーバーがアクティブであることを示します。</returns>
    public int Heartbeat()
    {
        // ハートビート通知
        PrintMethodInfo();
        return 1;
    }

    /// <summary>
    /// このメソッドは、Excel が新しいデータを取得するときに呼び出されます。
    /// </summary>
    /// <param name="topicCount">必ず指定します。整数型 (Integer) の値を指定します。
    /// RTD サーバーは、TopicCount の値を、取得された配列内の要素数に変更する必要があります。</param>
    /// <returns>新しいデータが格納されたバリアント型 (Variant) の配列。</returns>
    [return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)]
    public Array RefreshData(ref int topicCount)
    {
        PrintMethodInfo();

        // 更新されているアイテムの抽出
        var modifiedItems = TopicMap.Where(entry => entry.Value.Modified).ToList();

        int count = modifiedItems.Count; // アイテムの個数
        object[,] data = new object[2, count]; // TOPICID, 値のペアからなる2次元配列

        int index = 0;
        foreach (var entry in modifiedItems)
        {
            int topicId = entry.Key;
            object value = entry.Value.Current;
            Debug.Print("* topidId={0}, value={1}", topicId, value);

            data[0, index] = topicId; // topicId
            data[1, index] = value; // 現在値

            entry.Value.Modified = false; // 返却したアイテムはModifiedをクリアする
            ++index;
        }

        topicCount = count;
        return data;
    }

    /// <summary>
    /// ServerStart メソッドは、
    /// リアルタイム データ サーバーがインスタンス化された直後に呼び出されます。
    /// </summary>
    /// <param name="callback">必ず指定します。
    /// IRTDUpdateEvent オブジェクトを指定します。
    /// コールバック オブジェクトを指定します。</param>
    /// <returns>負の値またはゼロはサーバーの起動に失敗したことを示し、
    /// 正の値は起動が成功したことを示します。</returns>
    public int ServerStart(IRTDUpdateEvent callback)
    {
        PrintMethodInfo();
        if ((task == null || task.IsCompleted) && callback != null)
        {
            this.callback = callback;

            // RTDイベントはSTAのため
            // 別スレッドから呼び出すためはマーシャリングする必要がある.
            synchronizationContext = SynchronizationContext.Current ??
                    new WindowsFormsSynchronizationContext();
            Debug.Print("synchronizationContext={0}", synchronizationContext);

            // タイマー的な無限ループタスクを開始する.
            cancelTokenSource = new CancellationTokenSource();
            task = new Task(WatchLoop,
                    cancelTokenSource.Token,
                    TaskCreationOptions.LongRunning);
            task.Start();
            return 1;
        }
        return 0;
    }

    /// <summary>
    /// キャンセルが指示されるまで現在保持しているTopicIdの状態を1秒ごとに検査し、
    /// 更新があればRTDイベントコールバックでExcelに更新を通知する無限ループタスク
    /// </summary>
    private void WatchLoop()
    {
        // キャンセルされるまで無限ループする
        while (!cancelTokenSource.Token.IsCancellationRequested)
        {
            if (!cancelTokenSource.Token.WaitHandle.WaitOne(1000)) // 1000mSec待ち
            {
                // タイムアウトした場合 = キャンセルされていない場合
                PrintMethodInfo("Timeout");

                if (UpdateAllTopic() > 0)
                {
                    // Excelに更新を通知する.
                    synchronizationContext.Post(x => {
                        try
                        {
                            PrintMethodInfo("UpdateNotify");
                            callback?.UpdateNotify();
                        }
                        catch (Exception ex)
                        {
                            // コールバックに失敗した場合は
                            // Excelとの関係が異常になっている
                            Debug.Print("★Callback failed. " + ex.Message);
                            cancelTokenSource.Cancel();
                        }
                    }, null);
                }
            }
        }
        Debug.Print("★★end★★");
    }

    /// <summary>
    /// 保持している、すべてのTopicの更新を行う
    /// </summary>
    /// <returns>更新があった個数を返す</returns>
    private int UpdateAllTopic()
    {
        int modifiedCount = 0;
        foreach (var valueHolder in TopicMap.Values)
        {
            valueHolder.Refresh();
            modifiedCount += valueHolder.Modified ? 1 : 0;
        }
        return modifiedCount;
    }

    /// <summary>
    /// リアルタイム データ サーバーへの接続を終了します。
    /// </summary>
    public void ServerTerminate()
    {
        PrintMethodInfo();
        Dispose();
    }

    public SeraphyRTDServer()
    {
        PrintMethodInfo();
    }

    ~SeraphyRTDServer()
    {
        PrintMethodInfo();
        Dispose(); // ServerTerminate前にCOMがリリースされた場合の対応
    }

    /// <summary>
    /// 後片付け。タスク等を終了させます.
    /// </summary>
    private void Dispose()
    {
        cancelTokenSource?.Cancel();
        task?.Wait();

        cancelTokenSource = null;
        task = null;
        synchronizationContext = null;
    }

    /// <summary>
    /// 診断メッセージ表示
    /// </summary>
    /// <param name="member">関数名等。
    ///  省略された場合は呼び元のメソッド名が自動的に適用される</param>
    private void PrintMethodInfo(
    [System.Runtime.CompilerServices.CallerMemberName] string member = "")
    {
        Debug.Print("☆{0}: @{1:X} thread={3}({2})", member, GetHashCode(),
            Thread.CurrentThread.GetApartmentState(),
            Thread.CurrentThread.ManagedThreadId);
    }
}

ビルド方法

プロジェクトをビルドして生成されたdllは、COMとして登録する必要がある。

管理者権限でコマンドプロンプトを開き、

regasm /codebase SeraRtdSvr.dll

のように、Regasm.exe (アセンブリ登録ツール)を使って登録する。

regasm.exeは、DotNETフレームワークのフォルダ5にある。あるいは、Visual Studioをインストールするとスタートメニューに追加される「Developer Command Line Tool」を使うとパスが通っている。

『codebaseを指定する場合は署名をつけろ』とか警告がでるが、とりあえず登録はできる。

オプションとしては以下のようなものがある。

  • /unregister ... 登録解除する場合
  • /codebase ... DLLの場所(パス)を指定して登録する場合
  • /tlb ... COMオートメーションのタイプライブラリの登録を行う場合
  • regfile ... レジストリに書き込む内容をファイルとして出力する場合6

今回はタイプライブラリの登録は必要ない。既存のインターフェイスIRTDServerに対する実装しか行っていないためである。 (新しいインターフェイスは何も定義されておらず、tlbexpツールでtlbファイルをエクスポートしても中身の実体は何もないものとなる。)

実行例

たとえば、以下のようなシートを作成してみた。

f:id:seraphy:20180208191941p:plain
screencapture_excel_rtd

このRTDではディレクトリへのパスを指定して"list"フォーマットを指定すると、ファイルリストがタブ区切り文字列で返ってくるので、まず、これを配列に分解するための関数を作る。7

標準モジュールを追加して

Public Function ToArray(ByVal str As String) As String()
    ToArray = Split(str, Chr(9))
End Function

とする。

つぎに、B1にフォルダへのパスを入力し、 B2:B10のセルを選択した状態で以下の配列式を入力する。

=IFERROR(TRANSPOSE(toarray(RTD("serartdsvr",,$B$1,"list"))), "")

CTRL+SHIFT+ENTERで配列式を確定する。8

これでリアルタイムでファイル一覧がB2:B10の範囲で表示されるようになる。

次に、C2:C10のセルを選択した状態で以下の配列式を入力する。

=IF(B2:B10 <> "",RTD("serartdsvr",,B2:B10,"size"),"")

CTRL+SHIFT+ENTERで式を確定するとC2:C10の範囲でB2:C10のファイル名に対するファイルサイズがリアルタイムで表示されるようになる。

自作のToArray関数にDebug.Printするような診断メソッドを仕込むと、更新のたびに呼び出されていることが分かると思う。

ちなみに、ファイルの数が増えたら配列セルの範囲を超えた分は無視される。自動で拡張されたりしないので配列セルを手直ししなければならない。めんどくさい。

トレースログの確認

SystemInternalsのDebug View fow Windowsを入れてExcelの実行中のデバッグプリントをキャプチャすると、RTDのメソッドの呼ばれる順序が分かったりして面白い。

[12020] ☆.ctor: @378734A thread=1(STA) 
[12020] ☆ServerStart: @378734A thread=1(STA) 
[12020] synchronizationContext=System.Windows.Forms.WindowsFormsSynchronizationContext 
[12020] ☆ConnectData: @378734A thread=1(STA) 
[12020] ○ topicId=0 fname=c:\temp\testdata\sample2.txt, format=size 
[12020] ◇ modified old=, new=4 
[12020] ☆ConnectData: @378734A thread=1(STA) 
[12020] ○ topicId=1 fname=c:\temp\testdata\sample1.txt, format=size 
[12020] ◇ modified old=, new=14 
[12020] ☆ConnectData: @378734A thread=1(STA) 
[12020] ○ topicId=2 fname=c:\temp\testdata, format=list 
[12020] ◇ modified old=, new=c:\temp\testdata\sample1.txt c:\temp\testdata\sample2.txt 
[12020] ☆ConnectData: @378734A thread=1(STA) 
[12020] ○ topicId=3 fname=, format=size 
[12020] ◇ modified old=, new=パスの形式が無効です。 
[12020] ☆Timeout: @378734A thread=4(MTA) 
[12020] ☆Timeout: @378734A thread=4(MTA) 
[12020] ☆Timeout: @378734A thread=4(MTA) 
[12020] ☆Timeout: @378734A thread=4(MTA) 
[12020] ◇ modified old=14, new=5 
[12020] ☆UpdateNotify: @378734A thread=1(STA) 
[12020] ☆RefreshData: @378734A thread=1(STA) 
[12020] * topidId=1, value=5 
[12020] ☆Timeout: @378734A thread=4(MTA) 
[12020] ☆Timeout: @378734A thread=4(MTA) 
[12020] ☆Timeout: @378734A thread=4(MTA) 
[12020] ☆Timeout: @378734A thread=4(MTA) 
[12020] ☆Timeout: @378734A thread=4(MTA) 
[12020] ☆ServerTerminate: @378734A thread=1(STA) 
[12020] ★★end★★ 
[12020] ☆Finalize: @378734A thread=2(MTA) 

ちなみに、このログをみて気がついたが、topicId=3fnameが空のセルに対してRTDを問い合わせていた。C列でIF関数をつかっているのが原因だろうと思う。ワークシート上のIFは「関数」なので、関数の呼び出し前に、関数の引数である、真・偽のいずれの場合の計算式も事前に評価せざるを得ない為だろう。

ソースの説明

以下、メソッドごとに説明する。

SeraphyRTDServerクラスのヘッダ部

SeraphyRTDServerクラスが、IRtdServerを実装している、本サンプルの本体クラスである。

COMオブジェクトになるため、CLSIDとProgIDとして

[Guid("5B6AA03F-F280-4B06-AFA3-519C9215D6BD")]
[ProgId("SeraRtdSvr")]

のように指定している。

CLSIDは全世界で一意になるべきものなので、本サンプルを真似して別のクラスを作る場合には、Guidは、それぞれ生成し直す必要がある。

ProgIdはCOMオブジェクトの一般的な形式に従うと「ベンダ名.プログラム名.バージョン番号」のようなドット区切りで長い名前になるのが普通だが、今回は、あえて短くしている。

これはRTD関数で指定する名前なので、ProgIDが長いと数式がめんどくさくなるからである。(短すぎてシステムの予約語や一般語と衝突すると、いろいろ問題がでるので注意のこと。)

ServerStartメソッド: 開始処理

ExcelからRTDサーバーの利用が開始されると、最初に呼び出される。

複数ブックに同一RTDサーバへの接続がある場合は同じインスタンスが使われ、同じインスタンスでServerStartが二度呼び出されることはない。

ここでExcel側からIRTDUpdateEventインターフェイスを実装したコールバック用オブジェクトが渡される。

RTDサーバーは、このコールバック用オブジェクトを保存しておいて、監視している値のいずれかが変更された場合には、IRTDUpdateEvent#UpdateNotify()メソッドを呼び出してExcelに変更を通知する必要がある。

ただし、変更有無チェックをUIスレッドで常時実行するとExcelがカクカクして操作性が悪化するため、監視処理はUIスレッドとは別のスレッドで行う必要がある。

このサンプルではRTDサーバの開始時に無限ループタスクを立ち上げて、1000 mSec単位で保持しているTopicIdの値に変更があるかチェックし、もし変更があればUpdateNotifyを呼び出すようにしている。

コールバックで注意しなければならないのは、このIRTDUpdateEventオブジェクトはSTA(Single-Threaded Apartments)であり、ServerStartが呼ばれたときのスレッドと同じスレッドから呼び出されなければならない、ということである。

C++ではCoMarshalInterThreadInterfaceInStream関数などを用いてスレッドをまたいだ場合のマーシャリングを行うが、C#の場合では、SynchronizationContextを使うことになるようである。

    SynchronizationContext synchronizationContext = SynchronizationContext.Current; // 現在の同期コンテキストを保存

    // ... 別スレッドで処理開始 ...

    // 保存した同期コンテキストで実行
    synchronizationContext.Post(x => {
        callback?.UpdateNotify();
    }, null);

ただし、実際にはExcelからRTDサーバーが呼び出された時点ではSynchronizationContext#Currentは初期化されておらず、かわりに明示的にWindowsFormのSystem.Windows.Forms.WindowsFormsSynchronizationContextを生成して使うことになる。(参照設定で、System.Windows.Formsが必要となる。)

これはWindowsFormのUIスレッドにマーシャリングすることを意味しているが、RTDのServerStartメソッドもUIスレッドから呼び出されているので、結果的に同じになるので大丈夫、ということのようである。

(たしかに、トレースログに出ているThreadIdを見ても、ServerStartメソッドのIDと、UpdateNotify時のIDが同一になっている。)

なお、UI用タイマーであるSystem.Windows.Forms.Timerを使うことで、UIスレッドからタイマー処理を実行できるため、明示的にSynchronizationContextを使わずとも良く、実装も単純になるが、前述のとおり、UIスレッド内で重い処理は行うべきではない。

ConnectDataメソッド: Topicの追加

RTD関数に渡された引数の内容がTopicIdとともに渡されてくる。

TopicIdは、Excelが割り当てた番号である。

引数ごとに1つのIDが割り当てられるので、たとえば複数のセルに同一の引数をもつRTD式がある場合でも、TopicIdは1つだけである。(なので呼び出しも1回である)

関数に渡される引数は、1つ以上、最大253個までの配列として渡されてくる。

分かりにくいのはnewValues引数で、これはIn/Out引数である。

引数として渡されてくるとき、この値がTrueである場合、新規にRTDセルが入力されたことを表している。これに対してFalseである場合はRTDセルをもつブックが開かれた場合など、すでにRTDセルが存在しており更新するために呼び出されたことを意味している。

また、このnewValues引数は戻り値としても使われ、Trueを返した場合は、このメソッドが返す値でセルの内容を更新することを意味する。Falseの場合は更新しない。

(現在のセルの内容が何であるかは分からないので通常は常にTrueを返せばよいとおもわれるが、たとえば、すぐには最新情報が得られないケースなど、TopicId登録時はとりあえずFalseを返して、あとでRefreshDataで一括更新する、という実装もありえるだろう。)

DisconnectDataメソッド: Topicの削除

ExcelがRTDセルを消して、同一の引数をもつRTDセルが1つもなくなった場合に呼び出される。

もしくは、ブックを閉じて監視する必要がなくなった場合にも通知される。

以後、このTopicIdは未使用になる。

RefreshDataメソッド: Topicの状態の一括更新

Excel側からTopicの状態を最新化するために呼び出される。

これは一般的にはIRTDUpdateEvent#UpdateNotifyによってExcelに変更通知を行ったあと、Excelから呼び出されるものである。

TopicIdごとの値を格納した二次元配列と、その配列の個数を返す。

なお、すべてのTopicを返す必要はなく、更新の必要のあるTopicIdと、そのデータだけ返せば良い。

Heartbeatメソッド: 生存確認

Excel側からRTDサーバーの生存確認のために定期的に呼び出される。

正の値を返すことで生きていることを応答する。

インプロセスのCOMサーバーの場合には、DLLが死んだときはExcelプロセスも死んだ時なので意味はないが、リモートPC上のDCOMや、EXEのCOMサーバーの場合には意味があると思われる。

とりあえず、いつでも1を返しておけば良い。

ちなみに0を返したりすると、

f:id:seraphy:20180210095400p:plain
RTDサーバーの再起動の確認ダイアログ

のようにExcelからお問い合わせが表示される。

ServerTerminateメソッド: 終了処理

すべてのブックから、このRTDサーバーの持つ全てのTopicIDが削除されるか、すべてのブックが閉じられて、TopicIDが1つも必要としなくなったときに呼び出される。

本サンプルでは無限ループタスクに対してキャンセル指示を出してタスクの終了を行っている。

Finalize: ガベージ処理

COMとしてはServerTerminateメソッドを呼ばずにReleaseすることもありえるので、念のため、ServerTerminateメソッドと同じDispose処理を入れている。

その他

IRTDUpdateEvent.Disconnectについて

UpdateNotifyで更新を通知するかわりに、Disconnectを通知すると、Excel側からServerTerminateが呼び出されてRTDとの接続が終了する。

この場合、シート上のRTDセルは、RTDが接続できなかった場合と同様、エラー「#N/A」になる。

これ以上、リアルタイムデータを取得しつづけることができない、なんらかの事由がある場合に使ったりするのだろう。

例外処理について

DotNETのオブジェクトをCOMとして公開した場合、例外をスローすると受け取り側のCOMクライアントにはHRESULTのエラーコードに変換されて渡される。

https://blogs.msdn.microsoft.com/yizhang/2010/12/17/interpreting-hresults-returned-from-netclr-0x8013xxxx/

たとえば、ApplicationException例外をスローすると、COMの受け取り側のHRESULTはCOR_E_APPLICATION(0x80131600)になる。

ただし、実験では、RTDサーバー内で例外が発生してExcelとの連携が旨くゆかなくなった場合には、Excelも(しばらくしてから)異常終了することが多かったので、エラー処理はきちんとやっておいたほうが良いようだ。

ErrorWrapperの返却について

COMのVariantではVT_ERRORとしてSCODE(HRESULT)を送ることができるが、DotNETではSystem.Runtime.InteropServices.ErrorWrapperを使うことでVT_ERRORを表現できる。

Default Marshaling for Objects | Microsoft Docs

そこで、エラーになった場合に

    catch (Exception ex)
    {
        value = new ErrorWrapper(ex);
    }

のようにしてエラー値を返却してみたところ、Excel側ではRTDからエラー値を受け取ると、セルの値を更新せず無視するようである。(#N/Aにもならない。そもそも更新されない。)

とりあえずC#からVT_ERRORを返す仕組みはわかったので、この線でRTDまわりをGoogle検索したところ、興味深い投稿を見つけた。

https://groups.google.com/forum/#!msg/exceldna/Z6mmxJ4LSbM/ryiWJDjhu90J

どうやら、特定の値を返せば、Excelのセル上でエラーを表現できるらしい。

エラー種別 エラー値
ErrNA -2146826246
ErrName -2146826259
ErrNull -2146826288
ErrNum -2146826252
ErrRef -2146826265
ErrValue -2146826273
    value = new ErrorWrapper(-2146826246); // 「#N/A」と表示される

うまくいった。

まとめ

今回試作してみた感触では、C#ExcelのRTDサーバーを作るのは、とくに難しいところもなかった。 C++よりも容易に作れそうな感じである。

しかし、Excelでリアルタイムデータを引くことの意義は、それなりの有益なデータソースがあってこそ、と思うのだが、最近はラズパイやアルデイーノといったDIY感覚でネットワーク接続できるセンサー類が手に入りやすくなってきたので、むしろ、そっち方面での技術力なり発想力が求められているかもしれない。

参考文献/URL

kennykerrさんのところのブログ

RTDに関する詳細が揃っている。超オススメ

マイクロソフト公式

MSの記事、VB6を使うけれどRTDの作り方の基本が分かる。

以上、メモ終了。


  1. アプリケーション間のデータ交換としてDDEを使うことはなくなったが、Explorerがアプリを起動する際に、すでにアプリが起動している場合は新たにEXEを立ち上げるのではなく既存のEXEに対してドキュメントを開くように依頼する、というようなケースで現在でも使われている。

  2. どうしてもDotNETとDDEの両方を使いたいのであれば、C++/CLIを使えばどうとでもなると思う。あるいは、サードパーティのライブラリとしてはNDdeというのもあるようだ。

  3. リモートでCOMを扱うDCOMに対して、リモートでDDEを扱うNetDDEという仕様もあったが、NetDDEはVista以降は廃止されている。

  4. 今回はIRTDServer, IRTDUpdateEventインターフェイスは、ComImportとして型(vtbl)を合わせているだけであり、Excel側も、IRTDServerにはIDispatchは使っていないか、ちゃんと名前で索引しているようでDISPIDの値はどうでもいいらしいのだが、生成されたSeraphyRTDServerに対してIDispatch#GetIDsOfNamesで名前からDISPIDを取り出したときに、本来の定義されたDISPIDと異なる値が返ってくるのは気持ち悪いので一応、正しいものを返せるようにしておく。

  5. たとえば、"C:\Windows\Microsoft.NET\Framework\v4.0.30319"のような場所にある。

  6. ただしタイプライブラリは除く。もし必要なら、タイプライプラリ用のレジストリエントリは出力されないので手動で作成する必要がある。

  7. RTDサーバから直接、配列を返したかったのだが、方法がみつからなかった。

  8. 確定すると式の見た目は{}で囲まれ、配列式であることを示す。なお、配列式にした範囲では一括して編集・削除しなければならない。配列式の範囲が分からない場合はCTRL+Gで「ジャンプ」ダイアログを開き、「セル選択」ボタンから「アクティブセルの配列」を選ぶと、配列式の入ったセル範囲をまるごと選択してくれる。

Javaのジェネリック型引数をリフレクションによって取得する方法

Javaジェネリック型引数をリフレクションによって取得する方法

概要

Javaは、Java1.5からジェネリクス(総称型)が利用可能になったが、1.4以前のコードとの互換性のためにイレージャ(Erasure)とよばれる手法をとっている。

簡単にいうと、イレージャとは、コンパイラによってジェネリクスの型チェックが行われたあと、コンパイル結果であるJavaバイトコードからは、そのジェネリクス型の型情報を消してしまう、という手法である。

しかし、消されているのはバイトコードだけであり、クラスファイル中に含まれるクラスやインターフェイスの型情報、フィールドの型情報、メソッドの引数や戻り値の型情報などにはジェネリック型の情報が残されている。

これらのジェネリック型引数の情報はリフレクションによってアクセスすることができる。

※ 本ソースコード一式はgist:c8f088fc081054116266011ae56d4bd7に置いてあります。

イレージャは何を消し、何を残しているのか?

Javaジェネリクスコンパイル時のみ型チェックされイレージャによってバイトコードからは型情報が消去される。

したがって、

package jp.seraphyware.example;

import java.util.ArrayList;
import java.util.List;

class Foo<E> {
    private List<E> values = new ArrayList<>();

    public void add(E value) {
        values.add(value);
    }

    @Override
    public String toString() {
        return values.toString();
    }
}

public class GenericErasureExample {

    public static void main(String... args) {
        Foo<String> foo1 = new Foo<>();
        Foo<Integer> foo2 = new Foo<>();
        Foo fooRaw = new Foo();

        foo2.add(1234);
        ((Foo<String>)((Foo)foo2)).add("abcd"); // 大丈夫!?!?
        System.out.println(foo2); // [1234, abcd]と表示する
    }
}

というコードにおいて、foo1, foo2, fooRawの3つのオブジェクトは、いずれも、同じFooクラスからインスタンスが作成されることになる。

コンパイル時しかジェネリックの型チェックは行われていないので、このような強引なコードを書いてもエラーにはならず、実行すれば動作してしまう。

javapでバイトコードを逆アセンブルしてみると、

Classfile /C:/work/workspace/Java8Learn/bin/jp/seraphyware/example/GenericErasureExample.class
  Last modified 2018/01/31; size 1028 bytes
  MD5 checksum 39d5b60dcd560b681380dc774a1af52d
  Compiled from "GenericErasureExample.java"
public class jp.seraphyware.example.GenericErasureExample
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // jp/seraphyware/example/GenericErasureExample
   #2 = Utf8               jp/seraphyware/example/GenericErasureExample
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Ljp/seraphyware/example/GenericErasureExample;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Class              #17            // jp/seraphyware/example/Foo
  #17 = Utf8               jp/seraphyware/example/Foo
  #18 = Methodref          #16.#9         // jp/seraphyware/example/Foo."<init>":()V
  #19 = Methodref          #20.#22        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  #20 = Class              #21            // java/lang/Integer
  #21 = Utf8               java/lang/Integer
  #22 = NameAndType        #23:#24        // valueOf:(I)Ljava/lang/Integer;
  #23 = Utf8               valueOf
  #24 = Utf8               (I)Ljava/lang/Integer;
  #25 = Methodref          #16.#26        // jp/seraphyware/example/Foo.add:(Ljava/lang/Object;)V
  #26 = NameAndType        #27:#28        // add:(Ljava/lang/Object;)V
  #27 = Utf8               add
  #28 = Utf8               (Ljava/lang/Object;)V
  #29 = String             #30            // abcd
  #30 = Utf8               abcd
  #31 = Fieldref           #32.#34        // java/lang/System.out:Ljava/io/PrintStream;
  #32 = Class              #33            // java/lang/System
  #33 = Utf8               java/lang/System
  #34 = NameAndType        #35:#36        // out:Ljava/io/PrintStream;
  #35 = Utf8               out
  #36 = Utf8               Ljava/io/PrintStream;
  #37 = Methodref          #38.#40        // java/io/PrintStream.println:(Ljava/lang/Object;)V
  #38 = Class              #39            // java/io/PrintStream
  #39 = Utf8               java/io/PrintStream
  #40 = NameAndType        #41:#28        // println:(Ljava/lang/Object;)V
  #41 = Utf8               println
  #42 = Utf8               args
  #43 = Utf8               [Ljava/lang/String;
  #44 = Utf8               list1
  #45 = Utf8               Ljp/seraphyware/example/Foo;
  #46 = Utf8               list2
  #47 = Utf8               listRaw
  #48 = Utf8               LocalVariableTypeTable
  #49 = Utf8               Ljp/seraphyware/example/Foo<Ljava/lang/String;>;
  #50 = Utf8               Ljp/seraphyware/example/Foo<Ljava/lang/Integer;>;
  #51 = Utf8               SourceFile
  #52 = Utf8               GenericErasureExample.java
{
  public jp.seraphyware.example.GenericErasureExample();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 19: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ljp/seraphyware/example/GenericErasureExample;

  public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=2, locals=4, args_size=1
         0: new           #16                 // class jp/seraphyware/example/Foo
         3: dup
         4: invokespecial #18                 // Method jp/seraphyware/example/Foo."<init>":()V
         7: astore_1
         8: new           #16                 // class jp/seraphyware/example/Foo
        11: dup
        12: invokespecial #18                 // Method jp/seraphyware/example/Foo."<init>":()V
        15: astore_2
        16: new           #16                 // class jp/seraphyware/example/Foo
        19: dup
        20: invokespecial #18                 // Method jp/seraphyware/example/Foo."<init>":()V
        23: astore_3
        24: aload_2
        25: sipush        1234
        28: invokestatic  #19                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        31: invokevirtual #25                 // Method jp/seraphyware/example/Foo.add:(Ljava/lang/Object;)V
        34: aload_2
        35: ldc           #29                 // String abcd
        37: invokevirtual #25                 // Method jp/seraphyware/example/Foo.add:(Ljava/lang/Object;)V
        40: getstatic     #31                 // Field java/lang/System.out:Ljava/io/PrintStream;
        43: aload_2
        44: invokevirtual #37                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        47: return
      LineNumberTable:
        line 22: 0
        line 23: 8
        line 24: 16
        line 26: 24
        line 27: 34
        line 28: 40
        line 29: 47
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      48     0  args   [Ljava/lang/String;
            8      40     1 list1   Ljp/seraphyware/example/Foo;
           16      32     2 list2   Ljp/seraphyware/example/Foo;
           24      24     3 listRaw   Ljp/seraphyware/example/Foo;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      40     1 list1   Ljp/seraphyware/example/Foo<Ljava/lang/String;>;
           16      32     2 list2   Ljp/seraphyware/example/Foo<Ljava/lang/Integer;>;
}
SourceFile: "GenericErasureExample.java"

mainメソッドの中身をみると、(Java1.4以前のように)単純にFooクラスのnewを呼び出して、1234というint値をInteger#valueOfで変換したものと、定数プールから取り出した文字列オブジェクト「abcd」を、addで追加している様子がわかる。

ここではジェネリックの型引数はどこにも考慮されていない。

ところが、LocalVariableTypeTableセクション1をみると、ジェネリック引数が記録されているのが分かる。

つまり、クラスファイル中からジェネリック型引数が全て消去されているわけではない。

あくまでもバイトコードから消去されているだけである。

Fooクラスのクラスファイルもjavapで見てみる。

Classfile /C:/work/workspace/Java8Learn/bin/jp/seraphyware/example/Foo.class
  Last modified 2018/01/31; size 939 bytes
  MD5 checksum 38ecd671af23752df7f1ae96faab521d
  Compiled from "GenericErasureExample.java"
class jp.seraphyware.example.Foo<E extends java.lang.Object> extends java.lang.Object
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Class              #2             // jp/seraphyware/example/Foo
   #2 = Utf8               jp/seraphyware/example/Foo
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               values
   #6 = Utf8               Ljava/util/List;
   #7 = Utf8               Signature
   #8 = Utf8               Ljava/util/List<TE;>;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Methodref          #3.#13         // java/lang/Object."<init>":()V
  #13 = NameAndType        #9:#10         // "<init>":()V
  #14 = Class              #15            // java/util/ArrayList
  #15 = Utf8               java/util/ArrayList
  #16 = Methodref          #14.#13        // java/util/ArrayList."<init>":()V
  #17 = Fieldref           #1.#18         // jp/seraphyware/example/Foo.values:Ljava/util/List;
  #18 = NameAndType        #5:#6          // values:Ljava/util/List;
  #19 = Utf8               LineNumberTable
  #20 = Utf8               LocalVariableTable
  #21 = Utf8               this
  #22 = Utf8               Ljp/seraphyware/example/Foo;
  #23 = Utf8               LocalVariableTypeTable
  #24 = Utf8               Ljp/seraphyware/example/Foo<TE;>;
  #25 = Utf8               add
  #26 = Utf8               (Ljava/lang/Object;)V
  #27 = Utf8               (TE;)V
  #28 = InterfaceMethodref #29.#31        // java/util/List.add:(Ljava/lang/Object;)Z
  #29 = Class              #30            // java/util/List
  #30 = Utf8               java/util/List
  #31 = NameAndType        #25:#32        // add:(Ljava/lang/Object;)Z
  #32 = Utf8               (Ljava/lang/Object;)Z
  #33 = Utf8               value
  #34 = Utf8               Ljava/lang/Object;
  #35 = Utf8               TE;
  #36 = Utf8               toString
  #37 = Utf8               ()Ljava/lang/String;
  #38 = Methodref          #3.#39         // java/lang/Object.toString:()Ljava/lang/String;
  #39 = NameAndType        #36:#37        // toString:()Ljava/lang/String;
  #40 = Utf8               SourceFile
  #41 = Utf8               GenericErasureExample.java
  #42 = Utf8               <E:Ljava/lang/Object;>Ljava/lang/Object;
{
  private java.util.List<E> values;
    descriptor: Ljava/util/List;
    flags: ACC_PRIVATE
    Signature: #8                           // Ljava/util/List<TE;>;

  jp.seraphyware.example.Foo();
    descriptor: ()V
    flags:
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #12                 // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #14                 // class java/util/ArrayList
         8: dup
         9: invokespecial #16                 // Method java/util/ArrayList."<init>":()V
        12: putfield      #17                 // Field values:Ljava/util/List;
        15: return
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 6: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   Ljp/seraphyware/example/Foo;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   Ljp/seraphyware/example/Foo<TE;>;

  public void add(E);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Signature: #27                          // (TE;)V
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: getfield      #17                 // Field values:Ljava/util/List;
         4: aload_1
         5: invokeinterface #28,  2           // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        10: pop
        11: return
      LineNumberTable:
        line 10: 0
        line 11: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Ljp/seraphyware/example/Foo;
            0      12     1 value   Ljava/lang/Object;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Ljp/seraphyware/example/Foo<TE;>;
            0      12     1 value   TE;

  public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #17                 // Field values:Ljava/util/List;
         4: invokevirtual #38                 // Method java/lang/Object.toString:()Ljava/lang/String;
         7: areturn
      LineNumberTable:
        line 15: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Ljp/seraphyware/example/Foo;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Ljp/seraphyware/example/Foo<TE;>;
}
SourceFile: "GenericErasureExample.java"
Signature: #42                          // <E:Ljava/lang/Object;>Ljava/lang/Object;

ここで注目すべき点は、

  • class jp.seraphyware.example.Foo<E extends java.lang.Object> extends java.lang.Object
  • private java.util.List<E> values;
  • public void add(E);

のように、クラス定義、フィールド定義、メソッド定義の、それぞれのジェネリック型引数は、ちゃんと残っている。

クラスまたはインターフェイスといったもののように「型情報」としてクラスファイルに記録される場合にはジェネリックの型引数は保持されるのである。

Type型によるジェネリック型引数へのアクセス

型情報にジェネリックの型引数が記録されているが、これはリフレクションによってアクセス可能になっている。

Java1.5から、その目的のための「Type」というインターフェイスが追加されている。

Typeインターフェイスは以下の5つのいずれかの種類になる

このType情報は、ClassMethod, Field のリフレクション使用時に、ジェネリック対応情報用のメソッドとして呼び出すことで取得できるようになっている。(旧来の呼び方をすると、ジェネリック情報の無い型情報になる。)

たとえば、toStringにしても、toGenericStringというジェネリック対応メソッドが用意されている。

    System.out.println(Foo.class.toString()); // 従来のクラスの表示
    System.out.println(Foo.class.toGenericString()); // ジェネリック型を含むクラスの表示

結果は、

class jp.seraphyware.example.Foo
class jp.seraphyware.example.Foo<E>

のようになる。

準備

以下、ジェネリック型と、それを使うメソッドから、ジェネリックの型引数情報を取得するコードを示す

実験につかうジェネリックの定義

/**
 * ジェネリック型の定義
 * @param <E>
 */
interface GenericIntf<E extends Date> {
    void set(E value);
    E get();
}

/**
 * ジェネリック型を継承した具象クラスの定義
 */
class ConcreteCls implements GenericIntf<Timestamp> {

    private Timestamp value;

    @Override
    public void set(Timestamp v) {
        this.value = v;
    }

    @Override
    public Timestamp get() {
        return value;
    }
}

/**
 * ジェネリック型を継承したジェネリッククラスの定義
 */
class GenericCls<E extends Date> implements GenericIntf<E> {

    private E value;

    @Override
    public void set(E v) {
        this.value = v;
    }

    @Override
    public E get() {
        return value;
    }
}

ジェネリック型引数を解析・表示するコード

   /**
    * 型情報を表示する
    * 型情報がさらにネストした型情報をもっている場合は再帰的に掘り進む.
    * @param typ
    */
    private static void showType(Type typ) {
        showType(typ, "");
    }

    /**
    * 型情報を表示する.
    * 型情報がさらにネストした型情報をもっている場合は再帰的に掘り進む.
    * @param typ
    * @param prefix 印字する文字列のまえに付与する文字列
    */
    private static void showType(Type typ, String prefix) {
        System.out.print(prefix + "typ=" + typ);
        String typeName;
        if (typ instanceof Class) {
            typeName = "Class"; // クラス
        } else if (typ instanceof TypeVariable) {
            typeName = "TypeVariable"; // 型変数型
        } else if (typ instanceof ParameterizedType) {
            typeName = "ParameterizedType"; // ジェネリック型
        } else if (typ instanceof GenericArrayType) {
            typeName = "GenericArrayType"; // ジェネリック配列型
        } else if (typ instanceof WildcardType) {
            typeName = "WildcardType"; // ワイルドカード型
        } else {
            // 不明? (java9時点では、上記5種のみのはず)
            typeName = typ.getClass().getName();
        }
        System.out.println(" -- " + typeName);

        if (typ instanceof ParameterizedType) {
            // ジェネリック型の場合は、その型引数を取得する
            Type[] genericArgs = ((ParameterizedType) typ).getActualTypeArguments(); // 実際の型引数の取得
            for (int idx = 0; idx < genericArgs.length; idx++) {
                showType(genericArgs[idx], prefix + "(" + (idx + 1) + ") ");
            }

        } else if (typ instanceof GenericArrayType) {
            // ジェネリック配列の場合は、その要素型を取得する.
            Type compType = ((GenericArrayType) typ).getGenericComponentType(); // 要素型の取得
            showType(compType, prefix + "(elm) ");

        } else if (typ instanceof TypeVariable) {
            // 型変数型の場合は型の制約(範囲)を取得する
            Type[] bounds = ((TypeVariable<?>) typ).getBounds(); // 範囲の取得
            for (int idx = 0; idx < bounds.length; idx++) {
                showType(bounds[idx], prefix + "(bound#" + (idx + 1) + ") ");
            }

        } else if (typ instanceof WildcardType) {
            // ワイルドカード型の場合は型の制約(上限・下限)を取得する.
            Type[] ups = ((WildcardType) typ).getUpperBounds(); // 範囲(上限)の取得
            for (int idx = 0; idx < ups.length; idx++) {
                showType(ups[idx], prefix + "(upper#" + (idx + 1) + ") ");
            }
            Type[] lows = ((WildcardType) typ).getLowerBounds(); // 範囲(下限)の取得
            for (int idx = 0; idx < lows.length; idx++) {
                showType(lows[idx], prefix + "(lower#" + (idx + 1) + ") ");
            }
        }
    }

クラスのインスタンス化の実験

以下、ジェネリックの型生成方法ごとの、その型情報の取得結果を示す。

ジェネリック派生型をメソッド内で利用する場合

   @Comment("ジェネリック派生型をメソッド内で利用する例1")
    private static void testGenericUse1() {
        System.out.println("☆" + new ThisMethod() {});
        ConcreteCls obj = new ConcreteCls();
        System.out.println("target=" + obj.getClass().toGenericString());
        Type typ = obj.getClass().getGenericInterfaces()[0]; // 実装しているインターフェイスを取得
        showType(typ);
        System.out.println();
    }

結果

☆testGenericUse1(ジェネリック派生型をメソッド内で利用する例1)
target=class jp.seraphyware.example.ConcreteCls
typ=jp.seraphyware.example.GenericIntf<java.sql.Timestamp> -- ParameterizedType
(1) typ=class java.sql.Timestamp -- Class

この例では、ジェネリックインターフェイスGenericIntfを実装した具象クラスConcreteClsのクラス情報から、クラスが実装しているインターフェイス情報のジェネリック版(getGenericInterfaces)でインターフェイスの型を取得している。(1つしかインターフェイスを実装していないので添え字は0で決めうちしている)

ConcreteClsでは、ジェネリックの型引数としてjava.sql.Timestampを指定しており、リフレクションによっても、指定されているのがjava.sql.Timestamp型であることが示されている。

ジェネリック派生型をメソッド内で利用する場合2

   @Comment("ジェネリック派生型をメソッド内で利用する例2")
    private static void testGenericUse2() {
        System.out.println("☆" + new ThisMethod() {});
        GenericCls<java.sql.Date> obj = new GenericCls<>();
        System.out.println("target=" + obj.getClass().toGenericString());
        Type typ = obj.getClass().getGenericInterfaces()[0]; // 実装しているインターフェイスを取得
        showType(typ);
        System.out.println();
    }

結果

☆testGenericUse2(ジェネリック派生型をメソッド内で利用する例2)
target=class jp.seraphyware.example.GenericCls<E>
typ=jp.seraphyware.example.GenericIntf<E> -- ParameterizedType
(1) typ=E -- TypeVariable
(1) (bound#1) typ=class java.util.Date -- Class

この例ではジェネリックの型引数として、型変数を指定したままとなっており、それをメソッド中でjava.sql.Date型として利用している。

リフレクションによって取得できるのはクラスファイルに記録されている型情報であるので、この場合は、java.sql.Date型ではなく、GenericClsのクラス定義で使われた「E」という型変数名として取れている。

また、型変数Eは「E extends Date」という範囲を指定しているため、bounds情報として「java.util.Date」が明らかにされている。

ジェネリック型を匿名クラスとしてメソッド内で利用する例1

   @Comment("ジェネリック型を匿名クラスとしてメソッド内で利用する例1")
    private static void testGenericAnonymous1() {
        System.out.println("☆" + new ThisMethod() {});
        GenericIntf<Date> obj = new GenericIntf<Date>() { // 匿名型の生成
            @Override
            public void set(Date v) {}

            @Override
            public Date get() {
                return null;
            }
        };
        System.out.println("target=" + obj.getClass().toGenericString());
        Type typ = obj.getClass().getGenericInterfaces()[0]; // 実装しているインターフェイスを取得
        showType(typ);
        System.out.println();
    }

結果

☆testGenericUse0(ジェネリック型を匿名クラスとしてメソッド内で利用する例1)
target=class jp.seraphyware.example.GenericReflectionExample$2
typ=jp.seraphyware.example.GenericIntf<java.util.Date> -- ParameterizedType
(1) typ=class java.util.Date -- Class

GenericIntfインターフェイスをメソッド内で匿名クラスとして実体化している。

この場合、クラス定義が作成されることになるため、匿名クラス宣言時に使用した「Date型」が、ジェネリックの型引数として取得できるようになっている。

ジェネリック型を匿名クラスとしてメソッド内で利用する例2

    @Comment("ジェネリック型を匿名クラスとしてメソッド内で利用する例2")
    private static void testGenericAnonymous2() {
        System.out.println("☆" + new ThisMethod() {});
        GenericCls<Date> obj = new GenericCls<Date>() {}; // 匿名クラス化する
        System.out.println("target=" + obj.getClass().toGenericString());
        Type typ = obj.getClass().getGenericSuperclass(); // 匿名クラスの親クラスを取得
        showType(typ);
        System.out.println();
    }

結果

☆testGenericAnonymous2(ジェネリック型を匿名クラスとしてメソッド内で利用する例2)
target=class jp.seraphyware.example.GenericReflectionExample$4
typ=jp.seraphyware.example.GenericCls<java.util.Date> -- ParameterizedType
(1) typ=class java.util.Date -- Class

GenericClsジェネリッククラスをメソッド内で単にnewすると型情報としてはジェネリック引数は残らないが、この例のように、GenericClsジェネリッククラスをメソッド内で匿名クラス化することで、ジェネリック引数つきのクラスとしての型情報を作成することができる。このため、親クラスの型情報としてDate型の型引数を取得できるようになる。

メソッドの引数または戻り値としてのジェネリック型引数の情報の実験例

以下、メソッドの引数または戻り値としてジェネリックの型引数の情報を取得する実験を示す

ジェネリック型を引数にとるメソッドの例1

対象コード

   public static void method1(List<String> args) {
    }

    @Comment("ジェネリック型を引数にとるメソッドの例1")
    private static void testGenericArgument1() throws NoSuchMethodException, SecurityException {
        System.out.println("☆" + new ThisMethod() {});
        Method method = GenericExample.class.getMethod("method1", List.class);
        System.out.println("target=" + method.toGenericString());
        Type typ = method.getGenericParameterTypes()[0]; // 引数の型
        showType(typ);
        System.out.println();
    }

結果

☆testGenericArgument1(ジェネリック型を引数にとるメソッドの例1)
target=public static void jp.seraphyware.example.GenericExample.method1(java.util.List<java.lang.String>)
typ=java.util.List<java.lang.String> -- ParameterizedType
(1) typ=class java.lang.String -- Class

メソッドのジェネリック対応の引数情報はgetGenericParameterTypesで取得することができる。 (ここでは引数は1個なので、添え字0で先頭のTypeを決めうちで取得している)

引数がList<String>であることが明らかとなっている。

ジェネリック型を引数にとるメソッドの例2

対象コード

   public static void method2(List<List<GenericIntf<java.sql.Date>>> args) {
    }

    @Comment("ジェネリック型を引数にとるメソッドの例2")
    private static void testGenericArgument2() throws NoSuchMethodException, SecurityException {
        System.out.println("☆" + new ThisMethod() {});
        Method method = GenericExample.class.getMethod("method2", List.class);
        System.out.println("target=" + method.toGenericString());
        Type typ = method.getGenericParameterTypes()[0]; // 引数の型
        showType(typ);
        System.out.println();
    }

結果

☆testGenericArgument2(ジェネリック型を引数にとるメソッドの例2)
target=public static void jp.seraphyware.example.GenericExample.method2(java.util.List<java.util.List<jp.seraphyware.example.GenericIntf<java.sql.Date>>>)
typ=java.util.List<java.util.List<jp.seraphyware.example.GenericIntf<java.sql.Date>>> -- ParameterizedType
(1) typ=java.util.List<jp.seraphyware.example.GenericIntf<java.sql.Date>> -- ParameterizedType
(1) (1) typ=jp.seraphyware.example.GenericIntf<java.sql.Date> -- ParameterizedType
(1) (1) (1) typ=class java.sql.Date -- Class

リストのリストのジェネリック型のようにネストしたジェネリック型の場合でも、トラバースして末端に至るまでの型情報「List<List<GenericIntf<Date>>>」を特定することができている。

ジェネリック型を戻り値にとるメソッドの例

   public static Map<String, GenericIntf<java.sql.Date>> method3() {
        return Collections.emptyMap();
    }

    @Comment("ジェネリック型を戻り値にとるメソッドの例")
    private static void testGenericReturnType1() throws NoSuchMethodException, SecurityException {
        System.out.println("☆" + new ThisMethod() {});
        Method method = GenericExample.class.getMethod("method3");
        System.out.println("target=" + method.toGenericString());
        Type typ = method.getGenericReturnType(); // 戻り値の型
        showType(typ);
        System.out.println();
    }

結果

☆testGenericReturnType1(ジェネリック型を戻り値にとるメソッドの例)
target=public static java.util.Map<java.lang.String, jp.seraphyware.example.GenericIntf<java.sql.Date>> jp.seraphyware.example.GenericExample.method3()
typ=java.util.Map<java.lang.String, jp.seraphyware.example.GenericIntf<java.sql.Date>> -- ParameterizedType
(1) typ=class java.lang.String -- Class
(2) typ=jp.seraphyware.example.GenericIntf<java.sql.Date> -- ParameterizedType
(2) (1) typ=class java.sql.Date -- Class

ジェネリック対応の戻り値の型情報はgetGenericReturnTypeによって取得することができる。

引数の場合と同様にジェネリック型を特定できている。

この例ではMap型なので「Map<String, GenericIntf<Date>>」という2つの型引数をもっていることが分かる。

ジェネリック型を戻り値にとるメソッドの例2

   @Comment("ジェネリック型を戻り値にとるメソッドの例2")
    private static void testGenericReturnType2() throws NoSuchMethodException, SecurityException {
        System.out.println("☆" + new ThisMethod() {});
        GenericCls<java.sql.Date> obj = new GenericCls<>();
        Method method = obj.getClass().getMethod("get");
        System.out.println("target=" + method.toGenericString());
        Type typ = method.getGenericReturnType(); // 戻り値の型
        showType(typ);
        System.out.println();
    }

結果

☆testGenericReturnType2(ジェネリック型を戻り値にとるメソッドの例2)
target=public E jp.seraphyware.example.GenericCls.get()
typ=E -- TypeVariable
(bound#1) typ=class java.util.Date -- Class

GenericClsクラスの定義のgetメソッドの戻り値は「E extends Date」であるため、 メソッドの型情報として型変数名の「E」と、その範囲である「java.util.Date」が取得されている。

ジェネリック配列を戻り値にとるメソッドの例1

   @SuppressWarnings("unchecked")
    public static GenericIntf<Date>[] getGenericArray() { // メソッド定義の戻り値型の情報としてジェネリック配列型は有効である
        return new GenericIntf[0]; // ただし、ジェネリック配列を直接作成できないのでRAW型配列として作成する.
    }

    @Comment("ジェネリック配列を戻り値にとるメソッドの例1")
    private static void testGenericArray() throws NoSuchMethodException, SecurityException {
        System.out.println("☆" + new ThisMethod() {});
        Method method = GenericExample.class.getMethod("getGenericArray");
        System.out.println("target=" + method.toGenericString());
        Type typ = method.getGenericReturnType(); // 戻り値の型
        showType(typ);
        System.out.println();
    }

結果

☆testGenericArray(ジェネリック配列を戻り値にとるメソッドの例1)
target=public static jp.seraphyware.example.GenericIntf<java.util.Date>[] jp.seraphyware.example.GenericExample.getGenericArray()
typ=jp.seraphyware.example.GenericIntf<java.util.Date>[] -- GenericArrayType
(elm) typ=jp.seraphyware.example.GenericIntf<java.util.Date> -- ParameterizedType
(elm) (1) typ=class java.util.Date -- Class

Javaではジェネリック配列は鬼門であり、たとえば new List<String>[0]のようなジェネリックの配列を直接作成することはできない。3

が、しかし、型情報としては有効であり、そのための専用のタイプであるGenericArrayTypeというTypeも用意されている。

このgetGenericComponentTypeメソッドによって、配列のジェネリック要素型を取得することができる。

ジェネリック配列の配列を戻り値にとるメソッドの例2

   @SuppressWarnings("unchecked")
    public static GenericIntf<Date>[][] getGenericArray2() { // メソッド定義の戻り値型の情報としてジェネリック配列型は有効である
        return new GenericIntf[][]{}; // ただし、ジェネリック配列を直接作成できないのでRAW型配列として作成する.
    }

    @Comment("ジェネリック配列の配列を戻り値にとるメソッドの例2")
    private static void testGenericArray2() throws NoSuchMethodException, SecurityException {
        System.out.println("☆" + new ThisMethod() {});
        Method method = GenericExample.class.getMethod("getGenericArray2");
        System.out.println("target=" + method.toGenericString());
        Type typ = method.getGenericReturnType(); // 戻り値の型
        showType(typ);
        System.out.println();
    }

結果

☆testGenericArray2(ジェネリック配列の配列を戻り値にとるメソッドの例2)
target=public static jp.seraphyware.example.GenericIntf<java.util.Date>[][] jp.seraphyware.example.GenericExample.getGenericArray2()
typ=jp.seraphyware.example.GenericIntf<java.util.Date>[][] -- GenericArrayType
(elm) typ=jp.seraphyware.example.GenericIntf<java.util.Date>[] -- GenericArrayType
(elm) (elm) typ=jp.seraphyware.example.GenericIntf<java.util.Date> -- ParameterizedType
(elm) (elm) (1) typ=class java.util.Date -- Class

ネストしたジェネリック配列は、普通にGenericArrayTypeの中にGenericArrayTypeが入っているだけである。 扱い方は基本的に変わらない。

ジェネリックメソッドの型引数の取得方法

メソッドの引数や戻り値ではなく、ジェネリックメソッド自身のジェネリック型引数の取得例を示す。

   public static <A extends Date & java.io.Serializable, B>
                B genericMethodExample(A arg) {
        return null;
    }

    @Comment("ジェネリックメソッドの例")
    private static void testGenericMethod() throws NoSuchMethodException, SecurityException {
        System.out.println("☆" + new ThisMethod() {});
        Method method = GenericExample.class.getMethod("genericMethodExample", Date.class);
        System.out.println("target=" + method.toGenericString());
        {
            Type typ = method.getGenericParameterTypes()[0]; // 戻り値の型
            showType(typ);
        }
        {
            // メソッドに付与されているパラメータ
            Type[] typs = method.getTypeParameters();
            for (Type typ : typs) {
                System.out.println("*メソッドの型引数: " + typ);
                showType(typ);
            }
        }
        System.out.println();
    }

結果

☆testGenericMethod(ジェネリックメソッドの例)
target=public static <A,B> B jp.seraphyware.example.GenericExample.genericMethodExample(A)
typ=A -- TypeVariable
(bound#1) typ=class java.util.Date -- Class
(bound#2) typ=interface java.io.Serializable -- Class
*メソッドの型引数: A
typ=A -- TypeVariable
(bound#1) typ=class java.util.Date -- Class
(bound#2) typ=interface java.io.Serializable -- Class
*メソッドの型引数: B
typ=B -- TypeVariable
(bound#1) typ=class java.lang.Object -- Class

ジェネリックメソッドの型パラメータは、MethodクラスのgetTypeParametersによって取得することができる。

ワイルドカード型のフィールドの例

   public static final List<? super Number> wildcardFieldSuper = new ArrayList<>();
    public static final List<? extends Number> wildcardFieldExtends = new ArrayList<>();

    @Comment("ワイルドカード型のフィールドの例")
    private static void testWildcard() throws SecurityException, NoSuchFieldException {
        System.out.println("☆" + new ThisMethod() {});
        for (String name : new String[]{"wildcardFieldSuper", "wildcardFieldExtends"}) {
            Field field = GenericExample.class.getField(name);
            System.out.println("target=" + field.toGenericString());
            Type typ = field.getGenericType();
            showType(typ);
        }
        System.out.println();
    }

結果

☆testWildcard(ワイルドカード型のフィールドの例)
target=public static final java.util.List<? super java.lang.Number> jp.seraphyware.example.GenericExample.wildcardFieldSuper
typ=java.util.List<? super java.lang.Number> -- ParameterizedType
(1) typ=? super java.lang.Number -- WildcardType
(1) (upper#1) typ=class java.lang.Object -- Class
(1) (lower#1) typ=class java.lang.Number -- Class
target=public static final java.util.List<? extends java.lang.Number> jp.seraphyware.example.GenericExample.wildcardFieldExtends
typ=java.util.List<? extends java.lang.Number> -- ParameterizedType
(1) typ=? extends java.lang.Number -- WildcardType
(1) (upper#1) typ=class java.lang.Number -- Class

ワイルドカード型の場合は範囲として上限・下限を取得することができる。 上限、または下限はワイルドカードの指定がsuper, extendsのいずれかによって、いずれか一方は暗黙でObjectになる。(なので、superなのかextendsなのかを判定することができる。)

まとめ

Javaジェネリックはイレージャによってバイトコード上からは型引数は消されているが、クラスまたはインターフェイス、および、そのメソッド、引数、戻り値の型情報としては型引数は保持されているため、クラスまたはインターフェイスとして定義されていればジェネリックの型引数を取得することも可能である。

ジェネリックの型引数にアクセスしたいケースでは、匿名クラス化するなどしてジェネリック型引数を含むクラス型を作成するなどの工夫があれば便利にアクセスできるようになる。


おまけ

実験コード中にある

System.out.println("☆" + new ThisMethod() {});

のようなコードは、具体的には、以下のクラスを定義している。

/**
 * この抽象クラスをメソッド内で匿名クラス化した場合、
 * その匿名クラス化したメソッドへの情報にアクセスできるようにするためのヘルパ.
 */
abstract class ThisMethod {

    /**
    * この抽象クラスを匿名クラス化したメソッドを取得する.
    * @return メソッド
    */
    public Method getMethod() {
        return getClass().getEnclosingMethod();
    }

    /**
    * この抽象クラスを匿名クラス化したメソッドがCommentアノテーションをもっている場合、そのコメントを取得する.
    * なければnull
    * @return
    */
    public String getComment() {
        Comment comment = getMethod().getAnnotation(Comment.class);
        return comment != null ? comment.value() : null;
    }

    /**
    * この抽象クラスを匿名クラス化したメソッドのメソッド名とコメントをプリントする.
    */
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append(getMethod().getName());
        String comment = getComment();
        if (comment != null) {
            buf.append("(" + comment + ")");
        }
        return buf.toString();
    }
}

/**
 * メソッドにコメントを付与するアノテーション
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@interface Comment {
    String value();
}

Javaの匿名クラスの奇妙な性質に、匿名クラスを宣言したメソッドを取得する、という変なメソッド getEnclosingMethodが用意されている。

なので、上記のような抽象クラスを定義しておくと、new ThisMethod(){}で、そのメソッドの名前と、そのメソッドに付与されているアノテーション情報をプリントする、といったようなツールを作ることができる。

匿名クラス化は、このほかにも、標準のJavaEEでもAnnotationLiteralといった、匿名クラス化を利用することで型情報を取得する、といったテクニックに活用されている。


  1. なお、LocalVariableTable, LocalVariableTypeTable, は デバッグ情報なので、デバックOffでコンパイルすると出てこない。デバッグ情報に対してリフレクション等でアクセスする標準の手段はないので、コードからの利用価値は(残念ながら)ない。

  2. ClassクラスはJava1.5以降、Typeインターフェイスを実装するように拡張されている。

  3. ジェネリック配列は、RAW型として作成してからジェネリック型にcastすればつくれないことはない。(もちろん、@SuppressWarningsで警告をつぶすなどの汚い処理は必要になるし、むろん安全ではない。)

Excelのリンクが解除できない!!場合の問題箇所の特定方法

Excelのリンクが解除できない!!場合の問題箇所の特定方法

概要

マクロ付きExcel(*.xlsm)を作っていたら、いつのまにか開くたびにリンクエラーが発生して、しかも、それが消せない。という状況に陥った。

いったら、どうしたら消せるのか?

経緯

リンクエラーが発生している場合、ネットではセルのどこかに別bookを参照しているところがあるので、検索ボックスで.xlsを探せ、とか書いてある。

しかし、セルの数式、値いずれも.xlsを含むものは無かった。

excelの非表示オブジェクトで参照しているものがあるかもだから、VBAを使って、図形や名前定義を含めて.xlsを検索して該当アイテムを消すべし」というコード例があったので試してみたが、これでも解決しない。


....


そういえば、*.xlsx, *.xlsmはzip形式になっていて、展開すると中身はXMLだったよなー、ということを思い出した。

zip展開してgrepすれば何か分かるのではないか?

ZIP展開した結果

発見しました!

sheet2.xmlファイル
    ....
    <extLst>
        <ext uri="{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}" 
            xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main">
            <x14:dataValidations disablePrompts="1" count="2" 
                xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main">
                <x14:dataValidation type="list" showInputMessage="1" showErrorMessage="1">
                    <x14:formula1>
                        <xm:f>'C:\Users\seraphy\workbooks\[data201712.xlsm]Master'!#REF!</xm:f>
                    </x14:formula1>
                    <xm:sqref>B184:C184</xm:sqref>
                </x14:dataValidation>
                <x14:dataValidation type="list" showInputMessage="1" showErrorMessage="1">
                    <x14:formula1>
                        <xm:f>Master!$D:$D</xm:f>
                    </x14:formula1>
                    <xm:sqref>B2:C183 B185:C1048576</xm:sqref>
                </x14:dataValidation>
            </x14:dataValidations>
        </ext>
    </extLst>
    ....

(ちなみに、見やすくするためにVisual studio codeのxmltoolでフォーマットした)


uri="{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}"というのは、dataValidationsのことらしく、
まあ、sqrefの値が示すのが列範囲であることをみても、これは「入力規則」の定義であろう。


sqrefをみると、B2:C183、B185:C1048576の範囲で自ブックのMasterシートのD列を参照しているのに、B184:C184の1行分だけ、他ブックをみているようだ。(しかもエラーになってる)


ここに他ブックから入力規則ごとコピーしてきてしまったデータがあるようだ。


場所が特定できれば、元のxlsmを開いて修正するのは容易なことである。

結論

今日の教訓

  • リンクエラーが発生した場合、*.xlsx, *.xlsmならzip展開してgrepすると、場所を特定しやすい。
  • セルの値、オブジェクト、名前定義だけでなく、条件付き書式や、入力規則にも外部リンクが入りえるので要注意である。

関連

探してみると、同じことを指摘されている方が何名もおられました。FAQなのかな。

以上、メモ終了

CompletableFutureの使い方の基本形とFutureTaskとの連携例

Java8から利用可能になったCompluteableFutureとは、他言語におけるDeferredとかPromiseパターンといわれるものと同等のものであり、これにより並列処理を直列的に記述できるようになる。(つまり超便利。)

基本的な使い方

もっとも簡単な使い方としては、CompletableFutureが用意している静的コンビニエンスメソッドであるrunAsyncか、supplyAsyncを使う。


たとえば、以下のようなものになる。

1つのタスクの完了後に処理を継続する例
  ExecutorService es = Executors.newCachedThreadPool();
...
  CompletableFuture<String> cf =
      CompletableFuture.supplyAsync(() -> heavyWork("job1"), es);

  cf.whenComplete((ret, ex) -> {
    if (ex == null) {
      // 成功した場合
      System.out.println("result=" + ret);
    } else {
      // 失敗した場合
      System.err.println("err=" + ex);
    }
  });

このように、CompletableFutureを用いると、「完了したら何かを行う」という処理を容易に記述できる。

2つ以上のタスクを同時並列に実行して、すべての待ち合わせる場合
  List<CompletableFuture<String>> cfs = Arrays.asList(
      CompletableFuture.supplyAsync(() -> heavyWork("job1"), es),
      CompletableFuture.supplyAsync(() -> heavyWork("job2"), es),
      CompletableFuture.supplyAsync(() -> heavyWork("job3"), es));

  CompletableFuture<Void> cf = CompletableFuture.allOf(
      cfs.toArray(new CompletableFuture[cfs.size()]));

  cf.whenComplete((ret, ex) -> {
    if (ex == null) {
      // すべて成功した場合
      String msg = cfs.stream().map(future -> {
        try {
          return future.get();
        } catch (Exception iex) {
          return iex.toString();
        }
      }).collect(Collectors.joining(","));
      System.out.println("result=" + msg);

    } else {
      // いずれかが失敗した場合(いずれか1つの例外のみ取得される)
      System.err.println("err=" + ex);
    }
  });

CompletableFuture#allOf(...)メソッドは、指定された全てのCompletableFutureの完了によって完了するVoid型のCompletableFutureを返す。(つまり、すべてのCompletableFutureが実行し終えることで完了となる。)


返されるCompletableFutureは結果がVoid型なので値を得ることはできないため、個別のCompletableFutureで確認する必要がある。

しかし、いずれかのCompletableFutureが失敗している場合は、少なくとも、その例外の1つは把握できる。(複数例外が発生しているかどうかは、個別に確認する必要がある。)

2つ以上のタスクを非同期、且つ"直列"に実行する場合
private ExecutorService es = Executors.newCachedThreadPool();
...

@FXML
public void onClickLoadButton() {
  CompletableFuture<String> work1 =
      CompletableFuture.supplyAsync(() -> heavyWork("job1"), es);
  CompletableFuture<String> work2 =
      work1.thenCompose(work1Result ->
    CompletableFuture.supplyAsync(() ->
         heavyWork("job2(" + work1Result + ")"), es));

  work2.thenAcceptAsync(result -> {
    // work1, work2が完了した場合
    // ※ UIスレッドで何らかの更新処理を行う
    System.out.println(result);
  }, Platform::runLater);
}

典型的なシチュエーションとしては、たとえば、JavaFXなどのGUIでボタン押下により、job1を非同期で開始し、job1で得られた結果をもとにjob2を起動して、それが完了したら何らかのUI更新を行う、というようなことが比較的容易に記述できる。

(上記例ならば、ボタン押下時はCompletableFutureを起動したら直ちにメソッドを抜けているのでUIスレッドがブロックされることはない。)


※ なお、2段目の結果を受け取るthenAcceptAsyncのExecutorにPlatform::runLaterを指定しているので、この部分はJavaFXのUIスレッドで実行されることになる。

メソッド名の末尾がAsyncになっているメソッドの意味

runAsync, whenCompleteAsyncのようにメソッドの末尾にasyncとあるものは、引数に指定した任意のExecutorで実行することを示している。

AsyncメソッドでExecutorを省略した場合は、ForkJoinPool.commonPool*1が使われる。*2 *3


Asyncがつかないものは、前段の完了後に前段と同じスレッドで続けて呼び出される。

CompletableFutureはイベントリスナではない。

runAsyncでCompletableFutureを受け取った時点で、すでに処理が開始されているので、
もし、whenCompleteAsyncを実行するまえに処理が完了していたらどうなるかと心配する向きもあるかもしれない。

しかし、処理が完了したあとにwhenComplete...を呼び出しても、きちんと処理が継続されるようになっている。

たとえば、コンビニエンスメソッドのcompletedFutureでは、すでに値が確定して完了済みのCompletableFutureを返すので、完了している状態に対して、あとからwhenCompleteなどを呼び出すしかないわけだが、もちろん、ちゃんと機能する。


CompletableFutureは、あくまでも前段の確定した状態に対して後続の処理を接続させる仕組みであり、イベントリスナのようなものではない、ということである。

"未完了"なCompletaleFutureを自前で作成してレガシーなスレッドを"完了可能"にする方法

CompletableFutureが標準で用意しているコンビニエンスメソッドはRunnableかSupplierを受け取るメソッドしかないが、たとえば、Callableを受け取ってCompletableFutureを返すにはどうしたら良いか?


これは比較的簡単に実装できる。

CompletableFutureは、明示的にCompletableFutureのインスタンスを構築することで、自前で完了を通知する仕組みを作ることができるようになっている。

作り方

多くの他のプラットフォームのPromise/Deferredのような仕組みと同様に、

  1. まずコンストラクタで未完了状態のCompletableFutureオブジェクトを作成する。
  2. 処理が完了したらcompleteを呼び出して結果を返して、完了状態にする。
  3. もし処理が失敗により終了したら、completeExceptionallyを呼び出して例外を返して、完了状態にする。

基本、これだけでOK.


たとえば、直接Threadを作成してstartしているようなレガシーな処理に、待ち合わせの仕組みを追加したい場合には、
以下のように未完了なCompletableFutrueを構築し、処理終了したら完了状態への遷移を明示的に行えば良い。

  CompletableFuture<String> cf = new CompletableFuture<>(); // 未完了状態で構築
  new Thread() {
    @Override
    public void run() {
      try {
        IntStream.range(0, 10)
          .peek(System.out::println)
          .forEach(idx -> heavyWork(idx));

        // 正常による完了状態への遷移
        cf.complete("done!");

      } catch (Throwable ex) {
        // 例外による完了状態への遷移
        cf.completeExceptionally(ex);
      }
    }
  }.start();

  cf.whenComplete((ret, ex) -> {
    // 完了時に呼び出される
    System.out.println("finished. ret=" + ret + ", ex=" + ex);
  });


このように、completeまたはcompleteExceptionallyによって正常または例外による完了状態への遷移を行うことができる。

また、複数スレッドでの操作の場合、状態遷移は「先勝ち」であり、また、一度確定した状態はあとから変更されることはない。*4


これにより、CompletableFutureで待ち合わせしている後続処理に対して結果を繋げる事ができる。

CompletableFuture.runAsyncのやっていることとは?

実際のところ、コンビニエンスメソッドであるrunAsyncやsupplyAsyncは、簡略化して言えば、引数で渡されたRunnableやSupplierを以下のようなRunnableでラップしてExecutorに渡しているだけといって良い。

public CompletableFuture<Void> runAsync(Runnable runnable) {
  CompletableFuture<Void> cf = new CompletableFuture<>();
  executor.execute(() -> {
    try {
      runnable.run();
      cf.complete(null);

    } catch (Throwable ex) {
      cf.completeExceptionally(ex);
    }
  });
  return cf;
}

または、

public <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier) {
  CompletableFuture<T> cf = new CompletableFuture<>();
  executor.execute(() -> {
    try {
      cf.complete(supplier.get());

    } catch (Throwable ex) {
      cf.completeExceptionally(ex);
    }
  });
  return cf;
}


これが基本形とわかれば、いろいろ応用ができるだろう。*5

Callableを引数にとるメソッドを作成してみる

ExecutorServiceには、CallableをとってFutureを返すメソッドがあるのに、なぜか、CompletableFutureにはCallableを引数に取るコンビニエンスメソッドがない。

しかし、前述のパターンで実装すれば良いだけなので難しくは無い。

public static <T> CompletableFuture<T> callableAsync(Callable<T> c, Executor e) {
  CompletableFuture<T> cf = new CompletableFuture<>();
  e.execute(() -> {
    try {
      cf.complete(c.call());

    } catch (Throwable ex) {
      cf.completeExceptionally(ex);
    }
  });
  return cf;
}

既存のFutureTaskの置き換え

FutureTask類を使っているコードに対して、CompletableFutureによる待ち合わせを組み込みたい場合もあるかもしれない。


たとえば、ExecutorServiceを使う以下のようなコードがあった場合、

  ExecutorService es = Executors.newCachedThreadPool();
...
  Future<String> future = es.submit(() -> {
    Thread.sleep(1000);
    return "ok";
  });
...
  System.out.println(future.get());
...


CompletableFutrueFutureでもあるので、CompletableFuture.supplyAsyncで置き換えても、基本的な使い方は変わらない。

  ExecutorService es = Executors.newCachedThreadPool();
...
  CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException ex) {
      throw new RuntimeException(ex);
    }
    return "ok";
  }, es);

  cf.whenComplete((ret, ex) -> {
    System.out.println("done.");
  });
...
  System.out.println(cf.get());
...

シンプルである。

通常の用途であれば、それほど労せず単純な置き換えで十分と思われる。

CompletableFutureのcancelはスレッドに割り込みを発生させない

ただし、1つ問題があり、返されるCompletableFutureからは開始されたタスクをキャンセル制御することができない。


CompletableFutureに対してcancelを指示すると、即時にキャンセル例外による例外終了状態となる。(completeExceptionally(new CancellationException())の呼び出しと同義)


しかし、これはCompletableFutureのステートだけが変わることであって、実際に処理中のスレッドに対して割り込みなどは全く行われないため、つまり、処理中のスレッドは、実行中のまま放置されることになる。


これは、CompletableFutureの設計思想なのだろう。


そもそも、CompletableFutureはタスクを多段にするのが容易であるから、1つ1つのタスクが重くならないように細切れで作るのがベターなのだとは思われる。(たとえば単発のI/Oのようなものは、そもそも割り込みが無意味なことが多いだろう。)

そうすれば、処理中のスレッドに割り込みをかけるまでもなく、次段以降の処理はスマートに中止される。


しかし、1つのタスク内でループ等により長期的なワークを実行するようなCompletableFutureではキャンセルによりスレッドに停止を要求するための仕組みは考える価値はあるかもしれない。

既存のFutureTaskとの連携

従来型のFutureTaskではキャンセル処理により処理中のスレッドに対して"割り込み"によって中止を通知することができる仕組みをもっている。


(FutueTaskは、runされた時点のスレッドを記憶することで、cancel時に、そのスレッドに対してinterruptを発行できるようになっている。また、処理終了時には実行中スレッドがなくなったことを記憶するので、たとえばスレッドを使い回しているスレッドプールなどでも、終わったタスクのcancelによって他のタスクを誤爆する恐れはない。)


キャンセルによる割り込みを可能としつつ、CompletableFutureの継続の仕組みを使いたい場合には、このFutureTaskと連携を行うことで実現できる。

シンプルな連携例

FutureTaskに、CompletableFutureによる待ち合わせを追加するには以下のように修正する感じになる。

  ExecutorService es = Executors.newCachedThreadPool();
...
  FutureTask<String> future = new FutureTask<>(() -> {
    Thread.sleep(1000);
    return "ok";
  });

  CompletableFuture<Void> cf = CompletableFuture.runAsync(future, es);
  cf.whenComplete((ret, ex) -> {
    System.out.println("done"); // 完了通知
  });

  // 成功時の結果、もしくは何らかの失敗理由は、ここで取得できる
  System.out.println(future.get()); 
...

ExecutorServiceのsubmitの既定の実装は、実際のところFutureTaskでRunnable/Callableをラップして、それをexecuteに渡しているだけだから、基本的には上記のようにFutureTaskによる置き換えで良い。


タスクの実行結果/エラーへのアクセスや、cancelなどのコントロールは引き続き、このFutureTaskによって行う。

しかし、これに加えて、タスクの終了をもって後段の処理を自動的に開始させる仕組みはCompletableFutureによって実現できるようになる。


※ この方法では、CompletableFutureではFutureTask#run()が処理を終えたことしか分からず、成功か失敗かの結果は判断できない。(結果はFutureTaskの内部でもっているため。)

もう少し手をいれてCompletableFutureで結果の取得ができるようにする。

CompletableFutureでFutureTaskの結果を取得できるようにするには、前述の基本パターンでいける。

  ExecutorService es = Executors.newCachedThreadPool();
...
  FutureTask<String> future = new FutureTask<>(() -> {
    Thread.sleep(1000);
    return "ok";
  });

  CompletableFuture<String> cf = futureTaskAsync(future, es);
  cf.whenComplete((ret, ex) -> {
    System.out.println("done. ret=" + ret + ", ex=" + ex); // 結果の取得
  });

/**
 * FutueTaskをCompletableFutureでラップする.
 */
public static <T> CompletableFuture<T> futureTaskAsync(
      FutureTask<T> ft, Executor e) {
  CompletableFuture<T> cf = new CompletableFuture<>();
  e.execute(() -> {
    try {
      ft.run();
      cf.complete(ft.get());

    } catch (ExecutionException ex) {
      cf.completeExceptionally(ex.getCause());

    } catch (Throwable ex) {
      cf.completeExceptionally(ex);
    }
  });
  return cf;
}

FutureTaskの完了待ち後に、その結果をCompletableFutureに伝搬させている。

これでcancelによる割り込み以外はCompletableFutureとして扱えるようになる。


※ FutureTaskのget時に結果または例外を取得できるが、例外はExecutionException例外にラップされているので、CompletableFutureの結果に設定する場合には、ラップを解除するようにしている。

更に手をいれて、cancelもできるようにしてみる。

CompletableFutureでcancelしても、FutureTask側に伝わらないのが問題であった。

なので、CompletableFutureがキャンセルされたら、これをFutureTaskに明示的に伝えるようにすれば良い。

/**
 * FutueTaskをCompletableFutureでラップする.
 */
public static <T> CompletableFuture<T> futureTaskAsync(
      FutureTask<T> ft, Executor e) {
  CompletableFuture<T> cf = new CompletableFuture<>();
  e.execute(() -> {
    try {
      ft.run();
      cf.complete(ft.get());

    } catch (ExecutionException ex) {
      cf.completeExceptionally(ex.getCause());

    } catch (Throwable ex) {
      cf.completeExceptionally(ex);
    }
  });

  cf.whenComplete((ret, ex) -> {
    if (ex instanceof CancellationException) {
      // CompletableFutureがキャンセル例外によって完了している場合
      if (!ft.isDone()) {
        // FutureTaskが、まだ完了していなければ割り込みをかける
        ft.cancel(true);
      }
    }
  });
  return cf;
}

これで、以下のようにしてキャンセルを実施できる。

  FutureTask<String> future = new FutureTask<>(() -> {
    try {
      Thread.sleep(1000);
      System.out.println("done!!");
      return "ok";

    } catch (InterruptedException ex) {
      System.out.println(ex); // 割り込みされた場合にプリントされる
      throw ex;
    }
  });

  CompletableFuture<String> cf = futureTaskAsync(future, es);
  cf.whenComplete((ret, ex) -> {
    System.out.println("done. ret=" + ret + ", ex=" + ex); // 結果の取得
  });

  cf.cancel(true); // キャンセル

JavaFXのTaskとCompletableFutureの連携

キャンセルを行いたい場合以外でも、既存のFutureTaskとの連携が有用な場合もある。

たとえば、JavaFXのTaskなどとの連携があげられる。


JavaFXには、UIへのコールバックサポートを持つ特殊なTaskである、javafx.concurrent.TaskというFutureTask派生クラスがある。

  Label labelMessage = new Label();
  ExecutorService es = Executors.newCachedThreadPool();
...    
  Task<String> uiTask = new Task<String>() {
    @Override
    protected String call() throws Exception {
      int mx = 10;
      for (int idx = 0; idx < mx; idx++) {
        updateMessage("process " + (idx + 1) + "/" + mx);
        Thread.sleep(300);
      }
      return "done.";
    }
  };

  // UIのラベルのテキストプロパティにuiTaskのmessageプロパティを接続する.
  // タスク実行スレッドからの通知はUIスレッドにマーシャリングされるため安全である.
  labelMessage.textProperty().bind(uiTask.messageProperty());

  // 非UIスレッドでのタスクの実行
  es.execute(uiTask);
...

JavaFXではアプリケーションスレッド以外からシーングラフに組み込み済みのオブジェクトを操作することは許されていないため、ワーカースレッドから直接ラベルテキストなどを変更することはできない。


しかし、javafx.concurrent.Taskクラスの内部から呼び出されるupdateMessage, updateTitle, updateProgressといったメソッドはアプリケーションスレッドで通知されるようにマーシャリングされているため、直接、UIに接続可能になっている。
(といっても、単に、内部でPlatform.runLaterしているだけだが。)


このTaskクラスの特性を活かしたまま、CompletableFutureによる待ち合わせに加えたい場合にはどうするか?


先に示したFutureTaskの例と同じようにすれば良い。

(ここでは割り込みは必要ないのでシンプルな方法を用いている。)

  ExecutorService es = Executors.newCachedThreadPool();
...    
  Task<String> uiTask = new Task<String>() {
    @Override
    protected String call() throws Exception {
    ....
    }
  };

  // UIのラベルのテキストプロパティにuiTaskのmessageプロパティを接続する
  labelMessage.textProperty().bind(uiTask.messageProperty());

  // 非UIスレッドでのタスクの実行とCompletableFutureの取得
  CompletableFuture<Void> cf = CompletableFuture.runAsync(uiTask, es);
  
  // 待ち合わせ後、UIスレッドで実行
  cf.whenCompleteAsync((ret, ex) -> {
    labelMessage.textProperty().unbind();
    labelMessage.setText("finished!");
  }, Platform::runLater);

これにより、uiTaskは、そのまま機能しつつ、CompletableFutureによる待ち合わせにも参加できるようになる。*6


(なお、javafx.concurrent.Taskクラスにはスレッドの開始終了イベントをハンドルできる仕組みが備わっているが、スレッドの完了を把握して後続の処理を行うという用途であれば、断然にCompletableFutureのほうが使いやすい。)

結論

CompletableFutureは、コンストラクタによる「不完全なCompletableFuture」による基本形と、その仕組みが分かれば、いろいろ応用が利くし、制限事項も理解しやすいように思われる。

他のFutureまわりの仕組みとは異質ではあるが、他にはない特性を持った、とても使い勝手の良いAPIだと思われる。


以上、メモ終了。

*1:システムが既定で持っている論理CPU分のスレッドプール

*2:なお、ForkJoinPool.commonPoolはサンドボックス状態になっておりパーミッションが必要な処理はできないことに注意。

*3:また、JavaEEではアプリケーションコンテナで管理されたExecutorを明示的に使う必要がある。

*4:CompletableFutureは完了が確定したら、その後、状態が変わらないのが原則だが、obtrudeValue, obtrudeExceptionというイレギュラーなメソッドもある。

*5:なお、FingBugsではCompletableFutureのcomplete(null)を引数がnullによる潜在エラーとして報告するバグがあるようである。(2017/3現在) https://github.com/findbugsproject/findbugs/issues/79 ちなみにjdkの内部では内部用のcompleteNullというメソッドを呼んでいるため問題にはならない。

*6:ちなみに、CompletableFutureの***async系メソッドは引数としてExecutorを取るが、Executorは単一メソッドインターフェイス(single abstract method = SAM)であるため、メソッド参照によるPlatform.runLaterを、そのまま指定できる。

CDIで@Dependentなクラスを@Injectするときに引数を渡す方法(メモ)

概要

CDIで、@Dependentなクラスを定義し、利用する側のクラスのフィールドで@Injectするときに、任意のパラメータを渡したい。

@Dependent
public class Foo {

    @Inject
    private AnyService srv;

    private String arg;
    
    public void setArg(String arg) {
        this.arg = arg;
    }
}

@Dependent
public class Bar {

    @Inject
    private Foo foo; // ← ここでパラメータargを渡したい。
}

解決方法

引数つきの@Qualifierを定義し、@Producesを使うことで実現できる。

@Qualifier
@Retention(RUNTIME)
@Target({ TYPE, METHOD, FIELD, PARAMETER })
public @interface FooParameter {
    @Nonbinding String value();
}

@ApplicationScoped
public class FooProducer {

    @Inject
    @Default
    private Instance<Foo> fooProvider;

    @Dependent
    @Produces
    @FooParameter("")
    public Foo create(InjectionPoint ip) {
        FooParameter fooParam = ip.getAnnotated()
                .getAnnotation(FooParameter.class);
        String arg = fooParam.value();

        Foo inst = fooProvider.get();
        inst.setArg(arg);
        return inst;
    }
}

@Dependent
public class Bar {

    @Inject
    @FooParameter("baz") // ← Foo構築時にパラメータを渡せる
    private Foo foo;
}

@Qualifierを定義するとき、@Nonbindingを指定することで、マッチングする場合に引数の中身は無視されるようになる。

なので、@FooParameter("")の引数にかかわらず、@FooParameterの限定子をつけているものは、すべて、この@Producesメソッドにマッチするようになる。

@Producesでは、InjectionPoint引数を受け取ることでアノテーションを取得できる(@Qualifier以外も可)ので、ここでパラメータを受け取ることができる。

あとは、@DefaultにマッチするFooのインスタンスを生成して明示的にパラメータを渡してから返せば良い。

@Disposesの実装

Fooインスタンスが不要になったタイミングでの後始末が必要ならば、以下のように@Disposesを実装する。

    public void dispose(@Disposes @FooParameter("") Foo inst) {
        fooProvider.destroy(inst);
    }

Instance#get()で取得されたインスタンスは、対となるInstance#destroyメソッドによって明示的な破棄処理を行うことができる。

これにより、Fooクラス内に@PreDestroyメソッドが定義されている場合、これが呼び出されることになる。


(この処理を入れないと@PreDestroyを呼び出すべきタイミングがなくなってしまう。)

別解、もしくは、引数つきコンストラクタで使う場合

Apache DeltaSpikeBeanProvider.injectFieldsで直接入れることも可。


DeltaSpikeではinjectFielsで以下のようなことをやっている。

    public static <T> T injectFields(T instance) {
        BeanManager beanManager = CDI.current().getBeanManager();

        CreationalContext<T> creationalContext =
                beanManager.createCreationalContext(null);

        @SuppressWarnings("unchecked")
        AnnotatedType<T> annotatedType = beanManager
                .createAnnotatedType((Class<T>) instance.getClass());

        InjectionTarget<T> injectionTarget = beanManager
                .createInjectionTarget(annotatedType);
        injectionTarget.inject(instance, creationalContext);

        return instance;
    }


もし、Fooを以下のように引数つきコンストラクタにしてパラメータなしでのインスタンス化をできないようにしたとする。

また、CDIの管理外とする。

public class Foo {

    @Inject
    private AnyService srv;

    private String arg;
    
    public Foo(String arg) {
        this.arg = arg;
    }
}

これは、@Producesによって明示的に非管理Beanをインスタンス化できる。

@ApplicationScopred
public class FooProducer {

    @Dependent
    @Produces
    @FooParameter("")
    public Foo create(InjectionPoint ip) {
        FooParameter fooParam = ip.getAnnotated().getAnnotation(
                FooParameter.class);
        String arg = fooParam.value();

        Foo inst = new Foo(arg);

        // Fooクラスのフィールド中の@Injectを実施する。
        BeanProvider.injectFields(inst);

        return inst;
    }
}

newによって生成されたインスタンスなので、Fooの@Injectの指定のあるフィールドは未初期化状態のままである。

ここでDeltaSpikeのinjectFieldsによって@Injectのフィールドを明示的に注入する。*1


ただし、この方法では注入ができるだけで、@PostConstruct, @PreDestroyなどのイベント的な処理も行うわけではないので、既存の非管理クラスをインスタンス化する場合でなければ、javax.enterprise.inject.Instanceを使って管理ビーンとして生成するのが結局は簡単になると思われる。


また、もし、マニュアルでinjectしたビーンの@PreDestroyを動かしたいのであれば、BeanProvider#injectFieldsで使用したCreationalContextを保存しておいて、@Disposes内で、creationContext.releaseする必要がある。(javax.enterprise.inject.Instanceは、それを行っている。)

*1:メソッド インジェクションも可

Javaのダイレクトバッファの利用上の注意点と、各種バッファを使った読み込みパフォーマンスの比較

概要

前回では、ヒープ上に確保したByteBufferと、ダイレクトバッファとして作成したByteBufferでのデータの読み取りパフォーマンスを比較した結果、圧倒的にダイレクトバッファが速いことが分かった。


しかし上記ベンチマークは、あえてByteBuffer単体でのパフォーマンスの比較を行ったものであり、実際のファイル入出力処理を含んだ比較ではない。
いわば、純粋なByteBufferの性能比較である。


では、実際にファイルの読み取りを含んだ場合でも、はたしてダイレクトバッファを使ったほうが速いのであろうか?


今回は、実際の利用方法を意識して、このあたりを調べてみることとする。

ヒープバッファとダイレクトバッファの違い

ByteBufferには大きく分けて2種類がある。

  • ヒープ上の作成されるヒープバッファ
  • ネイティブ領域に作成されるダイレクトバッファ

ヒープバッファは以下のように作成される。

ByteBuffer buf = ByteBuffer.alloc(siz);
// または
ByteBuffer buf = ByteBuffer.wrap(new byte[siz]);

(allocは単純に内部でnew byte[]を実行しているだけである。)

これは、当然、Javaのヒープ領域からバッファが割り当てられる。
バッファとして確保した分、利用可能なヒープサイズは減ることになる。
その特性はJavaの一般的なオブジェクトと何ら変わることは無い。


これに対して、ダイレクトバッファは以下のように作成される。

    ByteBuffer buf = ByteBuffer.allocDirect(siz);

(そのほか、RandomAccessFileから直接MappedByteBufferを取得する方法もある。)


こちらはJavaのヒープではなく、ネイティブメモリ上から割り当てられる。


ダイレクトバッファはヒープ上にバッファは作らないので、ヒープはほとんど減らない。
そのかわり、ネイティブメモリ、つまりOS側からJavaVMが借りているコミットメモリが増える。

ネイティブメモリはガベージコレクタが管理しているメモリの範囲外であるため、ガベージコレクトなどのメカニズムが効かない領域である。

実験コード
public class BufferAllocExample {

    /**
     * バイトバッファをallocする方法
     */
    public enum ByteBufferAllocStrategy {

        /**
         * ヒープのByteBufferを構築する.
         */
        heap() {
            @Override
            public ByteBuffer alloc(int siz) {
                return ByteBuffer.allocate(siz);
            }
        },

        /**
         * ダイレクトのByteBufferを構築する.
         */
        direct() {
            @Override
            public ByteBuffer alloc(int siz) {
                return ByteBuffer.allocateDirect(siz);
            }
        };

        public abstract ByteBuffer alloc(int siz);
    }

    /**
     * エントリポイント.
     * 第一引数にheapかdirectを指定する.
     * @param args
     * @throws Exception
     */
    public static void main(String... args) throws Exception {
        ByteBufferAllocStrategy allocator = ByteBufferAllocStrategy
                .valueOf(args[0]);

        Runtime rt = Runtime.getRuntime();

        List<ByteBuffer> buffers = new ArrayList<>();

        // 10 MiBの割り当てを10回繰り返す
        for (int idx = 0; idx < 10; idx++) {
            System.gc();
            System.out.println(rt.freeMemory() + "/" + rt.totalMemory());

            ByteBuffer buf = allocator.alloc(1024 * 1024 * 10);
            buffers.add(buf);
        }

        // 100 MiB割り当て後のヒープの残量
        System.gc();
        System.out.println(rt.freeMemory() + "/" + rt.totalMemory());

        // OSのコミットサイズ、プライベートワーキングセットのサイズを
        // タスクマネージャで見られるように、アプリ終了前に一旦停止する。
        System.console().readLine();
    }
}

第一引数にallocかallocDirectを指定して実行すると、10 MiBごとのバッファを確保しながらヒープの残量を表示し、最後に入力待ちになって停止するアプリである。

ヒープバッファ確保の場合
>java -Xms128m -Xmx128m -cp classes jp.seraphyware.jmhexample.BufferAllocExample heap
127831384/128974848
118026720/128974848
107540896/128974848
97055072/128974848
86569248/128974848
76083424/128974848
65597600/128974848
55111776/128974848
44625952/128974848
34140128/128974848
23654304/128974848 

バッファを確保するたびにヒープがどんどん減ってゆく順当な動きである。


このときのタスクマネージャで取得したOSから予約されたコミットサイズは、

  • コミットサイズ = 184,444 k

となっていた。

ダイレクトバッファの場合
>java -Xms128m -Xmx128m -cp classes jp.seraphyware.jmhexample.BufferAllocExample direct
127831360/128974848
128512256/128974848
128512120/128974848
128511984/128974848
128511848/128974848
128511712/128974848
128511576/128974848
128511440/128974848
128511304/128974848
128511168/128974848
128511032/128974848

ヒープはぜんぜん減っていない。

このときのタスクマネージャで取得したOSから予約されたコミットサイズは、

  • コミットサイズ = 286,676 k

となっている。


ヒープバッファのときと比較して、コミットサイズ = OSからJavaVMが予約しているメモリとしては確実に100 MB(102,232 k)ほど増えている。

ダイレクトバッファの最大サイズの指定方法

ちなみに、ダイレクトバッファの最大値はJVMのオプション"-XX:MaxDirectMemorySize "のように明示的に設定することができる。


省略した場合は自動(≒ 制限なし)で、既定ではヒープの最大値と同じようになるため、通常は気にしなくてもよいらしい。

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html


ただし、最大ヒープサイズを超えてダイレクトバッファサイズを指定することもできるらしく、以下のように最大ヒープサイズを64 MBにしても、それを超える100 MBのダイレクトバッファを確保することが可能であった。(Java8u60で確認。)

>java -Xms64m -Xmx64m -XX:MaxDirectMemorySize=512m -cp classes jp.seraphyware.jmhexample.BufferAllocExample direct
63689944/64487424
64020424/64487424
63684664/64487424
63684528/64487424
63684392/64487424
63684256/64487424
63684120/64487424
63683984/64487424
63683848/64487424
63683712/64487424
63683656/64487424

バッファ確保のパフォーマンス比較

ヒープ上のByteBufferは、単に普通にヒープ上でnew byte[…]するだけのオーバーヘッドと同等である。
ヒープメモリは事前にJavaVMがOSよりメモリをもらっており、そこでJavaVMが領域を割り当てるだけである。


これに対して、ダイレクトバッファはネイティブ領域に都度、OSよりバッファを確保してもらっているような動きになっている。
(実際は、もう少し効率は良いかもしれないが、前述の実験からすると、要求されてから都度、OSからメモリをもらっている感じである。)


両者はメモリの確保方法が異なるため、確保するまでにかかる処理時間も異なると予想される。

ベンチマーク

そこで、以下のようなベンチマークコードを書いて計測してみた。

public class BufferAllocBenchmark {

    @State(Scope.Thread)
    public static class BufferAllocContext {

        private ByteBuffer buf;

        @Setup(Level.Trial)
        public void setup() {
        }

        public void setByteBuffer(ByteBuffer buf) {
            this.buf = buf;
        }

        public ByteBuffer getButeBuffer() {
            return this.buf;
        }
    }

    @Benchmark
    public void testAllocDirect8(BufferAllocContext ctx) {
        ctx.setByteBuffer(ByteBuffer.allocateDirect(8 * 1024));
    }

    @Benchmark
    public void testAllocHeap8(BufferAllocContext ctx) {
        ctx.setByteBuffer(ByteBuffer.allocate(8 * 1024));
    }
  … (中略) …
}

ByteBuffer.alloc()と、ByteBuffer.allocDirect()をバッファサイズを変えて計測している。

計測するバッファサイズは、8, 16, 32, 64, 128, 256, 512, 1024 kibである。


(過度な最適化が起きないように、得られたByteBufferは状態インスタンスに1世代のみ保存している。)


Javaはjdk1.8u60で実行し、オプションとして-Xmx128m -Xmx128m -XX:MaxDirectMemorySize=256mを指定したものである。

計測結果
size Direct Heap Direct Error Heap Error
8 183090.216 2018841.709 320.863 49630.037
16 93116.245 1096969.848 191.754 8257.477
32 47854.178 547637.302 82.986 13006.356
64 22689.129 266632.138 224.02 5163.652
128 10988.465 69412.152 518.684 864.918
256 5536.215 33852.752 223.108 84.935
512 2897.219 28651.803 24.236 406.872
1024 1574.254 14013.506 51.023 104.658

数値は、1秒あたりのベンチマークメソッドの実行回数の平均である。

激しくガベージコレクタが動作することが予想されるので、gcによる揺らぎを考慮して測定回数(イテレーション)は50回まわすことにした。


これをチャート化すると、以下のようになる。

チャート化するにあたり、数値は実行回数ではなく確保したバイト数に換算している。
(= 実行した回数 x 確保したバイト数)


これにより、バッファ確保の効率を比較できるようにしている。

これを見ると、明らかにダイレクトバッファはヒープバッファよりも時間がかかっている。


差が10倍ほど開いてしまったので、縦軸は対数表示している。
ダイレクトバッファは、確保するまでの時間が、むちゃくちゃ遅い。


この点については、そもそもJavaDocに書かれている。

ダイレクト byte バッファーは、このクラスのファクトリメソッド allocateDirect を呼び出すと作成されます。通常は、こちらのバッファーのほうが、非ダイレクトバッファーよりも割り当ておよび解放コストがやや高くなります。ダイレクトバッファーの内容が標準のガベージコレクトされたヒープの外部にあるなら、アプリケーションのメモリーフットプリントに対する影響はわずかです。このことから、ダイレクトバッファーには、基本となるシステム固有の入出力操作に従属する、寿命が長く容量の大きいバッファーを指定することをお勧めします。一般に、ダイレクトバッファーの割り当ては、プログラムの性能を十分に改善できる見込みがある場合にのみ行うべきです。

http://docs.oracle.com/javase/jp/8/docs/api/java/nio/ByteBuffer.html


チャートを見ると揺らぎがあるが、だいたいメモリ確保のスピードは確保するバッファサイズに係わらず、ほぼ一定のように思われるので、バッファが小さければ、あるいは、大きければバッファ確保が速くなるとか、そうゆうことはなさそうである。


いずれにしても、ヒープとのダイレクトバッファの確保時のパフォーマンスの差はかなり大きい。

DirerctByteBufferの連続確保と破棄の問題点

ヒープ上のbyte[]配列であるヒープバッファは連続確保・破棄したとしても、その動きは通常のJavaオブジェクトと変わらないことが予想される。

また、OSから見た使用メモリには増減は発生しないはずである。
(すでに確保済みのヒープ内で処理されるため。)


これに対して、ダイレクトバッファはOSより都度メモリを借り、そのメモリを解放するためには、そのダイレクトバッファのハンドルをもつヒープ上のByteBufferオブジェクトがgcされるまで待たなければならないはずである。
(ByteBufferのメソッドにはI/O処理につきものの「close」のようなメソッドがないため、バッファが使用中であるかどうかを判断するにはgcによるしかない。)

また、OSから見た使用メモリは、ダイレクトバッファは確保されるたびにコミットサイズが増加し、gcが発生するたびに解放されるような動きを示すはずである。

実験

そこで、以下のようなコードを書いて試してみた。

public class BufferStressExample2 {

    /**
     * 最新のバイトバッファを保持するスレッドローカル
     */
    public static ThreadLocal<ByteBuffer> lastBufTLS = new ThreadLocal<>();

    /**
     * バイトバッファをallocする方法
     */
    public enum ByteBufferAllocStrategy {

        /**
         * ヒープのByteBufferを構築する.
         */
        heap() {
            @Override
            public ByteBuffer alloc(int siz) {
                return ByteBuffer.allocate(siz);
            }
        },

        /**
         * ダイレクトのByteBufferを構築する.
         */
        direct() {
            @Override
            public ByteBuffer alloc(int siz) {
                return ByteBuffer.allocateDirect(siz);
            }
        };

        public abstract ByteBuffer alloc(int siz);
    }

    /**
     * バイトバッファの破棄方法.
     */
    public enum ByteBufferDeallocStrategy {

        /**
         * 何もしない.システムにお任せ.
         */
        none() {
            @Override
            public void dealloc(ByteBuffer buf) {
            }
        },

        /**
         * 明示的にgcを呼び出す.
         */
        gc() {
            @Override
            public void dealloc(ByteBuffer buf) {
                System.gc();
            }
        },

        /**
         * 明示的にcleanerを呼び出す.
         */
        cleaner() {
            @Override
            public void dealloc(ByteBuffer buf) {
                if (buf != null && buf.isDirect()) {
                    destroyDirectByteBuffer(buf);
                }
            }
        };

        public abstract void dealloc(ByteBuffer buf);
    }

    /**
     * エントリポイント
     * @param args
     * @throws Exception
     */
    public static void main(String... args) throws Exception {

        ByteBufferAllocStrategy allocator =
                ByteBufferAllocStrategy.valueOf(args[0]);
        ByteBufferDeallocStrategy deallocator;
        if (args.length > 1) {
            deallocator = ByteBufferDeallocStrategy.valueOf(args[1]);
        } else {
            deallocator = ByteBufferDeallocStrategy.none;
        }

        int mxthread = (args.length > 2) ? Integer.parseInt(args[2]) : 2;

        // 10 MiBの割り当てを永久に繰り返すジョブ
        Runnable job = () -> {
            for (;;) {
                try {
                    // ByteBufferの構築
                    ByteBuffer buf = allocator.alloc(10 * 1024 * 1024); // 10 MiB

                    // ByteBufferの破棄
                    deallocator.dealloc(lastBufTLS.get());

                    // 最後に確保したByteBufferの保存
                    lastBufTLS.set(buf);

                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            }
        };

        IntFunction<Thread> makeThread = (idx) -> {
            Thread t = new Thread(job);
            t.setDaemon(false);
            t.setName("bufferStress:" + idx);
            return t;
        };

        IntStream.range(0, mxthread)
                .mapToObj(makeThread)
                .forEach(Thread::start);
    }
}

JDK1.8u60で実行した。

テストコードでは2スレッドの同時バッファ確保を行っている。

またコマンドは以下のオプションで実行した。

java -cp classes -Xms128m -Xmx128m -XX:MaxDirectMemorySize=256m jp.seraphyware.jmhexample.BufferStressExample2 heap
ヒープによるByteBufferの連続確保と破棄

以下はWindowsのパフォーマンスモニタによるPage File Bytes(≒コミットサイズ)と、ワーキングセットサイズの推移グラフである。
(縦軸の単位は10 MiBである。以下のパフォーマンスモニタのチャートはすべて同様である。)

new byte[]の破棄と解放はJavaVMの中の出来事であり、OSから見てメモリ消費の増減はないのが分かる。

ダイレクトバッファによるByteBufferの連続確保と破棄と、OutOfMemory問題

このテストコードでは、ダイレクトバッファは1回につき10 Mbytesを割り当て、直近の1つのByteBufferだけを保存し、それ以前のByteBufferは参照がなくなりガベージコレクト対象としている。

これが2スレッドで動いているので、理想的にはダイレクトバッファは1世代前の10 Mbytes x 2と、作成されたばかりの10 Mbytes x 2の、計40 Mbytesのバッファ以外は不要のはずである。


しかし、パフォーマンスモニタによると、瞬間的には450 Mbytesほどのコミットサイズになっている。(ヒープサイズが128MB、これに加えてMaxDirectMemorySizeが256 MBなので、384 MB+α ぐらいが最大値になっていると思われる。)

つまり、不要になったネイティブメモリは、すぐには消えていない。


また、このテストでは、MaxDirectMemorySizeで十分なサイズを指定しておかないと、「OutOfMemoryError: Direct buffer memory」とメモリ不足エラーが発生しやすくなる。
また、スレッド数を増やすと、如実にOutOfMemoryErrorが発生するようになる。

実際には10 Mbytesしか要求しておらず、最大バッファサイズが256 MBもあり、十分に空きがあると考えられるにも係わらず、である。

おそらく確保するスピードが解放するスピードを上回っているのだろう。

ダイレクトバッファでOutOfMemoryを避ける方法

原理的には、ByteBufferがgcされるまでネイティブに確保されたダイレクトバッファも解放されないであろうため、最大ダイレクトメモリサイズよりヒープが大きい場合は、場合によってはヒープのgcが発生する前にダイレクトメモリが枯渇する可能性があるように思われる。(まさか、そんなナイーブな実装ではないとは思うし、OutOfMemoryをあげる前にgcは試行されるはずだが…。)

現実に、Windows7/8.1上のjdk1.8u60では、MaxDirectMemorySizeが十分に大きくないと、このテストでは、すぐにOutOfMemoryが発生する状況である。

System.gc()によるOutOfMemoryの回避

ダイレクトバッファを確保する直前にSystem.gc()をしてあげると、この事象はでてこない。
ByteBufferがgcされ、ネイティブの解放も逐次行われて、パフォーマンスモニタの波形も比較的穏やかなものになる。

ただし、はたして実運用コードの中に「System.gc()」のようなコードを書いて良いものか?
(あるいは、-XX:+DisableExplicitGC オプションによりgcが無視される可能性もある。)
、というような課題はある。

DirectBuffer内部のcleanerを直接使用した早期解放を使う場合

実は、どうやらダイレクトバッファの破棄が遅延されてOutOfMemoryErrorが発生する問題は、かなりメジャーな問題のようで、Stackoverflowあたりを検索すると沢山出てくる。


System.gc()を使うのが、おそらくもっとも正式な方法だが、DirectByteBufferが内部で持っているcleanerメソッドを呼び出して、それに対してcleanを実行することで、ネイティブのメモリ破棄処理を強制的に早期に実行させる、という方法もあるようである。


以下に引用する、ApacheHadoopのソースにも、ダイレクトバッファを破棄するためのコードが書かれている。
(および、どうしてダイレクトバッファの破棄が遅延するのかの理由も。)

https://hbase.apache.org/0.94/apidocs/src-html/org/apache/hadoop/hbase/util/DirectMemoryUtils.html#line.80

    /**
      * DirectByteBuffers are garbage collected by using a phantom reference and a
      * reference queue. Every once a while, the JVM checks the reference queue and
      * cleans the DirectByteBuffers. However, as this doesn't happen
      * immediately after discarding all references to a DirectByteBuffer, it's
      * easy to OutOfMemoryError yourself using DirectByteBuffers. This function
      * explicitly calls the Cleaner method of a DirectByteBuffer.
      * 
      * @param toBeDestroyed
      *          The DirectByteBuffer that will be "cleaned". Utilizes reflection.
      *          
      */
    public static void destroyDirectByteBuffer(ByteBuffer toBeDestroyed)
        throws IllegalArgumentException, IllegalAccessException,
        InvocationTargetException, SecurityException, NoSuchMethodException {
    
      Preconditions.checkArgument(toBeDestroyed.isDirect(),
          "toBeDestroyed isn't direct!");
    
      Method cleanerMethod = toBeDestroyed.getClass().getMethod("cleaner");
      cleanerMethod.setAccessible(true);
      Object cleaner = cleanerMethod.invoke(toBeDestroyed);
      Method cleanMethod = cleaner.getClass().getMethod("clean");
      cleanMethod.setAccessible(true);
      cleanMethod.invoke(cleaner);
    
    }

このコメントによると、参照がすべて切れたからといって、ただちにネイティブメモリが解放されるわけではなく、ダイレクトバッファはファントムリファレンスによって保持され、そのリファレンスキューをJavaVMがチェックして、gcによりキューに入れられたバッファがあれば、そのネイティブメモリを解放しているようである。
このコメントから察するに、タイミングは不明だが、ともかくリファレンスキューを見に行く必要のあるパッシブな動きになっているのだろう。


ためしに、このcleanerを呼び出すコードを使ってみると、パフォーマンスモニタの波形は、大変穏やかなものとなり、スレッド数を増やしてもOutOfMemoryも発生しなくなる。


ただし、はたして実運用コードの中にJavaVMの特定の実装にべったり依存するかのようなコードを書いてよいものかどうか、そのあたりの課題は残る。

ヒープバッファとダイレクトバッファのファイルの読み込み速度の比較

以上の考察から、ダイレクトバッファを使うには2つ注意点があることが分かった。

  1. ダイレクトバッファは確保するのが遅い。
  2. ダイレクトバッファは破棄するのが難しい。

つまり、ファイルを開くたびに、ダイレクトバッファを毎回確保するような方法では、バッファ確保時のオーバーヘッドと、バッファ破棄のトラブルによって、あまり良いパフォーマンスは得られない可能性がある。


しかし、「単にファイルをシーケンシャルに読み込みたい」という、ありふれたニーズであれば、これを対策するのは比較的容易である。


単に、ダイレクトバッファは確保したらキャッシュし、破棄しなければ良いだけである。
(同時アクセスするファイルの最大数分だけダイレクトバッファをキャッシュすれば良い。)
*1


これを実装した上で、ダイレクトバッファとヒープバッファでのファイル読み取りのパフォーマンスを比較してみたいと思う。

実験内容
  • テストデータはテンポラリ上に作成される10 Mbytesのファイルである。
  • テストデータはLittle EndianとBig Endianの2種類作成する
  • データはIntとDoubleの2種類作成する。
  • バッファサイズは、8, 16, 32, 64, 128, 256, 512, 1024 KiB
  • 読み取り方式として、
    • ダイレクトバッファ (バッファは@Setup時に予め初期化しておき、各イテレーションでは確保しない)
    • ヒープバッファ (バッファは@Setup時に予め初期化しておき、各イテレーションでは確保しない)
    • BufferedInputStream
    • InputStream (バッファ忘れケース)
  • ウォームアップとして10回、計測として10回のイテレーションを行う

なお、ウォームアップを10回も繰り返すと、10 MBytes程度のファイルならOSのファイルキャッシュに確実に乗っかることになる。
しかし、今回はディスクの性能を比較するわけではないので、これで良いものと考える。
ある意味、理想的なディスクを使った場合の読み取り方式の違いによるパフォーマンスの違いを計測するようなものといえるだろう。

データ作成コード

(Intデータ部のみ。Doubleデータのソースコードの記載は省略)

データ共通部

/**
 * テスト中にスレッドごとベンチマークごとの状態を保持するインスタンス.
 *
 * @author seraphy
 */
@State(Scope.Thread)
public abstract class AbstractFileBenchContext {

    protected final ByteOrder byteOrder;

    protected final int bufsiz;

    private Path tempFile;

    private ByteBuffer bufHeap;

    private ByteBuffer bufDirect;

    protected AbstractFileBenchContext(ByteOrder byteOrder, int bufsiz) {
        this.byteOrder = byteOrder;
        this.bufsiz = bufsiz;
    }

    /**
     * データの初期化.
     * Trialの場合、スレッドごとベンチマークの開始ごとに一度呼び出される.<br>
     * (ウォームアップイテレーション、および計測イテレーション中には呼び出されない。)
     * (Trialがデフォルト、そのほかにIteration, Invocationが指定可能.)
     */
    @Setup(Level.Trial)
    public void setup() throws IOException {

        // バッファの確保 (ヒープ)
        byte[] data = new byte[bufsiz];
        bufHeap = ByteBuffer.wrap(data);
        bufHeap.order(byteOrder);

        // バッファの確保 (ダイレクト)
        bufDirect = ByteBuffer.allocateDirect(data.length);
        bufDirect.order(byteOrder);

        // テストデータファイルの作成
        tempFile = initTempFile();
    }

    protected abstract Path initTempFile() throws IOException;

    /**
     * スレッドごとベンチマーク終了ごとに呼び出される.
     */
    @TearDown
    public void teardown() {
        try {
            //System.out.println("★delete tempFile=" + tempFile);
            Files.deleteIfExists(tempFile);

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public Path getTempFile() {
        return tempFile;
    }

    public abstract void verify(Object actual);

    public ByteBuffer getBufDirect() {
        return bufDirect;
    }

    public ByteBuffer getBufHeap() {
        return bufHeap;
    }
}

テストごとのデータ部(一部)

    public static class BenchContextBase extends AbstractFileBenchContext {

        public static final int reqSize = 1024 * 1024 * 10; // 10 MiBytes

        private long total;

        protected BenchContextBase(ByteOrder byteOrder, int bufsiz) {
            super(byteOrder, bufsiz);
        }

        /**
         * テストデータファイルの作成
         * @throws IOException
         */
        @Override
        protected Path initTempFile() throws IOException {
            Path tempFile = Files.createTempFile("filebufferbench", ".tmp");
            //System.out.println("★create tempFile=" + tempFile);

            ByteBuffer buf = ByteBuffer.allocate(bufsiz);
            buf.order(byteOrder);

            total = 0;
            int capacity = buf.capacity();
            long siz;
            try (FileChannel channel = (FileChannel) Files
                    .newByteChannel(tempFile, CREATE, TRUNCATE_EXISTING, WRITE)) {
                int idx = 0;
                int mx = reqSize / capacity;
                for (int loop = 0; loop < mx; loop++) {
                    buf.clear();
                    while (buf.position() < capacity) {
                        buf.putInt(idx);
                        total += idx;
                        idx += 1;
                    }
                    buf.flip();
                    channel.write(buf);
                }
                siz = channel.size();
            }

            if (reqSize != siz) {
                throw new RuntimeException();
            }
            //System.out.println("★total=" + total + "/size=" + siz);
            return tempFile;
        }

        @Override
        public void verify(Object actual) {
            if ((Long) actual != total) {
                throw new RuntimeException("actual=" + actual);
            }
         }
    }

    public static class BenchContextLE8k extends BenchContextBase {

        public BenchContextLE8k() {
            super(ByteOrder.LITTLE_ENDIAN, 1024 * 8);
        }
    }

    public static class BenchContextBE8k extends BenchContextBase {

        public BenchContextBE8k() {
            super(ByteOrder.BIG_ENDIAN, 1024 * 8);
        }
    }

※ このあと、バッファサイズ、LE/BEごとのコンテキストクラスが延々と続く。

テスト実行部

FileChannelを使ってByteBufferでファイルの中身を取り出しテストする部分。

    /**
     * バイトバッファからint値を読み出すテスト
     * @param buf
     */
    private static long runByteBuffer(FileChannel channel, ByteBuffer buf)
            throws IOException {
        long total = 0;
        for (;;) {
            buf.clear();
            int len = channel.read(buf);
            if (len < 0) {
                break;
            }
            buf.flip();
            int limit = buf.limit();
            while (buf.position() < limit) {
                total += buf.getInt();
            }
        }
        return total;
    }

データファイルを読み取り、ファイルチャネルを取り出して中身を走査する。

    private static void doBench(AbstractFileBenchContext ctx, ByteBuffer buf)
            throws IOException {
        try (FileChannel channel = (FileChannel) Files.newByteChannel(
                ctx.getTempFile(), READ)) {
            ctx.verify(runByteBuffer(channel, buf));
        }
    }

ヒープバッファ、ダイレクトバッファによるテスト

    @Benchmark
    public void testHeapBufferLE8k(BenchContextLE8k ctx) throws IOException {
        doBench(ctx, ctx.getBufHeap());
    }

    @Benchmark
    public void testDirectBufferLE8k(BenchContextLE8k ctx) throws IOException {
        doBench(ctx, ctx.getBufDirect());
    }
旧式のストリーム系APIによる読み取り

FileInputStream → BufferedInputStream → DataInputStream の連携でデータを読み取るテスト。

DataInputStreamはBigEndian専用なので、BigEndianのデータのみテストする。


DataInputStreamは一般的にいって使用頻度が高いとはいえないと思うが、byte[]からint値を取り出すための、昔からある、由緒正しい方法の1つなので、とりあえず、これで計測してみる。

    private static long readInts(InputStream is, int cnt) throws IOException {
        long total = 0;
        try (DataInputStream dis = new DataInputStream(is)) {
            for (int idx = 0; idx < cnt; idx++) {
                total += dis.readInt();
            }
        }
        return total;
    }

    @Benchmark
    public void testOldBufferedIO8k(BenchContextBE1m ctx) throws IOException {
        long total;
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(ctx.getTempFile().toFile()))) {
            total = readInts(bis, BenchContextBase.reqSize / 4);
        }
        ctx.verify(total);
    }

    @Benchmark
    public void testOldBufferedNIO8k(BenchContextBE1m ctx) throws IOException {
        long total;
        try (BufferedInputStream bis = new BufferedInputStream(
                Files.newInputStream(ctx.getTempFile()))) {
            total = readInts(bis, BenchContextBase.reqSize / 4);
        }
        ctx.verify(total);
    }

なお、Java7から、FilesクラスのnewInputStreamメソッドを使って、簡易にファイルを開くことができるようになったが、従来のFileInputStreamのコンテストラクタで開く場合と変わりないのか気になったので、そのテストも追加している。


また、FileInputStreamをBufferedInputStreamでラップしなかった場合、パフォーマンスの低下がどの程度なものなのか気になったので、こちらもテストを追加している。

計測結果

Intデータの読み取り
Benchmark                                                Mode  Cnt    Score    Error  Units
FileBufferAccessIntBenchmark.testDirectBufferBE128      thrpt   10  312.589 ±  4.157  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE16       thrpt   10  242.295 ±  3.186  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE1m       thrpt   10  317.480 ±  2.527  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE256      thrpt   10  313.295 ±  8.739  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE32       thrpt   10  294.236 ±  1.012  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE512      thrpt   10  312.779 ±  5.954  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE64       thrpt   10  307.159 ±  4.509  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE8k       thrpt   10  202.106 ±  0.781  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE128      thrpt   10  364.260 ± 10.193  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE16       thrpt   10  264.034 ±  0.895  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE1m       thrpt   10  360.495 ± 13.502  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE256      thrpt   10  362.586 ±  9.963  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE32       thrpt   10  327.563 ±  4.147  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE512      thrpt   10  359.784 ± 15.424  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE64       thrpt   10  355.960 ±  9.823  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE8k       thrpt   10  221.521 ±  0.949  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE128        thrpt   10  144.247 ±  0.341  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE16         thrpt   10  132.583 ±  0.299  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE1m         thrpt   10  162.469 ±  0.401  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE256        thrpt   10  162.569 ±  0.869  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE32         thrpt   10  135.383 ±  8.522  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE512        thrpt   10  158.935 ±  1.510  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE64         thrpt   10  136.175 ±  1.539  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE8k         thrpt   10  110.557 ±  1.032  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE128        thrpt   10  139.430 ±  0.641  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE16         thrpt   10  141.571 ±  0.264  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE1m         thrpt   10  161.376 ±  1.468  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE256        thrpt   10  164.129 ±  0.593  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE32         thrpt   10  139.513 ±  1.686  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE512        thrpt   10  164.563 ±  0.675  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE64         thrpt   10  142.430 ±  0.715  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE8k         thrpt   10  115.688 ±  0.349  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO128k      thrpt   10   21.981 ±  0.201  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO16k       thrpt   10   21.746 ±  0.176  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO32k       thrpt   10   21.734 ±  0.249  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO64k       thrpt   10   21.992 ±  0.109  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO8k        thrpt   10   21.298 ±  0.168  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO128k     thrpt   10   22.111 ±  0.117  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO16k      thrpt   10   21.814 ±  0.188  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO32k      thrpt   10   22.020 ±  0.252  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO64k      thrpt   10   22.072 ±  0.300  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO8k       thrpt   10   21.420 ±  0.185  ops/s
FileBufferAccessIntBenchmark.testOldPlainIO             thrpt   10    0.077 ±  0.001  ops/s
FileBufferAccessIntBenchmark.testOldPlainNIO            thrpt   10    0.072 ±  0.001  ops/s
Doubleデータの読み取り
Benchmark                                                Mode  Cnt    Score    Error  Units
FileBufferAccessDoubleBenchmark.testDirectBufferBE128   thrpt   10  350.916 ±  2.222  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE16    thrpt   10  273.792 ±  2.641  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE1m    thrpt   10  350.331 ±  2.718  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE256   thrpt   10  351.351 ±  3.570  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE32    thrpt   10  322.476 ±  1.697  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE512   thrpt   10  347.777 ±  5.238  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE64    thrpt   10  347.016 ±  2.038  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE8k    thrpt   10  220.529 ±  0.839  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE128   thrpt   10  346.273 ±  7.606  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE16    thrpt   10  275.622 ±  1.436  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE1m    thrpt   10  345.511 ±  6.223  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE256   thrpt   10  348.193 ± 14.641  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE32    thrpt   10  321.897 ±  2.498  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE512   thrpt   10  354.947 ±  2.515  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE64    thrpt   10  339.376 ±  5.021  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE8k    thrpt   10  218.614 ±  2.389  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE128     thrpt   10  135.477 ±  0.477  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE16      thrpt   10  128.826 ±  0.493  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE1m      thrpt   10  133.019 ±  1.100  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE256     thrpt   10  135.249 ±  0.382  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE32      thrpt   10  138.792 ±  0.382  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE512     thrpt   10  133.881 ±  1.120  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE64      thrpt   10  135.730 ±  0.320  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE8k      thrpt   10  110.442 ±  0.632  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE128     thrpt   10  134.690 ±  0.560  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE16      thrpt   10  128.883 ±  0.583  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE1m      thrpt   10  132.606 ±  1.049  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE256     thrpt   10  134.812 ±  0.587  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE32      thrpt   10  138.150 ±  0.572  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE512     thrpt   10  134.951 ±  0.408  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE64      thrpt   10  135.497 ±  0.874  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE8k      thrpt   10  111.449 ±  0.286  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO128k   thrpt   10   46.183 ±  0.480  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO16     thrpt   10   42.791 ±  0.701  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO32     thrpt   10   45.116 ±  0.071  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO64     thrpt   10   45.764 ±  0.452  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO8k     thrpt   10   43.101 ±  0.208  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO128k  thrpt   10   46.364 ±  0.373  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO16    thrpt   10   44.852 ±  0.131  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO32    thrpt   10   46.075 ±  0.125  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO64    thrpt   10   46.895 ±  0.301  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO8k    thrpt   10   43.116 ±  0.320  ops/s
FileBufferAccessDoubleBenchmark.testOldIO               thrpt   10    0.560 ±  0.006  ops/s
FileBufferAccessDoubleBenchmark.testOldNIO              thrpt   10    0.563 ±  0.008  ops/s
パフォーマンスの比較

10MbytesのファイルのInt値の連続読み取りパフォーマンスのチャート。
縦軸はテストメソッド(10 MBytesファイルの読み取り)の1秒間あたりの実行回数の平均を示す。


やはり、ダイレクトバッファはヒープバッファよりも2倍以上性能が高い。

またバッファサイズとしては256 KiB以上確保したほうが読み込み性能は良いように見える。しかし、256 KiB以上確保しても、それ以上は性能は変わらないようでもある。


10 MbytesのファイルのDouble値の連続読み取りパフォーマンスのチャート。
縦軸はテストメソッド(10 MBytesファイルの読み取り)の1秒間あたり実行回数の平均を示す。


やはり、ダイレクトバッファはヒープバッファよりも2倍以上性能が高い。

また、こちらもバッファサイズとしては256 KiB以上確保したほうが読み込み性能は良いように見える。しかし、256 KiB以上確保しても、やはり、それ以上は性能は変わらないようでもある。

OldBufferedIOの性能が思った以上に低い

今回のベンチマークで思い知ったことは、Javaの古典的なストリーム系のBufferedInputStreamとDataInputStreamの組み合わせが、相当にパフォーマンスが低い、ということだった。
本当に予想外に性能がかなり悪い。
ヒープ上のByteBufferと比較しても、ここまでパフォーマンスが悪いとは思わなかった。


また、うっかりBufferedInputStreamで包むのを忘れてしまったFileInputStreamの性能が大変悲惨であることをチャートで見るとよく分かる。
(これは想定されていたことだが、実際にチャートで比較してみると"悲惨"という言葉に尽きる性能である。)


ただ、Java7で追加されたFiles#newInputStream()はPathで示されるファイルをオープンしてInputStreamを返すコンビニエンスなメソッドだが、コンビニエンスメソッドとして用意されたのであれば、何らかのパフォーマンスのために最低限の構成ができたものが返ってきている可能性もあるのでは?と思ってみたのだが、ぜんぜんそうゆうことはなくて、ただのnew FileInputStream()の代わりにすぎなかったようだ。
(ドキュメントにかかれてないので、まあ、そうだろう、とは思っていたけれど、全然コンビニエンスじゃないところが違和感があるというか。)


また、BufferedInputStreamはデフォルトで8 KiBのバッファサイズをもっているが、これを増やしても、ほとんど性能は向上しなさそうだということも判明した。
(なので、たぶん、大多数の人が引数でバッファサイズを指定していないのだろう。)

結論

今回の実験により、以下のことが分かった。

  • ダイレクトバッファは確保と破棄に注意が必要である。
    • ダイレクトメモリサイズはヒープとは別のJavaVMオプションで設定できる。
    • 確保と破棄が連続する場合では解放が追い付かずOutOfMemoryErrorの発生の恐れがある。
      • System.gc()または内部クラスのCleanerを利用する対策がある。
    • ダイレクトバッファは逐次構築するのではなくキャッシュすると効率が良い。
  • ダイレクトバッファ > ヒープバッファ >> 古典的なストリームI/O という性能差のようである。


実のところ、ダイレクトバッファのベンチマークをとるのは、これで2〜3回目ぐらいである。
しかし、過去のテストでは、ダイレクトバッファは使いどころが難しい、うまくパフォーマンスを発揮させられない、という結論を出してしまっていた。
(今思えばベンチマークとして適切でない部分の時間も含んでいたような気もする。)
また、ヒープ上のByteBufferについても、同じくヒープ上で動作する古典的ストリームI/Oとパフォーマンスに大きな差はないだろう、という先入観のようなものもあったのかもしれない。


だが、今回のテスト結果をみると、どうも過去のテストは明らかに何かを間違えていたかのように思われる。
今回、確実に、かなり高速でデータの読み込みができることの確証を得た。


今後は、ByteBufferがうまくマッチする場合は、優先的にByteBufferによるアクセスに切り替えて行く方が良さそうだ、という教訓が得られたと思う。

今思うことは、どうして、もっと早く再評価してみようと思わなかったのか、ということだろうか。


※ 今回のベンチマーク等のソースコードは、https://gist.github.com/seraphy/c173e3d82afd1ad3f81c にあります。


以上、メモ終了。

*1:ただし、RandomAccessFileから取得されるMappedByteBufferについては、特定のファイルに結びついているので、これは不要になったら、すぐに破棄するべきである。また、ネイティブメモリを早期に解放させるためにgcをかけることが望ましいだろう。これはOSのネイティブなハンドルが閉じられていないと使用中のままになるため、ファイルの読み書き削除に支障がでるためである。

jmhマイクロベンチマークツールを使ったByteBufferのバイトオーダによるパフォーマンスの違いの比較

(ヒープおよびダイレクトバッファによるByteBufferの性能差、およびUnsafeとのパフォーマンスも比較するよ!)
(とりあえず計測結果を先に知りたい人は結論を見てね!)
(ファイルのシーケンシャルリードを含むダイレクトバッファの使い方やパフォーマンス比較については次の日記に書きました。)

概要

思うところあって、ヒープ上のbyte[]をラップしたByteBufferと、ダイレクトバッファを確保しているByteBufferの場合とのint型としての連続アクセスした場合のパフォーマンスの違いについて調べてみることにした。


私の認識では、ダイレクトバッファへのアクセスは非ネイティブ側(つまりJavaコード側)からのアクセスではオーバヘッドが大きいため、特殊なシチュエーション以外では遅くなってしまうため、あまり使いどころのない機能だというものだったのだが、では、その使いどころとなる「特殊なシチュエーション」とは何か、ということについては、いまいちわからなかった。

とくに、ByteBufferを連続的にintアクセスするような用途には、ヒープ上のバッファよりも、むしろ遅くなるのではないか?という漠然とした印象があった。


だが、実際にためしたところ、予想外にも、"ダイレクトバッファはリトルエンディアンのint型をバイト列から読み取るにはヒープ上のバッファよりもものすごく良いパフォーマンスが得られるかも?"という、個人的には非常に興味深い示唆に富む結果を得ることができた。
そこで、ベンチマークツールの使い方の備忘録も兼ねて、ここに記録しておくことにする。


※ なお、JAVAベンチマークは条件が少しでも変われば結果が大きく変わったりするので、あくまでも、「私が試したケース」ではという前提であり、また、どうして、このような結果になったかについては納得のゆく確証が得られていないため、もし、この記事を参考にすることがあるのならば、これを鵜呑みにせず、実際には個々のケースに応じてベンチマークをとるべきだろう。
(その際には、ここで紹介するベンチマークツールjmhが有益だと思われる。)

ベンチマークツールの準備

今回は、ベンチマークツールとして "jmh"を久しぶりに使うことにした。

このツールは、「まともに作るのが案外難しい」ベンチマークコードの作成をアシストして、さらにベンチマーク結果の統計情報(標準偏差や信頼区間)も計算して出してくれるようにしてくれる、大変便利なベンチマークジェネレータツールである。


1年以上つかってなかったのだが、久しぶりに使ってみたら、ちょっと変化があって、古いコードとは、ほんの少し書き方が変わっているようだ。
ネット上の資料でも古い書き方をしているものがあるので、その点は最新の書き方に改める必要がある。
とはいえ、基本的な考え方や使い方などは変わっていない。


jmhの概要や使い方については、少し古い記事だが以下のページが大変参考になる。

(あとは公式ページと、沢山ある付属サンプルから、なんとかする。)

jmhとは?

jmhは、openjdkで開発されている、java用のマイクロベンチマークツールである。

http://openjdk.java.net/projects/code-tools/jmh/


JAVAではコンパイル時最適化や実行時最適化によってソースコード上から予想される動きよりも、はるかに最適化された挙動を示すことが多々ある。

パフォーマンス比較するつもりで書いた単純なコードが実運用ではありえない形にアグレッシブに最適化されたり、あるいは逆に、通常ではJITによる最適化が行われるべきところをベンチマークの実行が浅くて最適化されてない状態で比較する、もしくは、評価したい部分とは関係ない部分でのパフォーマンスを計測に含んでしまうなど、頓珍漢なパフォーマンス比較になっていたりしたら、大変な判断ミスを犯すことになるだろう。

このような最適化がされるような状況でベンチマークをとるのは、めんどくさいことは容易に想像できる。


これらを比較的容易にベンチマークをとれるようにお膳立てしてくれるのが、jmhである。


参考:


ただし、jmhはjunitのようなテストライブラリではない。


jmhは、マイクロベンチマークを行うためのソースコードを自動生成して、その自動生成されたテストコードの上から、ユーザーが定義したベンチマークコードを実行するための、実行可能jarを生成する、いうなれば「テストベンチジェネレータ」ツールなのである。

ベンチマークプロジェクトの準備

jmhを使うのは簡単で、mavenから新規アーキタイプとして"jmh-java-benchmark-archetype"を指定してプロジェクトを作成すれば、必要なものは自動的に手に入るようになっている。

<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-java-benchmark-archetype</artifactId>
	<version>1.11.2</version>
</dependency>

最近のLunaとかMarsあたりのEclipseであればmavenは初めから使えるようになっている。*1



初期状態では、このアーキタイプはローカルマシン上のMavenリポジトリには登録されていないと思われるので、まずは、アーキタイプを登録しておく。(登録後、一度ダイアログを閉じて再度新規プロジェクト作成を開きなおすと、一覧に出てくるようになる。)


※ ここでは現時点の最新であるバージョン1.11.2を使っている。(最新はリポジトリを確認のこと。)



こうしてプロジェクトが作成されるので、まずはpom.xmlを修正して、
java8をターゲットに変えておく。

※ 当然ながら、javaのバージョンが違えばパフォーマンスも変わってくるだろう。



(pom.xmlの抜粋)

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jmh.version>1.11.2</jmh.version>
        <javac.target>1.8</javac.target>
        <uberjar.name>benchmarks</uberjar.name>
    </properties>
空のベンチマークの実行

初期状態で生成されたクラスは空のベンチマークであるが、ビルドすればパフォーマンスの計測は可能である。

package jp.seraphyware;

import org.openjdk.jmh.annotations.Benchmark;

public class MyBenchmark {

    @Benchmark
    public void testMethod() {
        // This is a demo/sample template for building your JMH benchmarks. Edit
        // as needed.
        // Put your benchmark code here.
    }
}

プロジェクト内にあるクラスがスキャンされ、@Benchmarkアノテーションがついているすべてのメソッドがベンチマークメソッドとして認識される。

(※ ちなみに、以前は@Benchmarkではなく、@GenerateMicroBenchmarkというアノテーションが使われていたように思う。)


ただし、このベンチマークを実行するためには、まずはmavenによるビルドが必要である。


先に説明したとおり、jmhはライブラリではなく、テストベンチジェネレータであり、mavenでビルドすることによりさまざまなファイルを自動生成する。
これらのファイルがないとベンチマークを動かすことができない。

mvn clean package



(画面キャプチャを取り損ねたので素のEclipseで撮りなおしています。)


ビルドが完了すると、targetフォルダ上に実行可能なjar"benchmarks.jar"が生成されている。

コマンドプロンプトを開き、これを実行することができる。


以下はコマンド実行と、その結果である。

>set PATH=C:\java\jdk1.8.0_60\bin;%PATH%
>java -jar benchmarks.jar "MyBenchmark.testMethod" -f1 -wi 1 -i 1
# JMH 1.11.2 (released 15 days ago)
# VM version: JDK 1.8.0_60, VM 25.60-b23
# VM invoker: C:\java\jdk1.8.0_60\jre\bin\java.exe
# VM options: <none>
# Warmup: 1 iterations, 1 s each
# Measurement: 1 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: jp.seraphyware.MyBenchmark.testMethod

# Run progress: 0.00% complete, ETA 00:00:02
# Fork: 1 of 1
# Warmup Iteration   1: 3680300939.059 ops/s
Iteration   1: 3687070753.959 ops/s


Result "testMethod":
  3687070753.959 ops/s


# Run complete. Total time: 00:00:02

Benchmark                Mode  Cnt           Score   Error  Units
MyBenchmark.testMethod  thrpt       3687070753.959          ops/s

>

オプション引数は"-h"で確認できる。


上記の場合の引数は、テストする「クラス.メソッド名」を明示して、ウォームアップイテレーションx1, イテレーションx1, フォークx1、という指定である。
(いうまでもなく、これは単なる動作確認で、計測するには不十分な回数である。)


また、テストするクラス・メソッド名は正規表現で判断されるので、この場合、たとえば、"MyBenchmark"としても、それにマッチするすべてのベンチマークが実行されることになる。(何も指定しないと、すべてのベンチマークメソッドが実行される。)


実行結果の「Score 3687070753.959 ops/s」というのは、デフォルトのテストモードが「スループット計測モード」であるため、これは1秒間に何回テストベンチを処理できたかの回数を表すものである。
(1イテレーションベンチマークはデフォルトでは1秒間となっている。)
(ベンチマークメソッドが空なので、ものすごい回数が回っている。)


今回はイテレーション数が1のためベンチマーク実行回数が1回しかないため表示されていないが、
複数回のイテレーションを行った場合には、最小値、最大値、平均値、標準偏差、99.9%信頼区間などの数値が計算される。

IDEからの実行

一度mavenでビルドすると、テストベンチ用のソースコードも生成されているのでEclipseからも実行できるようになっている。

Mainメソッドとして、ベンチマークを起動するために、以下のようなものを追加する。

    /**
     * IDEから実行する場合は、
     * まず先にmvn package等を実行してサポートファイル類を生成しておく必要がある.
     * (少なくともクラスまたはメソッドのシグネチャを変更追加するたびにmvnでビルドする必要がある。)
     * その後、IDEから、このメイン関数を呼び出すことができるようになる.
     * @param args
     * @throws RunnerException
     */
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                // 実行すべきベンチマークが定義された"クラス名.メソッド名"を正規表現で指定
                .include(MyBenchmark.class.getSimpleName())
                // ウォームアップイテレーション数 "-wi"オプションに相当
                .warmupIterations(20)
                // イテレーション数 "-i"オプションに相当
                .measurementIterations(10)
                // フォーク数 "-f"オプションに相当
                .forks(1)
                // 1回のテストメソッド実行時間 "-r"オプションに相当
                .measurementTime(TimeValue.seconds(1))
                // 同時実行スレッド数 (1測定毎のスレッド数 (サンプル数が増える))
                // .threads(Math.max(1, Runtime.getRuntime().availableProcessors() / 2))
                // 測定するモード = スループット
                .mode(Mode.Throughput)
                .build();
        new Runner(opt).run();
    }

これで、このクラスを実行クラスに選択すれば、ベンチマークを実行することができるようになる。


ただし、ベンチマーククラスまたはメソッド(およびメソッドの引数等のシグネチャ)を追加削除変更した場合には、再度、mavenによるビルドが必要となる。(その後、Eclipseのパッケージエクスプローラのリフレッシュを忘れずに。)


※ OptionsBuilderのincludeメソッドで実行するベンチマークの「クラス名.メソッド名」を正規表現で絞りこめるので、ベンチマークコードを試行錯誤している間は、メソッド名まで指定すると良いだろう。

ByteBufferのパフォーマンス比較コードの作成

ByteArrayには、ヒープ上のbyte[]配列をラップしたものと、ヒープ外に確保したダイレクトバッファによるものの2種類がある。


ダイレクトバッファはヒープ外にあるため、OSなどネイティブなヒープ外のコードからアクセスする場合にはオーバヘッドがないかわりに、逆に、Javaのコードからアクセスするにはオーバヘッドが生ずる。
このトレードオフを見極めるのが難しく、ダイレクトバッファを使えばなんでも速くなるけではない。(むしろ、普通のバイトバッファのように扱えば、大抵遅くなるだろう。)


ただし、この2種類はインターフェイスは共通であり、同じようなコードで扱うことができる。


今回は、バイトバッファからint値を連続して読み込むために以下のようなコードを用いる。

    /**
     * バイトバッファからint値を読み出すテスト
     * @param buf
     */
    private static void runByteBuffer(ByteBuffer buf) {
        buf.rewind();
        int limit = buf.limit();
        long total = 0;
        while (buf.position() < limit) {
            total += buf.getInt();
        }
        if (total != 8589869056L) {
            throw new RuntimeException("actual=" + total);
        }
    }

このコードは単純に、ByteBufferのバッファ上から、すべてのint値を読み込んでtotal変数に足しこんでいるだけである。

最後に、total変数を8589869056Lという謎の数値と照合させているが、これはテストデータとして用意したバイト列を正しく読み出した場合に計算される値である。


ヒープ上のbyte[]配列をByteBufferにラップしたものと、ダイレクトバッファの双方で、このコードを使って計測する。


テストデータは以下のようにして生成している。

(バッファサイズは今回は512KiBとしている。)

    /**
     * データを初期化する.
     * @param buf
     */
    private void initData(ByteBuffer buf) {
        IntBuffer intBuf = buf.asIntBuffer();
        long total = 0;
        for (int idx = 0; idx < data.length / 4; idx++) {
            intBuf.put(idx);
            total += idx;
        }
        //System.out.println("★total=" + total);
        if (total != 8589869056L) {
            throw new RuntimeException("actual=" + total);
        }
    }

テストの状態を保持する@Stateアノテーションの利用

このテストデータを生成するタイミングをコントロールするために、jmhの@Stateという仕組みを用いる。

前述のとおり、テストベンチメソッドは繰り返し大量に呼び出されるものなので、都度、テストデータを作成していては計測がおぼつかない。

このため、テストベンチに対してテストの状態を保持するインスタンスを持たせることができるようになっている。


@Stateというアノテーションがつけられたクラスを、テストベンチメソッドの引数にすることで、
jmhテストベンチによってベンチマークメソッドが呼び出されるときに自動的に、そのインスタンスが引き渡されてくるようになる。

@Stateアノテーションがつけられたクラスは、内部に「@Setup」「@TearDown」のアノテーションがついたメソッドを定義することができ、

の3種類のタイミングでメソッドを呼びすように設定できる。

(前述のとおり、テストベンチメソッドは繰り返し大量に呼び出されるものなので、Invocationの使用には注意が必要となる。)


ここで、Trialレベルの@Setupメソッドでテストデータを準備すれば、
純粋にByteBufferをint値でアクセスする部分だけを大量に繰り返しテストできるようになる。

※ なお、テストベンチはマルチスレッドでテストすることもできるので、その場合、@StateアノテーションにScope.Threadを指定することにより、スレッドスコープとしてスレッド間の衝突なく使えるようにすることができる。(このほかに、Benchmark, Groupのスコープがある。)

    /**
     * テスト中にスレッドごとベンチマークごとの状態を保持するインスタンス.
     *
     * @author seraphy
     */
    @State(Scope.Thread)
    protected static class BenchContextBase {

        /**
         * バイトオーダー
         */
        private final ByteOrder byteOrder;

        /**
         * ヒープ
         */
        private byte[] data;

        /**
         * ヒープをラップしたバッファ
         */
        private ByteBuffer bufHeap;

        /**
         * ダイレクトバッファ
         */
        private ByteBuffer bufDirect;

        /**
         * バイトオーダを指定して構築する.
         * @param byteOrder
         */
        protected BenchContextBase(ByteOrder byteOrder) {
            this.byteOrder = byteOrder;
        }

        /**
         * データの初期化.
         * Trialの場合、スレッドごとベンチマークの開始ごとに一度呼び出される.<br>
         * (ウォームアップイテレーション、および計測イテレーション中には呼び出されない。)
         * (Trialがデフォルト、そのほかにIteration, Invocationが指定可能.)
         */
        @Setup(Level.Trial)
        public void setup() {
            // ヒープ上にデータを作成する
            byte[] data = new byte[512 * 1024]; // 512kib
            this.data = data;

            // ヒープ上のByteBufferを作成する.
            bufHeap = ByteBuffer.wrap(data);
            bufHeap.order(byteOrder);
            // ※ テストデータを作成する
            initData(bufHeap);

            // ダイレクトメモリを確保してByteBufferを作成する
            // (データはヒープのものをコピーする)
            bufDirect = ByteBuffer.allocateDirect(data.length);
            bufDirect.order(byteOrder);
            bufDirect.put(data);
        }
   ... (中略) ...
   }

今回は、ByteBufferにつめるデータ形式として、JavaのデフォルトであるBigEndianと、Intel系CPUのデフォルトであるLittleEndianの2種類のデータを作成できるようにしている。(上記コード中のbyteOrder変数が、それにあたる。)

    /**
     * データの並びがLittleEndianのバッファをもつテストデータを作成する.
     */
    public static class BenchContextLE extends BenchContextBase {

        public BenchContextLE() {
            super(ByteOrder.LITTLE_ENDIAN); // Unsafe(intel)にあわせる
        }
    }

    /**
     * データの並びがBigEndianのバッファをもつテストデータを作成する.
     */
    public static class BenchContextBE extends BenchContextBase {

        public BenchContextBE() {
            super(ByteOrder.BIG_ENDIAN); // JAVA標準にあわせる
        }
    }

Unsafeによるbyte[]へのint値としての直接アクセスの実験もしてみる

今回はByteBufferでのアクセスの計測の他に、sun.misc.Unsafeというネイティブ的な動作をするAPI群があり、この中にオブジェクト上の任意のバッファをint値として読み取ることのできる機能があるらしいので、これも、ためしに使って計測してみる。


参考元:


もちろん、sun.misc.Unsafeは、非公開、非推奨のベンダ固有APIである。

OpenJDKとSun系のJDKにはとりあえず現在は存在しており、各種有名どころのライブラリも使っていたりするらしい。

(また、JavaVMまわりの制御を行えるので、特にロックまわりではsun.misc.Unsafeを使わないとどうしても実現できないようなものもあるといううわさも聞いた。)*2

    /**
     * ヒープからUnsafeを使ってint値を読み出すテスト
     * @param unsafe
     * @param data
     */
    @SuppressWarnings("restriction")
    private static void runUnsafe(sun.misc.Unsafe unsafe, byte[] data) {
        int offset = sun.misc.Unsafe.ARRAY_BYTE_BASE_OFFSET;
        int len = data.length + offset;
        int pos = offset;
        long total = 0;
        while (pos < len) {
            total += unsafe.getInt(data, (long) pos);
            pos += 4;
        }
        if (total != 8589869056L) {
            throw new RuntimeException("actual=" + total);
        }
    }

このコードはbyteを4バイト飛ばしでint値として無理やり読み込んでいるものである。

byte配列の読み出しには、オフセットとしてsun.misc.Unsafe.ARRAY_BYTE_BASE_OFFSETが必要になるらしい。*3


また、そもそもUnsafeを扱うためには、直接、Unsafeを取り出そうとするとヌルクラスローダ上のクラスからの呼び出しであるか判定して、システム外クラスからの呼び出しはセキュリティエラーにする実装となっているため、リフレクションを経由して無理やり取ってくる必要がある。

    /**
     * アンセーフ
     */
    @SuppressWarnings("restriction")
    private sun.misc.Unsafe unsafe;

    /**
     * アンセーフをリフレクションによって取得する.
     */
    @SuppressWarnings("restriction")
    private void initUnsafe() {
        try {
            Field field = sun.misc.Unsafe.class
                    .getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (sun.misc.Unsafe) field.get(null);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

さらに、byte[]に格納されたLittle Endianを自力でint値にしてみる

何回かBenchmarkをとったところ、予想外にヒープのByteBufferの性能が悪そうなので、比較のために、自力でbyte[]からLittle Endianのint値をアクセスするコードのベンチマークもとってみることにする。

    @Benchmark
    public void testHeapByteArray(BenchContextLE ctx) {
        byte[] data = ctx.getData();
        int len = data.length;
        int pos = 0;
        long total = 0;
        while (pos < len) {
            total += ((data[pos + 3] & 0xFF) << 24)
                    | ((data[pos + 2] & 0xFF) << 16)
                    | ((data[pos + 1] & 0xFF) << 8)
                    | (data[pos + 0] & 0xFF);
            pos += 4;
        }
        if (total != 8386560) {
            throw new RuntimeException("actual=" + total);
        }
    }

※ 参考: https://www.jpcert.or.jp/java-rules/fio12-j.html

ベンチマークメソッドの定義

これで、

  • ヒープ/ダイレクトのByteBuffer
  • BigEndian/LittleEndian
  • Unsafe#getIntによるintアクセス
  • byte[]の自力でのintアクセス

の準備が整った。

このテストベンチは以下のようになる。

    /**
     * LittleEndianのデータをヒープ上のバイトバッファから読み出すベンチマーク.
     * @param ctx
     */
    @Benchmark
    public void testHeapByteBufferLE(BenchContextLE ctx) {
        runByteBuffer(ctx.getHeapByteBuffer());
    }

    /**
     * LittleEndianのデータをダイレクトバイトバッファから読み出すベンチマーク.
     * @param ctx
     */
    @Benchmark
    public void testDirectByteBufferLE(BenchContextLE ctx) {
        runByteBuffer(ctx.getDirectByteBuffer());
    }

    /**
     * LittleEndianのデータをアンセーフを使って読み出すベンチマーク
     * @param ctx
     */
    @Benchmark
    public void testUnsafeLE(BenchContextLE ctx) {
        runUnsafe(ctx.getUnsafe(), ctx.getData());
    }

    /**
     * BigEndianのデータをヒープ上のバイトバッファから読み出すベンチマーク.
     * @param ctx
     */
    @Benchmark
    public void testHeapByteBufferBE(BenchContextBE ctx) {
        runByteBuffer(ctx.getHeapByteBuffer());
    }

    /**
     * BigEndianのデータをダイレクトバイトバッファから読み出すベンチマーク.
     * @param ctx
     */
    @Benchmark
    public void testDirectByteBufferBE(BenchContextBE ctx) {
        runByteBuffer(ctx.getDirectByteBuffer());
    }

ベンチマークの実行結果

これでベンチマークを実行したところ、以下のような結果が得られた。(抜粋)


実行したのはWindows8.1(x64)で、CPUはXeon E3-1270v2@3.5GHzの環境である。

(Windows7(x64) で、i7-3770@3.40GHzでも、ほぼ同じ結果となった。)

# JMH 1.11.2 (released 16 days ago)
# VM version: JDK 1.8.0_60, VM 25.60-b23
# VM invoker: C:\Java\jdk1.8.0_60\jre\bin\java.exe
# VM options: -Dfile.encoding=UTF-8
# Warmup: 20 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: jp.seraphyware.jmhexample.BufferAccessBenchmark.testDirectByteBufferBE

Result "testDirectByteBufferBE":
  14749.779 ±(99.9%) 54.380 ops/s [Average]
  (min, avg, max) = (14690.298, 14749.779, 14806.503), stdev = 35.969
  CI (99.9%): [14695.400, 14804.159] (assumes normal distribution)

Result "testDirectByteBufferLE":
  21970.174 ±(99.9%) 154.358 ops/s [Average]
  (min, avg, max) = (21833.488, 21970.174, 22122.518), stdev = 102.098
  CI (99.9%): [21815.816, 22124.532] (assumes normal distribution)

Result "testHeapByteArray":
  4744.254 ±(99.9%) 83.476 ops/s [Average]
  (min, avg, max) = (4618.953, 4744.254, 4799.570), stdev = 55.214
  CI (99.9%): [4660.778, 4827.730] (assumes normal distribution)

Result "testHeapByteBufferBE":
  4518.875 ±(99.9%) 491.083 ops/s [Average]
  (min, avg, max) = (4124.723, 4518.875, 4919.513), stdev = 324.821
  CI (99.9%): [4027.792, 5009.958] (assumes normal distribution)

Result "testHeapByteBufferLE":
  4581.578 ±(99.9%) 618.657 ops/s [Average]
  (min, avg, max) = (4104.452, 4581.578, 4948.241), stdev = 409.203
  CI (99.9%): [3962.921, 5200.234] (assumes normal distribution)

Result "testUnsafeLE":
  21981.865 ±(99.9%) 125.801 ops/s [Average]
  (min, avg, max) = (21841.979, 21981.865, 22068.251), stdev = 83.209
  CI (99.9%): [21856.064, 22107.665] (assumes normal distribution)


# Run complete. Total time: 00:03:02

Benchmark                                      Mode  Cnt      Score     Error  Units
BufferAccessBenchmark.testDirectByteBufferBE  thrpt   10  14749.779 ±  54.380  ops/s
BufferAccessBenchmark.testDirectByteBufferLE  thrpt   10  21970.174 ± 154.358  ops/s
BufferAccessBenchmark.testHeapByteArray       thrpt   10   4744.254 ±  83.476  ops/s
BufferAccessBenchmark.testHeapByteBufferBE    thrpt   10   4518.875 ± 491.083  ops/s
BufferAccessBenchmark.testHeapByteBufferLE    thrpt   10   4581.578 ± 618.657  ops/s
BufferAccessBenchmark.testUnsafeLE            thrpt   10  21981.865 ± 125.801  ops/s


※ このベンチマークは512kibのバッファの読み込みを繰り返すものであり、上記スコア値はops/sの平均、つまり1秒間あたりの実行回数の平均なので、簡単に処理速度を求めるなら、単純に512kibをかければ良い。

他の計測モード

jmhでは計測モードとして、デフォルトのスループットの他に、平均時間、サンプルタイム、シングルショットタイムを選択できる。

  • スループット(ops/s)はベンチマークを1秒間に何回実行したかを示すものである。
  • 平均時間は、これを時間に換算したものである。(ops/sで1を割れば時間が出てくる。)
  • サンプルタイムは、実行時間の分散を調べるものであり、たとえば実行したベンチマークのうち、50%は最大で、どのぐらいの所要時間が必要だったか?また、実行したベンチマークの90%は最大で、どのくらい所要時間が必要だったか?といった値を見ることができる。また、結果としては最大、最小、平均の所要時間と標準偏差、サンプル総数などが得られるので、時間を計測するならばサンプルタイムのほうが良いかもしれない。
  • シングルショットタイムは、イテレーションではなくバッチサイズで指定し、バッチサイズ分実行したトータルタイムを出す。(単純なパフォーマンス計測に向く?)


以下はサンプルタイムで計測しなおした結果である。

計測モードとして、サンプルタイムを指定し、単位はマイクロ秒とする。
(コマンドラインから実行する場合は、オプションとして-bmオプションでモード指定する。)

Options opt = new OptionsBuilder()
    .(中略)
    .timeUnit(TimeUnit.MICROSECONDS)
    .mode(Mode.SampleTime)
    .build();

サンプルタイムの結果 (抜粋)

# JMH 1.11.2 (released 16 days ago)
# VM version: JDK 1.8.0_60, VM 25.60-b23
# VM invoker: C:\Java\jdk1.8.0_60\jre\bin\java.exe
# VM options: -Dfile.encoding=UTF-8
# Warmup: 20 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Sampling time
# Benchmark: jp.seraphyware.jmhexample.BufferAccessBenchmark.testDirectByteBufferBE

Iteration   1: n = 14259, mean = 70 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 73, 85, 129, 164, 180, 181 us/op
Iteration   2: n = 14022, mean = 71 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 76, 87, 138, 176, 377, 485 us/op
Iteration   3: n = 14341, mean = 70 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 67, 70, 84, 135, 175, 342, 390 us/op
Iteration   4: n = 14122, mean = 71 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 76, 87, 137, 172, 241, 241 us/op
Iteration   5: n = 14138, mean = 71 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 76, 87, 139, 165, 198, 205 us/op
Iteration   6: n = 14624, mean = 68 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 66, 69, 77, 103, 145, 181, 184 us/op
Iteration   7: n = 14589, mean = 68 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 66, 69, 78, 103, 153, 173, 174 us/op
Iteration   8: n = 14578, mean = 69 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 66, 69, 78, 107, 148, 163, 164 us/op
Iteration   9: n = 14606, mean = 68 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 66, 69, 76, 105, 147, 165, 169 us/op
Iteration  10: n = 14259, mean = 70 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 75, 85, 121, 155, 207, 233 us/op

Result "testDirectByteBufferBE":
  69.583 ±(99.9%) 0.088 us/op [Average]
  (min, avg, max) = (65.664, 69.583, 484.864), stdev = 10.159
  CI (99.9%): [69.495, 69.672] (assumes normal distribution)
  Samples, N = 143538
        mean =     69.583 ±(99.9%) 0.088 us/op
         min =     65.664 us/op
  p( 0.0000) =     65.664 us/op
  p(50.0000) =     67.712 us/op
  p(90.0000) =     70.912 us/op
  p(95.0000) =     83.840 us/op
  p(99.0000) =    126.492 us/op
  p(99.9000) =    162.304 us/op
  p(99.9900) =    207.104 us/op
  p(99.9990) =    443.401 us/op
  p(99.9999) =    484.864 us/op
         max =    484.864 us/op


Iteration   1: n = 10857, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 53, 88, 108, 149, 149 us/op
Iteration   2: n = 10619, mean = 47 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 45, 47, 55, 97, 117, 150, 151 us/op
Iteration   3: n = 10738, mean = 47 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 55, 90, 110, 142, 143 us/op
Iteration   4: n = 10942, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 47, 72, 106, 133, 133 us/op
Iteration   5: n = 10874, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 53, 73, 105, 136, 136 us/op
Iteration   6: n = 10878, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 53, 74, 106, 119, 119 us/op
Iteration   7: n = 10955, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 48, 71, 106, 107, 107 us/op
Iteration   8: n = 10945, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 47, 71, 106, 107, 108 us/op
Iteration   9: n = 10842, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 54, 86, 110, 172, 174 us/op
Iteration  10: n = 10852, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 53, 86, 111, 122, 122 us/op

Result "testDirectByteBufferLE":
  46.035 ±(99.9%) 0.064 us/op [Average]
  (min, avg, max) = (43.968, 46.035, 174.336), stdev = 6.423
  CI (99.9%): [45.971, 46.099] (assumes normal distribution)
  Samples, N = 108502
        mean =     46.035 ±(99.9%) 0.064 us/op
         min =     43.968 us/op
  p( 0.0000) =     43.968 us/op
  p(50.0000) =     44.224 us/op
  p(90.0000) =     46.592 us/op
  p(95.0000) =     53.312 us/op
  p(99.0000) =     81.408 us/op
  p(99.9000) =    107.520 us/op
  p(99.9900) =    131.635 us/op
  p(99.9990) =    172.333 us/op
  p(99.9999) =    174.336 us/op
         max =    174.336 us/op


Iteration   1: n = 6413, mean = 156 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 162, 169, 220, 326, 342, 342 us/op
Iteration   2: n = 6407, mean = 156 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 161, 170, 240, 321, 344, 344 us/op
Iteration   3: n = 6409, mean = 156 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 169, 288, 343, 444, 444 us/op
Iteration   4: n = 6436, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 162, 169, 218, 313, 354, 354 us/op
Iteration   5: n = 6473, mean = 154 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 158, 165, 216, 333, 413, 413 us/op
Iteration   6: n = 6445, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 168, 220, 317, 344, 344 us/op
Iteration   7: n = 6462, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 168, 209, 322, 360, 360 us/op
Iteration   8: n = 6460, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 168, 214, 316, 334, 334 us/op
Iteration   9: n = 6473, mean = 154 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 168, 220, 319, 352, 352 us/op
Iteration  10: n = 6440, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 160, 168, 220, 325, 344, 344 us/op

Result "testHeapByteArray":
  155.058 ±(99.9%) 0.194 us/op [Average]
  (min, avg, max) = (149.504, 155.058, 444.416), stdev = 14.943
  CI (99.9%): [154.864, 155.251] (assumes normal distribution)
  Samples, N = 64418
        mean =    155.058 ±(99.9%) 0.194 us/op
         min =    149.504 us/op
  p( 0.0000) =    149.504 us/op
  p(50.0000) =    150.528 us/op
  p(90.0000) =    159.488 us/op
  p(95.0000) =    168.448 us/op
  p(99.0000) =    227.072 us/op
  p(99.9000) =    325.632 us/op
  p(99.9900) =    357.947 us/op
  p(99.9990) =    444.416 us/op
  p(99.9999) =    444.416 us/op
         max =    444.416 us/op


Iteration   1: n = 4607, mean = 217 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 212, 226, 243, 359, 433, 477, 477 us/op
Iteration   2: n = 4666, mean = 214 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 221, 233, 330, 454, 470, 470 us/op
Iteration   3: n = 4706, mean = 212 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 219, 227, 311, 423, 442, 442 us/op
Iteration   4: n = 4708, mean = 212 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 218, 227, 306, 418, 498, 498 us/op
Iteration   5: n = 4697, mean = 213 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 225, 228, 289, 444, 565, 565 us/op
Iteration   6: n = 4701, mean = 213 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 218, 227, 314, 460, 479, 479 us/op
Iteration   7: n = 4707, mean = 212 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 219, 226, 295, 397, 446, 446 us/op
Iteration   8: n = 4703, mean = 212 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 219, 227, 307, 436, 467, 467 us/op
Iteration   9: n = 4695, mean = 213 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 225, 226, 339, 454, 461, 461 us/op
Iteration  10: n = 4608, mean = 217 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 212, 225, 243, 362, 464, 481, 481 us/op

Result "testHeapByteBufferBE":
  213.519 ±(99.9%) 0.304 us/op [Average]
  (min, avg, max) = (206.848, 213.519, 565.248), stdev = 19.993
  CI (99.9%): [213.215, 213.823] (assumes normal distribution)
  Samples, N = 46798
        mean =    213.519 ±(99.9%) 0.304 us/op
         min =    206.848 us/op
  p( 0.0000) =    206.848 us/op
  p(50.0000) =    207.360 us/op
  p(90.0000) =    220.672 us/op
  p(95.0000) =    229.888 us/op
  p(99.0000) =    323.072 us/op
  p(99.9000) =    437.760 us/op
  p(99.9900) =    479.888 us/op
  p(99.9990) =    565.248 us/op
  p(99.9999) =    565.248 us/op
         max =    565.248 us/op


Iteration   1: n = 5174, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 200, 207, 261, 382, 426, 426 us/op
Iteration   2: n = 5205, mean = 192 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 197, 203, 260, 398, 419, 419 us/op
Iteration   3: n = 5196, mean = 192 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 197, 203, 262, 392, 408, 408 us/op
Iteration   4: n = 5180, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 201, 207, 268, 381, 398, 398 us/op
Iteration   5: n = 5196, mean = 192 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 197, 206, 259, 399, 514, 514 us/op
Iteration   6: n = 5183, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 199, 206, 263, 389, 414, 414 us/op
Iteration   7: n = 5169, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 201, 207, 261, 390, 429, 429 us/op
Iteration   8: n = 5185, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 199, 207, 252, 395, 426, 426 us/op
Iteration   9: n = 5186, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 199, 206, 253, 360, 392, 392 us/op
Iteration  10: n = 5177, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 200, 207, 257, 398, 435, 435 us/op

Result "testHeapByteBufferLE":
  192.723 ±(99.9%) 0.216 us/op [Average]
  (min, avg, max) = (187.904, 192.723, 513.536), stdev = 14.930
  CI (99.9%): [192.508, 192.939] (assumes normal distribution)
  Samples, N = 51851
        mean =    192.723 ±(99.9%) 0.216 us/op
         min =    187.904 us/op
  p( 0.0000) =    187.904 us/op
  p(50.0000) =    188.416 us/op
  p(90.0000) =    198.656 us/op
  p(95.0000) =    206.592 us/op
  p(99.0000) =    259.328 us/op
  p(99.9000) =    389.632 us/op
  p(99.9900) =    424.656 us/op
  p(99.9990) =    513.536 us/op
  p(99.9999) =    513.536 us/op
         max =    513.536 us/op


Iteration   1: n = 11004, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 70, 98, 107, 107 us/op
Iteration   2: n = 11028, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 69, 106, 146, 148 us/op
Iteration   3: n = 11026, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 46, 47, 65, 105, 114, 115 us/op
Iteration   4: n = 11000, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 71, 106, 114, 115 us/op
Iteration   5: n = 11001, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 72, 106, 156, 158 us/op
Iteration   6: n = 11084, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 63, 106, 114, 115 us/op
Iteration   7: n = 10998, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 46, 47, 71, 100, 131, 133 us/op
Iteration   8: n = 11009, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 71, 105, 106, 106 us/op
Iteration   9: n = 10933, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 47, 73, 106, 117, 117 us/op
Iteration  10: n = 10578, mean = 47 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 45, 47, 63, 88, 116, 145, 146 us/op

Result "testUnsafeLE":
  45.545 ±(99.9%) 0.053 us/op [Average]
  (min, avg, max) = (43.968, 45.545, 157.696), stdev = 5.347
  CI (99.9%): [45.492, 45.598] (assumes normal distribution)
  Samples, N = 109661
        mean =     45.545 ±(99.9%) 0.053 us/op
         min =     43.968 us/op
  p( 0.0000) =     43.968 us/op
  p(50.0000) =     44.224 us/op
  p(90.0000) =     46.592 us/op
  p(95.0000) =     46.912 us/op
  p(99.0000) =     72.064 us/op
  p(99.9000) =    105.856 us/op
  p(99.9900) =    124.071 us/op
  p(99.9990) =    156.756 us/op
  p(99.9999) =    157.696 us/op
         max =    157.696 us/op


# Run complete. Total time: 00:03:02

Benchmark                                       Mode     Cnt    Score   Error  Units
BufferAccessBenchmark.testDirectByteBufferBE  sample  143538   69.583 ± 0.088  us/op
BufferAccessBenchmark.testDirectByteBufferLE  sample  108502   46.035 ± 0.064  us/op
BufferAccessBenchmark.testHeapByteArray       sample   64418  155.058 ± 0.194  us/op
BufferAccessBenchmark.testHeapByteBufferBE    sample   46798  213.519 ± 0.304  us/op
BufferAccessBenchmark.testHeapByteBufferLE    sample   51851  192.723 ± 0.216  us/op
BufferAccessBenchmark.testUnsafeLE            sample  109661   45.545 ± 0.053  us/op

結果

int型の場合

まず、(驚いたことに)DirectBufferでのint型(32bit)のLittleEndianのデコードが、かなり速い



そして、Unsafeが、当然速いというのは分かったが、DirectByteBuffer(Little Endian)も、Unsafeとかわらないぐらい速い。
これはバッファサイズが512kibの場合だが、16kibなど小さいバッファで比較しても同様である。

(このくらい速ければ、わざわざ非推奨なベンダ依存のAPIを使う必要もなさそうである。)


正直、この速さは意外だった。
しかし、どうして速いのかは、よく分からない。


CPUのネイティブなレイアウトであるLittle Endianと同じだから変換不要で速くなるとかかな?と思ってみたものの、同じDirectByteBufferのBigEndianのケースでも、byteをラップしたヒープによるByteBufferよりも速いので、そうゆう問題ではないのかもしれない。

byteをラップしたByteBufferに問題があるのかと思って、自前でbyte[]をLittle Endianのintとしてアクセスするベンチマークとも比較してみたが、似たような性能となったので、特別にByteBufferの仕組みに問題があるわけではなさそう。


どうやら、StackOverflowで見つけた記事によると、ヒープのByteBufferとダイレクトのByteBufferでは、以下のように読み取り方法が違うらしい。
http://stackoverflow.com/questions/6943858/bytebuffer-getint-question

// HeapByteBuffer

public int getInt() {
    return Bits.getInt(this, ix(nextGetIndex(4)), bigEndian);
}

public int getInt(int i) {
    return Bits.getInt(this, ix(checkIndex(i, 4)), bigEndian);
}

// DirectByteBuffer

private int getInt(long a) {
    if (unaligned) {
        int x = unsafe.getInt(a);
        return (nativeByteOrder ? x : Bits.swap(x));
    }
    return Bits.getInt(a, bigEndian);
}

public int getInt() {
    return getInt(ix(nextGetIndex((1 << 2))));
}

public int getInt(int i) {
    return getInt(ix(checkIndex(i, (1 << 2))));
}

これから考えると、

  1. 単純にUnsafe#getIntで読み取れれば最速である。
  2. DirectBufferでは、もし、エンディアンが一致しない場合には、後続にswap処理が入る分、遅くなる。

これがDirectBufferでのintアクセス時のリトルエンディアンとビッグエンディアンとのパフォーマンスの違いのようだ。

Double型の場合

ついでに、Double(8バイト)型の場合も計測してみた。

Benchmark                                       Mode  Cnt      Score     Error  Units
BufferAccessBenchmark2.testDirectByteBufferBE  thrpt   10  18947.259 ± 179.970  ops/s
BufferAccessBenchmark2.testDirectByteBufferLE  thrpt   10  19048.996 ±  50.217  ops/s
BufferAccessBenchmark2.testHeapByteBufferBE    thrpt   10   3955.089 ±   6.115  ops/s
BufferAccessBenchmark2.testHeapByteBufferLE    thrpt   10   3869.472 ±  57.203  ops/s
BufferAccessBenchmark2.testUnsafeLE            thrpt   10  18790.156 ±  24.251  ops/s


BigEndianとLittleEndianとでのパフォーマンスの違いが見られなくなっているが、UnsafeとDirectBufferのパフォーマンスが同等であることはintの場合と同じようである。(BE/LEの違いが見られない理由はわからないのだが...)


バッファサイズは512kibのままであり、double型は8バイト表現となるので、データの読み取り回数としてはintの場合に対して半分になるが、パフォーマンスとしてはダイレクトバッファを使うことにより、intの場合と遜色ない高速な読み取りができるように見える。


ヒープ上のバッファからのdouble読み取りは、やはり遅いようだ。

Float型の場合

intと同様に4バイト表現であるfloatでは、以下のような結果になった。

Benchmark                                       Mode  Cnt     Score    Error  Units
BufferAccessBenchmark2.testDirectByteBufferBE  thrpt   10  9538.018 ± 45.020  ops/s
BufferAccessBenchmark2.testDirectByteBufferLE  thrpt   10  9528.538 ± 25.403  ops/s
BufferAccessBenchmark2.testHeapByteBufferBE    thrpt   10  4981.542 ± 85.246  ops/s
BufferAccessBenchmark2.testHeapByteBufferLE    thrpt   10  5059.595 ± 74.801  ops/s
BufferAccessBenchmark2.testUnsafeLE            thrpt   10  9533.223 ± 22.078  ops/s

BigEndianとLittleEndianとでのパフォーマンスの違いが見られなくなっているが、UnsafeとDirectBufferのパフォーマンスが同等である傾向は同じようである。

やはり、double同様に、ヒープからfloatを取得するよりも、ダイレクトバッファから直接floatを読み取ったほうが速い。


ただし、doubleと比較すると、ダイレクトバッファの場合は、floatのほうがパフォーマンスが低いように見える。

これはJavaでは内部的には実数がdouble型として扱われているところに起因するのだろうか?(根拠はないが...。)

(1つの推測としては、バッファサイズよりも、項目数 = get回数が効いているのかもしれない。)


※ ヒープ同士の比較では、doubleよりはfloatのほうがパフォーマンスは良いようである。

Long型の場合

long値(8バイト)の場合は、以下のようになった。

Benchmark                                       Mode  Cnt      Score      Error  Units
BufferAccessBenchmark2.testDirectByteBufferBE  thrpt   10  29425.796 ±  659.844  ops/s
BufferAccessBenchmark2.testDirectByteBufferLE  thrpt   10  36945.695 ± 1012.917  ops/s
BufferAccessBenchmark2.testHeapByteBufferBE    thrpt   10   3924.688 ±   66.375  ops/s
BufferAccessBenchmark2.testHeapByteBufferLE    thrpt   10   3929.151 ±   57.227  ops/s
BufferAccessBenchmark2.testUnsafeLE            thrpt   10  38615.889 ±  186.971  ops/s


(intよりもパフォーマンスが高くなったのでグラフの横幅が変わっていることに注意。)


long型は8バイトなのでdouble型のケースと同様に、同じ512kibバッファサイズではintやfloatと比較して半分の項目数しかない。


intのケースよりも一秒間当たりの実行回数が倍近く伸びているので、やはり、Unsafe#getX() メソッドの呼び出し回数がパフォーマンスの大きな因子のような感じである。

結論

これらの結果をみるかぎり、整数、実数ともに連続的に大量の項目を読みとる必要がある場合は、ダイレクトバッファを使うのが良さそうである。
(というより、このグラフを見てしまうとダイレクトバッファを使わないのは、もはや間違いといえるレベルかも...。)


とりあえず、ファイル等から直接データを読み取る場合には、一旦バイト配列としてヒープにもってきてからint等に展開するよりも*4直接、ダイレクトバッファ上からint値などを取得したほうが速くなりそうな結果が得られたように思える。

(ただし、本当にアテにしていいのかは、実際にコードを書いて比較するべきだろう。)


この結果は、個人的には、なかなか興味深い、今まで気にも留めなかったダイレクトバッファの正しい使い道が分かったような新鮮な発見があり、
今後のI/Oまわりのパフォーマンスチューニングに使えそうな、とても有意義な勉強になったと思う。


※ 今回のソースコードは、以下のgistに置いた。
(pom.xmlと、ベンチマークjavaクラスのみ。それ以外のソースはmavenで自動生成されたものなので手は入れていない。追試する場合は、上記手順に従ってmavenからjmhのアーキタイプを指定してプロジェクトを構築してから、本ソースを追加すれば良い。)
https://gist.github.com/seraphy/6276dbae1eb4ee96325d


以上、メモ終了。

*1:便利だと思うので、最近は、だいたいPleiades (Eclipse 4.5 Mars)を使っている

*2:こちらは、jdk9でUnsafeから公開APIになるかもしれない? http://openjdk.java.net/jeps/193 http://openjdk.java.net/jeps/260

*3:http://www.docjar.com/docs/api/sun/misc/Unsafe.html#objectFieldOffset

*4:FileChannelからByteBuffer/MappedByteBufferなどを使ってダイレクトバッファを取得できる。ただし、MappedByteBufferは最速らしいが、unmapする方法が現状ではgcしかないので、開きっぱなしにするファイルでないと使いどころは難しいかも?

JavaFX8で簡単なアプリケーションを簡単に作る方法

動機、なぜJavaFXでアプリを作るのか?

Javaには、Swingという、十分に成熟しておりパフォーマンスも悪くないUIライブラリがある。

現状、普通に使っている分には何の不自由もない。

また、Swingは今後発展することはないとはいえ、(すぐに)廃止されるわけではない。*1

ならば、どうしてJavaFXなどという、新しいUIライブラリを覚えなければならないのか。

見るからにめんどくさそうなFXMLというマークアップ言語まで付いている。

....。

と、思っていたのだけれど、

実際に使ってみたらSwingよりも思いのほか簡単だった。


案ずるより産むが易し、というやつである。

JavaFXを使うメリット

実際にJavaFXを使ってみて、Swingに対して以下のようなメリットを感じることができた。

  • FXMLを使うことによりUIのレイアウト構造をコードから分離することができる
    • Swingではロジックとレイアウトがごちゃまぜになっていて、作るのは良いが、修正する場合に困難が伴わなかったことがない。
    • その点、JavaFXはコードではレイアウトしないので保守性が高そうである。
  • FXMLはXML構造であるため、GUIのようなオブジェクトツリー構造を記述するのに適している
    • XMLであるためツールとの親和性が高い。(Scene Builderという外部ツールでレイアウトを編集できる。)
    • やってることも、実は単純だったりする。(手で作るのも難しくないと思われる。やりたくはないけど。)
  • バインディングはめんどくさい反面、とても便利で強力だったりする。
  • CSSによる細かなレイアウトの調整やデコレーションが、プログラムコードとは分離して表現できる。
    • (ただしウェブ的なcssとは、ちょっと違う。)

上記のようなメリットを実感したため、今後はJavaFX8を優先的に使ってゆこうと思い始めている。


ここでは、これまで経験した、JavaFXで簡単にアプリを作るための簡単な方法について、軽くまとめておきたいと思う。

(また、先日のJava Day Tokyo 2015の櫻庭さんのセッションで得られた新たな知見も追記しておく。)

用意するもの

  • JDK8u40以降
  • Scene Builder 2.0
  • IDE
    • Eclipse 4.4 Luna + e(fx)clipse
    • NetBeans8
JDK8u40を使う理由

JavaFX2はJava7より利用可能になっているが、ここではJava8u40以降を使う。


Java7からJavaFX2が同梱され利用可能だったが、JavaFXのランタイムがクラスパスに通っていないため、通常の方法ではアプリケーションを配布することも実行することもめんどくさかった。

しかし、Java8からはJavaFX8は標準のクラスパスに入っており、単純に実行可能jarに丸めて配布しても動くようになっている。

JavaFX2(Java7)時代のJavaFXのビルドと配備方法は大変複雑だったが、JavaFX8では、ただのアプリと同じである。


また、JavaFX2にはメッセージボックスのような、よく使われるダイアログが(何故か)存在しない。


これも、Java8u40から、JavaFX Dialogsの成果物Alertクラスが標準に取り込まれたので、メッセージボックスの表示も標準でできるようになっている。

http://code.makery.ch/blog/javafx-dialogs-official/

※ Java8u40未満では上記のJavaFXDialogsのライブラリを使うと良いだろう。

Scene Builder 2.0

Scene Builderは、JavaFXGUIをレイアウトするためのツールである。

このツールを使うことにより、コンポーネントの配置状態を表すFXMLというXMLファイルをグラフィカルに編集、保存することができる。


これはJDKには付属していないので、別途ダウンロードする必要がある。

http://www.oracle.com/technetwork/java/javase/downloads/javafxscenebuilder-info-2157684.html

ただし、現行のSceneBuilder2はFXMLのすべての機能をサポートしているわけではなく、
手作業で手の込んだFXMLを作るとScene Builderでは開けなくなったりするので注意が必要である。


簡単なアプリケーションを作るうえでは、Scene Builderだけで十分ではないか、と思う。


なお、最新のScene BuilderのバイナリはOracleからは配布されておらず、OpenJDK側でソースとして提供されている。

これを使うには、自分でビルドするか、だれかがビルドしてくれたバイナリをもらってくる必要がある。*2

Scene Builder自身もJavaFXで書かれているので、jdk8環境があればビルドできるようである。*3

IDE

JavaFX8は、ふつうのJavaコード + FXMLというXMLファイルで作れるので、原理的には何のIDEでも良い。


Eclipseの場合は、Java8をサポートするLuna以降が必要であり、且つ、FXMLのサポートがつく"e(fx)clipse"プラグインを入れる。

Eclipsejavafxで始まるパッケージを非公開クラスの利用だとしてエラー表示するが、このプラグインを入れると解消される。

設定から、Scene Builderの実行ファイルの位置などを指定する必要がある。


NetBeans8の場合でも、FXMLの編集にはScene Builderを使うので、ツールメニューからオプションを開き、JavaのタブからScene Builderのホームディレクトリを指定する必要がある。

これでIDEとScene Builderとの連携ができるようになる。

(なお、NetBeans8でJavaFXプロジェクトをMavenで作ると、Java7時代のめんどくさいビルドスクリプトが出来上がるの注意。JavaFX8では、普通のアプリのビルド方法と同じで良い。)

JavaFXのお約束

エントリポイント

従来、SwingのJFrameを使った簡単なアプリケーションは以下のような形で作っていたと思う。

class MyFrame extends JFrame {

    public MyFrame() {
        // 初期化処理
    }

    public static void main(String... args) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                MyFrame inst = new MyFrame();
                inst.setLocationByPlatform(true);
                inst.setVisible(true);
            }
        });
    }
}

mainメソッドでイベントディスパッチスレッド(EDT)に入ってJFrameを作成する、という流れである。

(手抜きなコード例だと、EDTに入らずに、いきなりnew JFrameしていたりする例も散見されるが、これは禁止事項である。)


これに対して、JavaFXでは、アプリケーションのエントリポイントは、かならず以下のような形式になる。

package jp.seraphyware.simpleapp0;

import javafx.application.Application;
import javafx.stage.Stage;

public class SimpleApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        // ここからスタートする
    }

    public static void main(String... args) {
        launch(args);
    }
}

まず、アプリケーションは、必ずApplicationを継承したクラスになる。

JavaFXに対応したランチャは、Javaアプレットのようにmainは経由せず、startから開始する。*4


mainから開始する古典的な実行形式の場合には上記のように単にlaunchを呼ぶ形にしておく。
これにより、JavaFXアプリケーションスレッドに入ったあとでstartメソッドが呼び出されるようになる。

(この場合でSwingと異なる点は、launchメソッドを呼んだ後、JavaFXアプリケーションスレッドが終了するまで制御が返らない点であろう。)


startメソッドは呼び出された時点でJavaFXアプリケーションスレッド上にいる。


引数にはStageが1つ受け取られているが、これがSwingでいうところのJFrameに相当する、メインウィンドウとなる。

このstartメソッド内でJavaFXコンポーネントを作成して、Stageに上に乗せることで、ウィンドウを作成する。

(アプリケーションは、必ずしも、このprimaryStageを使う必要はなく、任意にStageを作成して使うこともできるが、アプレット等でブラウザに埋め込まれている場合は、このprimaryStageが、その埋め込み領域を表している。)


コンポーネントを作成する方法としては、

のいずれの方法でもよい。

必ずFXMLを使わなければならない、というものではない。


JavaFXアプリケーションスレッドが終了する場合にはstopメソッドが呼び出されるので、ここで後始末ができる。

UIスレッド/JavaFXアプリケーションスレッド

JavaFX8では、JavaFXアプリケーションスレッド以外からUIを操作したい場合には、Swingの場合と同様にJavaFXアプリケーションスレッド上で実行するRunnableを引き渡す必要がある。

Platform#runLater(Runnable r)


JavaFXアプリケーションスレッドは、Swing/AWTのEDT(UIスレッド)とは別のスレッドである。

JavaFX8では、SwingNodeによって、JavaFXの中でSwingを利用することもできるが、
SwingとJavaFXのスレッドは別物なので、SwingNode上のSwingコンポーネントを作成、操作する場合には、必ず、SwingのUIスレッドで行う必要がある。


この場合、Swing用にはSwingUtilities#invokeLaterを引き続き使用する必要がある。


全部がObservableである。

JavaFXでは従来のJava Beansと異なり、"なんとかProperty"という形式のプロパティを持つ。

たとえば、textというプロパティを持つJavaFX Beansであれば、だいたい以下のようになる。

package jp.seraphyware.simpleapp0;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class JavaFXSimpleModel {

    private SimpleStringProperty textProperty;

    public StringProperty textProperty() {
        if (textProperty == null) {
            textProperty  = new SimpleStringProperty(this, "text");
        }
        return textProperty;
    }

    public String getText() {
        return textProperty().get();
    }

    public void setText(String text) {
        textProperty().set(text);
    }
}

javafx.beans.***Propertyクラスは、値の変更を検知可能な(Observable)値のホルダーであり、
JavaFXでは、あらゆるプロパティがObservableである。


また、これはバインド可能(一方向、双方向)であり、ある値が変更されると連結されている別のコンポーネントのプロパティも自動的に変更される、という動作ができる。

バインドは、かなり強力で、複数のプロパティの演算結果をバインドする、というようなこともできるようになっている。


JavaFXでは、とにかく全部がObservableである。

JavaFXによる簡単なGUIアプリの作成

まず、以下のような簡単なGUIを作りたいと思う。

まずはFXMLによるGUIのレイアウトを行う

画面のレイアウトのデザインにはScene Builderを使う。


EclipseNetBeans上で適当に空のFXMLファイルを作成し、そのファイルを右クリックして「Scene Builderで開く」みたいなメニューがあれば、それでScene Builderが開かれる。*5

Swingの場合はJPanelの中にレイアウトマネージャを設定してレイアウトを組んでいたが、JavaFXの場合では、はじめからGridPane, BorderPane, FlowPane, VBox, HBoxのようなレイアウトペインになっている。

この上に必要なコンポーネントを乗せて行くことで画面をデザインしてゆくことになる。


実際に画面のレイアウトするうえでの注意は「アンカーペインには頼りすぎない」ことが使ってみたところの教訓といえようか。


アンカーペイン(AnchorPane)は、Visual Studioのフォームエディタのような感覚でコンポーネントを並べてゆけるうえに、アンカーを指定することで画面サイズにあわせて拡縮するようなレイアウトも容易に作れる。

しかし、コンポーネント自身の適切なサイズは使用するフォントや文字数により変わるため、ローカライズした場合やWindows/Linux/Mac間でフォントが変わった場合などでサイズが変わるため、なかなか調整が難しい。


なので、今のところの感触では、Swingを手動でレイアウトを組んでいたころのように、GridPane, BorderPane, FlowPane, VBox, HBoxなどのレイアウトを使ってコンポーネントのサイズを自動調整させつつ、それらを組み合わせて配置してゆくのが良いだろう、と思う。

fx:idの割り当て

プログラム側からコンポーネントを制御できるようにするため「fx:id」属性で名前をつけておく必要がある。


これによりFXMLとコントローラクラス上のフィールドとが結び付けられるようになる。

  • txtDir
  • txtNamePattern
  • btnBrowse
  • chkSubdir
  • btnOK
アクション名の割り当て

ボタンのアクションに対しては、以下のようにメソッド名を割り当てておく。


これによりFXMLとコントローラクラス上のメソッドと連結されることになる。

  • #onBrowse
  • #onOK
  • #onCancel

※ ここにスクリプトを直接書くことも可能。


SceneBuilderの画面を示す。

出来上がったFXMLの例

できあがったFXMLは以下のようなものである。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="ディレクトリ" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="ファイル名パターン" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <TextField fx:id="txtDir" GridPane.columnIndex="1" />
            <Button fx:id="btnBrowse" mnemonicParsing="false" onAction="#onBrowse" text="参照" GridPane.columnIndex="2" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="サブディレクトリを含む" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="実行" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="キャンセル" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

FXMLはとても汎用的にできていて、fxで始まる名前空間のもの以外は、特殊なものはない。

これはオブジェクトツリーを作成するための表記そのものといってよい。

http://docs.oracle.com/javafx/2/api/javafx/fxml/doc-files/introduction_to_fxml.html


FXMLのルールは、ざっくりといえば、

  • 大文字のタグはクラス名であり、そのクラスをnewすることを意味する
  • 小文字のタグまたは属性名がプロパティを意味する
    • 属性名を使う場合は文字列または数値のリテラルのみ指定可能
    • 小文字のタグを使う場合には、リスト表現や、fxで始まる名前空間を利用してJavaの定数を参照することも可能。

これらをつなげてツリーを作ることでUI要素の親子関係を表現しているのである。


また、要素の出現順で、タブキーによるフォーカス移動順(フォーカストラバーサル)が決まるなどの性質もある。


上記例では、このFXMLをロードすると、ルート要素として各種UI要素を配置済みのVBoxが返されることになる。

FXMLに対応するコントローラクラスの作成

前述のように、コンポーネントに名前を割り当てたり、ボタンのアクションのメソッド名を割り当てた場合、それらを受ける「コントローラ」クラスの定義を行う必要がある。

前述の例であれば、以下のようなコントローラが必要となる。

public class SimpleAppController implements Initializable {

    @FXML
    private TextField txtDir;

    @FXML
    private TextField txtNamePattern;

    @FXML
    private CheckBox chkSubdir;

    @FXML
    private Button btnOK;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // OKボタンの活性制御
        btnOK.disableProperty().bind(
                Bindings.or(
                        txtDir.textProperty().isEmpty(),
                        txtNamePattern.textProperty().isEmpty()
                        ));
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        // ここにBrowseボタンのアクションを定義する
    }

    @FXML
    protected void onOK(ActionEvent evt) {
        // ここにOKボタンのアクションを定義する
    }

    @FXML
    protected void onCancel(ActionEvent evt) {
        // 現在ボタンを表示しているシーンを表示しているステージに対して
        // クローズを要求する.
        ((Stage) ((Button) evt.getSource()).getScene().getWindow()).close();
    }
}

コントローラにするクラスには、フィールドまたはメソッドに対して@FXMLアノテーションをつけたものが、FXMLLoaderによってJavaFXコンポーネントと結び付けられる。

また、Initializableインターフェイスを実装する場合、
各種フィールドやメソッドがコンポーネントと関連づけられたあとに、initializeメソッドが呼び出されるので、ここで追加の初期化処理を行うことができる。

ここでは、2つのテキストフィールドに何らかの文字が入力されていないかぎりOKボタンを有効としないような活性制御のバインドを設定している。
(txtDirが空か、txtNamePatternが空の場合に、btnOKのdisableプロパティをTrueに設定するバインドを設定している。)


なお、onCancelメソッドでは、ウィンドウを閉じるために、やや回りくどいことをしているが、
コントローラはどこからも継承していない独立しているクラスなので、あらかじめフィールドにステージの情報を入れておくか、何かしらのUI要素を遡って、現在表示しているウィンドウを取得するなどの方法を取る必要がある。

コントローラクラスの指定方法

コントローラクラスの指定方法には、いくつかあり

  • FXMLLoaderで直接、コントローラのインスタンスを指定する (fx:controller属性は書かない)
  • FXMLコード中のルート要素のfx:controller属性にクラス名を指定する
    • FXMLLoaderでコントローラのファクトリを指定することもできる.
      • SpringなどDIコンテナ管理のコントローラを作りたい場合などは、ここでインジェクションしたあとのインスタンスを渡すなどのファクトリクラスの定義ができる。


今回は、FXMLにはコントローラクラスは記述せず、FXMLLoaderで直接コントローラのインスタンスを指定する方法を用いる。

FXMLのロード

FXMLは以下のようにしてロードすることができる。

/**
 * 簡単なFXMLアプリケーション作成例.
 *
 * @author seraphy
 */
public class SimpleApp extends Application implements Initializable {
....(前略)....

    /**
     * アプリケーション開始
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        // FXMLをリソースから取得する.
        // (タブオーダーもFXMLの定義順になる.)
        // (FXML中に「@参照」による相対位置指定がある場合は、このURL相対位置となる.)
        URL fxml = getClass().getResource(getClass().getSimpleName() + ".fxml");

        // FXMLをロードする.
        // (ローカライズしない場合はリソースバンドルを指定しなくて良い)
        FXMLLoader ldr = new FXMLLoader(fxml, null);

        // このインスタンス自身をコントローラとする.
        // @FXMLアノテーションによりFXMLと結び付けられる.
        ldr.setController(this);

        // FXMLをロードする.
        Parent root = ldr.load();

        // ステージのタイトル
        primaryStage.setTitle("SimpleApp Sample");

        // ステージの表示
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        // ディレクトリ選択テキストにフォーカスを当てる
        txtDir.requestFocus();
    }

....(後略)....
}

FXMLLoaderクラスによってFXMLをロードできる。

FXMLファイルの指定方法にはURLによる方法とストリームによる方法などがあるが、基本はURLによる指定を使うほうが良い。


これはFXML表記で"@picture1.png"のような形式でFXMLファイルからの相対位置でリソースのありかを指定する表記方法があるが、
ストリームでFXMLを読み込ませる場合には、その位置の指定が取れなくなるためである。


FXMLをロードしフィールドやメソッドは、setControllerメソッドに対してthisを渡すことで明示している。

これにより、このクラス自身がコントローラを兼ねることになる。


あとは、これをloadすると、FXMLの最初の要素である"VBox"のインスタンスが得られるので、
これをSceneオブジェクトに入れて、それをStageにセットすることで、画面上に表示できるようになる。


※なお、FXMLLoaderはオブジェクトツリーの汎用ファクトリであり、任意の独自のクラスや、あるいはStageそのものをルートとしてオブジェクトツリーを作成することも可能である。
(ただし、その場合はSceneBuilderでは編集できなくなるので、実用性は少ないかもしれない。)

完全なソースコード
package jp.seraphyware.simpleapp1;

import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextField;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import jp.seraphyware.utils.ErrorDialogUtils;

/**
 * 簡単なFXMLアプリケーション作成例.
 *
 * @author seraphy
 */
public class SimpleApp extends Application implements Initializable {

    @FXML
    private TextField txtDir;

    @FXML
    private TextField txtNamePattern;

    @FXML
    private CheckBox chkSubdir;

    @FXML
    private Button btnOK;

    /**
     * ディレクトリ選択ダイアログ
     */
    private DirectoryChooser dirChooser = new DirectoryChooser();

    /**
     * アプリケーション開始
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        // FXMLをリソースから取得する.
        // (タブオーダーもFXMLの定義順になる.)
        // (FXML中に「@参照」による相対位置指定がある場合は、このURL相対位置となる.)
        URL fxml = getClass().getResource(getClass().getSimpleName() + ".fxml");

        // FXMLをロードする.
        // (ローカライズしない場合はリソースバンドルを指定しなくて良い)
        FXMLLoader ldr = new FXMLLoader(fxml, null);

        // このインスタンス自身をコントローラとする.
        // @FXMLアノテーションによりFXMLと結び付けられる.
        ldr.setController(this);

        // FXMLをロードする.
        Parent root = ldr.load();

        // ステージのタイトル
        primaryStage.setTitle("SimpleApp Sample");

        // ステージの表示
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        // ディレクトリ選択テキストにフォーカスを当てる
        txtDir.requestFocus();
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // ダイアログのタイトルの設定
        dirChooser.setTitle("フォルダの選択");

        // OKボタンの活性制御
        btnOK.disableProperty().bind(
                Bindings.or(
                        txtDir.textProperty().isEmpty(),
                        txtNamePattern.textProperty().isEmpty()
                        ));
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        try {
            String srcDir = txtDir.textProperty().get();
            if (srcDir != null && !srcDir.isEmpty()) {
                File initDir = new File(srcDir);
                if (initDir.isDirectory()) {
                    dirChooser.setInitialDirectory(initDir);
                }
            }
            File selectedDir = dirChooser.showDialog(((Button) evt.getSource())
                    .getScene().getWindow());
            if (selectedDir != null) {
                txtDir.setText(selectedDir.getAbsolutePath());
                txtDir.requestFocus();
            }

        } catch (Exception ex) {
            ErrorDialogUtils.showException(ex);
        }
    }

    @FXML
    protected void onOK(ActionEvent evt) {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setHeaderText("実行!");

        StringBuilder buf = new StringBuilder();
        buf.append("dir=").append(txtDir.getText()).append("\r\n")
                .append("namePattern=").append(txtNamePattern.getText())
                .append("\r\n").append("subdir=")
                .append(chkSubdir.isSelected() ? "Yes" : "No");
        alert.setContentText(buf.toString());

        alert.showAndWait();
    }

    @FXML
    protected void onCancel(ActionEvent evt) {
        // 現在ボタンを表示しているシーンを表示しているステージに対して
        // クローズを要求する.
        ((Stage) ((Button) evt.getSource()).getScene().getWindow()).close();
    }

    /**
     * エントリポイント
     * @param args
     * @throws Exception
     */
    public static void main(String... args) throws Exception {
        launch(args);
    }
}

このように、単純なアプリであればコントローラ兼メインクラスとなるクラス1つと、レイアウトを定めているFXMLリソースが1つあれば簡単に作ることができる。

FXMLファイルの中身もわかってしまえば簡単である。


JavaFXでやってみると、同等な画面をSwingで手作業で作るよりも簡単であることが分かった。

リソースバンドルを使った簡単なアプリの作成例

JavaFXアプリケーションをローカライズする場合にはリソースバンドルを指定することで簡単に対応できるようになっている。

FXML上に文字列リテラルを書く代わりに、"%resource.key"のような形式とすることで、リソースバンドルより自動的に文字列を引き当てることができるのである。

文字列リテラルからリソースキーに変更したFXML
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="%directory" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="%namePattern" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <TextField fx:id="txtDir" GridPane.columnIndex="1" />
            <Button fx:id="btnBrowse" mnemonicParsing="false" onAction="#onBrowse" text="%browse" GridPane.columnIndex="2" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="%includeSubDirs" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="%ok" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="%cancel" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>


これは前述のFXMLから文字列リテラルを全て%resource.keyの形式に置き換えただけである。

リソースバンドルの定義

リソースバンドルには、ここではXML形式のリソースバンドルを使用するものとする。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>SimpleApp Resource</comment>
<entry key="title">SimpleApp サンプル#2</entry>
<entry key="dirChooser.title">フォルダの選択</entry>

<entry key="directory">ディレクトリ</entry>
<entry key="browse">参照</entry>
<entry key="namePattern">ファイル名パターン</entry>
<entry key="includeSubDirs">サブディレクトリを含む</entry>

<entry key="ok">実行</entry>
<entry key="cancel">キャンセル</entry>
</properties>

※ 本サンプルで使っているXMLResourceBundleControlは、XMLプロパティファイル形式をリソースバンドルとして読み込めるようにリソースバンドルコントローラを拡張した実装であるが、本質的に、ただのリソースバンドルでも同じである。
(実装例: https://gist.github.com/seraphy/b421ac03d64d541667b2)

FXMLLoader側の修正

FXMLLoader側は、リソースバンドルを指定すれば良いだけである。

    @Override
    public void start(Stage primaryStage) throws Exception {
        // FXMLをリソースから取得する.
        // (タブオーダーもFXMLの定義順になる.)
        // (FXML中に「@参照」による相対位置指定がある場合は、このURL相対位置となる.)
        URL fxml = getClass().getResource(getClass().getSimpleName() + ".fxml");

        // XMLリソースバンドルを読み込む
        ResourceBundle rb = ResourceBundle.getBundle(getClass().getName(),
                new XMLResourceBundleControl());

        // FXMLをロードする.
        // (ローカライズするのでリソースバンドルも指定する)
        FXMLLoader ldr = new FXMLLoader(fxml, rb);

        // このインスタンス自身をコントローラとする.
        // @FXMLアノテーションによりFXMLと結び付けられる.
        ldr.setController(this);

        // FXMLをロードする.
        Parent root = ldr.load();

        // ステージのタイトル
        primaryStage.setTitle(rb.getString("title"));

        // ステージの表示
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        // ディレクトリ選択テキストにフォーカスを当てる
        txtDir.requestFocus();
    }

見ての通り、コード側は、ほとんど修正はない。


ただリソースバンドルを読み込むだけで、%resource.keyを取得する部分はFXMLLoaderが全部やってくれている。


JavaFXでやればFXMLのリテラル修正とリソースバンドルの読み込み、これだけでローカライズに対応できるので、
SwingでリソースバンドルのgetStringを延々と手作業で記述するよりもはるかに簡単にできることが分かる。

カスタムコンポーネントの作成

JavaFXではFXMLを使ったカスタムコンポーネントの定義も考慮されている。

ここでは、テキストフィールドと参照ボタンの2つのコンポーネントを組み合わせた「DirTextField」というカスタムコンポーネントを定義してみる。


2つのコンポーネントをもつカスタムコンポーネント用のFXML
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<fx:root type="javafx.scene.layout.BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <center>
      <TextField fx:id="txtDir" BorderPane.alignment="CENTER">
         <opaqueInsets>
            <Insets />
         </opaqueInsets>
         <BorderPane.margin>
            <Insets right="5.0" />
         </BorderPane.margin></TextField>
   </center>
   <right>
      <Button mnemonicParsing="false" onAction="#onBrowse" text="%browse" BorderPane.alignment="CENTER" />
   </right>
</fx:root>

こちらも、Scene Builder2で編集可能である。


サイズ可変でも対応できるように、BorderPaneでセンターにテキスト、Rightにボタンを配置している。


今までと違うのは、ルート要素が「fx:root」というタグになっており、type属性にBorderPaneのクラスを指定している。

これにより、Scene Builder上では、type属性に示されたBorderPaneであるかのようにふるまう。


この意味するところは、FXMLLoaderによってロードされるときルート自身はnewされずに、親として指定された作成済みインスタンスに対して属性や子アイテムを追加してゆく、ということである。

カスタムコンポーネント用FXMLのロード

このFXMLはルート、すなわち自分自身がBorderPaneを継承するカスタムクラスであることを示しており、以下のようにBorderPaneを継承するカスタムクラスのコンストラクタでロードする形となる。

package jp.seraphyware.simpleapp3;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.DirectoryChooser;
import jp.seraphyware.utils.ErrorDialogUtils;
import jp.seraphyware.utils.XMLResourceBundleControl;

public class DirTextField extends BorderPane {

    @FXML
    private TextField txtDir;

    /**
     * ディレクトリ選択ダイアログ
     */
    private DirectoryChooser dirChooser = new DirectoryChooser();

    /**
     * コンストラクタ
     */
    public DirTextField() {
        URL url = getClass().getResource(getClass().getSimpleName() + ".fxml");
        ResourceBundle rb = ResourceBundle.getBundle(getClass().getName(),
                new XMLResourceBundleControl());
        FXMLLoader ldr = new FXMLLoader(url, rb);

        // このインスタンス自身がルートオブジェクト
        ldr.setRoot(this);

        // このインスタンス自身がコントローラ
        ldr.setController(this);

        try {
            // ルートを指定済みなので、このインスタンスにFXMLがロードされる.
            ldr.load();

        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }

        // ダイアログのタイトル設定
        dirChooser.setTitle(rb.getString("dirChooser.title"));

        // フォーカスリスナ
        focusedProperty().addListener((o, old, newval) -> {
            if (newval) {
                txtDir.requestFocus();
            }
        });
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        try {
            String srcDir = txtDir.textProperty().get();
            if (srcDir != null && !srcDir.isEmpty()) {
                File initDir = new File(srcDir);
                if (initDir.isDirectory()) {
                    dirChooser.setInitialDirectory(initDir);
                }
            }
            File selectedDir = dirChooser.showDialog(((Button) evt.getSource())
                    .getScene().getWindow());
            if (selectedDir != null) {
                txtDir.setText(selectedDir.getAbsolutePath());
                txtDir.requestFocus();
            }

        } catch (Exception ex) {
            ErrorDialogUtils.showException(ex);
        }
    }

    public StringProperty textProperty() {
        return txtDir.textProperty();
    }

    public String getText() {
        return textProperty().get();
    }

    public void setText(String text) {
        textProperty().set(text);
    }
}

自分自身がコントローラであることは先の例と同様である。


今回は、それに加えてルートが自分自身であることを示すためにFXMLLoader#setRoot()で自分自身をルートであるように指定する。


また、このクラス専用のリソースバンドルを読み込んでいる。


rootがthisとなっていることにより、loadメソッドによって自分自身に設定することになるため、loadメソッドの戻り値は無視して構わない。

textPropertyなどのプロパティは、このカスタムコンポーネントの持つ値を外部からアクセス可能にするためのものである。

カスタムコンポーネントの利用

カスタムコンポーネントの利用は、FXML上で、カスタムクラス名を指定すれば良いだけである。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import jp.seraphyware.simpleapp3.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="%directory" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="%namePattern" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <DirTextField fx:id="dirTextField" text="C:\" GridPane.columnIndex="1" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="%includeSubDirs" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="%ok" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="%cancel" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

この中にある「DirTextField」というタグがカスタムコンポーネントのクラス名になる。

★ちなみに、Scene Builderでは、カスタムコンポーネントは、コンポーネントペインの歯車アイコンから「Import JAR/FXML File...」を選択し、そのFXMLを内包するjarクラスをインポートすることにより、左ペインに「カスタム」の一覧にカスタムコンポーネントが使えるようになり、グラフィカルエディタ上でもコンポーネントを認識するようになる。

しかし、とくにカスタムコンポーネントをインポートしない場合でも、SceneBuilderでは得体の知らないクラスとして画面上では穴が開いた状態になり、グラフィカルな画面からの操作は不可能ではあるが、とくにエラーになるわけではない。

ここでは手抜きのため、SceneBuilderにはインポートしないものとする。

※ なお、SceneBuilder2でインポートしたカスタムコンポーネントを消すには、現状、消すコマンドがないので、%APPDATA%\Scene Builder\Library上にあるFXMLファイルかJARファイルを手動で削除する必要がある。


「fx:id」で名前を「dirTextField」と指定しているので、コントローラ側では以下のように、カスタムクラスのフィールド名で受けることができる。

    @FXML
    private DirTextField dirTextField;
実行例

カスタムコンポーネントのFXMLによる定義方法も利用方法も簡単である。


また、このカスタムコントロールの手法は、FXMLにカスタムコントロールを直接埋め込む場合だけでなく、VBoxのようなペインに対して、プログラム的に2個以上連続して作成させるような場合にも、とても有効な手法ではないかと考えられる。

ネストしたコントローラの利用

カスタムコンポーネントに似た別の方法として、ネストしたFXMLの利用がある。

ネストしたFXMLとは、FXMLの中で別のFXMLをincludeするものである。


includeといってもC言語プリプロセッサ的な外部ファイルを流し込むだけの単純なincludeではなく、外部の独立したコントローラを生成して外部のFXMLを親のFXMLに埋め込むものである。


親子関係をもった状態で作成されるため、実質的に、カスタムコントロールとほとんど同じような感じとなる。

FXMLの定義

FXMLの定義は前述のカスタムコントロールの例をモデルとするが、異なるのはルート要素が「fx:root」ではなく、普通のFXMLと同様に明示的に「BorderPane」のようなクラス指定になっている、ということである。


これの意味するところは、FXMLLoaderによって実際にBorderPaneがnewされる、ということである。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<BorderPane fx:id="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
	fx:controller="jp.seraphyware.simpleapp4.DirTextFieldController">
   <center>
      <TextField fx:id="txtDir" BorderPane.alignment="CENTER">
         <opaqueInsets>
            <Insets />
         </opaqueInsets>
         <BorderPane.margin>
            <Insets right="5.0" />
         </BorderPane.margin></TextField>
   </center>
   <right>
      <Button mnemonicParsing="false" onAction="#onBrowse" text="%browse" BorderPane.alignment="CENTER" />
   </right>
</BorderPane>

もう一つ、重要な点は、ルート要素に「fx:controller」による、コントローラクラスの指定がある点である。


これにより、このFXMLを構築するときに自動的に"jp.seraphyware.simpleapp4.DirTextFieldController"クラスがデフォルトコンストラクタでインスタンス化され、コントローラとして使用される。

(※ FXMLLoaderでコントローラのファクトリを指定している場合は、インスタンス取得のためにファクトリによってコントローラのインスタンスを取得する。)

コントローラの定義

FXMLの受けるコントローラは以下のように定義する。

package jp.seraphyware.simpleapp4;

import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.DirectoryChooser;
import jp.seraphyware.utils.ErrorDialogUtils;

public class DirTextFieldController implements Initializable {

    @FXML
    private BorderPane root;

    @FXML
    private TextField txtDir;

    /**
     * ディレクトリ選択ダイアログ
     */
    private DirectoryChooser dirChooser = new DirectoryChooser();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // ダイアログのタイトル設定
        dirChooser.setTitle(resources.getString("dirChooser.title"));

        // フォーカスリスナ
        root.focusedProperty().addListener((o, old, newval) -> {
            if (newval) {
                txtDir.requestFocus();
            }
        });
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        try {
            String srcDir = txtDir.textProperty().get();
            if (srcDir != null && !srcDir.isEmpty()) {
                File initDir = new File(srcDir);
                if (initDir.isDirectory()) {
                    dirChooser.setInitialDirectory(initDir);
                }
            }
            File selectedDir = dirChooser.showDialog(((Button) evt.getSource())
                    .getScene().getWindow());
            if (selectedDir != null) {
                txtDir.setText(selectedDir.getAbsolutePath());
                txtDir.requestFocus();
            }

        } catch (Exception ex) {
            ErrorDialogUtils.showException(ex);
        }
    }

    public StringProperty textProperty() {
        return txtDir.textProperty();
    }

    public String getText() {
        return textProperty().get();
    }

    public void setText(String text) {
        textProperty().set(text);
    }
}

実質的に、カスタムコントローラの場合と、ほとんど同じである。


なお、"fx:id"属性を使って、BorderPane自身を"root"という名前にしてコントローラのフィールドに割り当てているので、initializeによる初期化時にルートに対する操作も可能としている。

(この方法はネストしたコントローラのケースに限らず、FXML全般で使える。)


なお、initializeメソッドで受け取るリソースバンドルは、fx:includeタグのresources属性で指定可能であり、省略した場合は親のリソースバンドルが、そのまま子にも渡ってくる。

ただし、includeタグで指定したリソースの場合は内部的には

resources = ResourceBundle.getBundle(childResourceName, Locale.getDefault(),
    parentResources.getClass().getClassLoader());

と同等な処理で取得されるため、取得方法が柔軟ではないことに注意が必要である。

親のFXML

ネストしたコントローラを使うFXMLは、単に「fx:include」を使うだけである。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="%directory" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="%namePattern" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <fx:include fx:id="dirTextField" source="DirTextField.fxml" GridPane.columnIndex="1" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="%includeSubDirs" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="%ok" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="%cancel" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

この中の「」が、ネストしたコントローラである。

カスタムコントロールとほとんど同じであるが、SceneBuilder2で開くと、インポートなどの事前処理をせずとも普通にFXMLを読み込めるので、それがBorderPaneであることがわかるようになっている。


(なお、現状ではSceneBuilder2からはfx:includeを扱うのはめんどくさいので、FXMLを手作業で直したほうが簡単である。)

親コントローラ

親コントローラでは、子コントローラを受け取るために以下のような記述ができる。

    /**
     * 入れ子になったFXMLのルートがパイントされる.
     * フィールド名は子FXMLのidと同じでなければならない.
     */
    @FXML
    private BorderPane dirTextField;

    /**
     * 入れ子になったFXMLでインスタンス化されたコントローラがバインドされる
     * フィールド名は"子FXMLのid + Controller"でなければならない.
     */
    @FXML
    private DirTextFieldController dirTextFieldController;

"ネストしたコントローラのid"と同じフィールド名によって、そのコンポーネントを得ることができ、
"ネストしたコントローラのid + Controller"というフィールド名によって、その子コントローラを得ることができる。


ここでは親FXMLのロード方法は変更する必要はない。

実行結果

実行結果としては、カスタムコンポーネントもネストしたコントローラも同じである。


なので、画面キャプチャは省略する。


カスタムコントロールとネストしたコントローラの使い分けとしては、

ということではないか、と考えられる。

CSSの適用

FXMLはUIコンポーネントの構造を表現しており、スタイルはcssで表現することができるようになっている。

http://docs.oracle.com/javafx/2/api/javafx/scene/doc-files/cssref.html


CSSといっても、FXML自体にレイアウトペイン各種が存在しているため、CSSでレイアウトを決めるということはない。

CSSではフォントや前景色、背景色などの、こまかな調整を行うために使うことができる。


同様なことはFXML上のインラインスタイルでも指定可能であるが、パフォーマンス的にはcssのほうが良いようである。*6



CSSではあるが、記述ルールがCSSというだけであり、HTMLなどで使われるウェブのCSSとは何の関係もない。


すべての属性は「-fx-」のベンダプレフィックスにより始まっており、使われているプロパティ名も、基本的にはJavaFXコンポーネントのプロパティ名を元にしている、とのことである。*7

たとえば、ボタンテキストの前景色(いわゆるフォントの色)を指定するのには「-fx-text-fill」という属性になる。

これはHTMLとは何の関係もなく、textFillプロパティ側からの命名である。


また、FXMLのCSSは、使えるクラス名としては、

  • .root → ルート(すべてに適用)
  • .クラス名を小文字化・ハイフン区切りにしたもの → そのクラスのコンポーネントに適用
    • Buttonクラスならば、.button
    • ToggleButtonクラスならば、.toggle-button
  • FXML側でUI要素ごとに任意のクラス名を追加指定することも可能
  • ボタンなどはhoverなどの擬似セレクタも使用可能

などの既定のルールがある。


使用例

以下のCSSをFXMLと同じディレクトリ上の「SimpleApp5.css」という名前で保存しておく。


また、同じディレクトリ上に背景画像用の「background-image.jpg」を置いておく。*8

.root {
    -fx-background-color: #ffff80;
    -fx-font-size: 12pt;
    -fx-background-image: url("background-image.jpg");
    -fx-background-repeat: stretch;
    -fx-background-size: 800 400;
    -fx-background-position: right bottom;
}

.button {
    -fx-background-color: #8888ff;
    -fx-text-fill: #ffff88;
    -fx-background-insets: 2;
    -fx-background-radius: 10;
    -fx-padding: 3 20 3 20;
}

.text-field {
    -fx-background-insets: 3;
    -fx-background-color: #eeeeee;
}

.label {
    -fx-text-fill: black;
}

.button:hover {
    -fx-background-color: #80f0ff;
}


FXML側では"@"による相対位置指定で、このCSSリソースを相対参照するように設定する。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
	xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
	stylesheets="@SimpleApp5.css">
...(中略)...
</VBox>

内部でfx:includeされているネストされたコントローラは暗黙で親のCSSを引き継ぐので、同じCSSが適用される。


CSSはプログラム的に切り替えることも可能である。

実行結果

このようにCSSによってレイアウトや見た目の細かな調整ができるようになっている。


これらについてはコードは不要であり、デザインとロジックが分離されているため、コードで書くよりも何をしているのか、あとから見てもわかりやすいといえる。


※ 問題は、cssで使える属性が何か?を把握することであるが、Java Day Tokyo 2015で聞いてきたテクニックとして、
Scene BuilderのViewメニューから「Show CSS Analyzer」を選択すると、適用可能な属性一覧と、現在選択している要素の属性のソースがどこからきているか、などをチェックできるので、これを使うと便利とのことである。
(ただし、Scene Builder側からはcssを編集することはできない。確認には、そこそこ便利に使える。)


Antによる実行可能jarの作成と、JavaFXのランタイムを含む実行イメージの作成方法

Java8には"ant-javafx.jar"というant用のプラグインが同梱されており、
JavaFXをjar化するためのタスクを使えるようになっている。

実行可能jarの作成

JavaFX8は通常の実行可能jarとしてビルドできるが、せっかくなので、ant-javafxプラグインを使ってビルドしてみる。


以下、build.xmlを示す。

<?xml version="1.0" encoding="UTF-8"?>
<project name="project" default="default" basedir="."
    xmlns:fx="javafx:com.sun.javafx.tools.ant">

    <!-- ビルド情報 -->
    <description>SimpleApp5のビルド</description>
    <property name="appName" value="SimpleApp5"/>
    <property name="mainClass" value="jp.seraphyware.simpleapp5.SimpleApp5"/>
    <property name="vendorName" value="seraphyware.jp"/>

    <!-- Jdk8のjavapackagerのantタスクを定義する -->
    <taskdef resource="com/sun/javafx/tools/ant/antlib.xml"
        uri="javafx:com.sun.javafx.tools.ant"
        classpath=".:${java.home}/../lib/ant-javafx.jar" />

    <!-- アプリケーション情報 -->
    <fx:application
        id="appid"
        name="${appName}"
        mainClass="${mainClass}"/>

    <!-- 実行可能jarとしてビルドする -->
    <target name="default" description="ビルドと実行可能jarの作成">
        <delete dir="work"/>
        <mkdir dir="work"/>
        <javac srcdir="src" destdir="work"
            includeantruntime="false">
            <classpath>
                <pathelement path="work"/>
            </classpath>
        </javac>

        <fx:jar destfile="${appName}">
            <fx:application refid="appid"/>

            <fileset dir="work"/>
            <fileset dir="src" excludes="**/*.java"/>

            <manifest>
                <attribute name="Implementation-Vendor" value="${vendorName}"/>
                <attribute name="Implementation-Version" value="1.0"/>
            </manifest>
        </fx:jar>
        <delete dir="work"/>
    </target>
</project>

このant-javafx.jarは、JDK8の中に含まれているが、Antから暗黙では参照できないので「${java.home}/../lib/ant-javafx.jar」のように指定している。

JREを含む実行可能イメージの作成

このant-javafxプラグインには他にも様々な機能があり、JREを含む"自己完結型アプリケーションのパッケージ化"が可能になっている。

たとえば、fx:deployタスクを使い、nativeBundler="all"と設定すると、

が作成される。


※ それぞれの環境でビルドする必要がある。(dmg,exe,debが欲しいならば3つのosでビルドしなければならない。)


以下はJREを含む実行可能イメージの作成用スクリプトである。

    <!-- バンドルの作成 -->
    <target name="makeBundles" depends="default"
        description="自己完結型アプリケーションのパッケージ化">
        <delete dir="dist"/>
        <mkdir dir="dist"/>

        <!-- JREを含む実行イメージの作成.(imageタイプ)

         nativeBundles="all"にすると、現在のビルド時の環境で作成できる、
         すべてのタイプのインストーラを含む配布パッケージを作成する.

         * WindowsならInnoSetupがあればexe形式、WiXがあればmsi形式を作成する.
         * Macの場合はdmgとpkgを作成する.
         * Linuxの場合はdebとrpmを作成する.
         -->
        <fx:deploy
            width="800"
            height="600"
            nativeBundles="image"
            outdir="dist"
            outfile="${appName}">

            <fx:application refid="appid"/>

            <fx:resources>
                <fx:fileset dir="." includes="*.jar"/>
            </fx:resources>

            <fx:info
                title="${appName}"
                vendor="${vendorName}"/>
        </fx:deploy>
    </target>

これをWindowsで実行すると、以下のようなフォルダが得られる。

dist
└─bundles
    └─SimpleApp5
        │  msvcp100.dll
        │  msvcr100.dll
        │  packager.dll
        │  SimpleApp5.exe
        │  SimpleApp5.ico
        │  
        ├─app
        │      SimpleApp5.cfg
        │      SimpleApp5.jar
        │      
        └─runtime
            │  COPYRIGHT
            │  LICENSE
            │  README.txt
            │  THIRDPARTYLICENSEREADME-JAVAFX.txt
            │  THIRDPARTYLICENSEREADME.txt
            │  Welcome.html
            │  
            ├─bin
            │  │  attach.dll
            │  │  awt.dll
            │  │  bci.dll
            │  │  dcpr.dll
            │  │  decora_sse.dll
            │  │  deploy.dll
            │  │  dt_shmem.dll
            │  │  dt_socket.dll
            │  │  fontmanager.dll
            │  │  fxplugins.dll
            │  │  glass.dll
            │  │  glib-lite.dll
            │  │  gstreamer-lite.dll
            │  │  hprof.dll
            │  │  instrument.dll
            │  │  j2pcsc.dll
            │  │  j2pkcs11.dll
            │  │  jaas_nt.dll
            │  │  java.dll
            │  │  JavaAccessBridge-64.dll
            │  │  javafx_font.dll
            │  │  javafx_font_t2k.dll
            │  │  javafx_iio.dll
            │  │  java_crw_demo.dll
            │  │  jawt.dll
            │  │  JAWTAccessBridge-64.dll
            │  │  jdwp.dll
            │  │  jfr.dll
            │  │  jfxmedia.dll
            │  │  jfxwebkit.dll
            │  │  jli.dll
            │  │  jpeg.dll
            │  │  jsdt.dll
            │  │  jsound.dll
            │  │  jsoundds.dll
            │  │  kcms.dll
            │  │  lcms.dll
            │  │  management.dll
            │  │  mlib_image.dll
            │  │  msvcr100.dll
            │  │  net.dll
            │  │  nio.dll
            │  │  npt.dll
            │  │  prism_common.dll
            │  │  prism_d3d.dll
            │  │  prism_es2.dll
            │  │  prism_sw.dll
            │  │  resource.dll
            │  │  sawindbg.dll
            │  │  splashscreen.dll
            │  │  sunec.dll
            │  │  sunmscapi.dll
            │  │  t2k.dll
            │  │  unpack.dll
            │  │  verify.dll
            │  │  w2k_lsa_auth.dll
            │  │  WindowsAccessBridge-64.dll
            │  │  zip.dll
            │  │  
            │  ├─plugin2
            │  │      msvcr100.dll
            │  │      npjp2.dll
            │  │      
            │  └─server
            │          classes.jsa
            │          jvm.dll
            │          Xusage.txt
            │          
            └─lib
                │  accessibility.properties
                │  calendars.properties
                │  charsets.jar
                │  classlist
                │  content-types.properties
                │  currency.data
                │  flavormap.properties
                │  fontconfig.bfc
                │  fontconfig.properties.src
                │  hijrah-config-umalqura.properties
                │  javafx.properties
                │  javaws.jar
                │  jce.jar
                │  jfr.jar
                │  jfxswt.jar
                │  jsse.jar
                │  jvm.hprof.txt
                │  logging.properties
                │  management-agent.jar
                │  meta-index
                │  net.properties
                │  plugin.jar
                │  psfont.properties.ja
                │  psfontj2d.properties
                │  resources.jar
                │  rt.jar
                │  sound.properties
                │  tzdb.dat
                │  tzmappings
                │  
                ├─amd64
                │      jvm.cfg
                │      
                ├─cmm
                │      CIEXYZ.pf
                │      GRAY.pf
                │      LINEAR_RGB.pf
                │      PYCC.pf
                │      sRGB.pf
                │      
                ├─ext
                │      access-bridge-64.jar
                │      cldrdata.jar
                │      dnsns.jar
                │      jaccess.jar
                │      jfxrt.jar
                │      localedata.jar
                │      meta-index
                │      nashorn.jar
                │      sunec.jar
                │      sunjce_provider.jar
                │      sunmscapi.jar
                │      sunpkcs11.jar
                │      zipfs.jar
                │      
                ├─fonts
                │      LucidaBrightDemiBold.ttf
                │      LucidaBrightDemiItalic.ttf
                │      LucidaBrightItalic.ttf
                │      LucidaBrightRegular.ttf
                │      LucidaSansDemiBold.ttf
                │      LucidaSansRegular.ttf
                │      LucidaTypewriterBold.ttf
                │      LucidaTypewriterRegular.ttf
                │      
                ├─images
                │  └─cursors
                │          cursors.properties
                │          invalid32x32.gif
                │          win32_CopyDrop32x32.gif
                │          win32_CopyNoDrop32x32.gif
                │          win32_LinkDrop32x32.gif
                │          win32_LinkNoDrop32x32.gif
                │          win32_MoveDrop32x32.gif
                │          win32_MoveNoDrop32x32.gif
                │          
                ├─jfr
                │      default.jfc
                │      profile.jfc
                │      
                ├─management
                │      jmxremote.access
                │      jmxremote.password.template
                │      management.properties
                │      snmp.acl.template
                │      
                └─security
                        blacklist
                        blacklisted.certs
                        cacerts
                        java.policy
                        java.security
                        javaws.policy
                        local_policy.jar
                        trusted.libraries
                        US_export_policy.jar

SimpleApp5.exeという、本物の実行可能ファイルが作成されており、
従来であれば、launch4jなどのサードパーティのツールを使ってexe化し配布用パッケージを作成していたものが、標準の方法だけでexe化することができる。

また、これは配下のruntime下にコピーされているJREのサブセットを利用して実行されるように設定されている。


したがって、この「dist\bundles\SimapleApp5」以下のフォルダごと圧縮して他のマシンに持って行けば、マシンに既にJava8u40がインストールされているか問わず、そのまま利用可能となる。


※ なお、これはJREをまるごとコピーしたものではなく、ファイルをみると実行に必要のないツール類(*.exe)は含まれていない。

※ なお、JNLPを使ったウェブ配備用のファイルも作成されるが、今回は使う予定はないので無視している*9


これはWindowsの例であるが、同様にMacで実行すると、やはりMacで実行するためのアプリケーションバンドルが作成される。

とくにMacの場合はアプリケーションバンドルにすることでスクリーンメニュー上のアプリケーション名も、きちんと設定されるようになる、という重要なメリットがある。

結論

使ってみたところ、とくには嵌る点もなく、案外使いやすい。

複雑なものについては、まだ試していないので分からないのだが、簡単に使う分には、気軽に使える程度のものであることは分かった。


Scene Builderによるグラフィカルな編集ができるのは、やはり大きなメリットだと思う。


また、Scene Builderを使わずとも、FXML自体もわかりやすいので、パッと見て、どんな画面になるのか想像できる。

Swingのようにコードを読みながら画面がどう構成されるのかを考える難しさは軽減されており、レイアウトまわりの見通しが大変良いため、画面改修時の保守性が高いことは間違いないと思われる。


なので、とりあえず、今後はJavaFXの習得を兼ねて、簡単なところからJavaFXを使ってみたいと思う。


今回のコードは、GitHubに置いてあります。

今までに判明している注意点
  1. JavaFX8に対応していない環境だと画面が激しく乱れる。(サポートされていないグラボがあるらしい?)
    • MacBook2007LateのLion上に入れたJdk8で発生。Sceneがぐちゃぐちゃになり何も見えない。
  2. 動きがおかしなところがあったりする。たとえば、とても長いテーブルのホイールスクロールとか、かなり怪しい。(使い方が悪いのかも?)
  3. JavaFX標準のチャートのカスタマイズは大変難しい。(cssを使わざるを得ず、且つ、それが大変めんどくさい。)
  4. JogAmpや、ImageIOといった類がSwingやBufferedImageを想定しており、JavaFXでは直接使えないので、いろいろ苦労しそう。
  5. バージョンによって動きが変わることがある。バグが結構残存している感があり、updateのたびに改善さ

*1:http://www.oracle.com/technetwork/jp/java/javafx/overview/faq-1446554-ja.html#6

*2:JDT2015の櫻庭さんのセッションによると http://gluonhq.com/gluon-supports-scene-builder/ からビルド済みのものが取得できるらしい?

*3:JDT2015の櫻庭さんのセッションによると、Eclipse NetBeans等があればSceneBuilderはビルドできるらしい。

*4:初期化のためにをオーバーライドできる。

*5:自分でソースからビルドしたScene Builderの場合は連携がうまくゆかない場合もあるらしい。

*6:JDT2015で聞いてきたところによると

*7:JDT2015で聞いてきたところによると

*8:画像は、こちらのフリー素材からいただいたものをサイズ加工しています。 http://flowerillust.com/html/orange/orange1001.html

*9:いらないので作成したくないのだが、どうやっても必ず作成されるようだ。

ForkJoinPoolにおけるタスクのブロッキングの実装方法 (2014/06/03)

ForkJoinPoolにおけるタスクのブロッキングの方法

先にForkJoinPoolの特性や使い方について調べたが、「同期処理」や、単純な「待ち」を含むタスクが、どのようになるのか調べてみた。


結論からいうと、同期をとる必要があるタスクは、Java8でサポートされるCompletableFutureを使って、タスクを細切れにして分割したり結合したりするのが、もっとも手っ取り早い。


しかし、ForkJoinPoolが持つメカニズムとしては、少し手間をかけることで自前で実装することも可能となっている。

単純にブロッキングした場合にForkJoinPoolはどうなるか?

ForkJoinPoolは、そのタスクの中でjoinすることでタスクを待ち状態にすると、ただちに空いたCPU資源を他のタスクに割り当てるようにスケジューリングする。

もちろん、スケジューラは、joinメソッドの中で、この切り替えを行っている。


では、たとえば単純に「Thread.sleep()」などでタスクを待ち状態にしたら、どうなるであろうか?


スケジューラはタスクから何も通知を受けることが無いため、実際にはスレッドは寝ているにもかかわらず、他のスレッドにCPUを割り当てることができない。

これは、synchronized/wait/notifyによるスレッドのブロックや、あるいは同期クラスを使ったスレッドのブロックも同様である。

もし、アクティブなスレッドがすべてブロックしたならば、それらのブロックが解除されるまでタスクは全く実行されなくなってしまうことになる。


したがって、ForkJoinTaskの中では、これらのスレッドを直接ブロックするような同期処理は使うべきではない。


もし、ForkJoinTaskの中でタスクのブロックが必要な場合には、ManagedBlockerを使うことができる。

これは、ブロッキングを行うべきか判定するメソッドと、実際にブロッキングを行うメソッドの2つのメソッドをもつ簡単なインターフェイスである。

ここに同期に必要な処理を記述したのちに、ForkJoinPool.managedBlock(java.util.concurrent.ForkJoinPool.ManagedBlocker)メソッドに渡すことで、ブロッキングが必要であればアクティブなタスクを切り替えることができるようにスケジューラと協調してブロッキングを行うことができるようになる。

ForkJoinTaskの中で協調的に動作するSleepの実装例
    // 任意の時間、スリープする管理されたブロッカーを構築する関数
    LongFunction<ManagedBlocker> managedSleepFactory =
            (long tim) -> {
                // 開始時間
                long st = System.currentTimeMillis();
                
                return new ManagedBlocker() {
                    @Override
                    public boolean block() throws rruptedException {
                        if (!isReleasable()) {
                            // まだ指定時間経過していなければ
                            // 10mSecスリープする.
                            // (最小分解能を10mSecとする)
                            Thread.sleep(10);

                            // まだスリープする必要があるか?
                            return isReleasable();
                        }
                        return true;
                    }

                    @Override
                    public boolean isReleasable() {
                        // 開始時間から指定時間が経過しているか?
                        long span = System.currentTimeMillis() - st;
                        return span > tim;
                    }
                };
            };
ForkJoinTaskの中で協調的に動作するSleepの使用例
    // タスクごとのループ回数
    int mxLoop = 5;

    // タスクを連続して生成する
    ForkJoinPool fjPool = ForkJoinPool.commonPool();
    List<ForkJoinTask<?>> tasks = new ArrayList<>();
    IntStream.rangeClosed(0, 14).forEach(idx -> {
        tasks.add(fjPool.submit(ForkJoinTask.adapt(()->{
            IntStream.rangeClosed(0, mxLoop).forEach(loop -> {
                // タスク内ループからログ出力する
                logWriter.accept(idx, loop);
                try {
                    // 一定時間スリープする
                    ForkJoinPool.managedBlock(managedSleepFactory.apply(200));
                    //Thread.sleep(200); // 管理されていないスリープ

                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                    // 無視する.
                }
            });
        })));
    });
    
    // すべてのタスクの完了を明示的に待つ
    // (すべてのスレッドがブロックしているとタスク完了前に
    // awaitTerminationが抜けてしまうため。Java8u4で確認。)
    tasks.forEach(ForkJoinTask::join);
    
    // ForkJoinPoolを終了する。
    fjPool.shutdown();
    fjPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
ログ出力用ヘルパ関数
    // タスクからのログを受け取る
    AtomicInteger cols = new AtomicInteger();
    BiConsumer<Integer, Integer> logWriter = (idx, loop) -> {
        String text = String.format("[%02d] %02d%s  ",
                idx, loop, (loop == mxLoop) ? '*' : ' ');
        synchronized (cols) {
            int col = cols.incrementAndGet() % 10; // 10個ごとに改行
            if (col == 0) {
                System.out.println(text);
            } else {
                System.out.print(text);
            }
        }
    };
実行結果

上記のコードを実行すると、タスクを連続して15個作成し、
それぞれのタスクで5回ログを出しつつ、1回ごとに200mSecのSleepを行う。


このとき、ManagedBlockerによって協調的にスリープするため、スリープしている間は他のタスクがアクティブとなる。


その結果、すべてのタスクが、あたかも同時実行的に並列に進行してゆくことが確認できる。

角括弧の中はタスク番号であり、「*」マークが1タスク中の最終処理を表していて、すべてのタスクの最終処理がまとまって処理されていることがわかる。

[05] 00   [04] 00   [00] 00   [06] 00   [02] 00   [01] 00   [03] 00   [07] 00   [08] 00   [09] 00   
[10] 00   [11] 00   [12] 00   [13] 00   [14] 00   [04] 01   [02] 01   [03] 01   [01] 01   [05] 01   
[00] 01   [06] 01   [11] 01   [07] 01   [08] 01   [09] 01   [10] 01   [12] 01   [13] 01   [14] 01   
[04] 02   [00] 02   [02] 02   [03] 02   [05] 02   [01] 02   [06] 02   [11] 02   [07] 02   [08] 02   
[09] 02   [10] 02   [13] 02   [12] 02   [14] 02   [04] 03   [05] 03   [03] 03   [02] 03   [00] 03   
[01] 03   [06] 03   [11] 03   [07] 03   [09] 03   [08] 03   [10] 03   [14] 03   [12] 03   [13] 03   
[05] 04   [01] 04   [03] 04   [04] 04   [02] 04   [00] 04   [06] 04   [11] 04   [07] 04   [09] 04   
[08] 04   [10] 04   [14] 04   [13] 04   [12] 04   [03] 05*  [05] 05*  [00] 05*  [01] 05*  [04] 05*  
[02] 05*  [06] 05*  [11] 05*  [07] 05*  [08] 05*  [09] 05*  [10] 05*  [12] 05*  [13] 05*  [14] 05*  


これに対して、コメントアウトしている「Thread.sleep(200)」のほうを有効とすると、タスクの切り替えは発生しないため、処理をはじめたタスクのスリープ解除と完了を待ってから次のタスクに進む、という効率の悪い動きとなる。

また、タスクの最終処理はタスクの処理順となっている。
直列的な動作となるため、当然、トータルの所要時間は前のものよりも長くなる。

(以下は、8個の論理CPUがあるマシンの場合の動きである。)

[01] 00   [03] 00   [05] 00   [00] 00   [06] 00   [04] 00   [02] 00   [03] 01   [01] 01   [02] 01   
[05] 01   [04] 01   [00] 01   [06] 01   [01] 02   [05] 02   [02] 02   [00] 02   [03] 02   [04] 02   
[06] 02   [06] 03   [00] 03   [01] 03   [03] 03   [05] 03   [02] 03   [04] 03   [03] 04   [02] 04   
[05] 04   [06] 04   [00] 04   [01] 04   [04] 04   [03] 05*  [01] 05*  [02] 05*  [05] 05*  [00] 05*  
[06] 05*  [04] 05*  [07] 00   [10] 00   [08] 00   [09] 00   [11] 00   [12] 00   [13] 00   [10] 01   
[11] 01   [07] 01   [09] 01   [12] 01   [08] 01   [13] 01   [09] 02   [07] 02   [10] 02   [08] 02   
[12] 02   [11] 02   [13] 02   [07] 03   [11] 03   [10] 03   [09] 03   [12] 03   [08] 03   [13] 03   
[12] 04   [10] 04   [07] 04   [11] 04   [08] 04   [09] 04   [13] 04   [08] 05*  [12] 05*  [10] 05*  
[09] 05*  [07] 05*  [11] 05*  [13] 05*  [14] 00   [14] 01   [14] 02   [14] 03   [14] 04   [14] 05*  


Java8のCompletableFutureを使う

複数のタスクに処理を分解し、それぞれのタスクの実行順序を決めるのであれば、CompletableFutureをつかうと、それらのタスクの実行順序や待ち合わせなどが手軽に実装できる。

3つのタスクを順番に実行するタスクをつくり、これらのタスクを一括して待ち合わせるタスク
    // 複数個の非同期フューチャーを生成して即時実行を開始する.
    @SuppressWarnings("unchecked")
    CompletableFuture<Void>[] cfs = IntStream.rangeClosed(0, 11).oObj(idx -> {
        return CompletableFuture.supplyAsync(() -> {
                // 一段目
                String msg = String.format("[%02d]job-A", idx);
                System.out.println(msg);
                uncheckedSleep.accept(1000);
                return msg;

            }).thenApplyAsync(prevResult -> {
                // 二段目
                String msg = String.format("[%02d]job-B: %s", idx, Result);
                System.out.println(msg);
                uncheckedSleep.accept(500);
                return msg;

            }).thenApplyAsync(prevResult -> {
                // 三段目
                String msg = String.format("[%02d]job-C: %s", idx, Result);
                System.out.println(msg);
                uncheckedSleep.accept(800);
                return msg;

            }).thenAcceptAsync(System.err::println);
        }).toArray(len -> new CompletableFuture[len]);

    // すべてのフューチャーの完了を待ち合わせる
    CompletableFuture<Void> jobs = CompletableFuture.allOf(cfs);
    System.out.println("waiting...");
    jobs.join();
    System.out.println("done!");

なお、CompletableFutureの非同期タスク(*async)系メソッドは、暗黙でForkJoinPoolの既定のプールを使用するので、上記コードは裏ではタスクはForkJoinPool上で動いている。

スリープ関数のヘルパ

スリープが検査例外を出すとラムダ式が面倒なことになるので、ちょっとヘルパを噛ませておく。

    // 検査例外を出さないスリープ
    LongConsumer uncheckedSleep = tm -> {
        try {
            ForkJoinPool.managedBlock(managedSleepFactory.apply(tm));
            //Thread.sleep(tm); // 管理されていないスリープ
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    };
実行結果

waiting...
[06]job-A
[04]job-A
[00]job-A
[01]job-A
[05]job-A
[02]job-A
[03]job-A
[07]job-A
[08]job-A
[09]job-A
[10]job-A
[11]job-A
[06]job-B: [06]job-A
[01]job-B: [01]job-A
[11]job-B: [11]job-A
[02]job-B: [02]job-A
[09]job-B: [09]job-A
[08]job-B: [08]job-A
[03]job-B: [03]job-A
[04]job-B: [04]job-A
[05]job-B: [05]job-A
[07]job-B: [07]job-A
[10]job-B: [10]job-A
[00]job-B: [00]job-A
[11]job-C: [11]job-B: [11]job-A
[06]job-C: [06]job-B: [06]job-A
[01]job-C: [01]job-B: [01]job-A
[03]job-C: [03]job-B: [03]job-A
[07]job-C: [07]job-B: [07]job-A
[02]job-C: [02]job-B: [02]job-A
[08]job-C: [08]job-B: [08]job-A
[09]job-C: [09]job-B: [09]job-A
[00]job-C: [00]job-B: [00]job-A
[10]job-C: [10]job-B: [10]job-A
[04]job-C: [04]job-B: [04]job-A
[05]job-C: [05]job-B: [05]job-A
[07]job-C: [07]job-B: [07]job-A
[08]job-C: [08]job-B: [08]job-A
[02]job-C: [02]job-B: [02]job-A
[06]job-C: [06]job-B: [06]job-A
[01]job-C: [01]job-B: [01]job-A
[03]job-C: [03]job-B: [03]job-A
[11]job-C: [11]job-B: [11]job-A
[00]job-C: [00]job-B: [00]job-A
[10]job-C: [10]job-B: [10]job-A
[05]job-C: [05]job-B: [05]job-A
[04]job-C: [04]job-B: [04]job-A
[09]job-C: [09]job-B: [09]job-A
done!

タスクが順序よく実行されていることが確認できる。

まとめ

  • ForkJoinTaskの中では直接ブロッキングするメソッドは使わない
  • ブロッキングが必要となる場合は ManagedBlocker を実装する
    • ただし、Java8で CompletableFuture を使えるなら、そのほうが簡単である。

以上、メモ終了

ForkJoinPoolとForkJoinTaskの特徴と使い方

Fork/Joinとは?

JavaSE7でサポートされるjava.util.concurrent.ForkJoinPoolは、ExecutorServiceの一員であり、一見するとThreadPoolExecutorに似たようなものに思えるが、実際は全く別の、異質のものである。


ForkJoinPoolは、ありていに言えばWork-stealingアルゴリズムの実装であり、無数のタスクを無駄なく論理CPU分のスレッドに割り当てるものである。

ForkJoinPoolはデフォルトでは論理CPU数分のスレッドプールをもつように構築される。

そして、あるタスクが完了するか、もしくは待ちになったら、すぐに別のタスクがアクティブとなり、常に、論理CPU数分のスレッドだけがアクティブになるように調整される。


また、ForkJoinPoolに入れたForkJoinTaskは、そのタスクの中で新たなタスクをforkすると、同じForkJoinPool内にタスクを予約する。
このとき、タスクは既定ではStackのようにタスクが積まれるため、再帰的な計算をさせるのに向いている。


ForkJoinTaskのタスクは他のタスクと連携しあっており、タスク内でforkした子タスクをjoinして結果を待つ場合には、そのスレッドは「待ち」に入るのではなく、タスクが「中断」された状態となり、スレッドは他のタスクの処理に回される(Work-stealing)ようになっている。

(つまり、常に論理CPUに空きがでないようにタスクが割り当てられ、joinしても、スレッドは待ち状態にならない、ということでもある。)


このようなWork-stealing型の他の言語の処理ライブラリとしては、C/C++やFortanで広く使えるOpenMPや、MicrosoftのPPL(並列パターン ライブラリ)のようなものがある。


また、ForkJoinTaskでは、forkしてjoinする場合、最後にforkしたタスクがまだスレッドに割り当てられていなければ、join後に、そのまま同じスレッドでforkした処理を継続する、つまりスレッドを切り替える必要がない、といった最適化を施すことができるようになっているようである。



(なお、ForkJoinTask#invokeは、forkとjoinを同時に行うコンビニエンスメソッドである。)


このように、ForkJoinPoolでキモとなるのは、その名のとおり「fork」と「join」である。

とりわけjoinによってタスクが中断してスレッドが別のforkしたタスクの処理に回されるところが重要である。

実験コード

package jp.seraphyware.forkjoinsample;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    /**
     * タスク内からタスクを生成する最大深さ
     */
    private static final int maxDepth = 4;

    /**
     * 実行中、もしくは待機中のタスクの数.
     */
    private static AtomicInteger activeTaskCnt = new AtomicInteger(0);

    /**
     * タスクを生成して返す.
     * @param no タスクの識別子、深さも表す
     * @return タスク
     */
    private static ForkJoinTask<?> createRecursiveTask(final String no) {
        return new RecursiveAction() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void compute() {
                int numOfEntered = activeTaskCnt.incrementAndGet();
                log("[enter] activeTask=" + numOfEntered);
                try {
                    if (no.length() < maxDepth) {
                        // 最大深さに達していなければ、さらに10個の子タスクを生成する.
                        ForkJoinTask<?>[] tasks = new ForkJoinTask<?>[10];
                        for (int idx = 0; idx < tasks.length; idx++) {
                            tasks[idx] = createRecursiveTask(no + idx);
                        }
                        // 子タスクをすべて一括実行し、それを待機する.
                        // (内部的にはforkしてjoinしている)
                        invokeAll(tasks);
                    }
                } finally {
                    numOfEntered = activeTaskCnt.decrementAndGet();
                    log("[leave] activeTask=" + numOfEntered);
                }
            }

            /**
             * 診断メッセージ表示用
             * @param prefix
             */
            private void log(String prefix) {
                // 1つのスレッドの使われ方を見るために、特定のスレッドだけを診断メッセージを表示する。
                if (Thread.currentThread().toString().equals("Thread[ForkJoinPool-1-worker-1,5,main]")) {
                    StringBuilder buf = new StringBuilder();
                    buf.append(prefix);
                    buf.append(" no.").append(no);
                    buf.append(" :numOfActive=").append(getPool().getActiveThreadCount());
                    buf.append(" :poolSize=").append(getPool().getPoolSize());
                    buf.append(" :").append(Thread.currentThread());
                    System.out.println(buf.toString());
                }
            }
        };
    }

    public static void main(String[] args) throws Exception {
        // ForkJoinスレッドプール作成、デフォルトは論理CPU数
        ForkJoinPool forkJoinPool = new ForkJoinPool();

        // 最初のタスクを生成し、完了を待つ
        forkJoinPool.invoke(createRecursiveTask("0"));

        // スレッドプールの終了
        forkJoinPool.shutdown();
        forkJoinPool.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("done");
    }
}

実験結果

特定のスレッドの動きだけを確認するため、1つのスレッドのみにログを絞り込んでいる。

[enter] activeTask=1 no.0 :numOfActive=1 :poolSize=1 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=3 no.00 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=8 no.000 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=15 no.0000 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0000 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=17 no.0001 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=18 no.0001 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=22 no.0002 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0002 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=16 no.0003 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=17 no.0003 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=19 no.0004 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0004 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=19 no.0005 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=17 no.0005 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=16 no.0006 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=12 no.0006 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=14 no.0007 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=13 no.0007 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=16 no.0008 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=15 no.0008 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=14 no.0009 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=15 no.0009 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=10 no.000 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=12 no.001 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=16 no.0010 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0010 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=18 no.0011 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0011 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=17 no.0012 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0012 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=17 no.0013 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=17 no.0013 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=20 no.0014 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0014 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=17 no.0015 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0015 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=15 no.0016 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=13 no.0016 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=15 no.0017 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=12 no.0017 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=15 no.0018 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=13 no.0018 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=13 no.0019 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=9 no.0019 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=10 no.001 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=9 no.0029 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=5 no.0029 :numOfActive=7 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=1 no.00 :numOfActive=1 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=0 no.0 :numOfActive=1 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
done

1つのスレッドで繰り返しcomputeメソッドがenterされていることが分かるようにタスクはjoinによって中断・再開されている。

ForkJoinTaskとRecursiveTask/RecursiveActionの違い

ForkJoinPoolに入れるタスクはForkJoinTask型であるが、実際にはRecursiveTaskかRecursiveActionのいずれかから派生するのが便利である。

RecursiveTaskは戻り値のあるタスク、RecursiveActionは戻り値が無いタスクを定義するのに使う。


この2つのクラスの実装は非常に単純で、その親であるForkJoinTaskが戻り値のあり・なしにかかわらず処理できるように汎用的になっているため、派生クラスで用途に応じて必要な部分だけを実装すれば良いように単純化しているだけの、単純なアダプタークラスにすぎない。

そのあたりはFork/Joinのメカニズムとは直接関係ない。

ForkJoinPoolのasyncモードでの利用

ForkJoinPoolを、論理CPU数分のスレッドに空きが出ないようにタスクを割り当てるスレッドプールとして利用することもできる。


通常、ForkJoinPoolに入れるタスクはfork/joinの利用に適したようにスタック式に積み上げられるが、
コンストラクタでasyncモードを指定することでイベント処理に適したFIFO式に設定することができる。


ForkJoinPoolは、タスク内で新たにタスクをforkする必要がなければ、普通なCallable, Runnableをタスクとして入れることができ、
また、Callable, RunnableをForkJoinTaskに変換するためのForkJoinTask.adapt()メソッドを使うこともできる。


すべてのタスクがCallable, Runnableのように内部で子タスクを作ることがなければ、ForkJoinPoolはasyncモードにしてタスクの実行順序をFIFOにしたほうが都合がよい場合が多いと思われる。

package jp.seraphyware.forkjoinsample;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args) throws Exception {
        ForkJoinPool pool = new ForkJoinPool(
                Runtime.getRuntime().availableProcessors(),
                ForkJoinPool.defaultForkJoinWorkerThreadFactory,
                null,
                true); // タスクの積み上げにFIFOモードを使用する.(通常はSTACKモード)

        for (int idx = 0; idx < 20; idx++) {
            final int cnt = idx;
            pool.submit(new Runnable() { // 内部的にはForkJoinTask.adapt()でForkJoinTaskに変換している.
                @Override
                public void run() {
                    System.out.println("idx=" + cnt + "/"
                            + Thread.currentThread());
                    try {
                        Thread.sleep((long) (Math.random() * 100));
                    } catch (InterruptedException ex) {
                        // 何もしない
                    }
                }
            });
        }
        pool.shutdown();
        pool.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("done");
    }
}

この場合、タスクを入れた順に処理されることになる。
ただし、それぞれのタスクの完了までの時間によっては並列実行しているために順序が入れ替わることもありえる。

結論

fork/joinと同じやり方で、ThreadPoolExecutorでタスク内で自分自身のスレッドプールに新しいタスクを割り当て(submit)して、そのタスクをjoin(get等)した場合、1つのスレッドが割り当てられ、且つ、1つのスレッドが待ち状態となるため、あっという間に論理CPU数分のスレッドを使いきって、スレッドプールが枯渇すると思われる。


タスクをfork/joinという小さな粒度で分割しスレッドを効率的に使いまわす仕組みは、従来のExecutorのタスクの単位の制御とは全然違うし、従来のExecutorを直接使うのでは得られない効果である。


ForkJoinPoolを使うことで、たしかに分割統治的なアルゴリズムで並列処理させたい場合には、論理CPUの個数にかかわらず、単に再帰的な処理を記述するだけで並列処理が実現できるようになる。

ForkJoinPoolに再帰タスクを入れるだけで、スレッドの割り当てなどの面倒な処理はお任せできるようになっている。

これは自前で用意するのは、なかなか面倒なものだと思うので、Fork/Joinは大変有益なAPIであるのではないか、と思われる。


また、Java8からは新たに、CompletableFutureというタスク処理のクラスが追加されており、こちらは従来のタスク処理とForkJoinPoolの処理とを統合した、最終形態的な大量メンバを抱える多機能クラスとなっている。

CompletableFutureは、C#などで使われる継続タスク、あるいはjQueryのDeferredのような形でタスクを定義してゆくことができるようになっており、これを非同期タスクとして処理する場合は暗黙でForkJoinPoolを使うようになっているため、Java8では、ForkJoinPoolの活用の機会も増えそうである。

なお、JavaEE7からはjavax.enterprise.concurrent.ManagedExecutorServiceなどでスレッドの制御が可能になっているものの、残念なことに、ForkJoinPoolは対象外になっている。*1 *2

関連資料

*1:fork/joinの細かなタスクの切り替え処理が、JavaEEのスレッドコンテキストまわりの調整とかで難しかった、とか、そうゆう理由だろうか?

*2:JavaEEで、CompletableFutureを使う場合は、ForkJoinPool以外のExecutorを明示的に渡す必要がある、とのこと。