- PR -

インスタンス間で List を受け渡す時、どの段階で複製を作るべきか?

投稿者投稿内容
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-05-01 19:36
unibon です。こんにちわ。

#長くてあまりまとまっていない質問ですがよろしくお願いします。

複数のインスタンス間で、List をやりとりするときに、
どこかで List の複製を作って List のインスタンスの共有を断ち切る必要がありますが、
どの段階で複製をおこなえばよいのでしょうか、という疑問をもっています。

後述するサンプルプログラムでは、
Producer クラスのインスタンスが内部で持っている List のインスタンスを、
getter メソッドで提供しており、
同様に Consumer クラスのインスタンスが内部で持つ List のインスタンスは、
その外部から setter メソッドで取り込むことができます。
で、Producer から get した List をそのまま Consumer に set してしまうと、
目論んでいない List の共有が起きてしまいます。

--- MyArrayList.java ---
コード:
import java.util.*;

class Producer {

    private List stringList;
    public Producer() {
        stringList = new ArrayList();
        stringList.add("zero");
    }
    public List getList() {
        return stringList; // (1) ここでコピーを作るべきか?
        // return new ArrayList(stringList); // コピーした例
    }
    public void setString(int index, String string) {
        stringList.set(index, string);
    }
}

class Consumer {

    private List stringList;
    public void setList(List anList) {
        stringList = anList; // (2) ここでコピーを作るべきか?
        // stringList = new ArrayList(anList); // コピーした例
    }
    public String getString(int index) {
        return (String) stringList.get(index);
    }
}

public class MyArrayList {

    public static void main(String[] args) {
        Producer p = new Producer();
        Consumer c = new Consumer();
        List stringList = p.getList(); // (3) ここでコピーを作るべきか?
        // List stringList = new ArrayList(p.getList()); // コピーした例
        c.setList(stringList);

        p.setString(0, "rei");
        String string = c.getString(0);
        System.out.println("Consumer's String = " + string); // "zero" を表示できれば成功。"rei" になると失敗。
    }
}


以下、徒然なるままに疑問点を書きます。

上記のコードでは (1), (2), (3) のいずれかひとつの個所で複製してしまえば、
目論まない List の共有は避けることができます。
しかし、(1), (2), (3) のどの段階でやるかは、どうやって取り決めればよいでしょうか。

安全なのは (1) と (2) の両方で複製することですが、これだとムダです。ムダを承知で安全を重視すべきなのでしょうか。
上記 (3) でやるのが一番良いのでしょうか、という気もしてきます。

なお、List に限って言えば、List の代わりに Iterator でやりとりする方法もあります。
これは (2) での複製と等価になると思います。
しかし、この問題は List 以外の、配列や他のクラスでも同様の問題があると思いますが、
そのような場合は Iterator は使えません。

また、List を引数や返り値で扱う getter や setter メソッドを実装するときは、
必ず javadoc にどの段階での複製を期待するかを書いておくべきなのでしょうか。
それとも書かなくてもよく、あくまでも javadoc 以外の個所で規定すべきなのでしょうか。
#実装コードを見てね、は論外だと思いますが。

また、フィールドのインスタンスを外に getter でアクセスできるようにすることが、
そもそもマズイのでしょうか(カプセル化ができていない?)。
でも、要素数が多い場合に、すばやく渡す場合はフィールドを直接 getter でアクセスできたほうが良いようにも思います。
index 値を指定しての getter や setter でガッチリとカプセル化すればよいのでしょうが、
たとえば 1万個の要素を持つものをやりとりするのに、
1万回 getter/setter を呼ぶのはなんだかやりきれないような気もします。

また getter での返り値を Collections クラスの unmodifiableList でラップして返すようにすれば、
返り値の要素の変更は禁止されますが、そもそも実行時になってはじめて例外が出せるものなので、
保険の意味しかないとも感じます。

