Javaのcloneは悪者か?

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)

Java: The Good Partsが(賛否両論の)話題を呼んでいるが、それ以前にEffective Javaは皆さん、読んだだろうか? この本の項目11に、「cloneを注意してオーバーライドする」というセクションがある。そのほかにも、Javaのcloneメソッドは各所で嫌われているようだ。

そのような論調において、clone代替案としては、コピーコンストラクタと、staticなファクトリメソッドがしばしば挙げられる*1

うん、確かにJavaのcloneメソッドはイケてない。俺もそう思う。まぁ、イケてない理由はみんなと同じだから上記の各文献を当たってください。まぁ要するに「きちんと実装するのが難しく、それをコンパイラに強制させられない」のです。

では、cloneメソッドは要らないのか? と言われれば、俺はNOだ。要る。要件として「コピー元と同じ実装クラスのインスタンスを生成しなければならない」というケースでcloneを使わざるを得ない場合がある。

  • コピーコンストラクタは、コピー元の実装クラスを知らなければ使えない。
  • staticなファクトリメソッドも、オーバーライドが出来ないため、やはりコピー元の実装クラスを知る必要がある。


まぁ、言いたいことはだいたい伝わったとは思うのだが、具体例で説明してみたい。…で、これを説明するにあたって、良く知られたクラスを例に出したかった。が、そんな例が見つからなかった…。不当に嫌われすぎていて、そんな実装例が見つからないのだ*2

というわけで、現実の話を少し曲げて説明に使ってみる。

JavaにはListというインターフェイスがあって、その実装としてArrayListやLinkedListがある。まず前提として確認したいのは、Listは Cloneable インターフェイスを実装しておらず、ArrayListとLinkedListはこれを実装しているということ。これは何故だろう、と考えると、ArrayListやLinkedListは、順序付き集合をオンメモリで扱う実装だからだ。メモリ上の集合ならばcloneしても構わない。しかし、Listはというと、実装がオンメモリであるとは限らない。add/removeなどを実行する度に、律儀にファイルやDB、もしくはリモートのサーバに内容を書き出すような実装クラスを作っても、Listインターフェイス契約的には何ら問題にならない。List型にcloneを認めてしまうと、このような実装を妨げてしまうからであろう。

ここで、現実の話を少し曲げる。Listとその実装型の間に、仮に OnMemoryList というインターフェイスを挟んであるものだと仮定する。さらにもう一つ、ArrayList#clone()も戻り値型はObject型ではなくArrayList型、そしてLinkedList#clone()の戻り値型は同様にLinkedList型である、とします。共変戻り値ですね。

ではここで問題。

/**
 * {@code in}に与えたリストが持つ要素のうち、{@code p}を満たす要素のみで
 * 構成される新しいリストを返す。
 * 
 * <p>戻り値のリストの実装型は{@code in}の実装型と同じである。
 * また、{@code in}は破壊してはならない。
 * 戻り値のリストの要素順は、{@code in}の要素間の相互の位置関係を維持する。
 * </p>
 * 
 * @param<E> 要素の型
 * @param in 入力のリスト
 * @param p 条件を表す述語
 * @return 新しい {@link OnMemoryList}
 * @throws IllegalArgumentException 引数に{@code null}を与えた場合
 */
public <E>OnMemoryList<E> filter(OnMemoryList<E> in, Predicate<? super E> p) {
  // ...
}
ArrayList<String> idList = new ArrayList<String>();
// LinkedList<String> idList = new LinkedList<String>();
// どちらでもテストは成功すること。

idList.add("dai.0304");
idList.add("daisuke_m");
idList.add("dai19780304");
idList.add("daisuke-m");
idList.add("dai0304");
idList.add("daisuke");

OnMemoryList<String> filtered = filter(idList, new Predicate<String>() {
  public boolean apply(String input) {
    return input.matches(".*[0-9].*"); // contains digits
  }
});

// idListの非破壊を確認
assertThat(idList.toString(), is("[dai.0304, daisuke_m, dai19780304, daisuke-m, dai0304, daisuke]"));

// 正常にフィルタリングされていることを確認
assertThat(filtered.toString(), is("[dai.0304, dai19780304, dai0304]"));

// 実装クラスが同じであることを確認
assertThat(filtered.getClass().equals(idList.getClass()), is(true));

このメソッド、どうやって実装しますか? よくある関数型っぽいことをするためのメソッドですね。ミソは「戻り値のリストの実装型は{@code in}の実装型と同じである」ってところで、これが恐らく、cloneを使わないと実装できないところだと思います。

Validate.notNull(in);
Validate.notNull(p);
OnMemoryList<E> result = in.clone();
Iterator<E> itr = result.iterator();
while (itr.hasNext()) {
  if (p.apply(itr.next()) == false) {
    itr.remove();
  }
}
return result;

このように、cloneにも重要な役割があるのであって、cloneはイケてないから一律使わない、と思考停止するのはあんまりよくないんじゃないかなー、と思っています。Effective Javaは、本当にcloneが必要なケースかを問いかけ、必要ならばうまくやれ、と言っているんだ。決して「cloneはイケてないから使うな」とは言っておらず「イケてないから、注意深く実装しようね」と指摘しているに過ぎないのだ。

cloneをうまく使っているコードってあまり見ないなぁ、不当に迫害され過ぎてんじゃないかなぁ、と思ったので、こんなん書いてみました。

まぁ、4ヶ月ほど前まで、自分が思考停止してたんですけどネ。

*1:もしくは、シリアライズ+デシリアライズの組み合わせ、なんてのを提案する場合もある。

*2:分かってる、コレは俺の願望だw