lasciva blog

開発して得た知見やwebビジネスのストック

「Effective Java 第3版」を読んだ

Effective Java 第3版

Effective Java 第3版

  • 作者:Joshua Bloch
  • 発売日: 2018/10/30
  • メディア: 単行本(ソフトカバー)

目的、モチベーション

Javaの理解を深める。

全体の感想

冗長な表現が多く読みにくいが、知らなかったことも多かったので読んで良かった。Java特有でない項目も結構あるので、省略してもよい項目も結構あった。
Javaは歴史があるが故に言語自体に負債もあり、その負債を回避するための項目もあり、辛みを感じた。
業務で扱う場合は、必須レベルな本ではあると思うので、他の言語経験者は2冊目ぐらいに読むには良い本だと思った。

目次

概要

第1章 はじめに

第2章 オブジェクトの生成と消滅

項目1 コンストラクタの代わりにstaticファクトリーメソッドを検討する
項目2 多くのコンストラクタパラメータに直面した時にはビルダーを検討する

引数が多くなるとバグのもとになるし、コンストラクタの種類がどんどん増えていく。
また、JavaBeansパターンだと、不変性が担保できない。

public class Sample {
    // 不変性を担保できる
    private final int size;
    private final String color;
    private final String brand;
    private final String category;

    public class Builder {
        // require
        private final int size;
        private final String color;
        // optional
        private String brand = "";
        private String category = "";

        public Builder(int size, String color) {
            this.size = size;
            this.color = color;
        }

        public Builder brand(String brand) {
            this.brand = brand;
            return this;
        }

        public Builder category(String category) {
            this.category = category;
            return this;
        }

        public Sample build() {
            return new Sample(this);
        }
    }

    private Sample(Builder builder) {
        this.size = builder.size;
        this.color = builder.color;
        this.brand = builder.color;
        this.category = builder.category;
    }
}

// 可読性も高い
Sample sample = new Sample.Builder(0, "blue").
            brand("brand").category("category").build();
項目3 privateのコンストラクタがenum型でシングルトン特性を強制する

enumは特に簡潔に書ける。

public enum Singleton {
  INSTANCE;

  public void hoge() {}
}
項目6 不必要なオブジェクトの生成を避ける

できるだけ自動ボクシングされた型よりも、プリミティブ型を使うなど。

項目7 使われなくなったオブジェクト参照を取り除く

参照を配列内の要素を nullにしないとメモリが開放されない。
陥りがちなのは、以下の3パターンが多い。

  • 独自のメモリを管理しているとき
  • キャッシュ( WeakHashMapを使うと、自動で取り除いてくれる)
  • リスナー、コールバック(弱参照 java.lang.ref.WeakReferenceを使うべき)
項目8 ファイナライザとクリーナを避ける

ファイナライザは実行が保証されないので、避ける。
オブジェクトの生成から開放まで数十倍のオーダーでコストがかかる。

項目9 try-finallyよりもtry-with-resourcesを選ぶ

try-finallyでは、 finally内で例外が発生すると、 try内のエラーが捕捉できなくなる。
try-with-resourcesを使えば簡潔になる上に、スタックトレースに追加される。

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

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

カプセル化のメリットなどが書かれていた。

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

但し、オブジェクトの生成にコストがかかる場合は、可変を許可することを考える。

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

継承でoverride等をすると、スーパークラスの変更による挙動の保証が難しい。ラッパークラスを作り、メンバーとしてインスタンスを保持して必要な転送メソッドを実装した方が安全。

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

継承によって、オーバーライドするメソッドが、想定していない箇所に影響を及ぼす可能性があるので、危険。
やるなら、副作用がでないと確証できるところだけオーバーライドする(現実にはかなり難しい)こと。
protectedメソッドで公開するのは最小限に抑え、複数のサブクラスのテストを書くこと。

これらを遵守し始めると、継承のメリットがなくなるので、基本的にはやらない方がベター。

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

複数の異なる抽象クラスを継承しないといけない場合、階層関係が生じてしまう。

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

