IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

Effective Java 読書会 5 日目 「最後に見せたツンデレ」

はじめに

今日も継承三昧><

今日読んだところ

86 ページ〜 100 ページ

オーバーライド可能なメソッド自己利用(self-use)のドキュメント化

たとえば、 AbstractCollection#remove の以下の部分

This implementation iterates over the collection looking for the specified element.
この実装は、指定要素を探すために走査をします。
If it finds the element, it removes the element from the collection using the iterator's remove method.
要素が見つかればイテレータの remove メソッドを使って削除します。
Note that this implementation throws an UnsupportedOperationException if the iterator returned by this collection's iterator method does not implement the remove method and this collection contains the specified object.
このコレクションが指定要素を含んでいるけど、イテレータが remove を実装していない場合には UnsupportedOperationException を投げます。

AbstractCollection (Java Platform SE 6)

このように、 iterator クラスの remove メソッドの self-use を明確にしている。
で、以下のように iterator の remove メソッドを実装しなければ、 Collection の remove も使えないことになる。

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;

public class Main {
    
    public static void main(final String[] args) {
        
        // args に "hoge" が含まれるとして。
        
        final Collection<String> collection = new AbstractCollection<String>() {

            @Override
            public Iterator<String> iterator() {
                return new Iterator<String>() {

                    private int index = 0;

                    @Override
                    public boolean hasNext() {
                        return index < args.length;
                    }

                    @Override
                    public String next() {
                        return args[index++];
                    }

                    // remove を実装しない
                    @Override
                    public void remove() {
                        throw new UnsupportedOperationException();
                    }
                };
            }

            @Override
            public int size() {
                return args.length;
            }

        };

        // ここで UnsupportedException が投げられる
        collection.remove("hoge");
    }
}

他にも removeAll メソッドや clear の箇所でも iterator の remove を使ってる記述がある
あと size メソッドに関しても、 toArray メソッドで最適化のヒントとして使っていることが明記されている。
また、 AbstractCollection は addAll で add を使っていることを明記している。
なので、以下のように add をオーバーライドすると addAll が実装できることが分かる。

import java.util.AbstractCollection;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class Main {
    
    public static void main(final String[] args) {
        
        final Collection<String> collection = new AbstractCollection<String>() {

            private List<String> impl = new LinkedList<String>(Arrays.asList("hoge", "fuga", "piyo"));
            
            @Override
            public Iterator<String> iterator() {
                final Iterator<String> iterator = impl.iterator();
                
                return new Iterator<String>() {
                    
                    @Override
                    public boolean hasNext() {
                        return iterator.hasNext();
                    }

                    @Override
                    public String next() {
                        return iterator.next();
                    }

                    @Override
                    public void remove() {
                        iterator.remove();
                    }
                };
            }

            @Override
            public int size() {
                return impl.size();
            }

            // add を実装すれば
            @Override
            public boolean add(String str) {
                return impl.add(str);
            }
        };

        // addAll もちゃんと動く
        collection.addAll(Arrays.asList("foo", "bar", "baz"));
        
        System.out.println(collection);
    }
}

このように、オーバーライド可能なメソッドをクラスに用意する場合はそのメソッドの self-use を解説した詳細なドキュメントが必要になる。

self-use を記述したドキュメントがカプセル化を壊している

前項の例が示すように、 AbstractCollection の add と addAll の関係や clear と iterator の remove の関係は、実装手段のレベルで切り離せないものになってしまっている。
これは、カプセル化のメリットと相反している。

継承を前提としたクラスがやることは self-use のドキュメント化だけじゃない

サブクラス実装者が何を求めているかを深く考えなければならない。
自分でサブクラスを実装してみたり、誰かにサブクラスを実装してもらって、意見を聞くなどするべき。

継承を前提とするクラスがやってはいけないこと

オーバーライド可能なインスタンスメソッドを、コンストラクタ、 clone 、 readObject メソッドから呼び出してはいけない。
スーパーコンストラクタとして呼び出された時点で、サブクラスのフィールドは初期化されていないので、サブクラスでオーバーライドされたメソッドを呼ぶのはダメ。

継承を前提とするクラスが Serializable を実装するときにやらなければならないこと

readResolve, writeReplace メソッドを protected にする

self-use の排除

self-use を排除することも一つの選択肢

    public void hoge() {
        fuga(); // self-use !!!!
    }

    public void fuga() {
        System.out.println("hello"); 
    }

以下のようにすると self-use がなくなる! fuga 拡張されても hoge は安心!

    public void hoge() {
        fugaHelper();
    }

    // private なので継承される可能性はない
    private void fugaHelper() {
        System.out.println("hello"); 
    }

    public void fuga() {
        fugaHelper();
    }

組み合わせ爆発

機能 A, B, C があって

  • 機能 A, B を実装したいクラス
  • 機能 A, C を実装したいクラス
  • 機能 A, B, C を実装したいクラス

などなど、様々なニーズがあったとして
これをクラスの継承だけでやろうとすると組み合わせが大変なことになるという話。

インタフェースと骨格実装 (Skeletal Implementation) を提供する

たとえば、インタフェース Collection に対する AbstractCollection などがある。
Java の標準 API 以外にも何かないかなーと思って調べたら
Google Collections Library に Iterator の Skeletal Implementation があった。

