Effective Java 3rd [Chapter 12] - シリアライズ

シリアライズ

Effective Java 第 3 版の個人的メモ

  1. 項目 85 Java のシリアライズよりも代替手段を選ぶ
  2. 項目 86 Serializable を細心の注意を払って実装する
  3. 項目 87 カスタムシリアライズ形式の使用を検討する
  4. 項目 88 防御的に readObject メソッドを書く
  5. 項目 89 インスタンス制御に対しては、readResolve より enum 型を選ぶ
  6. 項目 90 シリアライズされたインスタンスの代わりに、シリアライズ・プロキシを検討する

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

1.1. 結論

  • シリアライズは使うべきでない
    • シリアライズは脆弱でリモートコード実行(RCE)やDoS攻撃などの攻撃の弱点となってしまうから
  • 代替手段としてはJSONやProtobufなどのような他の手段を使うべし
    • データフォーマットが定まっていてシリアライズよりましだから?
      • XML bombもある。Jsonハイジャックもある。
      • ユーザコードを叩くかどうかの違い?
  • シリアライズが避けられない場合でも、信頼できないデータはデシリアライズするべきでない
    • 信頼できないデータは攻撃対象となっている可能性があるから
  • シリアライズが避けられないかつ信頼できないデータをデシリアライズしなければならない場合は、フィルタリングをするべし

1.2. シリアライズの脆弱性

1.3. デシリアライズ時のフィルタリング

1.4. 余談

  • シリアライズの扱いずらさ
    • 送信元と送信先が同じオブジェクトを持たなければならない
  • どういう局面でシリアライズを使うでしょうか
    • セッション
    • システム内通信
      • EJB(Enterprise Java Bean)

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

2.1. 結論

  • Serializableインターフェースは軽く考えて実装するべきではない
    • 以下の3つのコストがかかってしまうから
      • リリース後のクラスの実装の変更に対する柔軟性の低下
      • バグやセキュリティホールの可能性の増大
      • テストの負荷の増大
  • 継承するために設計されたクラスとインタフェースではSerializableを実装・拡張するべきでない
    • サブクラスの実装に手間がかかるから

2.2. Serializable実装に伴う3つのコスト

2.2.1. リリース後のクラスの実装の変更に対する柔軟性の低下

  • Serializableを実装したクラスを一旦リリースしてしまうと、その実装を変更することは容易ではない
    • クラスをシリアライズ可能にすると、そのシリアライズ形式がクラスの公開APIの一部となり、シリアライズ形式を永久にサポートし続ける必要があるから
      • デフォルトのシリアライズ形式の場合は、 privateフィールドも公開されてしまう
    • serialVersionUIDを明示的に指定しない場合は、コンパイラはクラス名やクラスが実装しているインタフェース名、publicとprotectedのメンバからserialVersionUIDを生成されるするので、メソッドを一つ追加しただけでserialVersionUIDが変更され、互換性が失われるから。
      • javaバージョンによって生成されるserialVersionUIDが違う可能性あり。だから指定しましょう

2.2.2. バグやセキュリティホールの可能性の増大

  • バグの可能性が増大する理由は、デシリアライズ時のオブジェクトは通常のコンストラクタを使わずに生成されるため、コンストラクタで保証される不変式が保証されないから。
  • セキュリティホールについては、項目85参照

2.2.3. テストの負荷の増大

  • 新たなリリースのインスタンスをシリアライズし、古いリリースのデシリアライズ処理でインスタンスが復元できるを確認する必要がある。 また逆に、古いリリースのインスタンスをシリアライズし、新しいリリースのデシリアライズ処理で復元できるかも確認する必要がある
  • これらのテストは、新旧のインスタンス間でシリアライズ/デシリアライズできるかというバイナリ互換性に加えて、 動作が意図しているものかどうかというセマンティクス互換性も検査する必要がある
    • 互換の一覧
      • ソース互換
        • ソースをコンパイルできる
      • バイナリ互換
        • バイナリを実行できる
      • 動作の互換性(≒セマンティクス互換性)
        • 動作が同じ

2.2.4. 継承するために設計されたクラス・インターフェースに対するSerializable実装

  • 継承するために設計されたクラスとインタフェースではSerializableを実装・拡張するべきでない
    • サブクラスの実装に手間がかかるから
  • ただし上記には例外があり、「例えば、全ての参加者がSerializableを実装しなければならない何らかのフレームワークに参加するためにクラスやインターフェースが主に存在している場合」は破ってもいい
    • 例えば、ThrowableはSerializableを実装しているので、リモートメソッド呼び出し(RMI)からの例外を、クライアントに渡せる
    • Component、HttpServletもSerializableを実装している

3. 項目 87 カスタムシリアライズ形式の使用を検討する

3.1. 結論

Javaにおけるシリアライズの実装方法は2通りある。

Effective Java 3rd [Chapter 11] - 並行性

並行性

