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. 正確な答えが必要ならばfloatdoubleを避ける

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の欠点(主にパフォーマンス)が許容できない場合、intlongを代わりに用いることも可能。 しかしこの場合、プログラマが小数点の位置を把握しなければならない。またintは9桁、long18桁の精度でしか計算できない。

以下は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ならIntegerdoubleならDoublebooleanなら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メソッドにおけるfirstsecondの比較に欠陥がある。

first < secondでは引数は自動アンボクシングされる。 しかしこの検査がfalseだった場合、first == secondではボクシングされた基本データの同一性比較が行われる。 この時、2つの値が同じ値を示している異なるインスタンスであった場合に、falsesecondfirstより小さい)と判定されてしまう。

ボクシングされた基本データに対して==演算子を利用するのは大抵まちがいである。 上記の問題は、以下のように比較の前に基本データ型として引数を取り出すことで解決が可能。

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. 他の方が適切な場所では、文字列を避ける

文字列はテキストを表現する以外の目的で利用すべきではない。 以下は、文字列を使用すべきでないケース。

  • 他の値型に対する代替としての利用。
    データをファイルやキーボード入力から取得した場合、大抵は文字列の形式で受け取るが、内容が数値ならばintfloat
    「はい」や「いいえ」ならばbooleanといった具合に、適切な値型へ変換すべき。
  • 列挙型(enum)の代替としての利用。
  • 集合型の代替としての利用。不要な文字列解析の手間が生じるので、その集合を表すクラスを用意する。
  • ケイパビリティ(偽造できないキー)としての利用。

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

n個の文字列を結合するのに、文字列結合演算子(+)を繰り返し利用すると、nに関して2次の時間を必要とするので注意すること。

代わりにStringBuilderappendメソッドを使用する。

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の場合、以下のような点を考慮すること

    • public の型を可変にすると不要な防御的コピーが必要となる可能性がある
    • コンポジションが適切な public のクラスで継承を選択した場合、スーパークラスによりパフォーマンスが制限される可能性がある
    • インタフェースでなく実装型を使用した場合、特定の遅い実装に拘束される恐れがある
  • 上記に気をつけて実装を全て終え、それでもパフォーマンスが不十分であるときに、最適化を検討すべきである

    • 最適化の前後では、プロファイラを用いるなどしてパフォーマンスを測定する
    • 必要であればアルゴリズムの変更を検討する(低レベルな最適化をしても目標とするパフォーマンスに達しないことがしばしばあるため)

項目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 を除いた名前であることが多い

Effective Javaを勉強します【第7章】

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

コンストラクタやメソッドの引数(パラメータ)に関して何らかの制約(参照がnullではいけないなど)がある場合は、 文書化し、メソッドの初めに検査をすることでその制約を強制すべきである。

上記を行わない場合、意図せぬ例外を引き起こしたり、オブジェクトが不正な状態のまま離れた場所でエラーを引き起こし、デバッグが困難になる恐れがある。

publicのメソッドの場合

スローされる例外を、Javadoc@throwsタグを使用して文書化する。

/**
 * 値が(this mod m)であるBigIntegerを返します。このメソッドは、
 * remainderメソッドとは異なり、常に負でないBigIntegerを返します。
 *
 * @param m 正でなければならないモジュラス
 * @return this mod
 * @throws ArithmeticException m <= 0の場合
 */
 public BigInteger mod(BigInteger m) {
     ...
 }

publicでないメソッドの場合

アサーションを用いてパラメータをチェックする。

private static void sort(long a[], int offset, int length) {
    assert a!= null;
    assert offset >= 0 && offset <= a.length;
    assert length >= 0 && length <= a.length - offset;
    ...
}

項目39. 必要な場合には、防御的にコピーする

クライアントからオブジェクトを受け取るメソッドやコンストラクタを書くときはいつでも、 クライアントが提供するオブジェクトが可変である可能性を考慮すべきである。

オブジェクトを受け取った後に変更があった場合に、クラスに問題が生じる可能性がある場合は、 受け取ったオブジェクトをそのまま利用するのではなく、防御的にコピーして利用しなければならない。

public class Period {
    private final Date start;
    private final Date end;

    /**
     * @param start 期間の開始。
     * @param end 期間の終わり。開始より前であってはならない。
     * @throws IllegalArgumentException start が end の後の場合。
     * @throws NullPointerException start か end が null の場合
     */
    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + " after " + end);
        }
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}

上記には2つの問題がある。

1つ目はコンストラクタで、startendをそのまま受け取っている点である。 Dateクラスは可変であるため、以下のように操作することでクラスの制約を破ることが可能。

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); //endの時間を操作可能

上記はコンストラクタを以下のように修正することで解決する。 コピーに対して正当性検査を行うことで、検査までの間にパラメータを不正に変更されることから保護している。

    public Period(Date start, Date end) {
        // 防御的コピー
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException(start + " after " + end);
        }
    }

2つ目の問題は、アクセッサーで内部のstartendをそのまま返却している点である。 受け取ったstartendは可変なので、以下のように操作することでクラスの制約を破ることが可能。

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); //endの時間を操作可能

この点については、以下のようにアクセッサーで防御的コピーを返すように修正することで解決する。

    public Date getStart() {
        return new Date(start.getTime());
    }
    public Date getEnd() {
        return new Date(end.getTime());
    }

一番良いのは、可能な限り不変オブジェクトを使い、不必要に可変オブジェクトを使わないこと。

項目40. メソッドのシグニチャを注意深く設計する

API設計時は以下の点を考慮すること。

  • メソッド名を注意深く選ぶ

    • 理解可能で、パッケージ内の他のメソッド名やjavaの標準ライブラリと矛盾が生じないようなメソッド名を選ぶ
  • 便利なメソッドを提供しすぎない

    • 多くのメソッドを定義したインタフェース/クラスは学習、使用、文書化、テスト、保守などを困難にする
    • 多くの役割を与えすぎないこと
  • 長いパラメータのリストは避ける

    • 4個以下が望ましい(それ以上は大抵覚えられない)
    • パラメータのリストを短くする方法は例えば以下
      • メソッドを分割する
      • パラメータの集まりを保持するヘルパークラスを作成する(staticなメンバークラスなど)
      • ビルダーパターンをオブジェクト生成からメソッド呼び出しに適用する
  • パラメータ型に関しては、クラスよりインタフェースを選ぶ

    • 例えば、HashMapの代わりにMapを使用すれば、HashTableHashMapTreeMapなどどれでも渡すことができるため、柔軟かつ拡張性も高い
  • booleanパラメータより2つの要素を持つenum型を使用する

    • booleanよりもパラメータの意味が理解しやすく、可読性が向上するため

項目41. オーバーロードを注意して使用する

オーバーロードしたメソッドの選択は静的(コンパイル時に決定)であるため、ユーザーを混乱させる可能性があるため。

オーバーロードの利用

以下の例を考える。

public class CollectionClassifier {
    public static String classify(Set<?> a) {
        return "Set";
    }
    public static String classify(List<?> a) {
        return "List";
    }
    public static String classify(Collection<?> a) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection <?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections) {
            System.out.println(Overload.classify(c));
        }
    }
}

上記のプログラムを実行した場合、"Set" → "List" → "Unknown Collection" と表示すると期待するかもしれないが、 実際は "Unknown Collection" が3回表示される。

これは、オーバーロードされたメソッドの選択はコンパイル時に行われるためである。 コンパイル時には、3回のループでclassifyメソッドに渡されるパラメータの型は全てCollection<?>である。

したがって同じ数のパラメータを持つ複数のシグニチャでメソッドをオーバーロードすることは、基本的に控えるべきである。 例えばObjectOutputStreamクラスは複数のwriteメソッドを持っているが、 writeBoolean(boolean)writeInt(int)writeLong(long)といったように異なったメソッド名を用いているため、 ユーザーは混乱しない。

またコンストラクタの場合、異なる名前を使用することができないため、staticファクトリーメソッドの利用を検討する。

オーバーライドの利用

オーバーライドしたメソッドの選択は動的に行われる。

class Wine {
    String name() { return "wine"; }
}

class SparklingWine extends Wine {
    @Override String name() { return "sparkling wine"; }
}

class Champagne extends SparklingWine {
    @Override String name() { return "champagne"; }
}

public class Overriding {
    public static void main (String[] args) {
        Wine[] wines = {
            new Wine(), new SparklingWine(), new Champagne()
        };
        for (Wine wine: wines) {
            System.out.println(wine.name());
        }
    }
}

上記を実行すると、 "wine" → "sparkling wine" → "sparkling wine" と表示される。 オーバーライドされたメソッドの呼び出しには、オブジェクトのコンパイル時の型は影響がない。

項目42. 可変長引数を注意して使用する

可変長引数メソッド

指定された型の0個以上の引数を受け付けるメソッドを、可変長引数メソッドと呼ぶ。 可変長引数の仕組みは、呼び出した時点で渡された引数の数と同じ大きさの配列を最初に生成し、 その配列に引数値を入れて、最後のその配列をメソッドに渡す。

例えば、複数のint変数の合計を返す可変長引数メソッドは以下。

static int sum(int ... args) {
    int sum = 0;
    for (int arg: args) {
        sum += arg;
    }
    return sum;
}

ある型の引数を0個以上ではなく1個以上必要とする場合は、第1引数を通常の引数とし、第2引数を可変長引数とするとよい。

static int min(int firstParam, int... params) {
    int min = firstParam;
    for (int param: params) {
        if (param < min) {
            min = param;
        }
    }
    return min;
}

可変長引数メソッドへの変更時の注意

可変長引数は最終的に配列としてメソッドに渡されるため、最後の引数が配列である既存のメソッドを可変長引数メソッドに 変更することは容易である。しかし、むやみに可変長引数メソッドに変更すべきではない。

例えば Arrays.asList メソッドは、1.4までは Object 型の配列を受け取って、List 型に変換していた。具体的には、配列の各要素を取り出し、List に格納し返却している。下記のコードでは、配列を List に変換することで、配列の各要素を確認することができている(配列に対して直接 toString を呼び出しても、ハッシュ値が表示されて役に立たない)。

System.out.println(Arrays.asList(myArray));

上記myArrayint 型の配列の場合、コンパイルエラーとなる。なぜなら java.lang.Object[]int[] の間に継承関係はないためである。

しかし、1.5から Arrays.asList メソッドは、Object 型の配列ではなく、Object 型の可変長引数を受け取るようになった。この時 myArrayint 型の配列の場合、Object 型に int[] は渡すことができるため、コンパイルエラーとはならないが、int[]myArray のみを要素とする List<int[]> が返却される。すると、要素は int[] であるため、ユーザーの意図する各要素の値ではなく配列のハッシュ値が表示されてしまう。

このように、可変長引数メソッドを乱用するとユーザーを困惑させることになりかねないため、注意が必要である。

項目43. null ではなく、空配列か空コレクションを返す

配列やコレクションを返すメソッドが、空配列や空コレクションの代わりに、null を返すべきではない。 null を返却すると、クライアントで null を処理する余分なコードを書かなければならないため。 もしチェックを忘れると NullPointerException が発生する恐れもある。

null を返す場合と比較して、空配列を返す方が配列を割り当てるコストがかかるため、好ましくないと言われることがあるが、 これは正しくない。なぜなら、このレベルでパフォーマンスが問題となることはほとんどない。

また、どうしても空配列を返却したい場合は、次のように同一のオブジェクトを使いまわせば良い。 下記イディオムはコレクションが空の場合、空配列を新規に生成しないため、空配列生成のコストは問題とならない。

private final List<Cheese> cheeses = ...;

private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheeses[] getCheeses() {
    return cheeses.toArray(EMPTY_CHEESE_ARRAY);
}

