Effective Javaを勉強します【第8章】
項目45. ローカル変数のスコープを最小限にする
コードの可読性と保守性を向上させ、誤りの可能性を減らすためにローカル変数のスコープは最小限にすべきである。
スコープを最小限にするには?
ローカル変数のスコープを最小限にする最も強力な技法は、ローカル変数が初めて使用される箇所で宣言すること。
ローカル変数を早めに宣言することで、以下のような問題が生じる。
- 処理を理解しようとしているプログラマの読み手の注意を逸らしてしまう
- いざ変数の使用箇所まで到達しても、その変数の型や初期値が思い出せない
- 本来意図した使用場所以外で呼び出された場合に、誤った動作をする可能性がある
スコープを最小限にするもう一つの方法は、メソッドを小さくして焦点をはっきりさせること。 1つの処理に対して1つのメソッドとすることで、関係のない処理にスコープを広げない。
ローカル変数宣言と初期化子
全てのローカル変数宣言は初期化子を含むべきである。 変数を合理的に初期化するために十分な情報がないのであれば、初期化が可能になるまで宣言を先送りすべき。
try-catch
文はこの規則の例外である。
例外をスローするメソッドの戻り値で変数が初期化されるのであれば、その変数はtry
ブロック内で初期化される必要がある。
しかしその変数がtry
ブロック外でも使用される場合、try
ブロックの前で宣言しなければ使用することができない。
その場合は変数を合理的に初期化することはできない。
ループ変数とスコープ
for
ループではfor
形式とfor-each
形式の両方で、ループ変数の宣言が可能であり、このスコープはループ内に限定される。
したがってループ変数の内容がループ変数後に不要な場合は、while
ループではなくfor
ループを選択すべき。
例えば以下のwhile
文には、コピー&ペーストミスによるバグが含まれているが、コンパイルエラーや例外は発生しない。
Iterator<Element> i = c2.iterator(); while (i.hasNext()) { doSomething(i.next()); } Iterator<Element> i2 = c2.iterator(); while (i.hasNext()) { //バグ doSomething(i2.next()); }
一方で上記の処理をfor
文で書き直した場合、コンパイルエラーとなるため、バグの発見が容易である。
for (Iterator<Element> i = c2.iterator(); i.hasNext()); { doSomething(i.next()); } for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) { //コンパイルエラー doSomething(i2.next()); }
項目46. 従来のfor
ループよりfor-each
ループを選ぶ
従来のfor
ループに含まれるイテレータ変数とインデックス変数はコードを散らかしているだけであり、時にはエラーの原因となる。
for-each
ループはコードを簡潔にし、余分なエラーの原因を排除してくれる。
従来のfor
ループと比較した時に、パフォーマンス上のペナルティも存在しない。
// リリース1.6以降におけるコレクションと配列をイテレートするための好ましいイディオム for (Elemetn e : elements) { doSomething(e); }
for-each
ループはコレクションと配列以外にも、Iterable
インタフェースを実装したあらゆるオブジェクトをイテレートすることが可能。
Iterable
は単一なメソッドから構成されるため、実装は難しくない。Collection
は実装しない場合であってもIterable
を実装すれば、
ループ処理でfor-each
文が利用可能となり、ユーザーから感謝されるだろう。
public interface Iterable<E> { iterator<E> iterator(); }
for-eachが利用できない場合
一般的にfor-each
文は従来のfor
文より優れているが、以下のケースではfor-each
文は利用できないので注意すること。
- フィルタリング:特定の要素のみを抽出し削除する必要がある場合
- 変換:特定の要素に新しい値を設定する場合
- 並列イテレーション:複数のコレクションを並列にイテレートする必要がある場合
項目47. ライブラリーを知り、ライブラリーを使う
車輪の再発明のために、無駄な努力をしないこと。
実装しようとしている処理がすでにクラス内やライブラリー内に存在するのであれば、それを利用すること。 存在の有無が分からなければ調べること。
特に、標準ライブラリーの利用には以下のメリットがある。
- 実装した専門家の知識と、これまでの他の利用者の経験を活用することが可能
- 本質的でない課題に時間を無駄にする必要がない(ランダム処理をさせたいがために擬似乱数発生法や整数論について勉強する必要があるのか?)
- 標準ライブラリーを開発しているコミュニティにより、パフォーマンスやバグが勝手に修正されてゆく
- アプリケーションのコアがコードの大部分を占めることで、可読性・保守性が向上する
標準ライブラリーを効率的に活用するためにも、主要リリースごとの新機能や改善点は把握しておくことが大切。
項目48. 正確な答えが必要ならばfloat
とdouble
を避ける
float
型とdouble
型は、主に科学計算と工学計算のために設計されている。
それらは2進浮動小数点算術を行うが、これは広い範囲の大きさに対して正確な近似を素早く行うために設計されており、正確な結果は提供しない。
したがってfloat
型とdouble
型は、特に金銭計算には適さない。
例) 1ドルで、10セント、20セント、30セント、・・・、1ドルと値段がついたキャンディを、安い順から1つづつ買う場合、いくつ買えるかを計算させる。 正しくは、10+20+30+40=100セント=1ドル購入可能。
double funds = 1.00; int itemsBought = 0; for (double price = .10; funds >= price; price += .10) { funds -= price; itemsBought++; } System.out.println(itemsBought + " items bought."); // => 3 items bought. System.out.println("Change: $" + funds); // => Change: $0.3999999999999999
上記のように正しい結果にならない。これは以下のようにBigDecimal
を利用すると解決可能。
final BigDecimal TEN_CENTS = new BigDecimal(".10"); int itemsBought = 0; BigDecimal funds = new BigDecimal("1.00"); for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) { itemsBought++; funds = funds.subtract(price); } System.out.println(itemsBought + " items bought."); // => 4 items bought. System.out.println("Change: $" + funds); // => Change: $0.0
しかしBigDecimal
には、計算が遅い、基本データの算術型より不便という欠点がある。
BigDecimal
の欠点(主にパフォーマンス)が許容できない場合、int
やlong
を代わりに用いることも可能。
しかしこの場合、プログラマが小数点の位置を把握しなければならない。またint
は9桁、long
18桁の精度でしか計算できない。
以下はint
を用いて、セントで計算を行った例。
int itemsBought = 0; int funds = 100; for (int price = 10; funds >= price; price += 10) { itemsBought++; funds -= price; } System.out.println(itemsBought + " items bought."); // => 4 items bought. System.out.println("Change: $" + funds); // => Change: $0
項目49. ボクシングされた基本データより基本データ型を選ぶ
Javaではすべての基本データ型は、ボクシングされた基本データ(参照型)を持っている。
例えば、int
ならInteger
、double
ならDouble
、boolean
ならBoolean
など。
基本データ型を自動でボクシングされた基本データへ変換する自動ボクシング、参照型を自動で基本データ型へ変換する自動アンボクシングがあるが、 基本データ型とボクシングされた基本データの間には以下のような違いが存在する。
- 基本データ型は値だが、ボクシングされた基本データはオブジェクトである
- 基本データ型は機能する値しか持たないが、ボクシングされた基本データは
null
という機能しない値を持つことが可能 - 基本データ型は、ボクシングされた基本データと比較して、時間的・空間的に効率的
ボクシングされた基本データの比較
以下はボクシングされた基本データに対するコンパレータの例。
Comparator<Integer> comparator = new Comparator<Integer>() { @Override public int compare(Integer first, Integer second) { return first < second ? -1 : (first == second ? 0 : 1); } };
一見正しそうに見えるが、compare
メソッドにおけるfirst
とsecond
の比較に欠陥がある。
first < second
では引数は自動アンボクシングされる。
しかしこの検査がfalse
だった場合、first == second
ではボクシングされた基本データの同一性比較が行われる。
この時、2つの値が同じ値を示している異なるインスタンスであった場合に、false
(second
はfirst
より小さい)と判定されてしまう。
ボクシングされた基本データに対して==
演算子を利用するのは大抵まちがいである。
上記の問題は、以下のように比較の前に基本データ型として引数を取り出すことで解決が可能。
Comparator<Integer> comparator = new Comparator<Integer>() { @Override public int compare(Integer first, Integer second) { int f = first; // 自動アンボクシング int s = second; // 自動アンボクシング return f < s ? -1 : (f == s ? 0 : 1); } };
null に対する自動ボクシング
ボクシングされた基本データ型を自動アンボクシングしようとすると、NullPointerException
が発生する。
Integer i = null; if (i == 42) { // 自動アンボクシングにより、 NullPointerException が発生 System.out.println("true"); }
自動ボクシング/アンボクシングによるパフォーマンス低下
以下のコードは、ローカル変数sum
をボクシングされた基本データ型Long
で宣言したことにより、
実行時にボクシングとアンボクシングが繰り返され、大幅にパフォーマンスが低下する。
public static void main(String[] args) { Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; } }
ボクシングされた基本データの使い所
以下のケースではボクシングされた基本データを使用すべき。
- コレクション内の要素、キー、値として。コレクションには基本データ型を入れることはできない。
- パラメータ化された型の空パラメータとして。同様に基本データ型は利用不可。
- リフレクションによりメソッドを呼び出す場合。(項目53にて)
項目50. 他の方が適切な場所では、文字列を避ける
文字列はテキストを表現する以外の目的で利用すべきではない。 以下は、文字列を使用すべきでないケース。
- 他の値型に対する代替としての利用。
データをファイルやキーボード入力から取得した場合、大抵は文字列の形式で受け取るが、内容が数値ならばint
やfloat
、
「はい」や「いいえ」ならばboolean
といった具合に、適切な値型へ変換すべき。 - 列挙型(enum)の代替としての利用。
- 集合型の代替としての利用。不要な文字列解析の手間が生じるので、その集合を表すクラスを用意する。
- ケイパビリティ(偽造できないキー)としての利用。
項目51. 文字列結合のパフォーマンスに用心する
n
個の文字列を結合するのに、文字列結合演算子(+)を繰り返し利用すると、n
に関して2次の時間を必要とするので注意すること。
代わりにStringBuilder
のappend
メソッドを使用する。
public String statement(){ String result = ""; for (int i = 0; i < numItems(); i++){ result += lineForItem(i); } return result; } // numItemsが100、lineForItemが80文字長の文字列を返す場合、上記よりパフォーマンスが85倍改善された public String statement(){ StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH); for (int i = 0; i < numItems(); i++){ b.append(lineForItem(i)); } return b.toString(); }
項目52. インタフェースでオブジェクトを参照する
適切なインタフェース型が存在するならば、パラメータ、戻り値、変数、およびフィールドはすべてインタフェース型で宣言されるべき。
// 良い - 型としてインタフェースを利用 List<Subscriber> subscribers = new Vector<Subscriber>(); // 悪い - 型としてクラスを利用 Vector<Subscriber> subscribers = new Vector<Subscriber>();
上記のように書くだけで、プログラムがかなり柔軟になる。 例えば実装を切り替えたい場合、コンストラクタで指定しているクラスを変更する、あるいは別のstaticファクトリーメソッドを使用するだけ。
ただし、元の実装がインタフェースの一般契約で要求されていない何らかの特別な機能を提供していて、コードがその機能に依存している場合、 新たな実装が同じ機能を提供するようにしなければならない。(下記の場合、Vectorクラス固有の同期機能に依存していないかどうか)
// VectorからArrayListへ切り替える List<Subscriber> subscribers = new ArrayList<Subscriber>();
項目53. リフレクションよりインタフェースを選ぶ
コア・リフレクション機構(java.lang.reflect
)を用いれば、Class
インスタンスからコンストラクタを表す Constructor
インスタンス、メソッドを表す Method
インスタンス、フィールドを表す Field
インスタンスを取得することができる。またこれらを用いて、実際のインスタンスを生成したり、メソッドを呼び出したり、フィールドにアクセスすることが可能である。
このようにコア・リフレクション機構は非常に強力であるが、次に示すようなデメリットが存在する。したがって、基本的にはアプリケーションの設計時(クラス内部の構造の調査など)のみに利用すべきであり、クラスへのアクセスにはインタフェースやスーパークラスを利用すべきである。
- 例外の検査を含め、コンパイル時の型検査の恩恵を全て失う
- リフレクションのコードは冗長であり、可読性が低い
- 通常のメソッド呼び出しと比較して、パフォーマンスが悪い
項目54. ネイティブメソッドを注意して使用する
ネイティブメソッドとは、C や C++ などのネイティブプログラミング言語で書かれたメソッドのことであり、 Java Native Interface により Java からネイティブメソッドを呼び出すことが可能である。
その用途としては、レジストリやファイルロックなどのプラットフォーム固有の機構へのアクセスや、古いコードのライブラリへのアクセスが挙げられる。また、アプリケーションの中でもパフォーマンスが重要な箇所で、パフォーマンス改善のために用いられてきた。
上記は過去の話であり、現在では次のような理由から、パフォーマンス改善のためにネイティブメソッドを使用する必要はない。
- 過去と比べて、JVMは高速になっており、ネイティブメソッドに引けを取らないパフォーマンスを得ることが可能である
- ネイティブメソッドにはメモリ破壊エラーの危険性がある
- ネイティブ言語はプラットフォーム依存であり、移植性が非常に低い
ネイティブメソッドは、低レベルのリソースへのアクセスや古いライブラリーへのアクセスなど、 どうしても必要な場合でのみ利用されるべきである。
項目55. 注意して最適化する
速いプログラムよりも良いプログラムを書くように努めること。 良いプログラムを書けばスピードは結果として得ることができる。具体的には次のような点について、よく考えること。
パフォーマンスを制限するような設計を避ける
APIの場合、以下のような点を考慮すること
上記に気をつけて実装を全て終え、それでもパフォーマンスが不十分であるときに、最適化を検討すべきである
- 最適化の前後では、プロファイラを用いるなどしてパフォーマンスを測定する
- 必要であればアルゴリズムの変更を検討する(低レベルな最適化をしても目標とするパフォーマンスに達しないことがしばしばあるため)
項目55. 一般的に受け入れられている命名規約を守る
以下の命名規約を守ること。(他人が混乱しないような命名をする)
活字的命名規約
パッケージ名は
- ピリオドで区切られた階層的構造をしている
- 区切られた要素は英小文字と数字から構成される
- 要素は一般的に8文字以下
- 組織外で使用されるパッケージ名は、その組織のインターネットドメイン名で始まるべき
クラス名、インタフェース名は(
enum
やアノテーション含む)- 1つ以上の単語で構成され、最初の文字は大文字である
- 一般的でない省略形は避ける
メソッド名とフィールド名は
- 1つ以上の単語で構成され、最初の文字は小文字である
- 一般的でない省略形は避ける
- 定数フィールドは全て大文字の1つ以上の単語から構成され、区切り文字は「_」である
- ローカル変数であれば省略形は許可される
- 型パラメータは通常1文字である
文法的命名規約
活字的命名規約に比べて、曖昧かつ柔軟である。
- クラスは、一般に単数名詞あるいは名詞句で命名される
- インタフェースは able や ible で終わる形容詞で命名される
- アノテーションは特に品詞に関する制限はない
boolean
でない機能や属性を返すメソッドは、名詞、名詞句、get で始まる動詞句で命名されることが多い- 属性を設定するメソッドは setAttribute と名付けるべき(⇔ getAttribute)
- オブジェクトの型を変換し、別の型の無関係なオブジェクトを返すメソッドは、大抵 toType と呼ばれる
- レシーバーオブジェクトの型と異なる型を持つビューを返すメソッドは、大抵 asType と呼ばれる
- 呼び出されたオブジェクトと同じ値を持つ基本データを返すメソッドは、大抵 typeValue と呼ばれる
static
ファクトリーに対応する名前は、valueOf、of、getInstance、newInstance、getType、newType など- フィールド名に関してはあまり確立された規約はないが、
boolean
型の場合、アクセッサ-メソッド名から is を除いた名前であることが多い