クラスとインタフェース

Effective Java 第 3 版の個人的メモ

  1. 項目 15 クラスとメンバーへのアクセス可能性を最小限にする
  2. 項目 16 public のクラスでは、public のフィールドではなく、アクセッサーメソッドを使う
  3. 項目 17 可変性を最小限にする
  4. 項目 18 継承よりコンポジションを選ぶ
  5. 項目 19 継承のために設計および文書化する、でなければ継承を禁止する
  6. 項目 20 抽象クラスよりインタフェースを選ぶ
  7. 項目 21 将来のためにインタフェースを選ぶ
  8. 項目 22 型を定義するためだけにインタフェースを使う
  9. 項目 23 タグ付クラスよりクラス階層を選ぶ
  10. 項目 24 非 static のメンバークラスより static のメンバークラスを選ぶ
  11. 項目 25 ソースファイルを単一のトップレベルのクラスに限定する
  12. 参考 URL

1. 項目 15 クラスとメンバーへのアクセス可能性を最小限にする

1.1. 結論

  • 常にアクセス可能性をできる限り小さくするべき
  • public のクラスは public のフィールドを持つべきではない
  • public static final のフィールドが配列の場合、その配列の内部も不変であることを保証するべき

1.2. 常にアクセス可能性をできる限り小さくするべき

情報秘匿(カプセル化)をすると以下の利点が得られるから。

  • モジュールを分離し、個別に開発、テストなどができる
  • モジュールを素早く理解できる
  • ソフトウェアの再利用をするための必要条件

1.2.1. カプセル化の方法

  • アクセス修飾子
    • private
    • パッケージプライベート
    • protected
    • public
      • protected 以上は API になることが多い。
  • パッケージで分ける。
  • モジュールシステムを利用する。

モジュールシステムは、このような記述をし、以下を定義するために使用するもの。
・ どのパッケージを外部から見えるようにするか
・ 他のどのモジュールに依存するか

1
2
3
4
module foo {
    exports foo.api;
    requires bar;
}

1.3. public のクラスは public のフィールドを持つべきではない

スレッドセーフにできないから。

メソッドにはsynchronizedが付けれるが、フィールド値にはsynchronizedを付けれないのでgetter/setterを利用するようにしましょう。

public static final のフィールドは定数として使ってよい

1.4. public static final のフィールドが配列の場合、参照されるオブジェクトが不変であることを保証するべき

以下のようなコードを記載すると、VALUESのアドレスはfinalであるが、要素はfinalではない。

1
2
3
4
5
public static final Thing[] VALUES={ ... };
  .
  .
  .
VALUES[0]=1; //のように配列内のデータを変更できてしまう。

1.4.1. 実装方法

  • 方法 1
    • 定数にしたい配列を private にして、public static final のリストに追加する
1
2
private static final Thing[] PRIVATE_VALUES={ ... };
public static final List VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
  • 方法 2
    • 定数にしたい配列を private にして、public メソッドで配列のコピーを返す
      1
      2
      3
      4
      
      private static final Thing[] PRIVATE_VALUES = { ... };
      public static final Thing[] values() {
          return PRIVATE_VALUES.clone();
      }
      

備考:配列は低レイヤな扱いになってしまうので、java.util.Collectionsを使うようにすると良さそう。

2. 項目 16 public のクラスでは、public のフィールドではなく、アクセッサーメソッドを使う

2.1. 結論

  • そのクラスのパッケージ外からアクセス可能であれば、アクセッサーメソッドを提供するべき
  • クラスがパッケージプライベート/private のネストしたクラスの場合、そのフィールドは公開しても良い

2.2. そのクラスのパッケージ外からアクセス可能であれば、アクセッサーメソッドを提供するべき

メソッドにはsynchronizedが付けれるが、フィールド値にはsynchronizedを付けれないのでgetter/setterを利用するようにしましょう。

1
2
3
4
5
public class TestData{
    private User user;
    synchronized public User getUser();
    synchronized public void setUser(User user);
}