項目44. すべての公開API要素に対してドキュメントコメントを書く

Javadoc を利用することで、ソースコードから自動的にAPIドキュメンテーションを生成することができる。 APIを適切に文書化するために、全ての公開されているクラス、インタフェース、コンストラクタ、メソッド、フィールド宣言の前に ドキュメントコメント(Javadoc)を書くべきである。

Javadoc を書く際は以下の点に気をつける。

  • メソッドに関するドキュメントコメントには、メソッドがどのように処理を行なっているかではなく、何を行なっているかを記述すべき

    • 事前条件と事後条件をすべて列挙すること
    • すべての副作用を文書化すること
    • スレッド安全性について記述すること
    • 上記を記載するために @param タグ、@return タグ、@throws タグを記載すること
  • ジェネリック型やジェネリックメソッドを文書化する際には、すべての型パラメータを文書化するのを忘れない

/**
 * An object that maps keys to values. A map cannot contain
 * duplicate keys; each key can map to at most one value.
 *
 * @param <K> the type of keys maintained by this map
 * @param <V> the type of mapped values
 */
public interface Map<K, V> {
  ...
}
  • enum 型を文書化する際には、型とすべての public のメソッドだけでなく定数も文書化すること
/**
 * An instrument section of a symphony orchestra.
 */
public enum OrchestraSection {
    /** Woodwinds, such as flute, clarinet, and oboe. */
    WOODWIND,
    /** Brass instruments, such as french horn and trumpet. */
    BRASS,
    ...
}
  • アノテーションを文書化する際には、その型自身だけでなく、全てのメンバーを文書化する
/**
 * Indicates that the annotated method is a test method that
 * must throw the designated exception to succeed.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    /**
     * the exception that the annotated test method must throw
     * in order to pass. (The test is permitted to throw any 
     * subtype of the type described by this class object.)
     */
    Class<? extends Throwable> value();
}

Effective Javaを勉強します【第6章】

項目30. int定数の代わりにenumを使用する

一年での季節、太陽系の惑星、トランプの役など、固定数の定数からその値が成り立つ型を列挙型と呼ぶ。Java4以前で列挙型を表現する際には int enum パターンが用いられていたが、Java 5以降では enum 型を用いて列挙型を表現すべきである。

int enum パターン

以下は int enum パターンの例である。

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLAE = 1;
public static final int ORANGE_BLOOD = 2;

上記は以下のような欠点を持つ。

  • 型安全性を提供しない。オレンジを期待するメソッドにアップルを渡したり、オレンジとアップルを==演算子で比較しても何のエラーにもならない。
  • 名前空間を提供しない。APPLE_等の接頭辞で名前の衝突を防ぐ手間がかかる。
  • int enum 定数を変更する度に再コンパイルの必要がある。再コンパイルされない場合、動作はするが結果がどうなるかはわからない。
  • 利便性が低い。デバッガ等で表示しても文字情報は取得することができず、定数をイテレートするなどの操作手段もない。

int 定数の代わりに String 定数が用いられる場合もあるが、文字列比較のコストやハードコートした際の誤字によるバグを考慮すると、さらに望ましくない。

enum

上記で説明した int enum 型の欠点を回避するのが enum 型である。以下はその例である。

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

enum 型は内部的にはクラスであり、クラス同様パッケージに属する。基本的には1ファイルに火取るの enum 型を定義し、型名と同じファイル名のファイルに記述する。内部クラスのように他のクラスの内部に定義することも可能。

上記Apple を定義した Apple クラスを javap コマンドで逆コンパイルすると、以下のように表示される。

Compiled from "Apple.java"
public final class Apple extends java.lang.Enum<Apple> {
  public static final Apple FUJI;
  public static final Apple PIPPIN;
  public static final Apple GRANNY_SMITH;
  public static Apple[] values();
  public static Apple valueOf(java.lang.String);
  static {};
}

これにより以下のようなことが分かる。

  • enum 宣言された型は java.lang.Enum を継承している
  • 個々の列挙定数は public static final であり、自身が宣言された型のインスタンスである
  • enum 型はアクセス可能なコンストラクタを持っていないため事実上 final であり、クライアントは enum 型のインスタンスの生成や拡張は不可能である(シングルトン)

enum は型安全性を提供している。たとえば型 Apple で引数を宣言した場合、null でないオブジェクト参照は必ず FUJI / PIPPIN / GRANNY_SMITHのいずれかの値であることが保証される。また、異なる enum 型の値を渡そうとしたり、異なる enum 型の値同士を == で比較しようとするとコンパイル時エラーになる。

また、各 enum 型は独自の名前空間を提供するため、同一名の定数が共存可能である。加えて toString メソッドにより文字列へ変換することも可能である。

enum型 へのフィールド・メソッドの追加

enum 型に任意のフィールド・メソッドを追加することにより、定数にデータを関連づけることが可能である。

例えば、太陽系の惑星について考えてみる。各惑星は、質量と半径を持ち、それらから表面重力や惑星表面上での物体の重さが計算可能である。

これらを enum で表現すると以下のようになる。

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;
    private final double radius;
    private final double surfaceGravity;

    public static final double G = 6.67300E-11;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        this.surfaceGravity = G * mass / (radius * radius);
    }

    public double mass() { return mass; }
    public double radius() { return radius; }
    public double surfaceGravity() { return  surfaceGravity; }
    public double surfaceWeight(double mass) { return mass * surfaceGravity; }
}

enum にデータを関連づけるためには、インスタンスフィールドを宣言し、データを受け取るコンストラクタを書き、そのフィールドにデータを保存すれば良い。enum は不変であるため、すべてのフィールドは final とすべきである。

enum の値セットを取得するにはvalues()メソッドを用いる。これにより、各惑星上での物体の重さを計算するプログラムを以下のように記述することができる。

double earthWeight = 1.0;
double mass = earthWeight / Planet.EARTH.surfaceGravity();
// 地球上で重さ1.0の物体の各惑星上での質量を計算する
for (Planet p : Planet.values())
    System.out.printf("Weight on %s is %f%n",
                      p, p.surfaceWeight(mass));

定数固有メソッド実装

Planet クラスの例では、定数ごとのフィールド値が異なってもメソッドの振る舞いは同じだったが、各定数にメソッドの振る舞いを変えたい場合がある。そのような時には定数固有メソッド実装を利用する。以下はその例。

public enum Operation {
    PLUS { double apply(double x, double y) { return x + y; } },
    MINUS { double apply(double x, double y) { return x - y; } },
    TIMES { double apply(double x, double y) { return x * y; } },
    DIVIDE { double apply(double x, double y) { return x / y; } };

    abstract double apply(double x, double y);
}

このように enum 内で抽象メソッドを宣言し、各定数ごとにオーバーライドすることで振る舞いを変化させることができる。仮に実装を忘れたとしても、コンパイル時エラーとなりそのまま動作することはない。

また、定数固有メソッドは定数固有データを同時に利用することができる。

public enum Operation {
    PLUS("+") { double apply(double x, double y) { return x + y; } },
    MINUS("-") { double apply(double x, double y) { return x - y; } },
    TIMES("*") { double apply(double x, double y) { return x * y; } },
    DIVIDE("/") { double apply(double x, double y) { return x / y; } };

    private final String symbol;
    Operation(String symbol) { this.symbol = symbol; }

    abstract double apply(double x, double y);
}

fromString メソッドの実装の検討

enum はデフォルトで、定数の名前を定数自身へ変換する valueOf(String) メソッドを持っている。(上記Operation の場合、返り値の型は Operation

もし enum 型で toString メソッドをオーバーライドする場合、それに対応する fromString メソッドの実装を検討すべきである。 fromString メソッドは、引数で渡されたカスタム文字列を定数自身へ変換する。

private static final Map<String, Operation> stringToEnum = new HashMap<>();
// toString() で定義したカスタム文字列を Map で記憶する
static {
   for (Operation op : values()) {
       stringToEnum.put(op.toString(), op);
   }
}
// toString() でカスタム文字列を定義
@Override
public String toString() {
    return symbol;
}
// toString に対応する fromString()
public static Operation fromString(String symbol) {
    return stringToEnum.get(symbol);
}

パフォーマンス

一般的に、パフォーマンスに関して enum は int 定数と比べても遜色ない。小さな差ではあるが、enum 型のロードと初期化には空間的・時間的コストが発生する。しかし資源制約の厳しいデバイス等でなければ、そのコストが顕著になることはない。

項目31. 序数の代わりにインスタンスフィールドを使用する

enumordinal() というメソッドを持っている。このメソッドは enum 型内の各 enum 定数の整数位置を返す。

public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;

    // Ensemble.SOLO.ordinal() であれば 1 を返す
    public int numberOfMusicians() { return ordinal() + 1; }
}

上記は動作するが、以下の理由より保守性が非常に低い。

  • 定数が並び替えられた場合、numberOfMusicians は動作しなくなる
  • 複数の定数の同じ整数値を割り当てることができない
    • 例えばダブルカルテット(4人×2)の場合、オクテット(8人)との区別ができない

そもそも ordinal メソッドは、EnumSet や EnumMap などの汎用の enum をベースとしたデータ構造の実装を手助けする手段として提供されている。したがって普通の開発者は ordinal メソッドを使うべきではない。

代わりに、下記のようにインスタンスフィールドを用いて、整数値を関連づける。これにより上記の保守性の問題はすべて解決する。

public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);

    private final int size;
    Ensemble(int size) { this.size = size; }
    public int numberOfMusicians() { return this.size; }
}

項目32. ビットフィールドの代わりにEnumSetを使用する

enum が集合で用いられる場合、従来はビットフィールドが利用されてきたが int enum パターンと同様の欠点を持つ。 代わりに EnumSet を用いることで、ビットフィールドの簡潔性とパフォーマンスを維持しつつ、欠点を解決することが可能である。

ビットフィールド

ビットフィールドとは、列挙型の各定数に異なる2の累乗を割り当てて、int enum パターンを使用する方法である。

public class Text {
    public static final int STYLE_BOLD   = 1 << 0; // 1
    public static final int STYLE_ITALIC = 1 << 1; // 2
    public static final int STYLE_UNDERLINE = 1 << 2; // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8

    // パラメータは、0個以上のSTYLE_定数のビット
    public void applyStyles(int styles) {
        ...
    }
}

ビットフィールドとして知られている複数の定数を1つの集合にまとめるために、 以下のようにビット和操作を行う。

// 以下はtext変数がボールドかつイタリックであることを指す(引数は 0011)
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

ビットフィールドは和集合や共通集合などの操作を効率的に行うが、int enum 定数の短所を全て持っている。

EnumSet

ビットフィールドの代わりに、EnumSet が利用可能である。 EnumSet は Set インタゲースを実装しており、豊富な機能、安全性、他の Set 実装との相互運用性が提供されている。 またパフォーマンスも、ビットフィールドと比べても遜色ない。

public class Text {
    public enum Style {
        BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
    }
    // どのようなSetでも渡せるが、EnumSet が明らかに最善
    public void applyStyles(Set<Style> styles) {...}
}

各属性は以下のように設定可能である。

// 以下はtext変数がボールドかつイタリックであることを指す
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

項目33. 序数インデックスの代わりにEnumMapを使用する

下記のようなハーブを表現したクラスがあるとする。 ハーブは3種に分類可能であり(一年生植物(annual)、多年生植物(perennial)、越年性植物(biennial))、 それらを enum で表現している。

public class Herb {
    enum Type {
        Annual, Perennial, Biennial
    }

    private String name;
    private String type;

    public Herb(String name, String type) {
        this.name = name;
        this.type = type;
    }
}

