学習のはじめに
Effective Javaを使用したコーディング学習会が社内で実施されている。Effective Javaに記載されている内容についての課題が作成され解いていき、解説を受ける。学習会を通してコーディング能力が向上している実感があるが、まだまだわからないことも多い。読んでも理解できたと言えない項目もあるが、少しずつでも理解できるようインデックスを張るイメージでまとめる。
また実際にてを動かすことを意識して、学習会の課題、本のサンプルコード、検索したサンプルコードを書いてみる。
https://github.com/kz-oita/effectivejava_sample/tree/master/src
(随時更新中)
[項目1]コンストラクタの代わりにstaticファクトリメソッドを検討する
クラスのインスタンスを提供するにはコンストラクタを使用するのが一般的と考えられているかが、その他にstaticファクトリメソッドというものがある。
それぞれにメリットデメリットがあるため、場合によってどちらを使うかを検討する必要があり、無意識にコンストラクタを使用することは避けるべきである。
staticファクトリメソッドとは
クラスのインスタンスを返す単なるstaticメソッド
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
staticファクトリメソッドの長所
1.名前を持つことができる
例えば確率的素数(probable prime) のBigIntegerを返す場合
コンストラクタ
BigInteger(int, int, Random)
staticファクトリメソッド
BigInteger.probablePrime(int, Random)
staticファクトリメソッドの方が、ぱっと見で何をするのかわかりやすい。
コンストラクタではクラス名と同じ名前に限定される。
引数違いによってコンストラクタを区別するためわかりずらい。
2.オブジェクトを再利用可能
何度staticファクトリメソッド呼び出しても同じオブジェクトを返すことができるので、インスタンスを制御することができる。
インスタンスを制御できることでクラスをシングルトンにできる、インスタンス化不可能にできるなどのメリットもある。
(詳しくは項目3,4)
3.返す型を柔軟に選べる
4.引数によって、返すオブジェクトの型を自由に決められる
enumが64個以下で構成されている場合は、RegularEnumSetInstanceを返し、64個を超える場合は、JumboEnumSetを返す。
パフォーマンスを考えてこのような実装にしているが、利用者は内部の実装を意識することなく利用できる。
5.返されるオブジェクトのクラスは実行時に決まってもよい
staticファクトリメソッドの短所
1.サブクラスを作れない
staticなメソッドのみを提供する場合、サブクラスを作ることができない。
2.staticファクトリメソッドであるかがわかりづらい
コンストラクタと比べて、JavaDocなどのドキュメンテーションが目立たないため、インスタンス化するを利用者が知ることが困難となる。
一般的な命名規則に沿って名前をつけるよう心掛ける。
[項目2] 多くのコンストラクタパラメータに直面したときはビルダーを検討する
項目1でのstaticファクトリメソッドとコンストラクタはどちらも、多くのパラメータを持つ場合上手く対応することができない課題がある。
このような場合Builderパターンを使用するのはいい選択である。
利用者がインスタンスを生成する時、Builderパターンはテレスコーピング・コンストラクタ・パターンより読みやすく、JavaBeanパターンよりも安全である。
テレスコーピング・コンストラクタ・パターン
メリット
- 不変オブジェクトにできる
デメリット
- 必須でないパラメータに対しても、利用者はパラメータを渡す必要がある
- 利用者はパラメータの順番を気にする必要があり、読みづらい
- 型があっていればエラーにならないため、不具合に気付かない
JavaBeanパターン
メリット
- インスタンス化するクラスのフィールドを初期化することで、必須のパラメータのみ利用者に強制することができる
- 読みやすい。パラメータの値を把握しやすい
デメリット
- インスタンス化した後に、パラメータ間で不整合が起こる可能性があり、その場合デバッグが困難
- setterがメソッドがあるため不変オブジェクトにできない
Builderパターン
上記の2パターンのデメリットを克服できる
メリット
- 読みやすい。パラメータの値を把握しやすい
- 不変オブジェクトにできる
- インスタンス化する前に、パラメータの不整合を検知できるので、デバッグが簡単
[項目3]privateのコンストラクタかenum型でシングルトンを特性を強制する
シングルトンは一度しか院セタンスを作成しないクラスである。
シングルトンを実装する方法は3種類ある。
//public staticのフィールド
public class Elvis{
public static final Elvis INSTANCE = new Elvis();
private Elvis(){}
}
//staticファクトリ
public class Elvis{
private static final Elvis INSTANCE = new Elvis();
private Elvis(){}
public static Elvis getInstance(){
return INSTANCE;
}
}
//enum
public enum Elvis {
INSTANCE;
public void someMethod();
}
public staticフィールド、staticファクトリの手法ではシングルトンでなくなリスクがあるため、回避する手間が必要となる。
enum型はその手間が必要なく、簡潔で分かりやすため、シングルトンを実装する場合はたいていが最善の方法となる。
#[項目4]privateのコンストラクタでインスタンス化不可能を強制する
staticのメソッドとstaticのフィールド構成されるクラス(ユーティリティクラス)はインスタンス化されるように設定されていない。
明示的なコンストラクタが存在しない場合、デフォルトコンストラクタを提供する。
ユーザーからはこのコンストラクタは区別ができず、本来の意図に反してインスタンス化してしまうこと考えられる。
このようなことがないようにprivateなコンストラクタを実装することで、外部からインスタンス化を防ぐ。
public class UtilityClass {
private UtilityClass() {
throw new AssertionError();//なくてもよいが、保険
}
}
#[項目5]資源を直接結び付けるよりも依存性注入を選ぶ
多くのクラスが仮想の資源に依存している。例えばスペルチェッカーは辞書に依存している。
以下2つの実装は、辞書が1つかないと仮定している。様々な種類の辞書についてテストできない。
下層の資源でパラメータ化rされた振る舞いを持つクラスには不適切である。
//静的なユーティリティの不適切な使用
public class SpellChecker{
private static final Lexicon dictionary = ...;
private SpellChecker() {} //インスタンス化できない(項目4)
public static boolean isValid(String word) { ... }
}
//シングルトンの不適切な使用
public class SpellChecker{
private final Lexicon dictionary = ...;
private SpellChecker() {}
public static SpellChecker INSTANCE = new SpellChecker(...);
public static boolean isValid(String word) { ... }
}
必要なことは複数のインスタンスをサポートしてそれぞれのインスタンスで、資源を使えること。
そのため新しいインスタンスを生成するときにコンストラクタに資源を渡す。これを依存性注入と言う。
依存性注入は柔軟性とテスト可能性を大幅に向上させる。
public class SpellChecker{
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public static boolean isValid(String word) { ... }
}
#[項目6]不必要なオブジェクトの生成を避ける
機能的に同じオブジェクトを必要とする場合、そのたびに新しいオブジェクトを生成するより、再利用することが望ましい。
オブジェクトが**不変(immutable)**である場合は常に再利用することができる。
//sample1
//NG
String s = new String("bikini");
//OK
String s = "bikini";
//sample2
//NG
new Boolean(String);
//OK
Boolean.valueOf(String);
//sample3
//NG
//isRomanNumeral()が呼び出されるたび、matchesの内部でPatternオブジェクトが生成される。
static boolean isRomanNumeral(String s){
return s.matches("hogehoge");
}
//OK
private static final Pattern ROMAN = Pattern.compile("hogehoge");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
//sample4
//NG
//sumに足されるたびにオートボクシングされる
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
// Longはイミュータブルなので、加算の度に新たなインスタンスが生成されてしまう。
sum += i;
return sum;
}
//OK
private static long sum() {
// プリミティブ型に変更(Long -> long)
long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
#[項目7]使われなくなったオブジェクト参照を取り除く
Javaではガベージコレククション機能により、オブジェクトを使い終えた時、占有していたメモリ領域を自動的に開放し、空き領域として再利用することができる。
ただメモリ管理を考える必要がないというわけではない。
スタックの実装を考えると、Pushをすれば配列に要素が入り、Popをすれば配列から最後に入れた要素が取得する動作となる。
Popする際は参照する番号をずらしているだけなので、オブジェクトの参照自体は残り続ける。参照が残っているためガベージコレクタは削除対象か判断できない。
メモリ不足を発生させないように、Popする際にnullを入れて参照をはずす処理が必要となる。
#[項目9]try-finallyよりもtry-with-resourcesを選ぶ
try-finally
句でcloseメソッド処理を実装すると、以下の問題が考えられる。
- クローズ忘れ
- ネストが深くなり読みづらい
- 例外を握りつぶしやすい
try-with-resources
を使うとこの問題が解消されるので、選択するようにする。
//try-finally
static void copy(String src, String dest) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dest);
try {
byte[] buf = new byte[100];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
//try-with-resources
static void copy(String src, String dest) throws IOException {
try (InputStream inputStream = new FileInputStream(src); OutputStream outputStream = new FileOutputStream(dest)) {
byte[] buf = new byte[100];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
}
}
#[項目10]equalsをオーバーライドするときは一般契約に従う
equalsをオーバーライドすることは簡単であるが、間違ったやり方で実装してしまうこともある。実装不備を起こさない最も簡単な方法はオーバーライドしないことである。
以下の条件に該当する場合はオーバーライドしないほうがいい。
- クラスの個々のインタタンスが、本質的に一意である
- 論理的等価性を検証する必要がない
- スーパークラスが既にequalsをオーバーライドしており、スーパークラスの振る舞いがこのクラスに対して適切である
- クラスがprivateあるいはパッケージプライベートであり、そのequalsメソッドが決して呼び出されないことが確かである
上記に該当しない場合にオーバーライドすることが適切である。これは**値クラス(value class)**である。
もしequalsをオーバーライドする場合は以下の一般契約を守る必要がある。
- 反射的(reflexive):nullでない任意の参照値xに対して、x.equals(x)はtrueを返さなければならない
- 対照的(symmetric):nullでない任意の参照値xとyに対して、y.equals(x)がtrueを返す場合にのみ、x.equals(y)はtrueを返さなければならない
- 推移的(transitive):nullでない任意の参照値x,y,zに対して、もしx.equals(y)とy.equals(z)がtrueを返すならば、x.equals(z)はtrueを返さなければならない
- 整合的(consistent):nullでない任意の参照値xとyに対して、オブジェクトに対するequals比較に使用される情報が変更されなければ、x.equals(y)の複数回呼び出しは、終始一貫してtrueを返すか、終始一貫してfalseを返さなけらばならない
一般契約についてはサンプルコードが書籍には載っているがここでは割愛する。実装する際はIDEの機能で自動作成できるので、リファクタリング、実装に手を加える時は一般契約を意識する
#[項目11]equalsをオーバーライドするときは、常にhashCodeをオーバーライドする
equalsをオーバーライドするクラスでは必ずhashCodeをオーバーライドする必要がある。
そうしなければObjects.hashCodeの一般契約を破ることになってしまう。
一般契約とは・・・
- hashCodeメソッドが複数回は呼ばれた場合に同じ値を返す
- 2つのオブジェクトがequalsメソッドで等しい場合、2つのオブジェクトのhashCodeメソッドは同じ値を返す
- 2つのオブジェクトがequalsメソッドで等しくない場合、2つのオブジェクトのhashCodeメソッドは別の値を返す必要はない
オーバーライドをしないと上記の2番目の項目を破ってしまう。
hashCodeを実装
- IDEの機能で自動作成
- ライブラリを使う(Lombok)
#[項目12]toStringを常にオーバーライドする
toStringをオーバーライドすることで、クラスの利用者がデバッグしやすくなる。
返却される文字列は簡潔だが、人が読みやすくなっている説明的な表現であることが望ましい。
オブジェクトに含まれる情報(フィールド)をすべて表示させる。
equalsやhashCodeと同様にIDEの機能で保管してくれる。
equalsやhashCodeほど必須とはいえないが、インスタンス化可能なクラスでは、スーパークラスがオーバーライドしていないなら、実装することが望ましい
public class Student {
int studentNo;
String name;
int age;
public Student(int studentNo, String name, int age) {
this.studentNo = studentNo;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return “Student [studentNo=” + studentNo + “, name=” + name + “, age=” + age + “]”;
}
}
public class App {
public static void main(String[] args) {
Student student = new Student(1, “tanaka taro”, 12);
System.out.println(student);
//StudentクラスでtoStringをオーバーライドしない場合
// → item12.Student@515f550a
//StudentクラスでtoStringをオーバーライドする場合
// → Student [studentNo=1, name=tanaka taro, age=12]
}
}
#[項目15]クラスやメンバーのアクセス可能性を最小限にする
うまく実装されているコンポーネントは、その実装のすべてを隠蔽し、実装とAPIを分離している。コンポーネントはAPIを通してのみ、他のAPIと通信し、お互いに内部の動作は知らない。この概念を情報隠蔽、カプセル化という。
情報隠蔽、カプセル化をすることにより、コンポーネントを個別に開発可能で、他のコンポーネントへのリスクを考えずに実装することができる。
情報隠蔽、カプセル化のメリット
- 各コンポーネントを並列して開発できるようにして、開発のスピードを速める
- 影響調査の手間を減らして、保守の負荷をさげる
- ボトルネックを局所的に解消できるようにして、パフォーマンスチューニングがしやすい
アクセス修飾子を使用して、クラス、インターフェース、メンバーのアクセス可能性を制御する
アクセス修飾子の種類
種類 | 概要 |
---|---|
private | メンバーは宣言されたクラス内のみアクセス可能 |
パーケージプライベート | メンバーは宣言された、パッケージ内からどこからでもアクセス可能 |
protected | メンバーはそのクラストサブクラスからアクセス可能 |
public | メンバーはどこからでもアクセス可能 |
常にプログラム要素のアクセス可能性を最小限にするべきであるが、これにあてはまらない場合もある
- メソッドがスーパークラスのメソッドはオーバーライドする場合、スーパークラスのメソッドが持っているより低いアクセス修飾子を持つことはできない。サブクラスをコンパイルする場合エラーとなる
- テスト容易性のため緩和する。publicクラスのpraveteメソッドパッケージプライベートにするのはOK。
#[項目16]publicのクラスでは、publicのフィールドではなく、アクセッサーメソッドを使う
クラスのフィールドは基本的にpublicとせずに、privateとする。
publicだと直接アクセスできるでのカプセル化の恩恵を提供できない。外部から値を変更される危険性もあり不変式の強制ができない。
解決策としてはフィールドをprivateとし、publicの**アクセッサーメソッド(getter),ミューテーターメソッド(setter)**を作成する。
ただし、不変オブジェクトとする場合はgetterのみ作成する。
getter,setterはIDEの機能で自動作成でき、getter,setterどちらかのみを作成することも選択できる。
#[項目18]継承よりもコンポジションを選ぶ
継承はコードを再利用するには便利な機能ですが、常に既存のメソッドを継承し、拡張することがベストな方法とは限らない。
サブクラスとスーパークラスが同じ開発者の管理下にあり、同じパッケージ内にある場合は比較的安全である。
しかしパッケージをまたがり具象クラスを継承すると、意図しない動作を起こす可能性もある。
書籍では不適切な継承の例として、HashSetを継承したInstrumentedHashSetクラスでadd,addAllをオーバーライドしている。
InstrumentedHashSetに要素を追加すると、要素の追加された回数がカウントアップされるが、addAllを呼び出すと、意図する結果の2倍の数が返される。これはaddAllの中でaddが呼ばれるため意図しない結果となってしまう。
コンポジション
このような事象を解決する手段にコンポジションと呼ばれるものがある。
既存クラスを拡張するのではなく、新たなクラスに既存のクラスのインスタンスをprivateフィールドで持たせ、既存クラスが新たなクラスの構成要素となる
新たなクラスのメソッドは既存クラスのインスタンスに対して、メソッドを読み出しそのまま結果を返します。これを**転送(fowarding)**と呼ぶ。
コンポジションを利用すれば、新たなクラスは既存のクラスの実装の詳細に依存することがなくなり、意図する結果が得られる。
では、継承とコンポジションをどのようにつか分けるべきか。
継承はサブクラスがスーパークラスのサブタイプである場合に拡張するべきです。
サブクラスとスパークラスとの間に**「is-a」**の関係が存在しているか考え、満たす場合拡張する。
#[項目19]継承のために設計および文章化する、でなければ継承を禁止する
スーパークラスを作成する際はJavadocを書いてドキュメント化しましょうという話。
ただし継承を前提にクラスを設計するのは大変である。クラスの自己利用を全て文書化する必要がある。そして一度文書化したらクラスはその内容を守り続けなければならない。
サブクラスの必要性がないならば、クラスをfinalにするコンストラクタを提供しないなどして継承を禁止するのが良い。
#[項目20]抽象クラスよりもインタフェースを選ぶ
#[項目21]将来のためにインタフェースを設計する
インターフェースの設計は慎重に行いましょうという話。変更するにはリスクが伴う。
Java8以降では、インターフェースに新たなメソッドを追加するが可能となりり、デフォルトメソッドを使うことで実装クラスが追加したインターフェースのメソッドを実装しなくてもコンパイルエラーとならなくなったが、実行時にエラーとなる可能性もある。
インターフェースを公開する前にしっかり検証を行うことが大切である。
#[項目22]型を定義するためだけにインタフェースを使う
定数を定義するためだけのインターフェースは不適切である。定数だけを定義するのであれば使用するクラスで宣言する、enumやユーティリティクラスを提供するべきである。
#[項目23]タグ付きクラスよりもクラス階層を選ぶ
インスタンスが2つ以上の特性を持っていて、その特性を示すためのタグフィールドを持つクラスがある。
特性が複数あるならばクラスを分けたほうがわかりやすい。継承、サブタイプ化をして階層構造で実装する。
#[項目24]非staticのメンバークラスよりもstaticのメンバークラスを選ぶ
クラス内でクラスを宣言する、ネストしたクラスの実装方法には4種類ある。
- staticのメンバークラス
- 非staticのメンバークラス
- 無名クラス
- ローカルクラス
4つのクラスのイメージの参考
https://zenn.dev/empenguin/articles/7f6f5a52fd2184
書籍には4つのクラスの使い分けが記載されている。使用する場面によって使い分けるとよい。
#[項目26]原型を使わない
リストを使う際は**List
ではなくList<String>
**のように型パラメータを書く。
Listのように型パラメータがないものを原型と言う。原型は実行時に例外がスローされるため使わないほうがよい。
ジェネリックスが導入されて以降は、既存コードと互換性のために原型は提供されている。
#[項目27]無検査警告を取り除く
ジェネリックスを使って実装すると、コンパイル時の警告が出る時がある。警告では無視してもコンパイル可能であるができるだけ警告が出ないように修正する。
理由があって警告が出ていても問題ない場合は**@SuppressWarning("unchecked")
**をつけて、警告を抑制する。
また警告を抑制すると決めた理由をコメントに残す。
#[項目28]配列よりもリストを選ぶ
配列とリストは異なる性質が2点ある。
1点目は配列は共変、リストは不変であるということ。
//実行時に失敗する
Object[] objectArray = new Long[1];
objectArray = "I don't fit it";
//コンパイルされない
List<Object> ol = new ArrayList<Long>();
ol.add("I don't fit it");
2点目は配列は具象化されていることである。配列は実行時にその要素型を知っていて、強制する。
一方ジェネリックスはイレイジャという方式で実装され、コンパイル時にのみ型を強制することを意味する。イレイジャの機能により、ジェネリックスを使っていない過去のコードとジェネリックスを使うコードが円滑に移行できる。
一般的に配列とジェネリックスは調和できないので、可能であれば配列をリストに置換して使用するのが良い。
#[項目30]ジェネリックメソッドを使う
ジェネリックス型と同様にジェネリックスメソッドは、入力パラメータと戻り値をキャストすることをクライアントに要求するメソッドより安全で使いやすい。
型と同様にキャストを必要とする既存のメソッドをジェネリック化するべきである。
既存の動作を確保しながら、ユーザーを楽にできる。
//非ジェネリックメソッド
public static Set union(Set s1, Set s2) {
Set reslut = new HashSet(s1);
result.addAll(s2);
return result;
}
//ジェネリックメソッド
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> reslut = new HashSet<>(s1);
result.addAll(s2);
return result;
}
また応用的な実装としてジェネリック・シングルトン・ファクトリ、再帰型境界などもある。
#[項目31]APIの柔軟性向上のために境界ワイルドカードを使う
APIの開発時は利用者が便利に使えるように柔軟性を持たせましょう。というような内容で、そのために境界ワイルドカードの使用を進めている。
境界ワイルドカードとは
利用者がAPIにオブジェクトを提供する場合、より具体的なオブジェクトを渡すことが可能
パラメータがproducer(生産者)の場合は、 extends E>
public <E> void pushAll(Iterable<? extends E> src) {
...
}
APIからオブジェクトを受け取る側の場合、より抽象的な型のオブジェクトとして受け取れる
パラメータがconsumer(消費者)の場合は、 super E>
public <E> void popAll(Collection<? super E> dst) {
...
}
#[項目34]int定数の代わりにenumを使う
一年の四季、太陽系の惑星など固定数の定数を表現するときはeenumを使う。
int enumパターン、String enumパターンには多くの欠点がある。
enumの特徴
- public static finalフィールドを通して、各列挙定数に対して1つのインスタンスを提供するクラス
- アクセス可能なコンストラクタを持たず、事実上final
- 利用者はenum型のインスタンスを生成できないし、拡張できない
- 基本的に単一要素のシングルトンである
- 異なるenum型であれば、同じ名前を定義できる
- to Stringメソッドでenumを文字列で表現できる
- 任意のメソッドやフィールドを追加でき、柔軟である
#[項目35]序数の代わりにインスタンスフィールドを使う
java.lang.Enumにordinal()というメソッドがある。定数がenum型の何番目に宣言されているかを返すメソッドである。
基本的にこのように何番目かの値を取得する際はordinal()メソッドを使うより、インスタンスフィールドに値を持たせるべきである。
#[項目36]ビットフィールドの代わりにEnumSetを使う
enum型が登場する前は要素が集合で使われる場合、ビットフィールドという手法で表現されていた。
現在はこの定数要素の組み合わせはEnumSetで表現できるのでこれを使うのが良い。
項目34にあげるenum の恩恵を受けることができる
#[項目37]序数インデックスの代わりにEnumMapを使う
配列をインデックスするために序数を使用することはほとんどの場合で適切ではない。そのような場合はEnumMapを使用する。
項目35でjava.lang.Enumにordinal()を使用するのは特別な場合なので、基本的には使用しない。
#[項目38]拡張可能なenumをインタフェースで模倣する
クライアントは実装されているenum実装に不足を感じ機能を追加したくなるかもしれない。
そのような場合のために公開するenumにインターフェースを実装することで、クライアントはより柔軟に使用することができる。
#[項目39]命名パターンよりもアノテーションを選ぶ
命名パターンとはクラスやメソッドの名前をつける際に規則に従って名前をつけること。例えばJunitではtestを名前の最初にしてテストメソッドを作成するなど。昔はこのような手法が取られていたが、明らかに脆弱である。メソッド名も間違った場合コンパイルエラーも実行エラーも出ないので、成功したように感じるかもしれない。
このような問題に対処するため**@(アノテーション)**を使用して、目印とする。
#[項目40]常にOverrideアノテーションを使う
スーパータイプのメソッドをオーバーライドする場合は**@Override**アノテーションをつける。もし実装に問題がある場合にコンパイラが指摘してくれる。
抽象メソッドをオーバーライドする場合はアノテーションをつける必要はない。(つけても問題はない)
#[項目42]無名クラスよりもラムダを選ぶ
歴史的に関数オブジェクトを表現するには無名クラスが使われてきたが、Java8以降ではラムダ式が導入された。
メリット
- 簡素に書くことができる
- 型推論するため、型を省略できる
デメリット
- 名前とドキュメントがないため、複雑な処理の場合わかりずらい。
- 抽象クラスを実装できない
#[項目43]ラムダよりもメソッド参照を選ぶ
無名クラスよりもラムダの方が簡潔ということを項目42で学習したが、さらに簡潔な方法としてメソッド参照というものがある。
メソッド参照
ラムダと同様にJava8から登場したもので、すでに定義済みのメソッドを引数なしで呼び出せる。
クラス名::メソッド名
メソッドの種類 | 例 | 同等のラムダ |
---|---|---|
static | Integer::parseInt | str -> Integer.parseInt(str) |
バウンド | Instant.now()::isAfter | Instant then = Instant.now();t -> then.isAfter(t) |
アンバウンド | String::toLowerCase | str -> str.toLowerCase() |
クラスコンストラクタ | TreeMap::new | () -> new TreeMap() |
配属コンストラクタ | int[]::new | len -> new int[len] |
上記のようにメソッド参照のほうがラムダよりも簡潔に記述できる。メソッド参照の方が簡潔な場合はメソッド参照を使用し、そうでない場合はラムダを使う。
#[項目44]標準の関数型インタフェースを使う
java.util.function
パッケージには標準の関数型のインタフェースを43個提供している。
代表的な6個の基本的なインタフェースは以下の通り。
インタフェース | シグニチャ | 例 |
---|---|---|
UnaryOperator | T apply(T t) | String::toLowerCase |
BinaryOperator | T apply(T t1, T t2) | BigInteger::add |
Predicate | boolean test(T t) | Collection::isEmpty |
Function | R apply(T t) | Arrays::asList |
Supplier | T get() | Instant::now |
Consumer | void accept(T t) | System.out::println |
#[項目45]ストリームを注意して使う
ストリームは一般的にコードを簡潔に記述でき便利であるが、乱用には注意が必要である。
ストリームの記述が長くなりすぎると、可読性がおち理解する難易度があがってしまう。場合によってはストリームではなくループで記述した方が分かりやすい場合もある。
コードを簡潔に記述するという目的を忘れずに、どう記載するかは検討が必要である。
#[項目49]パラメータの正当性を検査する
メソッドやコンストラクタを書く際は、パラメータの値が適切であるか冒頭で検査する。またパラメータにどんな制約があるかドキュメント化する。
Objects.requireNonNullがnull検査に便利なメソッドとしてJava7で追加されている。
#[項目50]必要な場合、防御的にコピーする
クラスを作成する際は、利用者が悪意がなくても意図しない利用をしてしまうことを想定して設計するべきである。具体的には不変性を担保するよう努力する。
- 不変クラスを使用する。Java8以降の場合、java.util.Dateクラスではなく、 **java.time.Instant(あるいはLocalDateTimeかZonedDaterime)**を使用するなど
- 値を外部から受け取る場合は、パラメータの値をコピーして受け取る
- 値を外部へ返却する場合は、値をコピーして返却する
- コピーのコストが高い、かつクライアントを信用できる場合はコピーせずにドキュメン化する
#[項目51]メソッドのシグニチャを注意深く設計する
クラス設計時のヒントを集めた項目。メソッド名やパラメータについて書いていて、クラスを作成する際に参考にしたい。
メソッド
- わかりやすい名前
- 同じパッケージ内の他の名前と矛盾しない名前
- できるだけ短い名前
- 便利なメソッドを提供しすぎない。多すぎると利用側も保守側も大変になる
パラメータ
- 4個以下を目標とする。多すぎると利用者が覚えられない。型があっていれば、順番を間違っていてもコンパイルエラーがでない
- 複数のメソッドに分割する
- ヘルパークラスを作成し、パラメータを集める
- Builderパターンを使用する(項目2)
- パラメータ型はクラスよりもインターフェースを使う(項目64)
- booleanパラメータよりも二つの要素をもつenum型を使う
#[項目52]オーバーロードを注意して使う
同じ数のパラメータを持つ複数のシグニチャでメソッドをオーバーロードすることは控えるべきである。コンストラクタでは別の名前を作ることはできないので、staticファクトリメソッドを提供する選択肢がある(項目1)。
#[項目53]可変長引数を注意して使う
可変長の引数を持つメソッドにおいて可変長引数は効果的であるが、パフォーマンスが重要な状況では注意が必要である。
メソッドが呼び出される度に、毎回配列を初期化するため、そのコストを受け入れられない場合は、よく使われる引数の個数を調べて個数ごとのメソッドを作成する。例えば95%が引数3個以内の場合は0-3個の引数を持つメソッドと3個を超えた場合の可変長引数のメソッドを作成する。5%のみが配列を初期化することになる。
また必須パラメータがあるときは、可変長引数に必須パラメータを含めずに別で定義する。
#[項目54]nullではなく、空コレクションか空配列を返す
空のコレクションや空配列を返す代わりにnullを返すのは良くない。利用者がnullに対して対応する必要があるし、対応漏れしてしまうかもしれない。
もし、空のコレクションの生成がパフォーマンスに影響がある場合はCollections.emptyList()の使用も検討する。
#[項目55]オプショナルを注意して返す
Java8より前では値を返さないメソッドを書く際は、例外をスローするかnullを返す、2種類の手法が取られていた。Java8以降はOptionalを返すことが可能となった。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/Optional.html
基本的にOptionalは戻り値を返す用途で使用する。
#[項目57]ローカル変数のスコープを最小限にする
内容としては項目15と同じようなことを言っている。C言語ではローカル変数をブロックの先頭で宣言する必要があったが、Javaではどこでも宣言できるので、初めて使われる前に宣言するとスコープを最小限とすることができる。
またループではループ変数を宣言でき、ループ内の必要な領域に限定できる。ループ終了後にループ変数を使用しない場合、whileループよりもforループを使う方が良い。
//拡張for文
for(Element e : c){
//eを使った処理
}
//イテレーターが必要な場合
for(Iterator<Element> i = c.iterator();i.hasNext();){
Element e = i.next();
//eとiを使った処理
}
//whileループ
//コピーした際にループ変数を修正し忘れる可能性がある
Iterator<Element> i = c.iterator();
while(i.hasNext()){
doSomething(i.next());
}
#[項目58]従来のforループよりもfor-eachループを選ぶ
**for-eachループ(拡張for文)**は基本的な場面でforループよりもわかりやすく、柔軟であるため、できるだけfor-eachループを使う。
ただし、3つの場面で使えないので従来のfor文を使う。
- 選択された要素を取り除きながらコレクションを走査する場合
- リストや配列を走査して要素の一部あるいは全部を置換する場合
- 複数のコレクションを並列に走査する場合
#[項目59]ライブラリを知り、ライブラリを使う
**「車輪の再発明はしない」**ということを念頭に何かを実装するときはそれが一般的な機能ならば、ライブラリにすでに実装がないかを確認する。
優秀な開発者が必要に応じてメンテナンスし、追加しているので、自分で開発するよりも柔軟で危険性がない。
java.lang, java.util, java.io
およびそのサブパッケージの機能は把握しておくのが良い。
#[項目60]正確な答えが必要ならば、floatとdoubleを避ける
正確な答えが必要な場合、例えば金銭計算にはfloatとdouble型は適していない。逆に科学計算や工学計算には適している。
このよう使用目的や取る値によって型を検討する必要がある。
金銭計算にはBigDecimalが適している。小数を丸める制御をすることができる。しかし遅いう短所もある。
パフォーマンスを考慮する必要があり、小数を把握することが気にならなければ、intやlongを使う。その場合はintなら9桁まで、longなら18桁まで、それ以上ならBigDecimalを使用する。
#[項目61]ボクシングされた基本データよりも基本データ型を選ぶ
Javaには基本データ型(int,double,boolean)と参照型(String,List)と言った2つの型がある。全ての基本データ型には対応する参照型をもっている(Integer,Double,Boolean)。
基本データ型 → 参照型 ボクシング
参照型 → 基本データ型 アンボクシング
ボクシングされた参照型は「==」(同一性比較)で比較すると意図する結果と違うなどの不具合の原因となるので、不必要に基本データをボクシングはしない方が良い。逆に参照型をアンボクシングするとNullPointerExceotion可能性が発生する。
また不必要にボクシングしたことによるパフォーマンス悪化の可能性もある、詳しくは[項目6]
#[項目62]他の型が適切な場所では、文字列を避ける
データとしてファイルや入力から値を受け取るとき、大抵は文字列の形式であるが、何も考えずにデータの型をStringにするのは避ける。
数値の場合はint,float,BigIntegerなどが適切で、「はい/いいえ」の場合はbooleanやEnumが適切である。
型を誤って定義すると扱いづらく柔軟性が乏しくなる。
#[項目62]文字列結合のパフォーマンスに用心する
文字列を何度も結合する場合は「+」で結合せずに、StringBuilderを使う方がパフォーマンスがいい。
#[項目63]インタフェースでオブジェクトを参照する
オブジェクトを参照するにはクラスよりもインターフェースを使うべきである。実装の修正をするときにインターフェースを使用していれば柔軟に対応できる
//OK
Set<Son> sonSet = new LinkedHashset<>();
//BAD
LinkedHashset<Son> sonSet = new LinkedHashset<>();
//インターフェースの場合変更できる
Set<Son> sonSet = new Hashset<>();
#[項目69]例外的状態にだけ例外を使う
言葉の通りであるが、例外は例外的状態で使われるべきである。通常の制御に対して例外を使わない。
以下の例は他の実装方法があるにもかかわらず、例外を使用しているかなりBadパターン。
//NG
int result = 0;
try {
int i = 0;
while (true) {
result += digets[i++];
}
} catch (ArrayIndexOutOfBoundsException ignored) {
}
return result;
//OK
int result = 0;
for (int current : digets) {
result += current;
}
return result;
#[項目70]回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使う
Javaは3種類の例外を提供している。
検査例外(チェックされる例外)
ログラマーが必ずtry/catchしなければならない例外
- FileNotFoundException
- IOException
- SQLException
など
非検査例外(実行時例外)
例外が発生する可能性はあるが、必ずしも例外処理を行う必要がない
- RuntimeException
- ArrayIndexOutOfBoundsException
- ClassCastException
- IllegalArgumentException
- NullPointerException
など
エラー
Error系例外は回復不可能な重大なエラー
- OutOfMemoryError
- NoClassDefFoundError
#[項目71]チェックされる例外を不必要に使うのを避ける
検査例外はプログラムの信頼性が向上するが、過剰に使用すると利用者にとって使いずらいものになる。呼び出しもとが失敗から回復できないのであれば非検査例外をスルーする。
回復可能である場合も検査例外には以下のデメリットもあることを念頭におく。
- try-catchを書かないといけない
- ストリーム内で使えない
上記より呼び出し元に例外処理を強制させたい場合、オプショナルを返すことを検討する。
#[項目73]抽象概念に適した例外をスローする
メソッド内の処理と関係のない例外をスローすると利用者の混乱を招いてしまう。下位レイヤーの例外を上位レイヤーでスローする場合にこの状況が起こりやすい。
この問題を避けるためには、上位レイヤの中で上位レベルの抽象概念の説明可能な例外をスローするべきである。その際下位レイヤーで発生した例外をcouseとして渡すことができ、例外が起きたとき元の例外調査がしやすくなる。
そもそもできるだけ例外を発生させないように努力する。下位レイヤーを呼び出す際に事前条件が成立しているかを確認する。
#[項目76]エラーアトミック性に務める
例外によってメソッドの呼び出しに失敗したあとに、オブジェクトがメソッドの呼び出し前の状態にしておくことをエラーアトミック性と言う。
エラーアトミック性を達成するためには以下のような手段がある。
- 不変オブジェクトを設計する(最も簡単)
- パラメータの正当性をチェックする
- 失敗するかもしれない処理を、オブジェクト変更の前に実行する
- オブジェクトのコピーを生成し、コピーに対して処理が完了したら、コピーの内容を置き換える
- 例外が発生したら、処理前まで戻る回復コードを作成する
#[項目77]例外を無視しない
例外をキャッチしてもcatch句が空では意味がなく、エラーを無視することになる。適切に処理する。
稀に意図して無視する場合があり、そのときはコメントで意図していることを残す。