2.3. クラスがパッケージプライベート/private のネストしたクラスの場合、そのフィールドは公開しても良い

外部からアクセスされる心配がなく、コードの可読性が上がるから。

IntellijEclipseの機能で自動的に検出されるから、意識しなくても大丈夫そう。

3. 項目 17 可変性を最小限にする

3.1. 不変クラスとは

インスタンスに保持される全ての情報は、生成時に提供され、その後は変更できないクラスのこと。

例として、java.lang.String、基本データ型のラッパークラス(java.lang.Integer等)、java.math.BigIntegerjava.math.BigDecimalなどが挙げられます。

備考:java.lang.Integerのオブジェクトで、以下のようなコードを実行すると、毎回新しいインスタンスが生成される。

1
2
for(Integer i=0; i<100; i++)
    ;

3.2. 不変クラスの規則

  1. setterなどオブジェクトの状態を変更するためのメソッドは提供しない。
  2. クラス拡張できないように、final classとしましょう。
  3. 変更不可能とするために、すべてのフィールドにprivate finalを付けましょう。

3.3. 不変クラスのメリット

  • スレッドセーフであること
  • 防御的コピーの必要なし、clone メソッドやコピーコンストラクタ作成の必要もなし
  • 内部の状態も共有可能

3.4. 不変クラスのデメリット

  • 一部だけ変更したいだけなので、全てを再生成しなければならないのがデメリット、、、しかし、コンパニオンクラスを使えば解決できる!(例:java.lang.StringBuilder

備考:コンパニオンクラスのjava.lang.StringBuilderについて調べてみた。 AbstractStringBuilder#appendを見てみると、内部で char[] valueのように持っておき、java.lang.Stringを何度も生成しないように工夫していた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
char[] value;
.
.
.
public AbstractStringBuilder append(String str) {
    if (str == null) str = "null";
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}
.
.
.
void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

4. 項目 18 継承よりコンポジションを選ぶ

4.1. 結論

継承は、実装全部を継承することを理解して使う。
機能を流用したいだけならば、コンポジションを使うことが適切なことが多い。
is-a 関係にあれば使える局面があるが、使うべき局面は多くない。
本当に多重階層にすることが妥当かを検討すべし。

4.2. 継承/コンポジションとはなにか

  • 継承
    • 上位の性質をすべて下位に受け継ぐ
    • 以下のときは使える
      • スーパークラスもサブクラスも特定のパッケージ配下にある場合
      • 拡張のために設計・文書化されているクラスを拡張する場合
      • インタフェース継承の場合
  • コンポジション
    • 英語辞書 的には、混成/合成 という意味。
    • A クラスのなかに、B クラスをもっている状態
      • is-a ではなく has-a になっている状態

4.3. 継承のデメリット

継承をした際に、サブクラスの実装によってクラスの整合性が失われやすい。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

上記クラスは一見、正しく動くように見えるが、現実には正しく機能しない。

1
2
3
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
s.getAddCount(); // => 6。3にならない。

なぜなら、HashSet の addAll は、内部的に add メソッドを利用して実装されているため。

4.4. コンポジションの例

コンポジションは、 処理を他のクラスにお願いする と思っておけばよい。

以下が例。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
package jp.tokyo.takuto_no.effective_java.chapter4;

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

public class ForwardingSet<E> implements Set<E> {
	private final Set<E> set;

	public ForwardingSet(Set<E> set) {
		this.set = set;
	}

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

	@Override
	public boolean isEmpty() {
		return set.isEmpty();
	}

	@Override
	public boolean contains(Object o) {
		return set.contains(o);
	}

	@Override
	public Iterator<E> iterator() {
		return set.iterator();
	}

	@Override
	public Object[] toArray() {
		return set.toArray();
	}

	@Override
	public <T> T[] toArray(T[] a) {
		return set.toArray(a);
	}

	@Override
	public boolean add(E e) {
		return set.add(e);
	}

	@Override
	public boolean remove(Object o) {
		return set.remove(o);
	}

	@Override
	public boolean containsAll(Collection<?> c) {
		return set.containsAll(c);
	}

	@Override
	public boolean addAll(Collection<? extends E> c) {
		return set.addAll(c);
	}

	@Override
	public boolean retainAll(Collection<?> c) {
		return set.retainAll(c);
	}

	@Override
	public boolean removeAll(Collection<?> c) {
		return set.remove(c);
	}

	@Override
	public void clear() {
		set.clear();
	}
}

public class InstrumentedHashSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

使うときの例

1
2
Ser<String> stringSet = new HashSet<>();
InstrumentedHashSet<String > ihSet = new InstrumentedHashSet<>(stringSet);

Spring で利用されているコンポジションの例

CompositeItemWriter.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    .
    .
    .
public class CompositeItemWriter<T> implements ItemStreamWriter<T>, InitializingBean {
    private List<ItemWriter<? super T>> delegates;
    .
    .
    .
    @Override
    public void write(List<? extends T> item) throws Exception {
	    for (ItemWriter<? super T> writer : delegates) {
	        writer.write(item);
	    }
    }
    .
    .
    .
    public void setDelegates(List<ItemWriter<? super T>> delegates) {
	    this.delegates = delegates;
    }
    .
    .
    .
}

様々なItemWriterを合成(Composite)して 、同じように扱って書き込み処理している。

4.5. コンポジションと委譲

コンポジションで実現することは転送(forward)と訳される事が多い。
その他一般的な用語では、委譲(delegate)という言葉がある。
この 2 つの違いは曖昧。
一節には、

1
2
転送は他のクラスに依頼すること。
委譲はそれに加えて、自身の参照を渡さなければならない。

といわれるが、これも誤りらしい。

たいていの開発者は、コンポジションを委譲と同義で使うと理解すれば良さそう。

5. 項目 19 継承のために設計および文書化する、でなければ継承を禁止する

5.1. 結論

一言でいってしまえば、protected なクラス/メソッドは、拡張される可能性があるため、 継承した場合にどのように使うのか、ルール/制約等を全部記述すべきだということ。

5.2. 理由

第 18 節の通り、かんたんに不整合がうまれるため。 これを防ぐには手厚いドキュメントが必要。

とはいえ、

本来、良い API ドキュメントは何するのかを記述し、どのように行うかは記述するべきではない。 継承されるクラスには、実装の詳細を記述する必要がある。理由は、継承によってカプセル化が破壊されやすいため

という一文の通り、良い API ドキュメントでは、手厚いドキュメントはあまり好ましいことではない。 付け加えるならば、本来 API は何も考えずに使える(脳死できる)ことが望ましく、 最新の注意を払う時点でバグを生む。

5.3. 継承できることを担保する

継承されるクラスを作成する際、 実際に、そのクラスを継承したクラスを用いたテストを十分にする必要がある。
拡張前提の AP フレームワークの場合、これを十分行う必要がある。

5.4. 継承させるべきでないもの

  • CloneableSerializable
    • この2つのどちらかのインタフェースを実装すると、クラスを継承するユーザにかなりの実装の負担をかける。
      Cloneableについてはこちらを参照してください。Serializableは、すべてのフィールドを serialize 可能 or 除外することを考えなくてはならない。

5.5. 継承させる / させない ことを考える

  • 継承させないなら、コンストラクタを privateパッケージプライベートにする or クラスをfinalにする
  • 継承させた上で、あるメソッドは参照させるのみならば final にする
    • どこまでやるか、は、使う人がだれか、を念頭に置くべし
      • 身内で、かつ、コントロールしきれる範囲のものならば、そこまで厳密でなくて OK
      • 世界中に公開される OSS ならば、厳密すぎるっていうことはない

6. 項目 20 抽象クラスよりインタフェースを選ぶ

以下の点で、インタフェースの方が抽象クラスよりも優れているので、インタフェースを選びましょう。

  • 既存クラスに新たなインタフェースを実装しやすいこと
  • インタフェースは、Mixinを定義するには理想的であること
    • 多重継承可であるから。
  • 階層を持たない型フレームワークを構築できること
  • 抽象クラスと組み合わせること(骨格実装クラス)で、よりユーザーにとって便利になること
    • ユーザがメソッドを実装しなくても良い。

ただし、publicのインタフェースは「リリース後の変更が不可である」ということを意識しておきましょう。(設計は注意深く、実装クラスのテストもしっかりと行いましょう。)

6.1. 抽象クラスとインタフェース

抽象クラスとインタフェースの違いは以下の 2 点。

  • 実装を含めれる
  • 継承が1:11:多

6.1.1. 抽象クラス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class AbstractClass {
    abstract void abstractMethod(int num, String str);

    // 実装を含めることができる。
    void nonAbstractMethod() {
        System.out.println("非抽象メソッドより出力");
    }
}

// クラスは1つの抽象クラスしか継承できない。
public class Main extends AbstractClass {

    public static void main(String[] args) {
        Main main = new Main();
        main.nonAbstractMethod();
    }

    public void abstractMethod(int num, String str) {
        // ...
    }

}

6.1.2. インタフェース

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

interface Sub {
    // 実装を含めることができなかった。(過去形)
    int sub(int a, int b);
}

interface Add {
    // Java8からインタフェースに実装を含めることができるようになった。
	default int add(int a, int b) {
		return a + b;
	}
}

// クラスは複数のインタフェースを継承できる。
class Calc implements Add, Sub {
    public int sub(int a, int b) {
        return a - b;
    }
}

備考:java8 から Interface にデフォルトメソッドを定義できるようになりましたね。

6.2. インタフェースの Good Point

6.2.1. 既存クラスに新たなインタフェースを実装しやすいこと

インタフェースでは単に実装するだけで良いが、抽象クラスでは継承階層を意識しなければいけない。(抽象クラスが、複数の既存クラスで継承されていたら悩まされることに、、、)

  • インタフェースの場合

    • 単に拡張したい既存クラスのクラス宣言に、implements節を追加して、メソッドを実装するだけで良い。
  • 抽象クラスの場合

    • 継承階層のどの位置に差し込むかについて考えた上で、extends節を追加する必要がある。

6.2.2. インタフェースは、Mixinを定義するには理想的であること

インタフェースは多重継承可能であるため、Mixinができる。

Mixinとは「クラスが「本来の型」に加えて、なんらかの任意の振る舞いを提供していることを宣言するための型のこと」である。

  • 例:Comparable
    • 「他の相互比較可能なオブジェクトに対しての順序付けされていること」を本来の機能に加えて宣言できるから。

6.2.3. 階層を持たない型フレームワークを構築できること

フレームワークを構築する前提で、N個の属性をサポートすることを考える。 抽象クラスの場合の場合、属性ごとに階層を考慮する必要があるが、インタフェースではその必要がない。

  • インタフェースの場合
    • フレームワーク構築者は、N個のインタフェースを作成すれば良い。
    • ユーザは、利用したい属性を、implements節に足すだけで良い。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// フレームワーク構築者が準備
public interface Singer {
  AudioClip sing(Song s);
}
// フレームワーク構築者が準備
public interface Songwriter {
  Song compose(boolean hit);
}
// ユーザが利用する
public class FukuyamaMasaharu implements Singer, Songwriter {
  AudioClip sing(Song s){
    ...
  }
  Song compose(boolean hit){
    ...
  }
  AudioClip strum(){
    ...
  }
  void actSensitive(){
    ...
  }
}
  • 抽象クラスの場合
    • フレームワーク構築者は、N個の属性を利用するか否かの2^N個のクラスを作成する必要がある。(組み合わせ爆発と呼ばれる事柄で、対処しづらくなっていく。)
    • ユーザは、利用した属性を2^N個のクラスの中から選択し、そのクラスを継承し利用する。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// フレームワーク構築者が準備
abstract class Singer {
  AudioClip sing(Song s);
}
// フレームワーク構築者が準備
abstract class Songwriter {
  Song compose(boolean hit);
}
// フレームワーク構築者が準備(Interfaceの場合では作らなくてもよかった。)
abstract class SingerSongWriter {
  AudioClip sing(Song s);
  Song compose(boolean hit);
  AudioClip strum();
  void actSensitive();
}
// ユーザ側が利用する
class FukuyamaMasaharu extends SingerSongWriter {
  AudioClip sing(Song s){
  ...
  }
  Song compose(boolean hit){
  ...
  }
  AudioClip strum(){
  ...
  }
  void actSensitive(){
  ...
  }
}

6.2.4. 抽象クラスと組み合わせること(骨格実装クラス)で、よりユーザーにとって便利になること

骨格実装クラスにより、プログラマがインタフェースを独自実装する際の手助けになる。下記、setValue(V value)メソッドのコメントはありがたいですね。

下記、 AbstractMapEntry.java の良い点は、単なる抽象クラスではなくて、インタフェースを実装しているから、型定義に関しての制約が課されることがない。

AbstractMapEntry.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Skeletal Implementation
package org.effectivejava.examples.chapter04.item18;

import java.util.Map;

public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
  // 基本操作
  public abstract K getKey();
  public abstract V getValue();

  // 変更可能なマップでのエントリーは、このメソッドをオーバーライドしなければいけない。
  public V setValue(V value) {
    throw new UnsupportedOperationException();
  }

  // Map.Entry.equals の一般契約を実装
  @Override
  public boolean equals(Object o) {
    if (o == this)
      return true;
    if (!(o instanceof Map.Entry))
      return false;
    Map.Entry<?, ?> arg = (Map.Entry) o;
    return equals(getKey(), arg.getKey())
        && equals(getValue(), arg.getValue());
  }

  private static boolean equals(Object o1, Object o2) {
    return o1 == null ? o2 == null : o1.equals(o2);
  }

  // Map.Entry.hashCode の一般契約を実装
  @Override
  public int hashCode() {
    return hashCode(getKey()) ^ hashCode(getValue());
  }

  private static int hashCode(Object obj) {
    return obj == null ? 0 : obj.hashCode();
  }
}

