LoginSignup
10
9

More than 5 years have passed since last update.

EffectiveJava読書会3日目 Part.1

Last updated at Posted at 2014-04-24

2日目(第3章)の記事はこちら → EffectiveJava読書会2日目

第4章 クラスとインターフェース

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

  • 最低限のpublicのAPIを設計し、関係のないクラス・インターフェース・メンバーがAPIの一部となるのを防ぐ。

  • 設計として各モジュール(機能)が独立し、それぞれの内部データ・実装の詳細が隠蔽され、実装とAPIがはっきりと分離されていることが重要
    ->オブジェクト指向プログラミング言語であるJAVAにとって大切な概念の一つである カプセル化

その利点

・開発・テスト・修正・保守が容易
・並行してモジュール開発が可能
・他に大きな影響を与えることなく最適化
等々

それを実現するために・・・

  1. トップレベルのクラスのアクセスレベルの考える   ->publicである必要がないなクラスはないか( 出来るだけパッケージプライベートに。 )
  2. 基本的にはメンバー(フィールド・メソッド・ネストクラス等)は private
  3. テストの容易化を理由にアクセスレベルを広げすぎない(最高でも パッケージプライベート )   (パッケージプライベートでJUnitの実装できます) 
  4. publicな可変のインスタンスフィールドを持つクラスはスレッドセーフではない staticなフィールドでも同様
  5. 4の(staticフィールドの)例外として、不変オブジェクトの 定数はOK
  6. 可変オブジェクト(配列など)のpublic static final(定数)のフィールドを返すアクセッサーメソッドは誤り(らしい)
BadPattern.java
// 潜在的セキュリティホール!
public static final Thing[] VALUES = { ... };
下記のように、変更されない形で返しましょう
GoodPattern.java
//Good pattern1
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
 Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

//Good patern2
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}

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

publicなクラスでは決して可変のフィールドを公開にせず、privateにし、アクセッサーメソッド(getter)・ミューテーター(setter)を提供すべき
でもseasarはDtoとかEntity Publicを推奨(?)してるような

以上。

項目15  可変性を最小限にする

  • クラスは可変にすべきかなり正当な理由がない限り、不変であるべき
  • 不変にできないのであれば、その可変性を出来るだけ制限すべき
  • やむを得ない理由が無い限り、全てのフィールドをfinalにする
不変にする利点
  • 不変オブジェクトは単純  ->正確に一つの状態であり、オブジェクトが作られた状態を保障する  (-->可変の場合は状態が複雑で、保証できない)
  • 不変オブジェクトは本質的にスレッドセーフで同期を必要としない ->防御コピーやコピーコンストラクタを用意する必要がない
  • オブジェクトの存在できる状態を減らすことで、オブジェクトについて明確に、論理的に考えることが容易になり、エラーの可能性も減る
不変クラスを作る5規則
  1. オブジェクトの状態を変更するためのメソッド(ミューテーター)を提供しない
  2. クラスが拡張できないことを保証する
  3. すべてのフィールドをfinalにする
  4. すべてのフィールドをprivateにする
  5. 可変コンポーネントに対する独占的アクセスを保証する
不変クラスの実装方法について

(上の5番の具体例)
戻り値として新たなインスタンスを戻り値とする

GoodSample.jajva
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
// Accessors with no corresponding mutators
public double realPart() { return re; }
public double imaginaryPart() { return im; }
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
              :

->オペランドを変更させない 関数的方法

サブクラスを防ぐ方法について
  1. finalをつける
  2. コンストラクタをprivate + static ファクトリーメソッドを使う
StaticFactoryMethod.java
public class Complex {
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
... // Remainder unchanged
}

項目16  継承よりコンポジションを選ぶ

  • 継承は強力な力を持つため、 カプセル化 を破ってしまう恐れがある
  • 継承を使用して良いパターンの見極めることが大事
継承を進めない理由**
  • クラスの拡張(継承)はスーパークラスの実装に依存(カプセル化の概念からずれる)
  • もしスーパークラスの実装が変われば、サブクラスも変えなければいけない。
