hatakeのブログ

社会人ひよこプログラマのtil

匿名クラスをシリアライズするときの奇妙な挙動について

TL;DR

  • 非staticのコンテキストで宣言された匿名クラスはエンクロージングインスタンスへの非transientな参照を持つ

匿名クラスでオブジェクトの初期化

Javaでは匿名クラスと初期化ブロックを組み合わせて以下のような書き方ができます。

User user = new User(){{
    setName("hoge");
}};

C#のオブジェクト初期化子のように書けるので一見便利ですが、シリアライズが絡む場合に直感的でない挙動をするので注意が必要です。
下記にオブジェクト初期化のパターンを4つ挙げてみます。

class User implements Serializable{
    private String name;
    public void setName(String name){
        this.name = name;
    }
}

class Main{
    public static Main(String[] args){
        User user1 = new User();
        user1.setName("hoge");

        User user2 = new User(){{
            setName("hoge");
        }};
        new Main.exec();
    }

    private void exec(){
        User user3 = new User();
        user3.setName("hoge");

        User user4 = new User(){{
            setName("hoge");
        }};
    }
}

このうち、user4のインスタンスをシリアライズしようとするとNotSerializableExceptionの実行時エラーが発生します。 これは非staticなコンテキストで宣言された匿名クラスが暗黙的にエンクロージングインスタンス(ここではMainクラスのインスタンス)を保持していることに起因します。 これによりMainクラスまでがシリアライズの対象となり本来の意図とは異なる動作をします。

エンクロージングインスタンスへの参照は明示的にtransientを付与することはできず、仮にMainクラスにSerializableでマークしてもさらに外側にクラスがある場合にそちらに伝播するだけなので注意が必要です。

参考 docs.oracle.com

C#で最長増加部分列(LIS)の実装サンプル

競プロTips勉強メモです。

挿入ソートの最悪計算量は  Ο(n^ 2) です。制約下のカードの最大数 3*10^ 4 でそのまま挿入ソートを実装するとTLEしそうです。(※)

ここでは昇順になっていないカードを一回で正しい位置に挿入する操作ができるので、逆に配列のなかで既に昇順になっている部分列のうち最長のものの長さを計算することで答えが求まります。 入力例1の135246が与えられた場合は、1346というような部分列の長さを求めればよいことになります。

以下、C#での最長増加部分列(LIS:Longest Increasing Subsequence)の長さを求めるサンプルコードです。 配列の長さをインデックス、長さに対応する部分列の最後の値の最小値をアイテムとする動的計画法での実装です。
このdpテーブルは単調増加なので更新箇所の探索は二分探索で行うことができます。
計算量は Ο(n\log n) になります。

Longest Increasing Subsequence(LIS) with C#

※ 実は Ο(n^ 2)解でも通ってしまいます。

DICOMでJPEGを扱うときの注意点

DICOMを知っている人向けです。 DICOMでは転送構文というものが定義されており、一般でいうところのエンコーディング規則に相当します。転送構文にはJPEGコーデック指すものが含まれていますが、その中には特殊な形式のJPEGが存在するので扱いには気を付けなければという備忘録です。

転送構文とコーデックの関係

DICOMでは公式に定義される転送構文(TransferSyntax:以下TS)からDICOMメッセージのデータセット部分のエンコーディング規則を決定します。TSの情報はDICOMメタ情報部分の(0002,0010)にセットされており、これは必ずImplicit VR Endianで読み取ります。

つまり、DICOMファイルからピクセルデータを抽出する場合、この転送構文をみてデコーダを変える処理が必要です。大体の場合ライブラリがサポートしてくれますが、基本的にこのメタ情報のTSからコーデックを判別するのでメタ情報の有無はチェックしたほうが良いでしょう。特にSTORAGE-SCPの実装時は受信したデータにメタ情報が入っていなかったりするので注意する必要があります。

DICOM通信における転送構文

DICOMでは35種類もの転送構文が定められています。よく使われるのは以下の転送構文です。

UID NAME 備考
1.2.840.10008.1.2 Implicit VR Endian RAWデータ
1.2.840.10008.1.2.4.50 JPEG Baseline 非可逆圧縮
1.2.840.10008.1.2.4.70 JPEG Lossless, Nonhierarchical, First- Order Prediction 可逆圧縮

アプリケーションエンティティ(以下AE)間の通信ではまず使用する転送構文を決定しなくてはなりません。
通信を開始するには
  1. 通信を開始する側(以下calling AE)がされる側(以下called AE)に対して自分が対応できる転送構文を1つ以上提示する。(A-ASSOSIATE-REQ)
  2. called AEは提示された転送構文の中から自分が対応できる転送構文を1つ選択し、レスポンスを返す。(A-ASSOSIATE-RES)
という手順が踏まれます。

さらに、この手順は下記のルールに従って行う必要があります。

  1. A-ASSOSIATE-REQの提示する転送構文には少なくともImplicit VR Endianが含まれていなければならない。
  2. (a)のルールに従ってA-ASSOCIATE-REQを受けたcalled AEは、このアソシエーションを拒否してはならない。