6.3. 余談(抽象クラスの利点)

**「抽象クラス : 実装クラス = 1 : 多」**となるのであれば、インタフェースよりも抽象クラスを使いましょう。理由は、抽象クラスにメソッドを追加すれば、全ての実装クラスに反映されて容易だから。(インタフェースの場合は、反映先のクラス数が多く、大変になる。)

極端な例ですが、、今からList<E>インタフェースに新たな public メソッドを追加すると、影響範囲は大変なことになりますよね。笑

7. 項目 21 将来のためにインタフェースを選ぶ

Java8 ではデフォルトメソッド構文が追加されたが、以下のことには気をつけておきましょう。

  • 既存の実装クラスに新規デフォルトメソッドを反映させて、エラーや警告なしでコンパイルできるかもしれないが、実行時に失敗することがある。
  • デフォルトメソッドを使うことになっても、インタフェースの設計が重要なことには変わりない。
  • リリース後の修正は困難なので、3 つの異なる実装に対して十分テストを行うことが大切である。

例として、Collection に removeIf がデフォルトメソッドとして追加された
しかし、Collectionimplements している Apache Commons ライブラリの SynchronizedCollection でこの removeIf() を呼び出した際には、同期されないメソッドとして呼び出され、SynchronizedCollection 使用における約束事が破られてしまう。
Java ライブラリではこれと同様のことを防ぐため、Collections.synchronizedCollection において removeIf() をオーバーライドして対処している。

