Saturday, September 23, 2017 / Java, Groovy

Java でパイプを使う / 複数の変換処理をつなげて処理する

Javaでパイプする方法がよくわからなかったので、いろいろ試行錯誤した結果をまとめました。 Javaで書くとコードが長くなるので、Groovy で記述しています。

ここでは話を簡単にするために、最初の入力として以下のような果物名を列挙したデータがあり、 これを二つのフィルタを使って処理する例を考えます。

melon lime lemon raspberry cherry kiwi banana apple orange grape

このデータを元に以下の2つのフィルタを使ってデータを変換します。

  • フィルタ1 : スペースで分割して 一行に一つの果物名を出力する
  • フィルタ2 : 果物名でソートして出力

Unixのコマンドラインで言えば以下のようなイメージです。

cat fruits.txt | filter1 | filter2 > result2.txt

パイプを使わない場合

最初にパイプは使わないで、フィルタ1 の結果をテキスト(result1)に書き出し、それを フィルタ2 に与えるコードです。

// スペースで分割して、一行に一つの果物名を出力するフィルタ
def filter1 = { Reader reader, Writer writer->
    reader.readLines().each { line->
        writer << line.split(/ /).join(System.getProperty('line.separator'))
    }
}

// 果物名でソートして出力
def filter2 = { Reader reader, Writer writer->
    def list = reader.readLines()
    Collections.sort(list, {String a, String b-> a.compareTo(b) } as Comparator)
    writer << list.join(System.getProperty('line.separator'))
}

// -------
// main
// -------
def text = 'melon lime lemon raspberry cherry kiwi banana apple orange grape'

def reader1 = new StringReader(text)
def writer1 = new StringWriter()
filter1( reader1, writer1 )

def result1 = writer1.toString() // いったん結果を文字列として result1 に書き出します.
//println result1

def reader2 = new StringReader(result1)
def writer2 = new StringWriter()
filter2( reader2, writer2 )

def result2 = writer2.toString()
println result2

処理結果は以下の通り

apple
banana
cherrykiwi
grape
lemon
lime
melon
orange
raspberry

意図通りソートされて一行の一つのフルーツ名になっています。

パイプを使う場合

パイプを使うことで result1 という文字列を介さないで処理することができます。

// スペースで分割して、一行に一つの果物名を出力するフィルタ
def filter1 = { Reader reader, Writer writer->
    new Thread({
        reader.readLines().each { line->
            writer << line.split(/ /).join(System.getProperty('line.separator'))
        }
        writer.flush()
        writer.close()
    } as Runnable).start()
}

// 果物名でソートして出力
def filter2 = { Reader reader, Writer writer->
    def list = reader.readLines()
    Collections.sort(list, {String a, String b-> a.compareTo(b) } as Comparator)
    writer << list.join(System.getProperty('line.separator'))
}

// -------
// main
// -------
def text = 'melon lime lemon raspberry cherry kiwi banana apple orange grape'

def reader1 = new StringReader(text)
def writer1 = new PipedWriter()
def reader2 = new PipedReader(writer1)
def writer2 = new StringWriter()

filter1( reader1, writer1 )
filter2( reader2, writer2 )

def result2 = writer2.toString()
println result2

前回からの変更点は PipedWriter, PipedReader を使っている点と フィルタ1 内での処理はスレッドを使っている 点です。 フィルタ1 と フィルタ2 の処理をつなげるために filter1 に与える writer1 と filter2 に与える reader2 を それぞれ PipedWriter と PipedReader にしています。

もしフィルタ1 の処理にスレッドを使わないとそこで処理がストップしてしまい意図通り処理できないので注意が必要です。

まとめ

Reader/Writer や InputStream/OutputStream で扱えるデータの変換処理にパイプを使うことで、 中間データを生成しないでストリーム処理できます。

このサンプルのように小さいデータの場合、パイプを使うことでコードが複雑になるだけでメリットはありません。 しかし、扱うデータが巨大な場合、パイプを使うことで Out of Memory エラーを避けたり、高速に処理できる可能性があると思います。

また、メモリ効率考えるなら変換処理を2つにわけないで、一つにまとめればいいじゃないの、と言われそうですが、 この例のように変換処理を複数のフィルタに分割して記述して、 それぞれを Reader/Writer のインタフェースに統一しておくことで、 さらにフィルタ3, フィルタ4 を追加する場合や逆に不要なフィルタを外す場合、 それからフィルタ適用順を変更する場合など、それらを自在に行うことができます。

つまりこの方法なら コードが将来の変更につよくなる というメリットがあると思います。