LoginSignup
13
14

More than 5 years have passed since last update.

JavaコレクションクラスのtoString()をスレッドローカル変数で補強する

Last updated at Posted at 2014-06-01

toString()を読んでみる

ArrayListを直にtoString()するような機会はデバッグ出力のときくらいにしかないものとは思いますが、そのtoString()、実際にコードを読んでみたことはあるでしょうか。
読んでみるとですね、単に内容をカンマ区切りで表示しているだけではないことがわかります。

AbstractCollection.java
public String toString() {
    Iterator<E> it = iterator();
    if (! it.hasNext())
        return "[]";

    StringBuilder sb = new StringBuilder();
    sb.append('[');
    for (;;) {
        E e = it.next();
        sb.append(e == this ? "(this Collection)" : e);
        if (! it.hasNext())
            return sb.append(']').toString();
        sb.append(',').append(' ');
    }
}

カンマ区切り文字列化する前に要素が自分自身でないか判定して、もし自分自身を格納していたら "(this Collection)" とだけ出力しそこから先は再帰しないようにしているのです。

自分自身を格納していた場合に無限再帰に陥るのを回避しているのですね。

しかしちょっと待ってくれ。 孫要素以下に自分自身を含んでいたらノーチェックで無限再帰突入ではないか。ぬるいぞtoString()

無限再帰を絶対阻止

一般論ですが、こういう回避処理は
1. 作らずに危険性を仕様に書く
2. 漏れなく回避できる処理を作り込む
のどちらかにしたいものです。中途半端な仕様がもっとも道具としてよろしくありません。
無論、今やろうとしていることは後者、漏れなく回避する方の処理です。

さて、この循環参照検出を書くにあたって難しいのは、要素を見たときそれが先祖の代で登場していたかをいかに判定するかであります。スタックフレームの中で同じオブジェクトのtoString()を通過していないか、と言い換えることもできます。

ではgetStackTrace()でスタックフレームを取得すれば判定できるんじゃないかとすぐ思いつきますが、それは無理。StackTraceElementオブジェクトは通過してきたクラス・メソッド情報を含むのですが肝心のインスタンス情報を持たないのです。

そこで別案。静的なハッシュセットにthisを、toString()開始時に登録、抜けるときに削除すれば通過してきたことの印にできます。

そんなことしたらマルチスレッド環境で記録が混乱する? いえいえ、そんなときのための スレッドローカル変数 です。スレッドローカル変数たるjava.lang.ThreadLocalクラスは、get(), set()でオブジェクトを一個だけ出し入れできるのですが、問い合わせてきたスレッドごとに別々に格納していてくれます。

静的なハッシュセットはスレッドローカル変数に持っておいてもらえばスレッド間での衝突もありません。

というわけでコード。

private static ThreadLocal<Set<Object>> stackedObjectsContainer = new ThreadLocal<>();

@Override
public String toString() {
    Iterator<E> it = iterator();
    if (! it.hasNext())
        return "[]";

    Set<Object> stackedObjects = stackedObjectsContainer.get();
    if (stackedObjects == null) {
        stackedObjects = new HashSet<Object>();
        stackedObjectsContainer.set(stackedObjects);
    }
    stackedObjects.add(this);
    try {
        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (; ; ) {
            E e = it.next();
            sb.append(stackedObjects.contains(e) ? "(recursion)" : e);
            if (!it.hasNext())
                return sb.append(']').toString();
            sb.append(',').append(' ');
        }
    } finally {
        stackedObjects.remove(this);
    }
}

このコードそのものより、スレッドローカル変数の存在を知っていることの方が役に立つことは多いと思います。お見知りおきを。

13
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
14