8. 項目 22 型を定義するためだけにインタフェースを使う

インタフェースの使用方法

  • 型を定義するためだけに使用する。
  • 定数を提供するためには使用すべきではない。

8.1. 型を定義するためだけに使用する。

インタフェースは、実装したクラスのインスタンスを参照するのに使用できる型(type)として機能する。(型(_type_) = 「何ができるのかを述べている。」)

何ができるのかを述べている。」以外の目的のために定義するのは不適切である。

8.2. 定数を提供するためには使用すべきではない。

8.2.1. 定数インタフェース

定数インタフェース(例:java.io.ObjectStreamConstants.java)は、「実装クラスに、この定数に関する責務を負わせることになる」ため使うべきではない。

定数を利用したい場合は、Enum 型や定数ユーティリティクラス(例:org.springframework.core.Constants.java)を利用しましょう。

9. 項目 23 タグ付クラスよりクラス階層を選ぶ

9.1. 結論

  • タグ付きクラスが適切な場合はほとんどない
  • タグ付きクラスを作りたくなったら、またはタグ付きクラスに出くわしたら、クラス階層で置き換えられないかを検討する

9.2. タグ付きクラスとは

  • 次のような、インスタンスが複数の特性を持っていて、その特性を示すためにタグフィールドを持つクラス
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Figure {
    enum Shape { RECTANGLE, CIRCLE }

    // タグフィールド 図の形
    final Shape shape;

    // shape が RECTANGLE の場合だけ利用される
    double length;
    double width;

    // shape が CIRCLE の場合だけ利用される
    double radius;

    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch (shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError();
        }
    }
}