送り手も受け手もImplicit VR Endianをサポートすれば最悪それで通信出来るようにしてねという意味合いが込められています。 しかし、このルールには以下のような例外があります。

  • calling AEが送信する非可逆なコーデックのピクセルデータにアクセスできるが、それを読み取れるような転送構文をサポートしていない場合は(a),(b)ともに無視してよい
  • calling AEが送信する非可逆なコーデックのピクセルデータにアクセスできて且つ、それを読み取れる転送構文をサポートしている場合は(b)を無視してよい

分かりにくいですが、これをルールを踏まえて言い換えると、callingAEは無損失圧縮の画像をImplicit VR Endianで送れるようにRAWデータにしてから送ることができる必要があるが、calling AEが損失圧縮の画像しか持たない場合、提示する転送構文にImplicit VR Endianを含まなくても良い、ということになります。

規格上はcalled AEはLossyな画像の転送構文のみ提示されたアソシエーションを受けた場合は拒否しても良いということになりますが、ユースケースを考えると大体の場合called AE側で圧縮画像が送られてきた場合の対応することになるので、このあたりも注意が必要です。

JPEG Lossless, Nonhierarchical, First- Order Predictionは普通のJPEGではない

Implicit VR EndianとJPEG Baselineでの読み取りは問題ありません。(それぞれ生データと普通のJPEGファイル) 問題はLossless JPEG形式の転送構文で送られてきた場合です。
JPEG Lossless, Nonhierarchical, First- Order PredictionはISOで定められた規格ですが、医療画像以外の分野ではほぼ使用されていません。なので通常のJPEGコーデックでサポートされないことがほとんどです。多くの場合DICOMライブラリにデコーダが含まれています。このデコーダの有無は非常に大きいので、ライブラリの選定基準の一つになると思います。

Javaからexeを起動したらデッドロックしてしまった話

TL;DR

下記の条件をすべて満たすとデッドロックします。

  • 呼び出されるexeが標準出力や標準エラー出力に何か書き込むプログラムを持つ
  • ProcessBuilder.start()やRuntime.exec()の後に何もせずProcess.waitFor()する

解決策はProcessが持つストリームのバッファを詰まらせないことです。

何が起きたのか

こんなコードを書いていました。

String[] Command ={"cmd","/c","Hogehoge.exe " + args};
Process process = runtime.exec(Command,null,workingDir);
process.waitFor();

やりたかったことはHogehoge.exeを起動し、その終了を待って次の処理へ…という感じでした。しかしいくら待ってもprocess.waitForから制御が戻ってきません。この現象は必ず発生し、Hogehoge.exeのログを見ると毎回異なる箇所で止まっていました。このexeはメモリを多く使う処理を含んでおり、且つ繰り返し呼ばれるものだったのでまず最初にメモリリークを疑いましたが、真の原因はこいつが使っている標準出力にありました…

原因は未処理の標準出力ストリーム

今回のHogehoge.exeはC#で作成されていました。そしてこのexeはコマンドライン上で単体試験する為に以下のようなコードが書かれています。

Console.WriteLine("ほげほげ");

Helloworldに使うやつですね。単体で起動すれば黒い画面が出てきて文字列が表示されるアレです。今回このexeが外部のプロセスから起動されたことでこの標準出力が親であるJavaプロセスの方へストリームの形で流れていました。
Java側では当然ストリームに対して読み出しをしていないので、すぐにバッファがいっぱいになります。ストリームに書き込めなくなった時点で、書き込めるようになるまで処理が止まってしまうことが今回の原因でした。

Processの罠

ストリームを開いているのに何もせず後処理もしないのはプログラマならヤバいと気づくと思います、問題はストリームが開いていることに気づきにくい点です。
外部プロセスを起動するときに使用するRuntime.exec()やProcessBuilder.start()は戻り値にProcessオブジェクトが返ります。これが各種ストリームを含んでいます。これを下記のように読み捨てる処理を入れないと今回のような事件が起きてしまうという話でした。

String[] Command ={"cmd","/c","Hogehoge.exe " + args};
Process process = runtime.exec(Command,null,workingDir);
InputStream is = p.getInputStream()
int c;
while ((c = is.read()) != -1) {}
process.waitFor();

Processは他に標準エラー出力ストリームを持ちます。こちらもexeで使用していると同時に読み出しが必要になるので、スレッドを立てて読み出しを行うなど工夫が必要です。ただし、ProcessBuilderからの起動であれば標準エラー出力を標準出力の方へリダイレクトできる機能があるので先の例のようなコードでOKです。もちろんストリームの後処理も忘れずにしましょう。

WSL(旧:Bash on Windows)のルートディレクトリが無い

ものごっつハマったので備忘録です。Ubuntu 18.04を再インストールしていた時の出来事。

昔ルートディレクトリがあった場所
C:\Users\%USERNAME%\AppData\Local\lxss
今(2018/07/25時点)ルートディレクトリがある場所
C:\Users\%USERNAME%\AppData\Local\Packages\CanonicalGroupLimited.UbuntuonWindows_79rhkp1fndgsc\LocalState\rootfs

去年(2017)のFall Creator's Updateから変わったそうです。MicroSoft Storeからディストリビューションをダウンロードできるようになった頃?
Location of WSL rootfs filesystem for post - Fall Creator's Update installations