あと、最後になりましたが、
これらの問題には根本的な解がありそれを目指すべき、と考えるべきなのか、
それとも習慣のような漠然としたものしかない、と考えるべきなのか、
という疑問もあります。
zaxx_MD
大ベテラン
会議室デビュー日: 2003/04/21
投稿数: 204
お住まい・勤務地: 千葉県柏市
投稿日時: 2003-05-02 13:52
ProducerとConsumer自体の設計を変えないのであれば、私なら3にします。

モデリングをもう少しすすめて
両方ともHelperクラスなのか
Listの拡張クラスなのかが明確になるように
設計したほうがいいと思いました。
北斗のポン
会議室デビュー日: 2003/05/02
投稿数: 17
投稿日時: 2003-05-02 14:16
はじめまして。今日書き込みデビューしたばかりのポンです。

えっと、明らかに私のほうがJava開発暦が少ないのですが
つい先日、似たような内容の討論があったので書き込んでみます。
といっても、その討論の内容ではなく最終的に私が思ったことですが・・・
Java初心者の浅知恵という感じで読んでみてください。

私的には、少なくてもGetterはそのままのインスタンスを返したほうがよいのではないかと思います。
理由としては、「コピーを作る=メモリ内に新しい領域を確保」ということが内部的に行われているので、特にコピーが必要でない時でもGetterを呼ぶたびに新しい領域ができるのはあまりよろしくないのではないかと思います。
もしコピーをProducerから返したいのであれば、既存のGetterとは別に新しくgetListCopy()などというのを作るのはどうでしょうか?

上記と同じような理由でSetterも中でコピーをセットするのはやめたほうがいいのではないかと思います。
これも同じように新しくsetListCopy()などを作ってみてはいかがでしょう?

そうすると結論は私的には(3)ということになるのでしょうかね。

Javaの世界の先輩に対して私なんかが口を出すことではなかったかもしれませんが
せっかく最後まで読んで私なりの考えが浮かんだので、書き込みをしてみました。
helmet
会議室デビュー日: 2003/05/01
投稿数: 2
投稿日時: 2003-05-02 15:18
引用:

unibonさんの書き込み (2003-05-01 19:36) より:
これらの問題には根本的な解がありそれを目指すべき、と考えるべきなのか、
それとも習慣のような漠然としたものしかない、と考えるべきなのか、
という疑問もあります。



最終的には、そのアプリケーションの要件によるのではないでしょうか。

たとえば、ProducerおよびConsumerが不特定多数に利用され、設計者の意図しない
使われかたをされないようにするのであれば、パフォーマンスを犠牲にしても
(1) と (2) の両方で複製するべきと考えます。

逆に、高性能を要求されており、少しでもパフォーマンスを向上させたいのならば
(3)になりますね。

私は、実装の際にこのような問題で悩んだ場合は、そのアプリケーションの要件をもとに
どうすべきか決定するようにしています。
(その他に保守性とか、既存のプログラムに対する影響とか、いろいろファクターはありますが)
そういった意味では、普遍的な解は存在しないのではないでしょうか。

なんだかぼやけた意見ですが、御参考になれば幸いです。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-05-05 22:41
unibon です。こんにちわ。
みなさまありがとうございます。数としては(3)が多いみたいですね。

引用:

helmetさんの書き込み (2003-05-02 15:18) より:
私は、実装の際にこのような問題で悩んだ場合は、そのアプリケーションの要件をもとに
どうすべきか決定するようにしています。
(その他に保守性とか、既存のプログラムに対する影響とか、いろいろファクターはありますが)
そういった意味では、普遍的な解は存在しないのではないでしょうか。


さまざまな要因により、ひとつのやりかたに定まらないのは、
おっしゃるとおりのような気がします。
しかし、上記で言うところの「決定」の結果は、どこに書けば良いのでしょうか、
という疑問を私は依然として持っています。