9.3. タグ付きクラスの欠点

  • 複数の実装が単一クラスに詰め込まれているので、可読性が低い
  • 他の特性に属する関係のないフィールドを含んでいるので、余分にメモリを使う
  • 誤ったフィールドを初期化しても、コンパイラは検知せず、実行時に失敗する
    • 例として、次のように、誤ったタグフィールドで初期化しても、実行時に失敗するまでわからない
1
2
3
4
Figure(double radius) {
        shape = Shape.RECTANGLE;
        this.radius = radius;
}
  • 新しい特性を追加するには、ソースコードを修正しなければならない

9.4. 解決策

  • クラス階層を用いる
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    double area() {
        return length * width;
    }
}

9.5. クラス階層の良い点

  • タグ付きクラスの欠点がすべて解決できている
    • 複数の実装が単一クラスに詰め込まれていない
    • 他の特性に属する関係のないフィールドを含んでいない
    • 誤ったフィールドを初期化することはない
  • 型間の自然な階層関係を反映できる
    • 例えば、正方形はクラス階層を利用して以下のように作成できる
      1
      2
      3
      4
      5
      
      class Square extends Rectangle {
          Square(double side) {
              super(side, side);
          }
      }
      

10. 項目 24 非 static のメンバークラスより static のメンバークラスを選ぶ