ここで、ハーブを種類ごとに Set などのコンテナに格納したいとする。 この時、種類の序数でインデックスされた配列で格納先の Set を区別してはいけない。 このような時は EnumMap を用いるのが適切である。

序数インデックスを用いた配列による分類

種類の序数でインデックスされた配列に各種ハーブを格納する場合、以下のようなコードになる。

// 種類の混在したハーブの集合
Herb[] all = new Herb[] { ... };

// 3種類のハーブに対応した3つの Set を配列にする
Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length];
for (int i = 0; i < herbsByType.length; i++) {
    herbsByType[i] = new HashSet<Herb>();
}

// ハーブを種類ごとに分類
for (Herb h: all) {
    herbsByType[h.type.ordinal()].add(h);
}

この方法は以下の問題を抱える。

  • 配列はジェネリックスと互換性がないため、無検査キャストを必要とし、コンパイル時には警告が出る
  • 配列のインデックスからハーブの種類を読み取ることができない
  • 配列のアクセス時に、意図した種類と対応したインデックスが利用されるかはクライアント次第(型安全でない)

EnumSet を用いた分類

EnumSet を用いてハーブを分類する場合、以下のようなコードになる。

// 種類の混在したハーブの集合
Herb[] all = new Herb[] { ... };

// ハーブの種類をキー、値を Set としたMapを作成
Map<Herb.Type, Set<Herb>> map = new EnumMap<>(Herb.Type.class);
for (Herb.Type type: Herb.Type.values() {
    map.put(herb.type, new HashSet<Herb>());
}

// ハーブを種類ごとに分類
for (Herb herb: all) {
    map.get(herb.type).add(all);
}

このプログラムは序数インデックスを用いる方法と比べて

  • より短く、より明瞭、より安全
  • パフォーマンスも遜色ない
  • 型安全

多次元関係

2つの enum からの対応付けを表す時も同様であり、序数を用いるよりEnumSetを用いた方が良い。 例えば水の相転移を表現したいとする。(液体(liquid)から個体(solid)は凍結(fleezing)、液体から気体(gas)は沸騰(boiling))

以下は序数を用いる例。

public enum Phase {
    SOLID, LIQUID, GAS;
    
    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

        // 状態遷移を配列で表現
        Transition[][] TRANSITIONS = {
            { null,    MELT,     SUBLIME},
            { FREEZE,  null,     BOIL},
            { DEPOSIT, CONDENSE, SUBLIME}
        };

        // 状態遷移時の現象を取得する
        public static Transition from(Phase src, Phase dst) {
            return TRANSITIONS[src.ordinal()][dst.ordinal()];
        }
    }
}

上記は、ハーブの分類を序数を用いて行った時と同様に、以下の欠点を持つ

  • 序数と配列のインデックスの関係をコンパイラは知らないため、転移表に誤りがあったりした場合、実行時エラーとなる
  • null が増えると空間が無駄になる

ハーブの例と同様に、EnumMap を用いることでこれらの問題は解決する。 新たにプラズマという相を追加する場合であっても、TransitionPhase に定数を追加するだけで良い。

public enum Transition {
    MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
    BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
    SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

    private final Phase src;
    private final Phase dst;

    public Transition(Phase src, Phase dst) {
        this.src = src;
        this.dst = dst;
    }

    // 二重 Map
    private static final Map<Phase, Map<Phase, Transition>> map = 
        new EnumMap<Phase, Map<Phase, Transition>>(Phase.class);
    static {
        for (Phase phase: Phase.values()) {
            map.put(phase, new EnumMap<Phase, Transition>(Phase.class));
        }
        for (Transition transition: Transition.values()) {
            map.get(transition.src).put(transition.dst, transition);
        }
    }

    public static Transition from(Phase src, Phase dst) {
        return map.get(src).get(dst);
    }
}

項目34. 拡張可能なenumをインタフェースで模倣する

基本の enum 型に伴うインタフェースを書いて、そのインタフェースを実装することで拡張可能な enum 型を模倣することが可能。

オペコード

オペコードは拡張可能な列挙型を使わざるをえない場面の1例。enum 型が任意のインタフェースを実装できるという事実を利用して、 オペコードに関するインタフェースを定義し、それを実装した enum を定義してやればよい。

interface Operation {
    double apply(double x, double y);
}

public enum BasicOperation implements Operation {
    PLUS("+") {
        @Override public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        @Override public double apply(double x, double y) {
            return x - y;
        }
    };

    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
}

// Enumは拡張不可だが、共通インタフェース Operation を利用して拡張した操作を定義している
public enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x  % y;
        }
    };

    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
}

項目35. 命名パターンよりアノテーションを選ぶ

命名規則とは、JUnitのように「テストケースメソッドの先頭には"test"をつける」といったルールのこと。

命名規則の代わりにアノテーションを利用することには以下のようなメリットがある。

  • 命名の際の誤字などをコンパイラでチェックできる
  • パラメータを関連づけることができる
  • クラスやメソッドなど、利用箇所を限定することができる

アノテーション

JavaにはOverrideアノテーションなど、いくつかのアノテーションが用意されているが、自分でアノテーションを定義することも可能。 その際は@interfaceを用いる。

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

上記ではTestアノテーションを定義している。

また@Target(ElementType.METHOD)のように、定義時に別のアノテーションを付与することもできる(メタアノテーション)。

@Retention(RetentionPolicy.RUNTIME)Testアノテーションがメソッド宣言にのみ付与できることを示している。 @Target(ElementType.METHOD)コンパイラによってクラスファイルに記録し、実行時にVMに保持することを示している。

マーカーアノテーション

Testアノテーションのように、パラメータを付与された要素を「マーク」するためのアノテーションを指す。

public class SampleTest {
    @Test public static void m1() {}
    public static void m2() {}
    @Test public static void m3() {}
}

上記の場合Testアノテーションが付与されたm1()m2()のみがテストとして実行される。

パラメータを持つアノテーション

ある特定の例外がスローされた場合のみ成功するテストを表すアノテーションを定義することを考える。 そのためには、アノテーションに特定の例外が何か教えなければならない。

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
    Class<? extends Exception> value();
}

上記のように定義することで、ExceptionTestアノテーションに、特定の例外を表すパラメータを与えることが可能となる。 以下のように例外を期待するテストに利用する。

public class SampleTest2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1() {
        int i = 0;
        i  = i / i;
    }
}

項目36. 常にOverrideアノテーションを使用する

スーパークラスの宣言をオーバーライドしている全てのメソッド宣言に対して Override アノテーションを使用すべき。

Override アノテーションを使用することで、例えば以下のようなオーバーライドミスを コンパイラが教えてくれる。

public class Bigram {
    private final char first;
    private final char second;
    public Bigram(char first, char second) {
        ...
    }
    // 以下は間違い。正しくは public boolean equals(Object o) {}
    public boolean equals(Bigram b) {
        ...
    }
    public int hasCode() {
        ...
    }
}