Effective Java 第 3 版の個人的メモ

  1. 項目 78 共有された可変データへのアクセスを同期する
  2. 項目 79 過剰な同期は避ける
  3. 項目 80 スレッドよりもエグゼキュータ、タスク、ストリームを選ぶ
  4. 項目 81 wait と notify よりも並行処理ユーティリティを選ぶ
  5. 項目 82 スレッド安全性を文書化する
  6. 項目 83 遅延初期化を注意して使う
  7. 項目 84 スレッドスケジューラに依存しない

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

1.1. 結論

  • synchronizedは同期化を行い、アトミック性と可視性を保証する
  • 複数スレッドが可変データを共有する場合には、そのデータを読み書きするスレッドは同期を行わなければならない
    • 同期を行わなければ、あるスレッドで行われた変更が他のスレッドから見えることが保証されないから
  • volatileはアトミック性は保証しないが、可視性を保証する
  • スレッド間通信だけが必要で、相互排他が必要なければ、volatileを使用できるが、正しく使うのは難しい
    • 相互排他が必要かどうかを判断するのが難しいから
  • 可変データの共有はなるべく回避すべき
    • 扱いが難しいから

1.2. アトミック性

  • 1つのスレッドだけがある時点で1つのメソッドやブロックを実行する性質
  • 下のようにsynchronizedを使うと、メソッドや処理をアトミックにできる
1
2
3
public synchronized void method() { 
  //アトミックな処理
}

1.3. 可視性

  • あるスレッドから変数に書き込んだ値が、別スレッドから観測できる性質
  • 下記のクラスのバックグラウンドスレッドは停止するか?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class StopThread1 {
  private static boolean stopRequested;

  public static void main(String[] args) throws InterruptedException {
    Thread backgroundThead = new Thread(() -> {
      int i = 0;
      while (!stopRequested) {
        i++;
      }
    });
    backgroundThead.start();
    TimeUnit.SECONDS.sleep(1);
    stopRequested = true;
  }
}
  • バックグラウンドスレッドは無限ループする可能性がある
    • バックグラウンドスレッドからメインスレッドのbackgroundTheadに書き込んだ値が見えるかが保証されないから
  • 下記のクラスのバックグラウンドスレッドは停止するか?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StopThread2 {
  private static boolean stopRequested;

  private static synchronized void requestStop() {
    stopRequested = true;
  }

  private static synchronized boolean stopRequested() {
    return stopRequested;
  }

  public static void main(String[] args) throws InterruptedException {
    Thread backgroundThead = new Thread(() -> {
      int i = 0;
      while (!stopRequested()) {
        i++;
      }
    });
    backgroundThead.start();
    TimeUnit.SECONDS.sleep(1);
    requestStop();
  }
}
  • バックグラウンドスレッドは停止することが保証される
    • 読み込み操作と書き込み操作の両方が同期されていて、バックグラウンドスレッドからメインスレッドのbackgroundTheadに書き込んだ値が見えることが保証されるから

1.4. volatile

  • volatileはアトミック性は保証しないが、可視性を保証する
  • 先ほどのStopThread2クラスはvolatileを使うことで、下記のようにより簡単なコードにできる
    • synchronizedを相互排他のためではなく、スレッド間通信のためだけにしようしているから
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class StopThread3 {
  private static volatile boolean stopRequested;

  public static void main(String[] args) throws InterruptedException {
    Thread backgroundThead = new Thread(() -> {
      int i = 0;
      while (!stopRequested) {
        i++;
      }
    });
    backgroundThead.start();
    TimeUnit.SECONDS.sleep(1);
    stopRequested = true;
  }
}
  • volatileは「アトミックのように見えて実はアトミックではない」という操作に対して使わないようにする注意が必要
    • volatileはアトミック性を保証しないから
    • 例えば、下記のgenerateSerialNumber()を複数のスレッドから呼び出したら、違う値が帰ってくるか?
    1
    2
    3
    4
    5
    
      private static volatile int nextSerialNumber = 0;
    
      public static int generateSerialNumber() {
        return nextSerialNumber++;
      }
    
    • 違う値が帰ってくることは保証されない
      • インクリメントは「アトミックに見えるが実はアトミックでない操作」だから。インクリメントは、値の読み出しと古い値に1を加えた値を書き戻す2つの操作を行う。スレッドが古い値を読み出して新たな値を書き戻す間に、別のスレッドがフィールドを読み出すと、最初のスレッドと同じ値が見えて、同じ値を返してしまう
    • nextSerialNumber + 1 はまずどこに書き込まれているか?
      • スタック上の一時領域
      • メモリは参照と書き込みだけしかできない。計算できない。

1.5. 可変データの共有の回避

  • 今までの議論のように、可変データへの共有は難しいので、回避できるのであれば回避すべき
  • 回避には2つの方法がある
    • 不変データを共有する
    • 可変データを共有しない(可変データは単一スレッドで使う)
  • 上記の方針を採用した場合は文書化することが重要
    • プログラムが発展する際に方針が維持されるから
  • AtomicBooleanのようなライブラリを使うと良い。

