Effective Java 3rd [Chapter 9] - プログラミング一般
プログラミング一般
Effective Java 第 3 版の個人的メモ
- 項目 57 ローカル変数のスコープを最小限にする
- 項目 58 従来の for ループより for-each ループを選ぶ
- 項目 59 ライブラリを知り、ライブラリを使う
- 項目 60 正確な答えが必要ならば、float と double を避ける
- 項目 61 ボクシングされたプリミティブ型(boxed primitive)より、プリミティブ型(primitive)を選択すべし
- 項目 62 他の型が適切な場所では、文字列を避ける
- 項目 63 文字列結合のパフォーマンスに用心する
- 項目 64 インタフェースでオブジェクトを参照する
- 項目 65 リフレクションよりもインタフェースを選ぶ
- 項目 66 ネイティブメソッドを注意して使う
- 項目 67 注意して最適化する
- 項目 68 一般的に受け入れられている命名規約を守る
- 参考URL
1. 項目 57 ローカル変数のスコープを最小限にする
1.1. 結論
- ローカル変数は初めて使用される時に宣言しましょう
- かなり前段階で宣言されていると、その時点から変更されているかを確認する必要があるから
- (ほとんど)全てのローカル変数の宣言時には初期化しましょう
- 初期化するための情報がない段階で、宣言すべきではないから
- メソッドを小さくして焦点をはっきりさせましょう
- 1つのメソッド内で2つの処理があれば、1つ目の処理用のローカル変数が2つ目の処理にも使われているかもしれないので、これを防ぐために
1メソッド:1処理
としましょう。
- 1つのメソッド内で2つの処理があれば、1つ目の処理用のローカル変数が2つ目の処理にも使われているかもしれないので、これを防ぐために
1.2. サンプル
|
|
1.3. ポイント
- 使用される直前に、ローカル変数を初期化して宣言しましょう
1メソッド:1処理
としましょう
備考:
while
文ではなくfor
文を好んで使いましょう。
2. 項目 58 従来の for ループより for-each ループを選ぶ
2.1. 結論
「簡潔」&「バグ予防」にもなるので、
以下の場合を除いて、for
文ではなくfor-each
文を使いましょう
- フィルタリング(要素の削除)
- ある要素だけ削除したい場合は、明示的なイテレータを使用する必要がある。(Iterator#remove()を使用するため。)
- 変換(値の置換)
- ある要素の値を変更するために、リストイテレータや配列インデックスが必要であるため。
- 並列イテレーション
- 複数のコレクションを並列にイテレートするためには、イテレータやインデックス変数を明示的に(自分で)制御する必要があるため。
Collectionに対して変更できるが、フェイルファストには気を付けましょう。
Collectionによっては、iteratorおよびlistIteratorメソッドによって返されるイテレータは、フェイルファストです。イテレータの作成後に、イテレータ自体のremoveまたはaddメソッド以外の方法でリストが構造的に変更されると、イテレータはConcurrentModificationExceptionをスローします。 https://docs.oracle.com/javase/jp/8/docs/api/java/util/ArrayList.html
2.2. サンプル
|
|
ネストしたイテレータを使った問題コード
BadIterator.java
|
|
外側のコレクション(
suits
)のイテレータに対してnext()
が過剰に呼び出されて、NoSuchElementException
がスローされるため。
GoodIterator.java
|
|
2.3. ポイント
以下の場合を除いて、for
文ではなくfor-each
文を使いましょう
- フィルタリング(要素の削除)
- 変換(値の置換)
- 並列イテレーション
for-each
文を使えるようにするため、Iterableを実装しましょう。
2.4. 付録 ( Iterableの実装 )
Main.java
|
|
以下の3つのメソッドをオーバーライドすれば良い。
- java.lang.Iterable#iterator()
- java.util.Iterator#hasNext()
- java.util.Iterator#next()
PrimeNumbers.java
|
|
3. 項目 59 ライブラリを知り、ライブラリを使う
3.1. 結論
- 標準ライブラリを使いましょう
- 時間を無駄にすることなく、他の有意義な箇所(実装)に時間を割り当てられる
- 専門家の知識が活かせる
- 他に同じライブラリを使った人達の経験が活かせる
- 詰まった際の解決方法がネット上にある
- パフォーマンスが良い
- 標準ライブラリ(java.lang, java.util, java.io)の内容は知っておきましょう
3.2. サンプル
便利なライブラリとして以下の2つは知っておきましょう
- Collections Framework
- 関連のないAPI間での相互運用ができる
- Concurrency Utilities
- マルチスレッドプログラムの処理を単純化するためのユーティリティがある
- java.util.concurrent.atomic
- java.util.concurrent.locks
3.3. ポイント
- 無駄な努力はしないでください
- ライブラリが存在するのかを知らなかったら調べてください
- apache, spring, guavaあたりはよく利用しますね。
- あなたのコードよりもライブラリの方が優れています
4. 項目 60 正確な答えが必要ならば、float と double を避ける
4.1. 結論
- 金銭計算など正確な計算が必要な場合は、
float
とdouble
を使わないでおきましょう- 9桁以内であれば、
int
- 18桁以内であれば、
long
- 正確な計算が必要もしくは、18桁以上であれば、BigDecimal
- 9桁以内であれば、
4.2. 理由
丸めの誤差が発生するから。
|
|
BigDecimal
には2つの欠点があるので、最後の手段。
- 計算式が不便(
+, -, *, /, %
が使えない。)1 2
BigDecimal price = new BigDecimal(); price.add(100);
- 処理が遅い
BigDecimal
を使わない方が速いですね。
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
StopWatch stopWatch; // long を利用した場合[1135(ms)] stopWatch = new StopWatch(); stopWatch.start(); long sumLong = 0; for(var i = 0; i < Integer.MAX_VALUE; i++){ sumLong += i; } stopWatch.stop(); System.out.println(stopWatch.prettyPrint()); // BigDecimal を利用した場合[3969(ms)] stopWatch = new StopWatch(); BigDecimal sumBigDecimal = new BigDecimal("0"); stopWatch.start(); for(var i = 0; i < Integer.MAX_VALUE; i++){ sumBigDecimal.add(new BigDecimal(i)); } stopWatch.stop(); System.out.println(stopWatch.prettyPrint()); // 実行結果 /** * StopWatch '': running time (millis) = 1135 * ----------------------------------------- * ms % Task name * ----------------------------------------- * 01135 100% * * StopWatch '': running time (millis) = 3969 * ----------------------------------------- * ms % Task name * ----------------------------------------- * 03969 100% */
4.3. 備考
正確に計算できなければ、ArithmeticException
がスローされる。
|
|
5. 項目 61 ボクシングされたプリミティブ型(boxed primitive)より、プリミティブ型(primitive)を選択すべし
5.1. 結論
基本データ型の方が演算が速く、処理の危険性がないため、
以下の場合を除いて、ボクシングされたプリミティブ型(Integer
やDouble
, Boolean
など)よりもプリミティブ型(int
やdouble
, boolean
など)を使いましょう。
Collection
の要素、キー、値として使う場合ThreadLocal<T>
を利用する場合- リフレクションによりメソッドを呼び出す必要がある場合
5.2. サンプル
5.2.1. 演算速度
プリミティブ型の方が10倍ほど速いですね。
|
|
5.2.2. 処理の危険性
|
|
first < second
の際は、自動アンボクシングが実行されて、ちゃんと比較される。first == second
の際は、同一性比較が行われ、異なるインスタンスであるため、false
が返却される。
備考:
naturalOrder.compare(new Integer(1), null);
とすると、NullPointerException
がスローされる。
ボクシングされたプリミティブ型では、null
が入る可能性があることにも注意。
5.3. ポイント
ボクシングされたプリミティブ型の使用を強いられない限り、プリミティブ型を使いましょう。
6. 項目 62 他の型が適切な場所では、文字列を避ける
6.1. 結論
以下の場合、文字列を使うべきではないです。
- 他の値型に対する代替
- 「
はい
/いいえ
」よりも、boolean
での「true
/false
」の方が優れている。
- 「
- 列挙型に対する代替
- 文字列型よりも、
Enum
型の方が優れている。(比較などもEnum
型の方が速かったですよね。)
- 文字列型よりも、
- 集合型に対する代替
- 集合型として「
String compoundKey = className + "#" + methodName
」とするよりも、単に集合を表すクラスを作成しましょう。- 文字列で集合を表すと、文字列解析が必要になるので。
- 集合型として「
- 偽造不可能なキー(capability)に対する代替
- あるAPIへのアクセスを与えるために文字列を使う(
set(String key, Object value)
)よりも、
java.lang.ThreadLocal<T>
を使いましょう。- 文字列に基づくAPIが抱える問題を解決でき、かつ、速いので。
- あるAPIへのアクセスを与えるために文字列を使う(
6.2. サンプル
偽造不可能なキー(capability)に対する代替 について例を示します。
以下は、2つの独立したクライアントが、それぞれのスレッドローカル変数に同じ名前のキーを使用すると、共有することになるため不完全。
|
|
キーとして、文字列を使うのはやめましょう。
|
|
実際のThreadLocalも同じような構成となっています。
|
|
備考:
ThreadLocal
は副作用が強いので、使用する際は気を付けた方が良いですね。
7. 項目 63 文字列結合のパフォーマンスに用心する
7.1. 結論
簡単な文字列結合でなく、
n
個の文字列を結合する場合はStringBuilderを使いましょう。
String
は不変(immutable)なので、2つの文字列が結合される場合、両方の内容がコピーされるので。
7.2. サンプル
80文字の固定長の文字列連結の場合、
Fast.java は Slow.java と比べて、85倍速い。
StringBuilder b = new StringBuilder();
とした場合は、50倍速い。by Joshua Bloch
Slow.java
|
|
Fast.java
|
|
備考:定数の文字列連結の場合は、
+
による連結だとコンパイル時に行われるので、実行時のコストは0になる。
8. 項目 64 インタフェースでオブジェクトを参照する
8.1. 結論
実装を切り替える際に、宣言の1行を変更するだけで済むので、
オブジェクト参照には、
- 実装クラスではなく、インタフェースを使いましょう。
- インタフェースがなければ、必要な機能を提供しているクラス階層中の最も上位のクラスを使用しましょう。
ただし、以下の場合を除く。
- 適切なインタフェースが存在しない場合
- 値クラス
- 例:
String
,BigInteger
など
- 例:
- オブジェクトがクラスに基づくフレームワークに属している。
- 値クラス
- インタフェースはあるが、そのインタフェースにない特別なメソッドをクラスが提供している場合
- 例:PriorityQueue#iterator()など。Queueにすると
iterator()
はない。
- 例:PriorityQueue#iterator()など。Queueにすると
8.2. ポイント
プログラムを柔軟にするために、APIの場合はインタフェースを使うようにしましょう。
9. 項目 65 リフレクションよりもインタフェースを選ぶ
9.1. 結論
リフレクションは、ある種の洗練されたシステムプログラミング処理では強力ですが、
以下の欠点があるので、インタフェース(もしくは、スーパークラス)を使いましょう
- コンパイル時の型検査の恩恵を全て失う
- 実行時に、呼び出すクラス名、メソッド名、フィールド名を確認するため。
- コードが冗長になり読みづらくなる
- サンプル参照
- パフォーマンスが悪くなる
- 2-50倍遅くなる。by
Joshua Bloch
- 1ns が 50nsになったところで、、なことが多い。機械学習とか計算量が多いと問題になるかもしれないですね。
- 2-50倍遅くなる。by
9.2. サンプル
基本的にリフレクションは使わないのですが、強力な機構であるので紹介します。
以下のようにSet
インタフェースにアクセスすることができる。
使用例としては以下。
Set
契約に従っているのかを調べる汎用的な検査プログラム- 汎用的な
Set
のパフォーマンス解析ツール
某プロジェクトでは、
Athena
からの取得結果をvarchar/integer/double
のいずれかに変換するために利用している。
|
|
9.3. ポイント
どの型が来ても良い場合にリフレクションは使えるが、
そうではない限りインタフェースを使いましょう。
10. 項目 66 ネイティブメソッドを注意して使う
10.1. 結論
- 低レベルのリソースや古いライブラリへアクセスするためのネイティブメソッドの使用は正当であるが、できるだけ局所的にし、徹底的にテストするべき
- パフォーマンス改善のためにネイティブメソッドを使用するべきでない
10.2. ネイティブメソッドとは
-
ネイティブメソッドとは、CやC++などのネイティブのプログラミング言語で書かれた特別なメソッド
-
ネイティブメソッドはJava Native Interface(JNI)を利用することで、Javaアプリケーションから呼び出せる
-
ネイティブメソッドの用途
- プラットフォーム特有の機能へアクセスのため
- ただし、たいていの機能はライブラリとしてある
- 古いデータへのアクセスを提供している古いコードのライブラリへのアクセスのため
- (パフォーマンス向上のため)
- 現在のJVM実装はかなり速く、ネイティブメソッドと遜色ないので使用するべきでない
- プラットフォーム特有の機能へアクセスのため
-
ネイティブメソッドの欠点
- ネイティブメソッドを使用しているアプリケーションは、Javaアプリケーションであってもメモリ破壊エラーの影響を受けるようになる
- ネイティブメソッドは安全ではないから
- 移植性がかなり低くなる
- ネイティブメソッドはプラットフォームに依存するから
- 性能劣化することもある
- ネイティブコードへの出入りでコストがかかるから
- 可読性が低くなる
- グルーコードが必要となるから
- ネイティブメソッドを使用しているアプリケーションは、Javaアプリケーションであってもメモリ破壊エラーの影響を受けるようになる
グルーコード:もともと互換性がない部分同士を結合するためだけに働くコード
10.3. まとめ
以下のネイティブメソッドの使い方は正当
- 低レベルのリソースや古いライブラリへアクセスするための使用は正当である
ただし、多くの欠点があるため、できるだけ局所的にし、徹底的にテストするようにする。
10.4. 余談
現在(2020年1月)、Project PanamaでJNR(Java Native Runtime)がJEP(JDK Enhancement Proposals)として要望されている
11. 項目 67 注意して最適化する
11.1. 結論
-
実装時には、速いプログラムよりも良いプログラムを目指すべし
- 健全なアーキテクチャを破壊すると、疎結合を保てなくなるから
-
性能を制限してしまう設計は避けるべし
- APIや通信レベルのプロトコル、永続データ形式を後から変更するのは困難だから
-
API設計において性能について考えるべし
- API設計によって性能に影響が出るから
-
最適化前後で性能測定をするべし
- 性能測定なしで、プログラムがどの部分で時間を費やしているかを推測するのは困難だから
- 最適化前後で性能測定をしないと、最適化によってどれくらいの効果があったか分からないから
-
アーキテクチャの設計の段階で性能について考えるべき
- 実装上の問題はのちの最適化で修正可能だが、終盤でアーキテクチャの欠陥をシステムを書き直すことなく修正することはほとんど不可能だから
-
API設計において性能について考えるべし
- publicの型を可変にすると不必要に多くの防御的コピーが必要になるかもしれない(項目50)
- APIにおいて、インターフェースではなく実装型を使用することで、後でより速い実装が書かれたとしても、特定の実装に拘束されてしまう(項目64)
11.2. 余談
性能試験はどうなったらゴールなのかはやる前に決めておく必要がある
- 性能試験をするということは性能目標があるはずなのでそれを満たしたら終わり
- 良いプログラムの計測方法は?
- 結合度、凝集度
- 結合度 - Wikipedia
- 結合にもたくさん種類がある
- できればスタンプ結合までにしたい
- SonarQubeのメトリクスを参考にする
- 複雑度
- コード重複率
- etc
- 結合度、凝集度
12. 項目 68 一般的に受け入れられている命名規約を守る
12.1. 結論
- 一般的に受け入れられている命名規約を守るべき
- API利用者やそのコードで作業する他の開発者が、誤解なくAPIやコードを理解できるようにするため
12.2. 命名規約の種類
命名規約の種類は以下の2つに分けられる
- 活字的命名規約
- 文法的命名規約
12.2.1. 活字的命名規約
-
ピリオドで区切られた要素を持ち、階層的であるべき
-
区切られた要素はアルファベットの小文字とまれに数字から構成されるべき
-
組織外で使用されるパッケージには、組織のインターネットドメイン名を逆さにしたものから始める
- (例えば、edu.cmu、com.sun)
-
パッケージ名の残りは、1つ以上の要素から構成され、8文字以下であるべき。
-
意味を持った省略形も使うべき(例えば、util、awt)
-
クラス名、インターフェース名、アノテーション名
- 各単語の先頭一文字を大文字に
-
メソッド名、フィールド名
- 最初の一文字を小文字に
-
定数フィールドでは大文字の単語をアンダースコアで区切る(例えばNEGATIVE_INFINITY)
-
ローカル変数名
-
フィールド名と同じだが、省略形も認められる(例えばi,xref)
-
型パラメータ名
- Tは任意の型、Eはコレクションの要素の型、KとVはmapのキーと値の型、Xは例外の型など
12.2.2. 文法的命名規約
文法的命名規約は活字的命名規約よりも柔軟で議論の余地がある
- パッケージ名
- 特になし
- クラス名
- インスタンス化可能なクラスは、単数名詞、名詞句で命名される(Timer、BufferedWriter)
- インスタンス化しないクラスは、名詞の複数形で命名されることが多い(Collectors、Collections)
- インターフェース名
- クラスと同じであったり、形容詞で命名される(Iterable、Runnable)
- アノテーション名
- 名詞、動詞、前置詞、形容詞すべてよく使われる
- 様々な意味でアノテーションが使われるから
- メソッド名
- 処理をするメソッドは動詞や目的語を含む動詞句で命名される(append、drawImage)
- booleanを返すメソッドはisまたはhasで始まり、名詞、名詞句が続いて、形容詞として機能するように命名される(isEmpty、hasSibiling)
- booleanでない機能や属性を返すメソッドは、名詞、名詞句、getで始まる動詞句で命名される
- getで始まる動詞句だけを認める声もあるが、下の例のように名詞、名詞句を使ったほうがたいていは可読性が上がる
|
|
- getで始まる動詞句はBeanクラスならば必須。また、クラスが同じ属性を設定するメソッドを持っているならば使う(getAttributeとsetAttribueなど)
いくつかのメソッドは特別な命名がされている
- toTypeメソッド
- インスタンスの型を変換し、独立したオブジェクトとして返すメソッド(toString)
- asTypeメソッド
- 受け取ったオブジェクトと異なる型のオブジェクトを返すメソッド(asList)
- typeValueメソッド
- 呼び出されるオブジェクトと同じ値のプリミティブな値を返すメソッド(Integer#intValue)
- staticファクトリーメソッド
- valueOf, of, getInstance, newInstance, getType, newTypeなど
- フィールド名、ローカル変数名
- フィールド名の文法規則はそあまり確立されていないし、それほど重要ではない
- うまく設計されたAPIではフィールドはほとんど公開されないから
名詞や名詞句が多い
- boolean型のフィールドはアクセサメソッドの最初の文字列だけ消えたものが採用されることが多い
- ローカル変数名はもっと緩い
12.3. 余談
- Readable Code を読むと良さそう
- フィールド名も他の人が見るから気を付けた方が良さそう。