10.1. 結論

  • ネストしたクラスはエンクロージングクラスに対して仕える目的のためだけに存在すべき
    • (エンクロージングクラスは内部クラスにとっての外部クラス)
  • ネストしたクラスが、他の何らかの状況で有用なのであれば、トップレベルのクラスにするべき
  • ネストしたクラスは 4 種類あり、それぞれ用途が異なる
    • static のメンバークラスはクラスに関連したクラス
    • 非 static のメンバークラスはインスタンスに関連したクラス
    • 無名クラスはその場限りのクラス
    • ローカルクラスもローカル変数のクラス版
  • メンバークラスやローカルクラスは、外部クラスを離れては意味のないクラスを内部に隠蔽することができるのでカプセル化の点でメリットがある。(参考:java の内部クラスおさらい)

10.2. ネストしたクラスの種類

  • static のメンバークラス
  • 非 static のメンバークラス
  • 無名クラス
  • ローカルクラス
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class OuterClass{
    private int outerField1;
    private static int outerField2;

    // staticのメンバークラス
    static class StaticMemberClass{
        // エンクロージングクラスのstaticのメンバーにアクセス可
        outerField2;
    }

    // 非staticのメンバークラス
    class NonStaticMemberClass{
        // エンクロージングクラスのメンバーにアクセス可
        outerField1;
        outerField2;
    }

    void hogeMethod(){
        // 匿名クラス
        FugaInterface fugaInterface = new FugaInterface(){
            void fugaMethod{
                // 処理
            }
        };

        // インターフェースだけではなくクラスでもOK。メソッドオーバーライドや抽象メソッドの実装をしないと意味ないが
        FugaClass fugaClass = new FugaClass(){
            @Override
            void fugaMethod
            {
                // 処理
            }
        }

        // ローカルクラス
        class LocalClass{

        }
    }

}