とくに、同じチーム内ならば、暗黙の了解や、あるいは○○仕様書のようなところで、
そういった取り決めをしておけばまだ良いと思いますが、
チーム外の第三者に提供したり、あるいは第三者からもらった .java ファイルや .class ファイルの場合、
そのような「決定」があることを、どうやって伝えたり、知ったりすれば良いでしょうか。
もし、これも「普遍的な解は存在しない」とすれば、
楽観的な人が提供するクラスを悲観的な人が使ったり、あるいは、
悲観的な人が提供するクラスを楽観的な人が使ったりするのは良いのですが、
楽観的な人が提供するクラスを楽観的な人が使うと、目論まない共有が起きてしまい、
悲観的な人が提供するクラスを悲観的な人が使うと、ムダな複製が起きてしまいます
(これはこのスレッドでのはじめに書いたことを換言したことになると思います)。

#なんだかhelmetさんが書かれた言葉の揚げ足取りみたいな感じになってすみません。

私としては javadoc の @param タグや @return タグあたりに、
そういった情報(メソッド内で複製するのか否か)を
書くことにしたほうが良いように思うのですが、
でも javadoc ではそこまでは要求していないので、
所詮いわゆる「ローカルルール」になってしまいます。

#と、ここまで考えたところで、詰まってしまいました。
kito
ベテラン
会議室デビュー日: 2003/03/24
投稿数: 59
お住まい・勤務地: Osaka
投稿日時: 2003-05-06 05:00
kitoです。
Producerの持つListを共有されることを防ぎたい
→(1)で変更不可能なListを返す。
return Collections.unmodifiableList(stringList);

Consumer内部のListは、外部から与えられる引数のListとは独立して変更したい。
→(2)で複製する。
stringList = new ArrayList(anList)

というのはどうですか?
もちろんProducer#getList()のjavadocには「変更不可能なListを返します」と明記します。

>また getter での返り値を Collections クラスの unmodifiableList でラップして返すようにすれば、
>返り値の要素の変更は禁止されますが、そもそも実行時になってはじめて例外が出せるものなので、
>保険の意味しかないとも感じます。

ここは人によって認識の違うところかもしれません。
unmodifialbeなListを変更した時にはUnsupportedOperationExceptionが飛びますが、そもそも実行時例外はNullPointerExceptionを見てもわかるように実装コードのバグと呼べるものです。ドキュメント化とテストをしっかりと行えば、保険といった消極的なものではなく、堅固なプログラムを作るための仕組みとして積極的に利用できると考えています。

と、ここまで書いて気が付きましたが、Collections.unmodifiableList(List)が複製であることを考えるとこれも「無駄な複製」になってしまいますねぇ・・・
うのきち
ベテラン
会議室デビュー日: 2003/02/17
投稿数: 55
投稿日時: 2003-05-06 12:52
Listを実装したラッパクラスを作って、変更が行われようとした段階で複製を作るという手もありますね。
未記入
ぬし
会議室デビュー日: 2002/03/28
投稿数: 255
投稿日時: 2003-05-06 14:27
>チーム外の第三者に提供したり、あるいは第三者からもらった .java ファイルや
>.class ファイルの場合、そのような「決定」があることを、どうやって伝えたり、
>知ったりすれば良いでしょうか。
これが正にその要件の一つでは.

不特定多数の第三者に提供することを前提とするモジュールで,Listの変更が
ないことを保証したいというのなら,その渡す前の段階で防御的コピーをすべきです.

#或いはListをラップするクラスを作って,取り出しは許可するが変更は
#許可しないようにするのも一つの手かも.特にパフォーマンスに拘る
#場合は.

ちなみにこういう設計は,設計自体がパフォーマンスに影響します.
もし,設計上防御的コピーが必要で,しかもそれがパフォーマンスのボトル
ネックになるというのなら,設計を見直す必要があるかもしれません.

スキルアップ/キャリアアップ(JOB@IT)