Bigram クラスでは equals` メソッドのオーバーライドに失敗し、誤ってオーバーロードしている。

このようなミスは見つけづらいが 以下のように Override アノテーションを利用することでコンパイラがエラーメッセージを出力してくれる。

    @Override public boolean equals(Bigram b) {
        ...
    }

項目37. 型を定義するためにマーカーインタフェースを使用する

マーカーインタフェースとは、メソッド宣言を含まないインタフェースのことであり、 そのインタフェースを実装しているクラスが何らかの特性を持っていることを示す。

例として Serializable インタフェースが挙げられる。これはメソッドを1つも持たないが、 ObjectOutputStream に利用できるという特性を示す。

マーカーインタフェースは型を定義できるため、コンパイル時のエラーチェックが可能であるという点で マーカーアノテーションより優れている。

ただし、クラスやインタフェース以外のプログラム要素をマークしたい場合は、マーカーアノテーションを使う必要がある。 (マーカーインタフェースはクラス以外をマークすることができない。)

Effective Javaを勉強します【第5章】

項目23. 新たなコードで原型を使用しない

ジェネリックとは

1つ以上の型パラメータを宣言に持つクラスやインターフェースのこと。

例)

public interface List<E> extends Collection<E> {
    ...
}
  • List<E>ジェネリック
  • E:仮型パラメータ
  • List<String>:パラメータ化された型
  • String:実型パラメータ
  • List:原型

原型を使用すべきでない

以下はリリース1.5以前の原型を用いたコレクション型の例。 コンパイルエラーにもならず、コレクションから要素を取り出し利用するまでエラーに気づくことができない。

// Stampsインスタンスだけを含むことを想定
private final Collection stamps = ...;
// 誤ってCoinインスタンスを代入してしまう
stamps.add(new Coin(...));
// 要素を取り出す
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
    Stamp s = (Stamp) i.next(); // ClassCastExceptionが発生する
}

以下はジェネリックスを用いたコレクション型の例。 stampsStampインスタンスだけを含んでいることがコンパイラにより保障される。 また要素取り出しの際のキャストも不要である。(コンパイラが自動でキャストしてくれる)

// 型安全
private final Collection<Stamp> stamps = ...;
// 以下のように書いた場合はコンパイルエラーになる
stamps.add(new Coin(...));
// 要素の取り出し(型安全)
for (Stamp s : stamps) { // キャスト不要
    ...
}
for (Iterator<Stamp> i = stamps.iterator(); i.hasNext(); ) {
    Stamp s = i.next(); // キャスト不要
}

List<Object>

Listなどの原型は使用すべきでないが、任意のオブジェクトの挿入が可能なList<Object>を利用することは問題ない。 前者と後者の違いは、後者はコンパイラにより型検査が行われるということ。

例えば、以下のように原型を用いた場合はコンパイル時に警告は出るが実行は可能であり、ClassCastExceptionがスローされる。

public static void main (String[] args) {
    List<String> strings = new ArrayList<String>();
    unsafeAdd(strings, new Integer(42));
    String s = strings.get(0); //ClassCastException
}
private static void unsafeAdd(List list, Object o) {
    list.add(o);
}

unsafeAddの宣言をprivate static void unsafeAdd(List<Object> list, Object o)に書き換えた場合、コンパイルエラーとなる。 これはList<String>List<Object>のサブタイプではないためである。(ジェネリックス型に継承関係はない)

stringsList<Object>型であればIntegerも格納可能である。

非境界ワイルドカード

ジェネリックスを利用したいが、実際の型パラメータが分からない、または気にしたくない、という場合は非境界ワイルドカード型を利用する。 非境界ワイルドカード型では、型パラメータの代わりに?記号を用いる。

非境界ワイルドカード型はすべてのパラメータ化された型パラメータのスーパータイプである。

例:List<?>List<Object>List<String>スーパークラス

以下は非境界ワイルドカード型の利用例である。

static int numElementsInCommon(Set<?> s1, Set<?> s2) {
    int result = 0;
    for (Object o1:s1) {
        if (s2.contains(o1)){
            result++;
        }
    }
    return result;
}

原型のリストと非境界ワイルドカードのリストの違いは型安全性である。 原型のリストにはいかなる要素も代入可能だが、非境界ワイルドカード型のリストにはnullを除いた通常のオブジェクトは代入不可能である。

例外的に原型を使うべき場合

  • クラスリテラルを使う場合
    • List<String>.classList<?>.classは不可
  • instanceofと一緒に使う場合
// Setであるかを確認して処理する
if (o instanceof Set) {
    // 事前にSetであることを検査しているので警告は出ない
    Set<?> m = (Set<?>) o;
    ...
}

項目24. 無検査警告を取り除く

ジェネリックスの利用時は以下のようなコンパイラの警告をよく目にする。

  • 無検査キャスト警告
  • 無検査メソッド呼び出し警告
  • 無検査ジェネリック配列生成警告
  • 無検査変換警告

無検査警告への対処は以下。

  • 基本的に取り除くことが可能なすべての無検査警告を取り除く
  • 警告を取り除くことができず、かつ該当箇所が型安全であると明示できる場合、@SuppressWarnings("unchecked")アノテーションで警告を抑制してよい
    • できるだけ最小のスコープに対して使用すること(クラス全体に使用することは絶対にしない)
    • 型安全であると明示できる理由をコメントに残すこと

項目25. 配列よりリストを選ぶ

ジェネリックスは不変である

配列型Sub[]Super[]のサブタイプであるが、List<Sub>List<Super>の間に継承関係は存在しない。

以下のような場合、配列を利用すると実行時にエラーがスローされるまで間違いに気づかない可能性があるが、 リストの場合コンパイルエラーで知ることができる。

Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // ArrayStoreExceptionのスロー

List<Object> ol = new ArrayList<Long>(); // 互換性のない型でコンパイルエラー
ol.add("I don't fit in");

ジェネリックスはイレイジャで実装されている

配列は実行時にその要素型を知っており、それを強制する。 一方でジェネリックスはコンパイル時のみ型制約を強制し、実行時には型情報が廃棄(erase)されるため、柔軟性が高く既存コードとの相互運用も容易である。

配列とジェネリックスが混在している時は、大抵は配列をリストに置き換えた方が良い

配列とジェネリックスは上手く調和しないので、混在している時は大抵は配列をリストに置き換えた方が良い。

項目26. ジェネリック型を使用する

以下のスタック実装を例に、ジェネリック型の書き方を学ぶ。

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    public boolean isEmpty() {
         return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements,  2 * size + 1);
        }
    }
}

上記の問題点は、スタックから取り出した値をクライアント側でキャストしなければならない点である。 したがって以下のようにジェネリックスを用いた形に修正する。

public class Stack<E> { //修正
    private E[] elements; //修正
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY]; //修正
    }

    public void push(E e) { //修正
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() { //修正
        if (size == 0) {
            throw new EmptyStackException();
        }
        E result = elements[--size]; //修正
        elements[size] = null;
        return result;
    }
    ...
}

上記ではelements = new E[DEFAULT_INITIAL_CAPACITY];コンパイルエラーとなる。 これはEなど具象化不可能型の配列の生成はできないため。

したがって以下のいずれかの方法で回避する。

1. Object配列を生成して、ジェネリック配列型へキャストする

elements = new E[DEFAULT_INITIAL_CAPACITY];elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];にする。

これだけでは警告が出るので@suppressWanings("unchecked")アノテーションを使って、警告を抑制する。 この時、項目24で述べた通りできる限り狭いスコープで抑制する必要がある。今回はコンストラクタ内に無検査配列生成の処理のみ含まれているため、 コンストラクタ全体で警告を抑制する。

    // elements配列はpush(E)からのEインスタンスのみ挿入されるため問題なし
    @suppressWanings("unchecked")
    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY]; //修正
    }

2. フィールドelementsの型をE[]からObject[]に変更する

private E[] elements;private Object[] elements;にして、

E result = elements[--size];E result = (E) elements[--size];にすると

方法1同様に警告が発生するため、@suppressWanings("unchecked")アノテーションを使って、警告を抑制する。

    public E pop() { //修正
        if (size == 0) {
            throw new EmptyStackException();
        }
        // pushは要素が型Eであることを要求しているため問題なし
        @suppressWanings("unchecked") E result = (E) elements[--size]; 
        elements[size] = null;
        return result;
    }

型パラメータへの制限

以下のように境界型パラメータを利用することで型パラメータに制限を設けることができる。

例) 実型パラメータがjava.util.concurrent.Delayedのサブタイプでなければならないことを要求

class DelayQueue<E extends Delayed> implements BlockingQueue<E>;

これにより、明示的なキャストの必要性やClassCastExceptionの危険性なくDelayQueueの要素に対してDelayedのメソッドが利用可能。

項目27. ジェネリックメソッドを使用する

staticのユーティリティメソッドはジェネリックメソッドにできないか検討すべき良い候補である。

ジェネリックstaticファクトリーメソッド

ジェネリックコンストラクタでは、変数宣言の左右で型パラメータを指定しなければならず煩わしい。

Map<String, List<String>> map = new HashMap<String, List<String>>();

ジェネリックstaticファクトリーメソッドを利用すれば、上記を簡潔にできる。

// ジェネリック static ファクトリーメソッド
public static <K, V> HashMap<K, V> newHashMap() {
    return new HashMap<K, V>();
}
Map<String, List<String>> map = newHahMap();

が、これはJava7のダイヤモンド演算子で解決された。

// ダイヤモンド演算子によりコンストラクタの実型パラメータは省略可能に
Map<String, List<String>> map = new HashMap<>();

ジェネリックシングルトンファクトリー

ジェネリックスを利用することで、多くの異なった型に適用可能な不変オブジェクトを生成することができる。

例) 恒等関数を作成する場合

恒等関数とは何らかの型Tの値を受け取って、その型の値を返す関数のこと。 内部状態は持たない(型が違うのみ)ためインスタンスを複数生成するのは無駄である。 したがってジェネリックスを用いた不変オブジェクトを返すとよい。

// ジェネリックシングルトンファクトリーパターン 
private static UnaryFunction<Object> IDENTITY_FUNCTION = 
    new UnaryFunction<Object>() {
        public Object apply(Object arg) { return arg; }
    };
// IDENTITY_FUNCTIONは状態を持たず、その型パラメータは非境界なので
// すべての型に対して1つのインスタンスを共有するのは安全
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
    return (UnaryFunction<T>) IDENTITY_FUNCTION;
}

上記は無検査キャスト警告が発生するが、IDENTITY_FUNCTIONは単に引数を返すだけなので安全であるため、 例外を抑制することができる。

再帰型境界

型パラメータがその型パラメータ自身が関係するなんらかの式で制限されていることを、再帰型境界と呼ぶ。

例) Comparableインタフェース

Comparableインタフェースは何らかの型Tと比較できることを示すインタフェースであり、 多くの場合、このインタフェースを実装する型は自分自身の型とのみ比較可能である。

public interface Comparable<T> {
    int compareTo(T o);
}

リストのソート、検索、最小値/最大値の計算をするために、Comparableを実装した要素のリストを受け取る 多くのメソッドがあるが、これを行うためにはリスト内の個々の要素同士の比較が可能である必要がある。

相互比較可能であることを示すジェネリックスは以下のように記述する。

// 再帰型境界
public static <T extends Comparable<T>> T max(List<T> list) {
    ...
}

これはリストの要素TComparable<T>のサブタイプであることを要求している。

すなわちリストの要素Tは自身の要素Tと比較可能であることという相互比較可能性を要求している。

項目28. APIの柔軟性向上のために境界ワイルドカードを使用する

不変性の扱いづらさ

不変性によりList<Object>List<String>の間に継承関係が存在しないことはジェネリックスの良さではあるが、 故に扱いづらい時がある。

例として以下のスタックAPIを考える。

public class Stack<E> {
    public Stack() {}
    public void push(E element) { ... }
    public E pop() { ... }
    public boolean isEmpty() { ... }
}

このスタックにInteger型の要素を追加する場合、以下のように記述することになる。

Stack<Number> numbers = new Stack<Number>();
numbers.push(new Integer(1));

IntegerNumberのサブタイプであるため、上記は動作する。

ここで複数の要素をまとめて挿入するpushAllメソッドを以下のように新しく実装するとする。

public void pushAll(List<E> src) {
    for (E e: src) {
        this.push(e);
    }
}

この時、先ほどと同様にInteger型の要素を挿入しようとした場合、コンパイルエラーとなってしまう。

Stack<Number> numbers = new Stack<Number>();
List<Integer> integers = ... ;
numbers.pushAll(integers);

エラーとなる原因はジェネリックスの不変性により、List<Number>List<Integer>の間に継承関係が存在しないためだが、 これはAPIとしては直感的でない。

同様に、スタックからInteger型の要素を取り出す場合を考える。単一の要素を取り出す場合、以下のように記述することになる。

Stack<Number> numbers = new Stack<>();
numbers.add("1");

Object obj = numbers.pop();

ObjectIntegerのスーパータイプであるため、上記は動作する。

次に、引数として渡したリストにすべての要素を追加してくれるpopAllメソッドを以下のように新しく実装するとする。

public void popAll(List<E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}

この時、List<Object>型のリストを渡そうとした場合、コンパイルエラーとなってしまう。

Stack<Number> numbers = new Stack<Number>();
numbers.add("1");
numbers.add("2");

List<Object> objects = ...;
numbers.popAll(objects);

エラーとなる原因はジェネリックスの不変性により、List<Number>List<Object>の間に継承関係が存在しないためだが、 やはりこれはAPIとしては直感的でない。

境界ワイルドカード

上記のような問題を解決するためには境界ワイルドカード型を使う。

pushAllメソッドの場合、引数の型は「Eのリスト」ではなく「Eの何らかのサブタイプのリスト」とすべきであり、 これは境界ワイルドカード型を利用して、以下のように記述することができる。

public void pushAll(List<? extends E> src) {
    for (E e: src) {
        this.push(e);
    }
}

popAllメソッドの場合、引数の型は「Eのリスト」ではなく「Eの何らかのスーパータイプのリスト」とすべきであり、 これは境界ワイルドカード型を利用して、以下のように記述することができる。

public void popAll(List<? super E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}

上記のように記述することで、コンパイルエラーなく下記のコードを実行することが可能となる。

Stack<Number> numbers = new Stack<Number>();

List<Integer> integers = ...;
numbers.pushAll(integers);

List<Object> objects = new ArrayList<Object>();
numbers.popAll(objects);

PECS原則

境界ワイルドカード型のうち、extendssuperのどちらを使えば良いか判断するための基本原則がPECS原則である。

Producer -extends and Consumer -super

pushAllメソッドの場合、引数srcはスタックの要素を生成する Producer であった。 一方でpopAllメソッドの場合、引数dstはスタックの要素を消費する Consumer であった。

明示的型パラメータ

PECS原則を用いて、以下のunionメソッドを書き換えることを考える。

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
}

引数s1s2を用いて、新たなSetを生成するため、どちらも Producer である。 したがってPECS原則に従うと、宣言を以下のように書き換えることが可能である。

この時、戻り値にはワイルドカード型を使用しないことに注意する。戻り値にワイルドカード型を利用すると クライアント側でワイルドカード型の使用を強制することになる。

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
}

修正したunionメソッドを以下のように使用することができるように思えるが、コンパイルエラーになってしまう。

Set<Integer> integers = new HashSet<>();
Set<Double> doubles = new HashSet<>();
Set<Number> numbers = union(integers, doubles);

上記エラーの原因は、Java型推論の制限によるものである。(ジェネリックの理論的には問題ないように思える) この時以下のように、明示的に型パラメータを記述することでエラーを解決することができる。

Set<Number> numbers = Union.<Number>union(integers, doubles);

上記Unionunionメソッドの宣言されたクラスである。 明示的な型パラメータの宣言はコードの冗長性を増すので頻繁に書くべきでないし、大抵は書く必要はない。

Comparableに対するPECS

PECS原則を用いて、以下のmaxメソッドを書き換えることを考える。

public static <T extends Comparable<T>> T max(List<T> list) {
    Iterator<T> it = list.iterator();
    T result = it.next();
    while (it.hasNext()) {
        T t = it.next();
        if (t.compareTo(result) > 0) {
            result = t;
        }
    }
    return result;
}

引数listを用いてresultを生成するため、listは Producer である。 この時イテレータitの型も修正することに注意する。

次に再帰型境界であるComparableについて考える。 <T extends Comparable<T>>はTと比較可能な(Comparableを実装した)何らかの型という意味だが、 Tインスタンスを「比較」という形で消費している Consumer と考えることができる。

したがってPECS原則に従うと、宣言を以下のように書き換えることが可能である。

public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    Iterator<? extends T> it = list.iterator();
    ... (以下変更なし) ...
}

コンパレータは Consumer であることを覚えておけば良い。

 項目29. 型安全な異種コンテナーを検討する

異種コンテナーとは

ジェネリックスは通常、Set<Integer>List<String>のように、特定の型の要素を持つコンテナーに対して利用する。Map<Integer, String>のように複数のキーを持つ場合であっても、型は固定されている。

ここでいうコンテナーとは、ListSetのように複数の値やオブジェクトの格納先となる入れ物のこと。

本項目では、「クラス」をキー、「オブジェクト」を値とするマップとすることで、様々な型の値を 格納することができる異種コンテナーを実装する。

Classクラス

「クラス」をマップのキーとして扱うためにClassクラスを利用する。 ClassクラスはJavaの「クラス」自体の情報を保持するためのクラスである。 利用例は以下のとおり。

String str = "Hello World!";
Class stringClazz = str.getClass();

// getName()メソッドでクラス名を取得
// この場合 java.lang.String
stringClazz.getName();

クラスリテラル

Object.getClass()メソッド以外にクラス名.class(クラスリテラル)でもクラスの情報が取得できる。

Class stringClazz = String.class;

// 同様にgetName()メソッドでクラス名を取得可
// この場合も java.lang.String
stringClazz.getName();

Classクラスはジェネリッククラスなので、型パラメータを与えることで型安全になる。

// OK
Class<String> stringClazz = String.class;

// コンパイルエラー
Class<Integer> integerClazz = String.class;

ただしクラスリテラルジェネリックスなど具象化不可能型では利用できない。

Class<List<String>> clazz;
// リテラルを使うとコンパイルエラー
Class<List<String>> clazz = List<String>.class;

型安全な異種コンテナー

Classクラスを利用することで、「クラス」をキーとしたマップ(異種コンテナー)を 実装できる。インターフェースは以下。

public interface Favorites {
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> T getFavorite(Class<T> type);
}

Favoritesコンテナーの利用例は以下のとおり。

Favorites favorites = new Favorites();
favorites.putFavorite(String.class, "Java");
favorites.putFavorite(Integer.class, new Integer(1));
String favorite = favorites.getFavorite(String.class);

上記のように、コンパイル/実行時の型情報を渡すために利用されるクラスリテラルを「型トークン」と呼ぶ。上記はジェネリックスにより型安全性が保証されているため、キーと値のクラスが異なることは起こり得ない。このように複数の型のオブジェクトを保持する型安全なコンテナーを「型安全異種コンテナー」と呼ぶ。

型安全異種コンテナーの実装は以下。

public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();

    public <T> void putFavorite(Class<T> type, T instance) {
        if (type == null) {
            throw new NullPointerException("Type is null");
         }
         favorites.put(type, instance);
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

上記のputFavoriteメソッドでは原型を利用することで、警告を無視することにはなるが 無理やりキーと型の異なる値を格納することが可能である。 そのような時はClass.castを用いると、実行時にClassCastExceptionを発生させることができる。

public <T> void putFavroite(Class<T> type, T instance) {
    // Class.castメソッドにより実行時に型チェックが可能
    favorites.put(type, type.cast(instance));
} 

境界型トーク

上記の異種コンテナーでは型トークンに関して特に制限はなかったが、境界型トークンを用いることで 制限が可能。下記の場合、型パラメータTAnnotationクラスのサブクラスであると要求されている。

public <T extends Annotation> T getAnnotation(Class<T> annotationType);

Effective Javaを勉強します【第4章】

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

情報隠蔽のメリット

実装とAPIを分離し、他のモジュールとはAPIを介して通信を行い、詳細な実装は外部へ公開しないことを 情報隠蔽という。メリットとしては、以下のような点が挙げられる。

  • 他モジュールとの依存性が低いため、モジュールの個別開発が可能であり開発スピードが向上する
  • モジュールの理解がしやすく、他モジュールへの影響を心配せずデバッグが可能
  • パフォーマンスに問題があるモジュールが特定できれば、他モジュールへの影響を心配せず最適化が可能
  • ソフトウェアの再利用を促進する
  • 大規模システムの開発において、個々のモジュールから作り込むことができる

情報隠蔽とアクセス修飾子

アクセス修飾子を用いて、各クラスやメンバーをできる限りアクセスできないようにすべき。

トップレベルのクラスやインタフェースの場合

以下の2種の修飾子が利用可能。公開APIに含まれない(情報隠蔽可能)ので基本的にパッケージプライベートにすべき。パッケージプライベートのクラス/インタフェースを呼び出すクラスが1つならば、そのクラスにネストしてアクセス範囲を絞ることを検討すべきである。

  • パッケージプライベート
    • パッケージ内からのみアクセス可能
    • public修飾子をつけなければパッケージプライベートになる
  • public
    • どこからでもアクセス可能
    • 公開APIとなるため、互換性維持のために永久的にサポートする必要が生じる

メンバーの場合

以下の4種の修飾子が利用可能。基本的にprivateにすべき。そうでないメンバーが多くなるのであれば設計を見直すべき。

  • private
    • 宣言されたクラス内でのみ利用可能
    • 基本的にprivateにすべき
  • パッケージプライベート
    • 宣言されたパッケージ内のどのクラスからでもアクセス可能
  • protected
    • 宣言されたクラスのサブクラスからと、宣言されたパッケージ内のどのクラスからでもアクセス可能
  • public
    • どこからでもアクセス可能

メソッドがスーパークラスのメソッドをオーバーライドする場合、スーパークラスでメソッドが持つアクセスレベルより低いアクセス修飾子を指定することはできない。スーパークラスより低いアクセスレベルを設定すると、オーバーライドしたメソッドからスーパークラスのメソッドが呼び出せない状況が生じうるため。

インスタンスフィールドはpublicにしない

インスタンスフィールドをfinalで宣言した場合であっても、インスタンスフィールドへの参照のみを保持するので、インスタンスフィールド内で保存する値を制限することができないため。

同様に配列をフィールドとして持つ場合も、配列内の値ではなく配列への参照を保持するのみであるため、長さが0でない配列は常に可変であると考える。public static finalの配列フィールドやアクセッサーを持つのは、セキュリティホールのもとである。

項目14. publicのクラスでは、publicのフィールドではなく、アクセッサーメソッドを使う

publicのフィールドにはどこからでもアクセス可能となるため、カプセル化に反する。またフィールド変更時の補助的な処理も提供することができない。そのためフィールドは基本的にprivateとし、publicのセッターとゲッターを提供するようにすればよい。

ただし、パッケージプライベートまたはprivateのネストしたクラスであれば、publicのフィールドとしたほうが可読性などの観点から望ましい場合もある。

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

不変オブジェクトのメリット

不変オブジェクトは以下に示すようなメリットを持つため、不必要にオブジェクトは可変とせずに不変オブジェクトの利用を検討すべきである。

不変オブジェクトは単純である

オブジェクトが生成された状態から変化しないことが保証されるため、クライアントは状態を理解し信頼して利用することが可能である。開発者もクライアントの利用を考慮して何かする必要がない。

不変オブジェクトは本質的にスレッドセーフである

複数のスレッドから並行してアクセスされても、状態が変化しないため同期を必要としない。すなわち制限なく共有できる。これを生かし、頻繁に利用される不変オブジェクトはpublic static finalの定数として提供したり、staticファクトリーメソッドで提供することが可能である。

また不変オブジェクトは、状態が変化しないため、コピーを行う必要がない。すなわちcloneメソッドやコピーコンストラクタを提供する必要はない。

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

継承はコードを再利用するための強力な方法だが、不適切に使用されると継承は脆いソフトウェアを生み出す。以下は継承が安全に利用できる場合である。

  • サブクラスとスーパークラスの実装が同じプログラマの管理下にあるようなパッケージ内で継承する場合
  • 拡張のために設計されてかつ文書化されているクラスを拡張する場合
  • クラスがインタフェースを実装したり、インタフェースが他のインタフェースを拡張した場合

一方、パッケージをまたがって普通の具象クラスから継承する場合は危険である。

サブクラスは適切に機能するために、スーパークラスの実装の詳細に依存する。スーパークラスの作成者が特に拡張される目的で設計および文書化を行なっていない限り、サブクラスはスーパークラスと一緒に変わっていかなければならない。

不適切な継承

不適切な継承の例として、HashSetを使用するプログラムを以下に示す。このクラスには格納している要素の数をカウントする機能を追加している。

public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

上記のコードは問題ないように見えるが、以下のようにaddAllメソッドで3つの要素を格納した後、getAddCountメソッドを呼び出すと6が返される。実際は要素数3なので、正しく動作していない。

InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
// 3が返ってくるのが正しいが、6が返される
s.getAddCount();

これはaddAllメソッドの内部でaddメソッドが呼び出されているためである。addメソッドとaddAllメソッドの両方で要素数が二重にカウントされている。

この問題はaddAllメソッドをオーバーライドしないことで解決することは可能である。しかし今後クラスを拡張する場合、HashSetaddAllメソッドがaddメソッドを使用しているという事実に依存することは変わらない。

また、継承したクラスで要素の追加時に何らかのバリデーションを実施するよう、オーバーライドをしたとする。この場合、実装時は問題が生じないかもしれないが、後のリリースでスーパークラスに要素を追加するための新しいメソッドが追加された場合、サブクラスでオーバーライドされていない新しいメソッドを利用することでバリデーションを回避することが可能である。

また、スーパークラスにサブクラスと同じシグネチャのメソッドが新たに追加された場合、戻り値の型が異なればサブクラスのコンパイルに失敗してしまうし、戻り値の型が同じであれば意図せずサブクラスでオーバーライドをすることで不適切な動作をするかもしれない。

要するに、パッケージを跨ったりドキュメントに記載がない意図されていない継承は、上記のような問題を引き起こしかねないため実施すべきでない。

継承における問題の回避方法

上記問題を全て回避するためには、コンポジションが有用である。コンポジションとは、既存のクラスを拡張する代わりに、既存のクラスのインスタンスを参照するprivateのフィールドを、新たなクラスに持たせることである。

新たなクラスでは、保持している既存クラスのインスタンスに対してメソッドを呼び出し、その結果を返すようにする。これは転送と呼ばれ、そのメソッドは転送メソッドと呼ばれる。

下記はInstrumentedHashSetコンポジションを利用して書き換えた例である。コンポジションを利用すれば、既存のクラスを変更したとしてもそれを保持しているInstrumentedHashSetには影響がない。

public class InstrumentedHashSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet(Set<E> s) {
        super(s);
    }
    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    public int getAddCount() {
        return addCount;
    }
}
// 再利用可能な転送クラス
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> set;
    public ForwardingSet(Set<E> set) { this.set = set; }

    @Override public int size() { return set.size(); }
    @Override public boolean isEmpty() { return set.isEmpty(); }
    @Override public boolean contains(Object o) { return set.contains(o); }
    @Override public Iterator<E> iterator() { return set.iterator(); }
    @Override public Object[] toArray() { return set.toArray(); }
    @Override public <T> T[] toArray(T[] a) { return set.toArray(a); }
    @Override public boolean add(E e) { return set.add(e); }
    @Override public boolean remove(Object o) { return set.remove(o); }
    @Override public boolean containsAll(Collection<?> c) { return set.containsAll(c); }
    @Override public boolean addAll(Collection<? extends E> c) { return set.addAll(c); }
    @Override public boolean retainAll(Collection<?> c) { return set.retainAll(c); }
    @Override public boolean removeAll(Collection<?> c) {return set.remove(c); }
    @Override public void clear() { set.clear(); }
}

上記のクラスは頑強さに加えて柔軟性も持ち合わせている。 InstrumentedSetクラスのコンストラクタには、以下の様にどのSet実装を渡しても問題がない。

Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(x));
Set<E> s2 = new InsrumentedSet<E>(new HashSet<E>(capacity));

これはInstrumentedSetインスタンスが既存のSetインスタンスをラップしているため、InstrumentedSetクラスはラッパークラスとも呼ばれる。またデコレータパターンとしても知られる。

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

継承されるクラスは、そのための設計及び文書化がなされていなければならない。これが具体的に何を指すかについて説明する。

オーバーライド可能なメソッドの自己利用について正確に文書化しなければならない

各メソッド/コンストラクタが、どのオーバーライド可能なメソッドをどのような順番で呼び出し、それが後の処理にどのように影響するかを正確に文書化しなければならないということ。一般的に「良いAPIドキュメンテーションは、あるメソッドが何をするのかを記述し、どのように行うかは記述すべきでない」と言われているが、安全にサブクラス化するためには例外的に許される。

賢く選択されたprotectedメソッドの形で、クラス内部の動作へのフックを提供しなければならないかもしれない

フックとは、プログラム中の特定の箇所に、利用者が独自の処理を追加できる様にする仕組み。例えばjava.util.AbstractListremoveRangeメソッドは、List実装の最終的なユーザーが直接利用することはないが、開発者はremoveRangeメソッドをオーバーライドすることでclearメソッドのパフォーマンスを向上させることが可能である。

removeRangeメソッドはclearメソッド内から呼び出され、デフォルト実装だと2次のオーダーである。)

どのようなフックを提供すべきかを簡単に知る手段はない。フックの1つ1つは実装の詳細に対する束縛となるため、必要最小限のフックを提供できるよう、開発者は実際にサブクラスを書いてテストを行い、十分に検討する必要がある。

コンストラクタはオーバーライド可能なメソッドを呼び出してはならない

スーパークラスのコンストラクタはサブクラスのコンストラクタより前に実行されるため、サブクラスでオーバーライドしているメソッドはサブクラスのコンストラクタより先に実行されることになる。そのため、オーバーライドしているメソッドがサブクラスのコンストラクタ内の処理に依存している場合、意図しない動作をすることになる。

これと同様の問題がCloneableインタフェースとSerializableインタフェースを実装した場合にも生じる。そのため継承のために設計しているクラスでは、CloneableインタフェースとSerializableインタフェースは実装すべきでない。

安全にサブクラス化するための設計及び文書化がされていないクラスのサブクラス化は禁止する

サブクラス化を禁止するには以下の2つの方法がある。

  • クラスをfinalと宣言する
  • すべてのコンストラクタをprivateかパッケージプライベートにして、代わりにpublicのstaticファクトリ〜メソッドを提供する

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

インタフェースは一般的に複数の実装を許す方を定義する最善の方法である。

抽象クラスとインタフェース

Javaでは複数の実装を許すクラスを定義するために、抽象クラスとインタフェースという2つの仕組みが提供されている。違いは以下。

  • 抽象クラスはメソッドの実装を含むことが許されるがインタフェースは許されない
  • 抽象クラスを実装するためには抽象クラスを継承する必要あり
  • インタフェースはクラス階層のどこでも実装可能

Javaでは単一継承のみが許されているため、抽象クラスの利便性を低下させている。なぜなら既に既存の抽象クラスを継承している場合、追加で新たな抽象クラスを実装することはできないため。

一方で新たなインタフェースを実装するように既存のクラスを変更することは容易である。具体的にはクラス宣言にimplements節を追加し、必要なメソッドを実装すればよい。

ミックスイン

インタフェースはミックスインを定義するには理想的。ミックスインとは、クラスが「本来の振る舞い」に加えて、追加で何らかの任意の振る舞いを提供するために実装するクラスのこと。例えばComparableインタフェースは、本来の機能に対して「順序付け」という振る舞いを追加するミックスインである。

前述した通り、抽象クラスは既存のクラスへ組み込むことができないため、ミックスインの定義には利用できない。

インタフェースは、階層を持たない型フレームワークの構築を可能にする。例えば歌手を表しているインタフェースとソングライターを表しているインタフェースがあると仮定する。実世界では、歌手でありソングライターであるという人もいて、その間に包含関係(階層)はない。インタフェースを用いれば、 SingerSongWriterの両方を拡張したSingerSongWriterを以下の様に定義することが可能である。

public interface Singer {
    AudioClip sing(Song s);
}

public interface Songwriter {
    Song compose(boolean hit);
}

public interface SingerSongwriter extends Singer, Songwrite {
    AudioClip sing(Song s);
    Song compose(boolean hit);
}

骨格実装

インタフェースはメソッドの実装を含むことはできないが、インタフェースに対応した骨格実装クラスを提供することで、実質的にデフォルト実装を提供することが可能。

以下では無名クラスとしてListの骨格実装(AbstractList)が提供されている。

static List<Integer> intArrayAsList(final int[] a) {
    if (a == null) {
        throw new NullPointerException();
    }

    return new AbstractList<Integer>() {
        public Integer get(int i) {
            return a[i]; // 自動ボクシング
        }

        @Override
        public Integer set(int i, Integer val) {
            int oldVal = a[i];
            a[i] = val; // 自動アンボクシング
            return oldVal; // 自動ボクシング
        }

        @Override
        public int size() {
            return a.length;
        }
    };
}

補足

Java8からインタフェースがデフォルト実装を持てるようになりました。これにより骨格実装をわざわざ別のクラスに分ける必要がなくなりました。(間違っていたら教えてください。)

項目19. 型を定義するためだけにインタフェースを使用する

インタフェースは型を定義するためだけに使用すべきである。定数を提供するために使用すべきではない。

定数インタフェース

以下はインタフェースの下手な使い方の一例。

// 定数インタフェース
public interface PhysicalConstants {
    // アボガドロ定数(1/mol)
    static final double AVOGADROS_NUMBER = 6.02214199e23;
    // ボルツマン定数(J/K)
    static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
    // 電子の質量(kg)
    static final double ELECTRON_MASS = 9.10938188e-31;
}

このような用途にはenum型を利用するのが正しい。

定数ユーティリティクラス

enum以外の定数の提供方法として定数ユーティリティクラスがある。定数ユーティリティクラスはインスタンス化不可能である。

public class PhysicalConstants {
     // インスタンス化を防ぐ
    private PhysicalConstants() {}
    // アボガドロ定数(1/mol)
    static final double AVOGADROS_NUMBER = 6.02214199e23;
    // ボルツマン定数(J/K)
    static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
    // 電子の質量(kg)
    static final double ELECTRON_MASS = 9.10938188e-31;
}

項目20. タグ付クラスよりクラス階層を選ぶ

タグ付きクラスが適切な場合は殆どない。タグ付きクラスを書こうとした時はクラス階層で表現できないか考え直すこと。

タグ付きクラス

以下はタグ付きクラスの例。

public class Figure {
    enum Shape { RECTANGLE, CIRCLE }

    // タグフィールド - 図の形
    final Shape shape;

    // shape が RECTANGLE の場合だけ、これらのフィールドは使用される
    double length;
    double width;

    // shape が CIRCLE の場合だけ、これらのフィールドは使用される
    double radius;

    // 円のためのコンストラクタ
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // 四角のためのコンストラクタ
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch (shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError();
        }
    }
}

上記ではShapeがタグフィールドである。タグ付きクラスはenum宣言やタグフィールド、switch文で可読性が低下する。またタグによっては必ずしも使用されないフィールドがインスタンスに含まれるため、メモリ量も増加する。要するに、タグ付きクラスは冗長で、誤りやすく、非効率である。

タグ付きクラスは以下のようにクラス階層で表現可能。これは上述の欠点を全て克服している。さらに新たなクラスSquareを追加することも容易であり、柔軟である。

// タグ付きクラスに対するクラス階層の置き換え
abstract class Figure {
    abstract double area();
}

public class Circle extends Figure {
    final double radius;
    Circle(double radius) {
        this.radius = radius;
    }
    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

public class Rectangle extends Figure {
    final double length;
    final double width;


    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    @Override
    double area() {
        return length * width;
    }
}

項目21. 戦略を表現するために関数オブジェクトを使用する

多くのプログラミング言語では、関数ポインタ、委譲、ラムダ式などの機構を利用して、関数を受け渡すことが可能である。

例えば、Cの標準ライブラリのqsort関数は、要素をソートするために要素を比較するコンパレータ関数へのポインタを引数で受け取る。異なるコンパレータ関数を渡すことで様々なソート順を得ることができ、これは要素のソートに関する戦略を表していると言える。(戦略パターン)

関数オブジェクトとは

Javaでは、オブジェクト参照を使用することで関数を渡すことができる。以下のように他のオブジェクトに対して何らかの操作を行うためのメソッドを1つだけ公開しているインスタンスは関数オブジェクトと呼ばれる。

class StringLengthComparator {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

具象戦略クラス

上記のStringLengthComparatorクラスのインスタンスは、文字列の長さに基づいて比較を行い順序づける、文字列比較に対する具象戦略クラスである。

具象戦略クラスの特徴として、フィールド(状態)を持たないことがあげられる。したがって全ての具象戦略クラスのインスタンスは機能的に同じであるため、生成コストを節約するためシングルトンにするのが適切である。

class StringLengthComparator {
    private StringLengthCoparator() {}
    public static final StringLengthComparator INSTANCE = new StringLengthComparator();
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

戦略インタフェース

StringLengthComparatorインスタンスをパラメータとして受け取る場合、そのパラメータの型をStringLengthComparatorとするのは不適切である。なぜならその他の具象戦略クラスのインスタンスを受け取ることができなくなるため。

このような場合は代わりにComparatorインタフェースを定義し、そのインタフェースをStringLengthComparatorが実装するようにする。この時Comparatorインタフェースを戦略インタフェースと呼ぶ。

public interface Comparator<T> {
    public int compare(T t1, T t2);
}

無名クラスを用いた具象戦略

具象戦略クラスは、多くの場合以下のように無名クラスを使用して宣言される。しかしこの方法は、呼び出しごとに新しいインスタンスが生成されることに注意すること。もし複数回実行されるのであれば、private static finalのフィールドに保存して再利用することを検討すべき。

Array.sort(array, new Comparator<String>() {
    public int compare(String s1, String s2)  {
        return s1.length() - s2.length();
    }
});

補足

Java8からラムダ式が導入されたので、無名クラスを使うよりも少ない記述量で関数オブジェクトを渡すことができるようになりましたね。

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

クラス内に定義されたクラスを「ネストしたクラス」と呼び、以下の4種類に分類される。

  • static のメンバークラス
  • 非 static のメンバークラス
  • 無名クラス
  • ローカルクラス

ネストしたクラスが、1つ以上のメソッドの外から見える必要がある、またはメソッド内に入れるには長すぎる場合はメンバークラスにする。メンバークラスの個々のインスタンスが、エンクロージングインスタンスへの参照が必要ならは非 static、そうでなければ static にする。

クラスがメソッド内に属しているべきであり、1箇所でのみ使用され、そのクラスを特徴付ける型がすでに存在している場合は無名クラスにする。そうでない場合はローカルクラスにする。

static のメンバークラス

エンクロージングクラスの static のメンバーであり、他の static のメンバーと同じアクセス可能性規則に従う。もし private である場合、エンクロージングクラス内からのみアクセス可能。

static のメンバークラスは、エンクロージングクラスのメンバーのすべて(private のメンバー含む)にアクセスできる。

エンクロージングクラスと一緒に使用すると有用な public のヘルパークラスとして利用されることが多い。

class Calcularor {
    static public enum Operation {
        PLUS() {...}
        MINUS() {...}
    }
}

非 static のメンバークラス

static のメンバークラスとの文法的な違いは、宣言にアクセス修飾子 static があるかないかのみ。

非 static メンバークラスの個々のインスタンスは、エンクロージングクラスと暗黙に関連付けされる。具体的には以下。

一般的な利用方法の1つは、以下のようなアダプターの定義手段として。

public class MySet<E> extends AbstractSet<E> {
    ...//大部分省略
    public Iterator<E> iterator () {
        return new MyIterator();
    }
    private class MyIterator implements Iterator<E>{
        ...
    }
}

エンクロージングインスタンスへアクセスする必要がないメンバークラスを宣言するのであれば、static 修飾子を常につけてstatic のメンバークラスとすべき。

private static のメンバークラスは、エンクロージングクラスが表すオブジェクトの構成要素を表現する時に一般的に利用される。

無名クラス

クラス名を持たず、エンクロージングクラスのメンバーではない。無名クラスは宣言と同時にインスタンス化される。式が許されている場所であれば、どこでも宣言可能。

ただし制限が多い。例えば、無名クラスが宣言された箇所以外で無名クラスをインスタンス化することはできない。また無名クラスのクライアントは、無名クラスがスーパータイプから継承しているメソッド以外を呼び出すことができない。

一般的な利用方法は、関数オブジェクトの生成手段として。(Java8 からはラムダ式の使用が可能となったので、利用シーンは減ったと思われる)

ローカルクラス

4種のネストしたクラスの中で、最も使用頻度が低いと思われる。ローカル変数が宣言できる場所であればどこでも宣言でき、ローカル変数と同じスコープ規則に従う。

Google Apps Script(GAS)で自作アプリを公開する

GASで簡単なWebアプリケーションを作って公開してみます!

作るもの

酒好きの友人と幹事当番制で月1で飲み会を開催していて、開催も20回を超えていろいろ美味しいお店も見つかったので、その飲み会の履歴を記録するページを作ってみようと思います。以下の様な機能を実装したい。

  • 飲み会履歴一覧機能
  • 飲み会履歴登録機能
  • 飲み会履歴編集機能

1つの飲み会履歴は以下のような属性を持つ想定。

  • 開催年
  • 開催月
  • 開催場所
  • 店名
  • URL
  • 参加者

検討事項

今回アプリを作成するに当たって検討が必要だった点は以下。

DBをどうするか

最初はスプレッドシートをDBに採用代わりにしようと思っていました。もともと飲み会の記録はスプレッドシートにメモ程度にとっていたので。

けど仮実装してみると、やはり機能が貧弱すぎる・・・。セルの範囲を処理のたびに指定するのがだるいし、DBみたいにSQLでソートや検索もできないし。

とはいえ、もともと手軽かつ無料でウェブアプリケーションが作れるというところに魅力を感じてGASを触ってみたので、Google Cloud SQLみたいな有料のサービスは使いたくない・・・。

どうしようと思っていたところで、Fusion Tablesというナイスなサービスを見つけました。

https://support.google.com/fusiontables/answer/2571232?hl=en

2017年7月現在試験運用ということですが、今回のアプリのユーザーは限られていますし、何より無料という点が非常に魅力的なので採用することを決定。

UIをどうするか

第三者に公開するということで、UIもある程度綺麗に作りたいところです。

調べてみたところCDNを介してBootStrapの利用が可能なようなので、BootStrapベースで作ることに決めました。

あとサーバー・UI間のデータのやりとりですが、以下の記事のサンプルを参考にしてMVVMライブラリであるknockout.jsを採用することにしました。

http://qiita.com/shibukawa/items/8fee715232e12f183698

実装

完成したものが以下になります(Fusion TablesはDEMO用に分けてあります)。 スマホから見るのを想定していて、PC用のCSSを作り込んでいないのでブラウザで見るとすごく拡大されると思います・・・。 デベロッパーツールとかでスマホ用の画面サイズにしてください・・・。

https://script.google.com/macros/s/AKfycbwBvUNboSuHQwFZ0PDuAUspfJZ5FkClStrMaZ5qIFx7hOjQPdq4/exec

Fusion Tables の作成・有効化

まずデータを格納するFusion Tablesを作ります。

oogleドライブにアクセスして 新規 > その他 > アプリを追加 を選択し、「fusion tables」で検索します。Google Apps Scriptが出てくるので追加し、ファイルを新規作成します。

f:id:uu64:20170706012549p:plain

そうするとNew Tableという新しいテーブルが表示されると思いますので、名前を適当に変えてカラムを追加しましょう。 メニューの Edit > Change columnsから列名の編集・追加が可能です。

f:id:uu64:20170706012704p:plain

Save ボタンを押して上記の操作を確定します。今回は、Year、Month、Location、Name、Member、Urlの6つのカラムを追加しました。

f:id:uu64:20170706012629p:plain

最後に、プロジェクトでFusion Tables APIを有効化する必要があります。 Google Apps Scriptのメニューのリソース > Googleの拡張サービスから Fusion Tables API を見つけ有効化します。

f:id:uu64:20170706012852p:plain

またGoogle API コンソールでも有効化する必要があります。ダイアログ下部のリンクからコンソールを開き、画面上部のAPIを有効にするをクリック、Fusion Table APIを検索して選択し、有効にするを選んでください。

f:id:uu64:20170706012832p:plain

これでFusion Tablesが利用可能になります。

サーバー側処理の実装

サーバー側処理(main.gs)の処理は以下になります。一番上の変数TABLE_IDには、作成したFusion TablesのURL末尾のdocidを指定してください。

例) https ://fusiontables.google.com/hogehoge?docid={ここの文字列}#rows:id=1

Fusion Tables操作のクエリは公式のドキュメントを見てもらえればわかりやすいと思います。 今回はknockout.jsで操作しやすいように、SQLのレスポンスを加工しています。

var TABLE_ID = "";
function doGet(e) {
  var template = HtmlService.createTemplateFromFile("Index.html");
  template.title = "飲み会 archive";
  template.data = getAllStores();
  return template.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

/** レコードの全取得 **/
function getAllStores() {
  var sql = "SELECT ROWID, Year, Month, Location, Name, Member, Url FROM " + TABLE_ID +
        " ORDER BY Year DESC, Month DESC",
      res = FusionTables.Query.sql(sql);
  return JSON.stringify(toHashArray(res));
}

/** レコードの追加 **/
function addStore(year, month, location, name, member, url) {
  var sql = "INSERT INTO " + TABLE_ID + 
        " (Year, Month, Location, Name, Member, Url) VALUES ('" + 
          year + "','" + month + "','" + location + "','" + name + "','" + member + "','" + url + "')";
  FusionTables.Query.sql(sql);
}

/** レコードの更新 **/
function updateStore(year, month, location, name, member, url, index) {
  var sql = "UPDATE " + TABLE_ID + 
        " SET Year = '" + year + 
        "', Month = '" + month + 
        "', Location = '" + location + 
        "', Name = '" + name + 
        "', Member = '" + member + 
        "', Url = '" + url + 
        "' WHERE ROWID = '" + index + "'";
  FusionTables.Query.sql(sql);
}

/** knockoutJSでバインドできるようSQLの返り値をhash形式に変換する **/
function toHashArray(res) {
  var hashArray = [],
      rows = res.rows,
      columns = res.columns,
      hash;
  for (var i in rows) {
    hash = {};
    for (var j in columns) {
      hash[columns[j]] = rows[i][j];
    }
    hashArray.push(hash);
  }
  return hashArray;
}

フロント側処理の実装

HTMLとJavaScriptCSSは以下になります。

bootstrapはCDNで読み込んでいて、ボタンデザインなど一部を上書きしています。

工夫したのは飲み会履歴の追加と更新の処理です。 どちらも共通のモーダルを用いていますが、追加処理の場合は openInsertModal 関数、 更新処理の場合は openUpdateModal 関数をモーダル起動と同時に呼び出し、モーダルのタイトルやボタンラベル、入力フィールドの値を更新しています。またhiddenフィールドに、処理の内容に応じて “add” または “update” という値を設定しています。

サーバーにpostする時は postStore 関数を呼び出します。上記で設定したhiddenフィールドの値を読み、更新時はサーバーの updateStore 関数、新規追加時は addStore 関数を呼び出します。これらの関数呼び出し時に、self.storesを一度空にしていますが、これは飲み会履歴の一覧を非表示にしユーザーの操作を防ぐためです。self.storesを空にするだけでUIが非表示となるのは、knockout.jsで監視をしているからです(self.stores = ko.observableArray(JSON.parse(data));の部分、詳細はknockout.jsのドキュメントを参照してください)。

また、サーバー処理中は「処理中・・・」のメッセージを表示する様にしています。これは通常時は見えない(display: none)メッセージ用のdivのcssを更新し、”display: block”にすることで実現しています。

サーバー処理が成功すれば、受け取ったレスポンスを再度self.storesに設定します。するとknockout.jsにより自動でUIが更新され、ユーザーには処理後のデータが表示されることになります。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title><?= title ?></title>
  <?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>
</head>
<body>
  <nav class="navbar navbar-fixed-top">
    <div class="container-fluid">
      <div class="navbar-header">
        <div class="navbar-brand"><?= title ?></div>
      </div>
    </div>
  </nav>
  <div class="container-fluid mainpanel">
    <div class="row">
      <div class="col-xs-12">
        <div class="tool">
          <button type="button" class="btn btn-success btn-lg btn-circle" data-toggle="modal" data-target="#storeModal" data-bind="click: openInsertModal">
            <i class="fa fa-plus fa-2x" aria-hidden="true"></i>
          </button>
        </div>
        <div class="panel panel-default" id="load-message">
          <div class="panel-body"></div>
        </div>
        <div data-bind="foreach: stores">
          <div class="panel panel-default">
            <div class="panel-body">
              <div class="edit-btn-div">
                <button type="button" class="btn btn-primary btn-lg btn-circle" data-toggle="modal" data-target="#storeModal" data-bind="click: $parent.openUpdateModal">
                  <i class="fa fa-pencil fa-2x" aria-hidden="true"></i>
                </button>
              </div>
              <div>
                <div><span data-bind="text: Year"></span><span data-bind="text: Month"></span></div>
                <div>開催場所: <span data-bind="text: Location"></span></div>
                <div>店名: <span data-bind="text: Name"></span></div>
              </div>
              <div>url: <a data-bind="attr: {href: Url}" target="_blank"><span data-bind="text: Url"></span></a></div>
              <div>参加者: <span data-bind="text: Member"></span></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  
  <div class="modal fade" id="storeModal" tabindex="-1">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h4 class="modal-title"></h4>
        </div>
        <div class="modal-body">
          <div><span class="require">*</span>: 入力必須</div><br>
          <form>
            <div class="form-group">
              <input type="hidden" id="mode">
              <input type="hidden" id="index">
              <label for="year-input"><span class="require">*</span></label>
              <select class="form-control" id="year-select">
                <option>2015</option>
                <option>2016</option>
                <option selected="selected">2017</option>
                <option>2018</option>
                <option>2019</option>
                <option>2020</option>
              </select>
            </div>
            <div class="form-group">
              <label for="month-input"><span class="require">*</span></label>
              <select class="form-control" id="month-select">
                <option selected="selected">1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
                <option>5</option>
                <option>6</option>
                <option>7</option>
                <option>8</option>
                <option>9</option>
                <option>10</option>
                <option>11</option>
                <option>12</option>
              </select>
            </div>
            <div class="form-group">
              <label for="location-input">開催場所</label>
              <input class="form-control" id="location-input"/>
            </div>
            <div class="form-group">
              <label for="name-input">店名 <span class="require">*</span></label>
              <input class="form-control" id="name-input"/>
            </div>
            <div class="form-group">
              <label for="url-input">url</label>
              <input class="form-control" id="url-input"/>
            </div>
            <div class="form-group">
              <label for="member-input">参加者</label>
              <input class="form-control" id="member-input"/>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-primary btn-lg modal-btn" data-bind="click: postStore"></button>
                <button type="button" class="btn btn-default btn-lg" data-dismiss="modal">キャンセル</button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
  <script>
    var data = <?= data ?>;
  </script>
  <?!= HtmlService.createHtmlOutputFromFile('JavaScript').getContent(); ?>
</body>
</html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/knockout/knockout-3.1.0.js"></script>
<script type="text/javascript">
  function storeVM() {
    var self = this;   
    self.stores = ko.observableArray(JSON.parse(data)); 
    self.openUpdateModal = function(store) {
      $(".modal-title").text("お店の更新");
      $(".modal-btn").text("更新");
      $("#mode").val("update");
      $("#index").val(store.rowid);
      $("#year-select").val(store.Year);
      $("#month-select").val(store.Month);
      $("#location-input").val(store.Location);
      $("#name-input").val(store.Name);
      $("#member-input").val(store.Member);
      $("#url-input").val(store.Url);
    };
    self.openInsertModal = function() {
      $(".modal-title").text("お店の追加");
      $(".modal-btn").text("追加");
      $("#mode").val("add");
      $("#index").val("");
      $("#year-select").val("");
      $("#month-select").val("");
      $("#location-input").val("");
      $("#name-input").val("");
      $("#member-input").val("");
      $("#url-input").val("");
    };
    self.postStore = function() {
      var mode = $("#mode").val(),
          index = $("#index").val(),
          year = $("#year-select").val(),
          month = $("#month-select").val(),
          location = $("#location-input").val(),
          name = $("#name-input").val(),
          member = $("#member-input").val(),
          url = $("#url-input").val();
      if (!year || !month || !name) {
        return false;
      }
      self.stores("");
      self.modalClose();
      $("#load-message").css("display", "block");
      $("#load-message .panel-body").text("処理中・・・");
      if (mode === "add") {
        google.script.run
          .withSuccessHandler(self.onPostSuccess)
          .addStore(year, month, location, name, member, url);
      }
      if (mode === "update") {
        google.script.run
          .withSuccessHandler(self.onPostSuccess) 
          .updateStore(year, month, location, name, member, url, index);
      }
    };
    self.onPostSuccess = function() {
      google.script.run.withSuccessHandler(function(data) {
        self.stores(JSON.parse(data));
        $("#load-message").css("display", "none");
      }).getAllStores();
    }
    self.onPostFailure = function() {
      $("#load-message .panel-body").text("処理に失敗しました。ページを再読み込みしてください。");
    }
    self.loadingView = function(flag) {
      $('#loading-view').remove();
      if(!flag) return;
      $('<div id="loading-view" />').appendTo('body');
    }
    self.modalClose = function() {
      $('body').removeClass('modal-open');
      $('.modal-backdrop').remove(); 
      $('#storeModal').modal('hide'); 
    }
  }
  $(document).ready(function () {
    ko.applyBindings(new storeVM());
  });
</script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<script src="https://use.fontawesome.com/a8b1ccabdb.js"></script>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
  <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
  <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<style>
body,.navbar-brand, .modal-title, .form-control, .modal-footer .btn {
  font-size: xx-large;
}
body {
  padding-top: 50px;
}
.navbar-brand { 
   font-weight: bold;
   color: #FFFFFF;
}
.navbar {
  padding-top: 30px;
  padding-bottom: 30px;
  padding-left: 5px;
  background-color: #000000;
}
.mainpanel {
  padding: 80px 15px;
}
.edit-btn-div {
  float: right;
}
.tool {
  padding-bottom: 10px;
  padding-right: 15px;
  text-align: right;
}
.year, .month, .location, .name, .member, th {
  text-align: center;
  vertical-align: middle !important;
}
.fa {
  margin: 10px;
}
.btn-circle {
  width: 84px;
  height: 84px;
  border-radius: 50px;
}
.btn-column {
  width: 46px;
}
.form-control {
  height: 60px;
}
.modal-dialog {
  width: 90%;
}
.modal-body {
  padding: 20px;
}
.modal-body-footer {
  text-align: right;
}
#load-message {
  display: none;
}
.require {
  color: red;
}</style>

感想

手軽に無料でWebサービスが作れるのはすごく魅力的です。がしかし、作り込もうとすればするほど物足りない点は出てきます。具体的には以下。

  • 基本画面遷移ができないので、アプリケーションで扱うモデル(今回はStoreのみ)が増えるときつい
  • 利用者数が多く、DBへのアクセスが多いのであれば、諦めてGoogle Cloud SQLなどを使うべき
  • CDN経由でないと他のライブラリが利用できない。最近流行りのフレームワークとか使おうと思っても使えない。
  • エディタが使いづらい(全角半角の見分けがつかないとかいろいろ)

要するに本格的にやりたくなったら諦めてお金を出すしかないと思います。 今回やってていろいろ機能追加したいなーと思ったので、そのうちS3, lambda, api gatewayとかのサーバーレス構成でAWSに乗せかえるかも。意外とお金かからないみたいだし。

否定的なことばかり書きましたが、ユーザー数が限られてて、すでにスプレッドシートとかで管理しているコンテンツは手軽にWebに公開できて便利だと思う。内輪のイベントのタイムテーブルの共有とか?せっかく勉強したので何か良い活用方法を探してゆきたい。

Google Apps Script(GAS)を試す

無料で簡単にWebアプリが公開できるGoogle Apps Scriptなるものがあると聞いて試してみました。

Google Apps Script を追加する

まずはGoogleドライブにアクセスします。

新規 > その他 > アプリを追加 を選択し、「script」で検索します。Google Apps Scriptが出てくるので追加し、ファイルを新規作成します。

f:id:uu64:20170630010843p:plain

GASプロジェクトを作成する

新規作成すると、無題のプロジェクトに「コード.gs」が表示されているはずです。 が、メニューのヘルプ > 開始画面 と選択し、左ペイン下部のウェブアプリケーションを選択します。

f:id:uu64:20170630010941p:plain

すると、以下の4ファイルがすでに生成されています。

  • コード.gs
  • Index.html
  • JavaScript.html
  • Styleshiit.html

コード.gs がサーバー側処理、それ以外が View になります。 JavaScriptCSSがhtmlファイルになっているのが気になるかもしれませんが、 GASでは現状このような形でしかJavaScriptCSSを分離することができません・・・。

プロジェクト名を適当に変えてください。とりあえず「Sample Project」としてみます。

Webアプリケーションを公開する

メニューから 公開 > ウェブアプリケーションとして導入 を選択します。

以下を選択して「導入」ボタンを押しましょう。 承認が必要です、というダイアログが出たら確認して承認してください。

  • プロジェクトバージョン: 新規作成
  • 次のユーザーとしてアプリケーションを実行: 自分
  • アプリケーションにアクセスできるユーザー: 全員(匿名ユーザーを含む)

なんとこれだけでWebアプリケーションが公開できます! 「現在のウェブアプリケーションのURL」が公開したWebアプリケーションのエンドポイントになります。

ちなみにURLをそのままブラウザにコピー&ペーストすると、自分のGoogleドライブのルート階層のファイル一覧が表示されるはずです。

URLパラメータにfolderIdを指定すると、Googleドライブにある特定のフォルダ直下のファイル一覧が見れるようになります。 folderIdというのは、Googleドライブ内のフォルダにアクセスした時のURL末尾についているランダムな文字列のことです。

例) https ://drive.google.com/drive/folders/XXXXXXX ←このXXXXXXXの部分です。

URLパラメータの指定方法ですが、アプリケーションのURLが「https ://example/hogehoge/」だとすると、以下のように末尾に指定します。

例) https ://example/hogehoge/?folderId=XXXXXXX

簡単にコードの解説

コード.gs 内の doGet 関数がアプリケーションのエンドポイントです。

HtmlService.createTemplateFromFile('Index') で、Index.html からViewを生成して返していますね。 そのあとのif文でリクエストのパラメータをチェックして、folderIdがなければ”root”に設定しています。

function doGet(e) {
  var template = HtmlService.createTemplateFromFile('Index');

  // Retrieve and process any URL parameters, as necessary.
  if (e.parameter.folderId) {
    template.folderId = e.parameter.folderId;
  } else {
    template.folderId = 'root';
  }

  // Build and return HTML in IFRAME sandbox mode.
  return template.evaluate()
      .setTitle('Web App Window Title')
      .setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

getFolderContents 関数でGoogleドライブにアクセスしています。

DriveApp.getRootFolder()DriveApp.getFolderById(folderId)で、フォルダの情報をごそっと取得しています。 で、そこからフォルダ名やら子要素を取り出しているわけです。

function getFolderContents(folderId) {
  var topFolder;
  var contents = {
      children: []
  };

  if (folderId == 'root') {
    topFolder = DriveApp.getRootFolder();
  } else {
    // May throw exception if the folderId is invalid or app
    // doesn't have permission to access.
    topFolder = DriveApp.getFolderById(folderId);
  }
  contents.rootName = topFolder.getName() + '/';

  var files = topFolder.getFiles();
  var numFiles = 0;
  while (files.hasNext() && numFiles < 20) {
   var file = files.next();
   contents.children.push(file.getName());
   numFiles++;
  }

  return contents;
}

この getFolderContents 関数を呼び出しているのが、JavaScript.html 内の無名関数です。

リクエスト失敗時と成功時のコールバックが含まれているのでやや複雑になっていますが、 google.script.run.getFolderContents(folderId)だけでも呼び出すことが可能です。

リクエスト成功時は、updateDisplay 関数でレスポンスから必要な情報を取り出してDOMに挿入しています。

  $(function() {
    // Call the server here to retrieve any information needed to build the page.
    google.script.run
       .withSuccessHandler(function(contents) {
            // Respond to success conditions here.
            updateDisplay(contents);
          })
       .withFailureHandler(function(msg) {
            // Respond to failure conditions here.
            $('#main-heading').text(msg);
            $('#main-heading').addClass("error");
            $('#error-message').show();
          })
       .getFolderContents(folderId);
  });
  ...
  function updateDisplay(contents) {
    var headingText = "Displaying contents for " + contents.rootName + " folder:";
    $('#main-heading').text(headingText);
    for (var i = 0; i < contents.children.length; i++) {
      var name = contents.children[i];
      $('#results').append('<div>' + name + '</div>');
    }
  }

まとめ

ざっくりとした説明ですが、とりあえずここまで。いろいろと制限はあると思いますが、無料でこれだけ簡単に個人でWebアプリを公開できるのはすごいことですよね。

HTMLによる View の生成、JavaScriptによるサーバー処理の呼び出しとUIの更新、Googleドライブとの連携と開発に必要なエッセンスが詰まっていると思います。

あとは公式のドキュメントを見ながら修正していくだけで、それなりのものが作れるでしょう。

公式ドキュメント: https://developers.google.com/apps-script/overview

ちなみに、メニューの 公開 > ウェブアプリケーションとして導入 からテスト用のURLも取得できます。 修正をした後はテスト用のURLで確認が可能ですし、もちろん第三者に公開しているURLには修正内容は見えません。

修正が済んで新しいバージョンとして公開したいときは、プロジェクトバージョンから新規作成を選んで「更新」ボタンを押してください。公開用URLに修正が反映されます。

次はこれを元にオリジナルアプリを作る予定。