4種類のネストしたクラスがある。

  • staticのメンバークラス: ネームスペースがあるだけで、親クラスへはアクセスできない。基本的にはこれを使うべき。
  • 非staticのメンバークラス: 親クラスのインスタンスへ関連付けられ、アクセスできる。したがって、参照も持つのでGCにも負荷がかかる。
  • 無名クラス: メソッド内に属しているべきで、1箇所からのみインスタンスを生成すべきで、型がすでに存在している場合に使うべき。
  • 内部クラス: メソッド内に属しているべきで、1箇所からのみインスタンスを生成すべきで、型がすでに存在していない場合に使うべき。

第5章 ジェネリック

項目31 API柔軟性向上のために境界ワイルドカードを使う

PECS(producer-extends, consumer-super)の規則に則る。

public void pushAll(Iterable<? extends E> src) {
  for (E e: src) {
    push(e)
  }
}
public void popAll(Collection<? super E> dst) {
  while (!isEmpty()) {
    dst.add(pop())
  }
}

型パラメータがメソッド宣言中に一度しか現れない場合、それをワイルドカードで置き換えた方が柔軟性のあるAPIとなる。
しかし、以下のようなswapメソッドは素直にはコンパイルできないので、コツが必要。

// コンパイルエラー: 
public static void swap(List<?> list, int i, int j) {
  list.set(i, list.set(j, list.get()))
}

public static void swap(List<?> list, int i, int j) {
  swapHelper(list, i, j)
}

private static <E> void swapHelper(List<E> list, int i, int j) {
  list.set(i, list.set(j, list.get()))
}

第6章 enumアノテーション

項目34 int定数の代わりにenumを使う

Stringから変換する実装

// Implementing a fromString method on an enum type
private static final Map<String, Operation> stringToEnum =
  Stream.of(values()).collect(
    toMap(Object::toString, e -> e));

// Returns Operation for string, if any
public static Optional<Operation> fromString(String symbol) {
  return Optional.ofNullable(stringToEnum.get(symbol));
}

メンバーに応じて関数などの振る舞いを変えたい場合、switchだとメンバーを追加した際に漏れる可能性があるのでよろしくない。
privateなenumを定義して、それをメンバーに持つように強制する。

// The strategy enum pattern
enum PayrollDay {
  MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);

  private final PayType payType;
  PayrollDay(PayType payType) { this.payType = payType; }
  PayrollDay() { this(PayType.WEEKDAY); } // Default

  int pay(int minutesWorked, int payRate) {
    return payType.pay(minutesWorked, payRate);
  }
  // The strategy enum type
  private enum PayType {
    WEEKDAY {
      int overtimePay(int minsWorked, int payRate) {
        return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
      }
    }, WEEKEND {
      int overtimePay(int minsWorked, int payRate) {
        return minsWorked * payRate / 2;
      }
    };
    abstract int overtimePay(int mins, int payRate);
    private static final int MINS_PER_SHIFT = 8 * 60;

    int pay(int minsWorked, int payRate) {
      int basePay = minsWorked * payRate;
      return basePay + overtimePay(minsWorked, payRate);
    }
  }
}
項目36 ビットフィールドの代わりにEnumSetを使う

EnumSetの存在自体知らなかった。

項目37 序数インデックスの代わりにEnumMapを使う

EnumMapの存在自体知らなかった。

第8章 メソッド

項目49 パラメータの正当性を検査する
  • @throws
    • Javadocに記載できる(ex. @throws FileNotFoundException 指定のファイルが見つかりません)
  • Objects.requireNonNull
    • 引数が nullのときに NullPointerExceptionを投げてくれる
  • assert
    • 起動時に java -eaなどオプションをつけてON/OFFを切り替えることができる
項目50 必要な場合、防御的にコピーする

private finalなどのメンバーでもimmutableでない可能性がある。
java.util.Dateはimmutableではないので、基本的には Instantを使うべき。

public class Hoge {
    private final Date date;

    /*
     下記で書き換えられる可能性があるのでインスタンスをコピーする
     Date date = new Date()
     Hoge hoge = new Hoge(date)
     date.setYear(2009)

     ※ cloneだとsubclassが返る可能性があるので避ける
     */
    public Hoge(Date date) {
        this.date = new Date(date.getTime());
    }