10.3. static のメンバークラス

10.3.1. 特徴

  • 最も単純なネストしたクラス
  • 通常の別クラスとして宣言し場合と近い感覚で利用できる
  • 通常のクラスとの違いはエンクロージングクラスの static のメンバーにアクセスできること
  • メンバーなので private,protected が使える

10.3.2. 使用例

  • その外部クラスを使用と一緒に使用すると有用な public のヘルパークラス
    • 例えば、その外部クラスだけに関連している enum クラス
    • 何かに属していることを明示したり、ジェネリクスを合わせたりといったメリット
      • Map.Entry  (public クラスの public なインターフェース)

10.4. 非 static のメンバークラス

10.4.1. 特徴

  • エンクロージングインスタンスと関連付けられている
    • エンクロージングインスタンスなしで、非 static のメンバークラスのインスタンスは生成できない
  • エンクロージングクラスのメンバーすべてにアクセスできる
  • メンバーなので private,protected が使える

10.4.2. 使用例

  • エンクロージングクラスのインスタンスを関係のないクラスのインスタンスとしてみなすことを可能にする Adapter を定義するときに使う - Adapter パターンは既存クラスを修正することなく、 異なるインタフェースを持たせることができるデザインパターン
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    public class MySet<E> extends AbstractSet<E>{
    // 省略
    
            public Iterator<E> iterator(){
                return new MyIterator();
            }
    
            private class MyIterator implements Iterator<E>{
                ...
            }
        }
        ```
    
    > static のメンバークラスと同様に何かに属していることを明示したりジェネリクスを合わせたりといったメリットがある
    

10.4.3. 主張

  • エンクロージングインスタンスへのアクセスが必要なければ、static のメンバークラスを利用するべき
    • static 修飾子をつけないと、個々のインスタンスはエンクロージングインスタンスへの参照を持ち、エンクロージングインスタンスが適切に GC されない可能性があるから(項目 22:廃れたオブジェクト参照を取り除く)
      • 以下の例のような状況がある。
      1
      2
      3
      4
      
      class Outer{
          class Inner{
          }
      }
      
      1
      2
      3
      
      var o = new Outer();
      var i = new o.Inner();
      o = null; //i から o への参照は残る
      

10.5. 無名クラス

10.5.1. 特徴

  • 他のネストしたクラスとは全く異なる
  • クラス名が無い
  • エンクロージングクラスのメンバーではない
  • 式が許されている場所であれば、コードのどの場所でも宣言できる
  • 宣言と同時にインスタンス化される
  • 非 static の文脈内で書かれたならば、エンクロージングインスタンスを持つ
  • static の文脈内で書かれても static のメンバーを持つことができない
  • instanceof 検査やクラスの名前を指定する必要がある処理はできない
  • 外側で定義されているローカル変数を使う場合は、そのローカル変数に暗黙に final が付いているものと見なされる(Java クラス使用メモ(Hishidama’s Java Class use Memo))

10.5.2. 使用例

  • その場で関数オブジェクトを生成

    • Comparator
      1
      2
      3
      4
      5
      
      Collections.sort(list, new Comparator<Hoge>() {
      	public int compare(Hoge o1, Hoge o2) {
      		return o1.id.compareTo(o2.id);
      	}
      });
      
  • プロセスオブジェクトを生成

  • static ファクトリーメソッド内での使用

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    static List<Integer> intArrayAsList(final int[] a) {
        if (a == null) {
            throw new NullPointerException();
        }
    
        return new AbstractList<Integer>(){
            @Override
            public int size() {
                return a.length;
            }
    
            public Integer get(int i) {
                return a[i];
            }
    
            @Override
            public Integer set(int i, Integer val) {
                int oldVal = a[i];
                a[i] = val;
                return oldVal;
            }
        };
    }
    
  • ラムダ式

    1
    2
    3
    
    hogeList.stream()
        .forEach((i) -> { System.out.print(i + " "); }
        );
    
    • 無名クラスの形に戻すと、以下。
    1
    2
    3
    4
    5
    6
    7
    
    hogeList.stream()
        .forEach(new Consumer<Integer>() {
            public void accept(Integer i)
            {
                System.out.print(i + " ");
            }
        });
    
    • ラムダ式をコンパイルしたものを逆コンパイルしたのを見ると、無名クラスの形になる。

10.6. ローカル内部クラス

10.6.1. 特徴

  • private,protected が使えない
  • 外部クラスのメンバーにはアクセス可能
  • 外側で定義されているローカル変数を使う場合は、そのローカル変数に暗黙に final が付いているものと見なされる

10.6.2. 主張

  • あまり使われない
  • 短くあるべき
  • ローカル変数のようなもの

11. 項目 25 ソースファイルを単一のトップレベルのクラスに限定する

11.1. 結論

  • 単一のソースファイルに複数のトップレベルのクラスを作るべきでない

11.2. 理由

  • コンパイルの順序によって結果が変わってしまうことがあるから
    • 例えば、下記のような Main クラス、Utensil.javaDessert.javaを考えるときに、
      • javac Main.javaまたは、javac Main.java Utensil.javaでコンパイルして実行すると、pancakeが表示される
      • javac Main.java Dessert.javaでコンパイルして実行すると、potpieが表示される
      • main メソッドSystem.out.println(Utensil.NAME + Dessert.NAME)System.out.println(Dessert.NAME + Utensil.NAME)に変更してjavac Main.javaでコンパイルして実行すると、piepotが表示される(Dessert.java の方)

Main.java

1
2
3
4
5
public class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }
}

Utensil.java

1
2
3
4
5
6
7
class Utensil {
    static final String NAME = "pan";
}

class Dessert {
    static final String NAME = "cake";
}

Dessert.java

1
2
3
4
5
6
7
class Utensil {
    static final String NAME = "pot";
}

class Dessert {
    static final String NAME = "pie";
}

11.3. 補足:javac の依存関係の解消の仕組み

1
2
3
4
5
6
7
 javacはファイル間の依存関係を、あくまで簡単にですが、把握し、ソースファイルを自動的にコンパイルします。以下では、その仕組みについて説明します。

 まず、javacは、直接コンパイルするよう指定されたソースファイルを解析し、そのソースファイル中で、どんなクラスが使用されているかを調べます。

 次に、javacはソースファイル中に使われているクラスを検索します。javacはJava VMのように、ブートクラスパス、エクステンション(拡張)ディレクトリ、クラスパスの順に、クラスファイルを検索しますが、それに加えソースパスを検索し、検索しているクラスに対応するソースファイルがないかどうか調べます。ソースパスについては、オプション-sourcepathの説明を参照してください。

 検索の結果、 もし、クラスファイルのみ見つかり、ソースファイルが見つからなかった場合は、単にそのクラスファイルを利用します。もし、ソースファイルのみ見つかり、クラスファイルが見つからなかった場合は、見つかったソースファイルをコンパイルします。

出展:Java プログラミングの前提知識

11.4. 解決法

  • 1つソースファイルには 1 つだけトップレベルのクラスを作る

  • static なメンバクラスとして定義する

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    public class Main {
        public static void main(String[] args) {
            System.out.println(Utensil.NAME + Dessert.NAME);
        }
    
        private static class Utensil {
            static final String NAME = "pan";
        }
    
        private static class Dessert {
            static final String NAME = "cake";
        }
    }
    

11.5. 意見

  • FQCN が完全に同じクラスが2つあった時、先に読み込んだ方のクラスを使う

12. 参考 URL