2. 項目 79 過剰な同期は避ける

2.1. 結論

  • 同期された領域内では、できる限り少ない処理をするべき
    • デッドロックやデータ破壊、パフォーマンス低下の原因となるから

2.2. 過剰な同期によるエラーやデッドロック

  • 下記クラスObservableSetは要素が追加されたら任意の処理ができるSetであり、SetObserverインターフェースはObservableSetのObserverインターフェースである

Effective Java 3rd [Chapter 10] - 例外

例外

Effective Java 第 3 版の個人的メモ

  1. 項目 69 例外的状態にだけ例外を使う
  2. 項目 70 回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使う
  3. 項目 71 チェックされる例外を不必要に使うのを避ける
  4. 項目 72 標準的な例外を使う
  5. 項目 73 抽象概念に適した例外をスローする
  6. 項目 74 各メソッドがスローするすべての例外を文書化する
  7. 項目 75 詳細メッセージにエラー記録情報を含める
  8. 項目 76 エラーアトミック性に努める
  9. 項目 77 例外を無視しない

1. 項目 69 例外的状態にだけ例外を使う

1.1. 結論

例外をGOTO文のように、フロー制御に使ってはいけない。

AWS CodeGuru

AWS CodeGuruを使ってみた

AWS re:Invent 2019で発表された機械学習を応用したソースコード解析ツールAWS CodeGuruを使ってみました。 その使用感や解析結果をまとめたいと思います。

Effective Java 3rd [Chapter 9] - プログラミング一般

プログラミング一般

Effective Java 第 3 版の個人的メモ

  1. 項目 57 ローカル変数のスコープを最小限にする
  2. 項目 58 従来の for ループより for-each ループを選ぶ
  3. 項目 59 ライブラリを知り、ライブラリを使う
  4. 項目 60 正確な答えが必要ならば、float と double を避ける
  5. 項目 61 ボクシングされたプリミティブ型(boxed primitive)より、プリミティブ型(primitive)を選択すべし
  6. 項目 62 他の型が適切な場所では、文字列を避ける
  7. 項目 63 文字列結合のパフォーマンスに用心する
  8. 項目 64 インタフェースでオブジェクトを参照する
  9. 項目 65 リフレクションよりもインタフェースを選ぶ
  10. 項目 66 ネイティブメソッドを注意して使う
  11. 項目 67 注意して最適化する
  12. 項目 68 一般的に受け入れられている命名規約を守る
  13. 参考URL

1. 項目 57 ローカル変数のスコープを最小限にする

1.1. 結論

  • ローカル変数は初めて使用される時に宣言しましょう
    • かなり前段階で宣言されていると、その時点から変更されているかを確認する必要があるから
  • (ほとんど)全てのローカル変数の宣言時には初期化しましょう
    • 初期化するための情報がない段階で、宣言すべきではないから
  • メソッドを小さくして焦点をはっきりさせましょう
    • 1つのメソッド内で2つの処理があれば、1つ目の処理用のローカル変数が2つ目の処理にも使われているかもしれないので、これを防ぐために 1メソッド:1処理 としましょう。

1.2. サンプル

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 良い例
for (Element e : c) {
  doSomething(e);
}

// 悪い例
Iterator<Element> i = c.iterator();
While (i.hasNext()) {
  doSomething(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
While (i.hasNext()) { // バグ(iではなく、i2が正しい。)
  doSomething(i2.next());
}

1.3. ポイント

  • 使用される直前に、ローカル変数を初期化して宣言しましょう
  • 1メソッド:1処理としましょう

備考:while文ではなくfor文を好んで使いましょう。

Effective Java 3rd [Chapter 8] - メソッド

メソッド

Effective Java 第 3 版の個人的メモ

  1. 項目 49 パラメータの正当性を検査する
  2. 項目 50 必要な場合、防御的にコピーする
  3. 項目 51 メソッドのシグネチャを注意深く設計する
  4. 項目 52 オーバーロードを注意して使う
  5. 項目 53 可変長引数を注意して使う
  6. 項目 54 null ではなく、空コレクションか空配列を返す
  7. 項目 55 オプショナルを注意して返す
  8. 項目 56 すべての公開 API 要素に対してドキュメントコメントを書く
  9. 参考URL

1. 項目 49 パラメータの正当性を検査する

1.1. 結論

  • 入力値のチェックをしましょう
  • Javadocに入力値の説明と例外の説明は記入しましょう

1.2. 良い例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
	/**
	 * 値が(this mod m)であるBigIntegerを返します。
	 * このメソッドは、remainderメソッドとは異なり、
   * 常に負でないBigIntegerを返します。
	 * 
	 * @param m 正でなければならない
	 * @return this mod m.
	 * @throws ArithmetricException m <= 0の場合.
	 */
	public BigInteger mod(BigInteger m) {
		if(m.signum() <= 0) {
			throw new ArithmeticException("Modulus <= 0: " + m);
		}
		// 計算を行う
	}

最初に入力値チェックをすることで、以下を防げる。