変更に気づかないと当初想定していない挙動やバグを生む危険がある

悪い例

HashSetを継承、addとaddAllをオーバーライドし、追加された要素が個数を返すクラス

BadInstrumentedSet.java
// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
    // The number of attempted element insertions
    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;
  }
}

下を実施後、getAddCountを実行すると3が帰ってくると思いきや、6が返ってきます。
理由としては、addAllは、スーパークラスでイテレートをして要素の個数分addメソッドを実行している
->これにより継承したaddクラスもよばれてしまい、addAllでカウントした3+addクラスでカウントされた3
 計6になってしまうというトラップ・・・・。

InstrumentedHashSet<String> s =
new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));

ここで、

コンポジションのススメ

コンポジションとは・・・
既存クラスの拡張の変わりに、新たなクラスにメンバとして
既存クラスのインスタンスを取り込む

** コンポジションを進めるワケ**
既存クラスの実装の詳細に依存しないため、既存クラスの変更に影響されない

本の例)
転送クラス

ForwardingSet.java
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
 private final Set<E> s; //ここがコンポジションです!!!
 public ForwardingSet(Set<E> s) { this.s = s; }
              :
 public boolean isEmpty() { return s.isEmpty(); }
 public int size() { return s.size(); }
 public Iterator<E> iterator() { return s.iterator(); }
 public boolean add(E e) { return s.add(e); }
 public boolean remove(Object o) { return s.remove(o); }
              :
 public boolean addAll(Collection<? extends E> c)
 { return s.addAll(c); }
              :
}

転送クラスのラッパークラス
ラッパークラスは、あくまで、新クラス(ForwardingSet)のメソッドをオーバーライドしているので、
継承の時のように、継承されたaddメソッドが動くことはない。

InstrumentedSet.java
public class InstrumentedSet<E> extends ForwardingSet<E> {
 private int addCount = 0;
 public InstrumentedSet(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;
 }
}

さらに、転送クラスでインターフェースを取り込んだことにより、継承では出来ない
柔軟な実装ができる

使用例

Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));
継承とコンポジションの使い分け

継承元クラスと継承したクラスの関係が、本当のサブタイプ関係である時のみ使うべき(IS-aの関係)
IS-aとは・・クラス同士がA is a Bである(例)PersonクラスとProgramerクラス)

コンポジションの場合は、クラスの関係がhas-aの関係の場合に用いる
(例ProgramerクラスとPcクラス)
参考
http://jp.fujitsu.com/solutions/sdas/technology/java-oo/05-object-composition.html
http://www.itsenka.com/contents/development/java/inheritance.html#S03

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

項目16の内容をも受け・・

クラスはオーバーライド可能なメソッドの自己利用を文書化しなければいけない。
  • オーバーライド可能なメソッドがどのように呼び出されるのか
  • 各呼び出しの後の処理にどのように影響するのか

例としてP.86の
java.util.AbstractCollection#remove

継承のために設計されたクラスのサブクラスを書きをテストする
  • サブクラスのテストを通して何を公開(protected)メソッド・フィールドにするか判断する
  • テストはスーパークラスの作成者以外に書いてもらうべき

-> 汎用的に使用されそうなクラスを作った場合、安易にメソッドやフィールドの実装を決定すると後々のパフォーマンス・機能改善を不可能にするので、上記のテストはすべき

コンストラクタ内でオーバーライド可能なメソッドは呼ばない

サブクラスで正しい挙動にならず、プログラムが失敗する原因となる

例)
2度日付が表示されることを想定しているが、スーパークラスのコンストラクタは
サブクラスのコンストラクタより先に実行されるため、初回時nullとなってしまう。

Super.java
public class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
Sub.java
public final class Sub extends Super {
private final Date date; // Blank final, set by constructor
Sub() {
date = new Date();
}
// Overriding method invoked by superclass constructor
@Override public void overrideMe() {
System.out.println(date);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}

以上から

  • 継承のために安全なクラスを設計すること->かなりの制限を課す
  • 安全にサブクラス化されるための設計と文書化がされてないクラスでのサブクラスは禁止

18章へ続く

10
9
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
10
9