import java.util.Iterator;

// Iterator の(後付け)Skeletal Implementation 
import com.google.common.collect.AbstractIterator;


public class Main {

    public static void main(final String[] args) {

        Iterator<String> it = new AbstractIterator<String>() {
            int n = 0;

            // computeNext だけ実装すれば良い
            @Override
            protected String computeNext() {
                if (n < 10) {
                    return Integer.toString(n++);
                }
                return endOfData();
            }
        };
        
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

骨格実装を使った擬似多重継承

骨格実装から作ったインスタンスを private に持って、そのインタフェースを継承することで擬似的に多重継承できる!!おおお!
ってことで擬似多重継承をやってみた。。。。

import java.util.AbstractList;
import java.util.AbstractQueue;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Queue;


public class Main {

    private static final class VariableAsQueueAndList<E> implements Queue<E>, List<E> {
        E impl = null;
        
        // Queue の骨格実装から実装を作る
        Queue<E> queueImpl = new AbstractQueue<E>() {
        
            @Override
            public Iterator<E> iterator() {
                return null;
            }
    
            @Override
            public int size() {
                return impl == null ? 0 : 1;
            }
    
            @Override
            public boolean offer(E e) {
                if (impl == null) {
                    impl = e;
                    return true;
                }
                return false;
            }
    
            @Override
            public E peek() {
                return impl;
            }
    
            @Override
            public E poll() {
                E r = impl;
                impl = null;
                return r;
            }

        };

        // List の骨格実装から実装を作る
        List<E> listImpl = new AbstractList<E>() {

            @Override
            public E get(int index) {
                if (index != 0) {
                    throw new IndexOutOfBoundsException();
                }
                return impl;
            }

            @Override
            public int size() {
                return 1;
            }
            
            
        };
        
        // 以下転送ラッパー
        @Override
        public boolean add(E e) {
            return queueImpl.add(e);
        }
        
        @Override
        public E element() {
            return queueImpl.element();
        }
        
        @Override
        public boolean offer(E e) {
            return queueImpl.offer(e);
        }
        
        @Override
        public E peek() {
            return queueImpl.peek();
        }
        @Override
        public E poll() {
            return queueImpl.poll();
        }
        @Override
        public E remove() {
            return queueImpl.remove();
        }
        @Override
        public boolean addAll(Collection<? extends E> c) {
            return queueImpl.addAll(c);
        }
        @Override
        public void clear() {
            queueImpl.clear();
        }
        @Override
        public boolean contains(Object o) {
            return queueImpl.contains(o);
        }
        @Override
        public boolean containsAll(Collection<?> c) {
            return queueImpl.containsAll(c);
        }
        @Override
        public boolean isEmpty() {
            return queueImpl.isEmpty();
        }
        @Override
        public Iterator<E> iterator() {
            return queueImpl.iterator();
        }
        @Override
        public boolean remove(Object o) {
            return queueImpl.remove(o);
        }
        @Override
        public boolean removeAll(Collection<?> c) {
            return queueImpl.removeAll(c);
        }
        @Override
        public boolean retainAll(Collection<?> c) {
            return queueImpl.retainAll(c);
        }
        @Override
        public int size() {
            return queueImpl.size();
        }
        @Override
        public Object[] toArray() {
            return queueImpl.toArray();
        }
        @Override
        public <T> T[] toArray(T[] a) {
            return queueImpl.toArray(a);
        }

        
        @Override
        public void add(int index, E element) {
            listImpl.add(index, element);
        }

        @Override
        public boolean addAll(int index, Collection<? extends E> c) {
            return listImpl.addAll(index, c);
        }

        @Override
        public E get(int index) {
            return listImpl.get(index);
        }

        @Override
        public int indexOf(Object o) {
            return listImpl.indexOf(o);
        }

        @Override
        public int lastIndexOf(Object o) {
            return listImpl.lastIndexOf(o);
        }

        @Override
        public ListIterator<E> listIterator() {
            return listImpl.listIterator();
        }

        @Override
        public ListIterator<E> listIterator(int index) {
            return listImpl.listIterator(index);
        }

        @Override
        public E remove(int index) {
            return listImpl.remove(index);
        }

        @Override
        public E set(int index, E element) {
            return listImpl.set(index, element);
        }

        @Override
        public List<E> subList(int fromIndex, int toIndex) {
            return listImpl.subList(fromIndex, toIndex);
        }
    }
    
    public static void main(final String[] args) {
        
        VariableAsQueueAndList<String> queue = new VariableAsQueueAndList<String>();
        
        // Queue として push
        queue.offer("hoge");
        
        // List として get
        System.out.println(queue.get(0));
    }
}

なげえええええええ><
これ本当に便利なんでしょうか><

インタフェースは最初が肝心

インタフェースは、一回決めたら影響範囲が大きすぎて仕様変更できません><
よく考えて設計しましょう

定数だけを持つインタフェースはやめましょう

混乱を招くので

タグ付けクラスをするくらいなら継承使ったらいいじゃない><

デレた!!!

最後に

明日、明後日は私用があるので、次回更新は明後日以降になるかもしれません><