    // hoge.date().setYear(2009)などで書き換えられる可能性があるのでインスタンスを生成
    public Date date() {
        return new Date(date.getTime());
    }
}
項目52 オーバーロードを注意して使う

期待していない挙動を起こすもとになるので、基本的には同じ引数の数を持つメソッドは定義しないこと。

第9章 プログラミング一般

項目60 正確な答えが必要ならば、floatとdoubleを避ける
  • 9桁を超えない => int
  • 18桁を超えない => long
  • 18桁を超えうる => BigDecimal
    • 内部で floatdoubleの処理を行ってくれる
項目61 ボクシングされた基本データよりも基本データ型を選ぶ

基本データ型に対するボクシングされた基本データの特徴

項目63 文字列結合のパフォーマンスに用心する

文字列の結合は、 +よりもStringBuilderを使ったほうがパフォーマンスが良い。

第10章 例外

項目73 抽象概念に適した例外をスローする

低レベルの例外は、使う側の関心に応じて高レベルの例外に変換して返すべき。
使う側で、詳細なレベルの例外の取り扱いをすべきでない。

try {
  ... // Use lower-level abstraction to do our bidding
} catch (LowerLevelException e) {
  throw new HigherLevelException(e);
}
項目77 例外を無視しない

基本的には、例外をcatchで潰さず、適切に処理するかログを吐くかなど対応しないといけない。
それでも稀に意図的に無視するケースでは、 ignoredを使って明示する。

try {
  numColors = f.get(1L, TimeUnit.SECONDS);
} catch (TimeoutException | ExecutionException ignored) {
  // Use default: minimal coloring is desirable, not required
}

第11章 並行性

項目78 共有された可変データへのアクセスを同期する

別スレッドでの値の変更を正しく参照するには、同期が必要。synchronized経由で参照、書き込みを行えばよい。

volatile修飾子は相互排除は行わないが、フィールドを読み込むスレッドから最後に書き込まれた値が見えることを保証する。

// 不完全: ++は読み込みと書き込みの2つの操作からなるため、同時にアクセスされると同じ値が生成されうる
// volatileの代わりにsynchronizedを使うべき
private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
  return nextSerialNumber++;
}

// better
private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
  return nextSerialNumber.getAndIncrement();
}

第12章 シリアライズ

項目85 Javaシリアライズよりも代替手段を選ぶ

攻撃的な操作を含むオブジェクトやコストのかかるオブジェクトをデシリアライズさせる脆弱性のもとなので極力使わず、JSONやprotobufの使用を検討すること。 どうしても使わないといけない場合は、 java.io.ObjectInputFilterを使って対象とするclassを絞ること。

項目86 Serializableを細心の注意を払って実装する

実装自体は容易だが、クラスの互換性を担保しないといけなくなったりするので、長期的にはコストがかかる。

項目88 防御的にreadObjectメソッドを書く

コンストラクタ内で値の検証を行っているクラスは、シリアライズする際にデフォルトでは検証されない。
readObjectメソッドで、final修飾子は取り除いた上で防御的にコピーして、そのあとに検証を行って保護すること。

項目89 インスタンス制御に対しては、readResolveよりenum型を選ぶ

シングルトンパターンでは、enumの方が簡単に安全性を担保できる。

項目90 シリアライズされたインスタンスの代わりに、シリアライズ・プロキシを検討する

直接シリアライズの実装を行わず、シリアライズ用のプロキシとなるクラスを作成して、そのクラス経由でシリアライズさせると、安全かつ簡単。

class Hoge {
  private final Date date;

  private void readObject(ObjectInputStream stream) throws InvalidObjectException {
    throw new InvalidObjectException("Proxy required");
  }

  private void writeReplace() {
    return new Proxy(this);
  }


  private static class Proxy implements Serializable {
    private final Date date;

    Proxy(Hoge hoge) {
       this.date = hoge.date;
    }

    private void writeReplace() {
      return new Hoge(date);
    }

    private static final long = 11111111111111111L
  }
}

次のアクション

JVMGC周りの理解を深めたい。

Effective Java 第3版

Effective Java 第3版

  • 作者:Joshua Bloch
  • 発売日: 2018/10/30
  • メディア: 単行本(ソフトカバー)