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

項目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に修正が反映されます。

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

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

Objectクラスのequals、hashCode、toString、clone、finalizeはすべて オーバーライドされるように設計されているため、明示的な一般契約がある。

これに従わないと、この契約に依存しているHashMapやHashSetなど他のクラスが 適切に機能しない恐れがあるため、いつ、どのようにオーバーライドすべきか学びましょうと いう話。 (ComparableはObjectではないが同様の特徴をもつため、本章で扱う)

項目8. equalsをオーバーライドする時は一般契約に従う

equals()をオーバーライドすべきでない場合

以下の条件のいずれかに当てはまる場合は、equals()をオーバーライドしない方が適切。

  • クラスの個々のインスタンスは本質的に一意である
    • Threadなどのように、値よりは能動的な実態を表しているクラスの場合
    • Objectクラスの提供するequals()の振る舞いが正しい
      • Objectの提供するequals()の実装は以下になってる。参照が同じである=等しいと判断される。
public boolean equals(Object obj) {
    return (this == obj);
}
  • 「論理的等価性」検査をクラスが提供するかどうかに関心がない

    • 例えばRandomインスタンスがこれから同じ乱数列を生成するか検査をする必要はない
    • この場合もObjectの提供するequals()の振る舞いが正しい
  • スーパークラスがすでにequalsをオーバーライドしており、スーパークラスの振る舞いがこのクラスに対して適切である

    • 例えばSetならAbstractSet、ListならAbstractList、MapならAbstractMap
  • クラスがprivateあるいはパッケージプライベートであり、そのequalsメソッドが決して呼び出されないことが確かである

    • 万が一呼び出された場合に備えて、以下のように例外をスローするようオーバーライドする
@Override public boolean equals (Object obj) {
    throw new AssertionError();
}

equals()をオーバーライドすべき場合

クラスが論理的等価性という概念を持っていて、スーパークラスequals()をオーバーライドしていないとき。

  • 論理的等価性=論理的に同値であるのかを知りたい
    • オブジェクトの同一性(参照先が同じ)を確かめたいのではなく、持つ値が正しいのかを知りたい

equalsをオーバーライドする時は、以下の一般契約を厳守しなければならない

反射性

nullでない任意の参照値xに対して、x.equals(x)はtrueを返さなければならない。

  • オブジェクトがそれ自身と等しくなければならないということ
  • これを意図的に破ることは難しい

対称性

nullでない任意の参照値xとyに対して、y.equals(x)がtrueを返す場合のみ、x.equals(y)はtrueを返さなければならない。

以下の例のように簡単に破ることが可能なので注意。

public class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        if (s == null) {
            throw new NullPointerException();
        }
        this.s = s;
    }

    // 間違った例
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        if (o instanceof String)
            // 引数がStringの場合、大文字小文字を無視して比較しているが
            // Stringクラスでは無視しないため対称性が成り立たない
            return s.equalsIgnoreCase((String) o);
        return false;
    }

    // 正しい例
    @Override
    public boolean equals(Object o) {
        // クラスが異なる場合はfalseを返すのが正解(Stringは比較しない)
        return o instanceof CaseInsensitiveString &&
            ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
    }
}

推移性

nullでない任意の参照値x、y、zに対して、x.equals(y)y.equals(z)がtrueを返すなら、x.equals(z)はtrueを返さなければならない。

  • インスタンス化可能なあるクラスを拡張して、equals契約を守ったまま値要素を追加したサブクラスを実装する方法はないので注意
    • あるクラスに値要素を追加したい時は、継承ではなくコンポジションを用いるべきである(項目16)

例) 2次元座標を保持したPointクラスと、Pointクラスを継承して色情報を付加したColorPointというサブクラスを考える。

 // スーパークラス
public class Point {
    private final int x;
    private final int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point)) {
            return false;
        }
        Point p = (Point) o;
        return p.x == this.x && p.y == this.y;
    }
}

// サブクラス
public class ColorPoint extends Point {
    private Color color;
    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
}

このときColorPointクラスでequalsをオーバーライドしなければ、ColorPointのequals比較では色情報が無視されてしまうため、 色情報も一致したときにtrueを返すよう以下のように実装することを考える。

// ColorPointの誤ったequals()の例1
// 対称性が守られていない
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) {
        return false;
    }
    return super.equals(o) && ((ColorPoint) o).color == color;
}

これは以下のように比較を行った場合、対称性が守られないため不適切。

Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
p.equals(cp); // => true(色情報が無視される)
cp.equals(p); // => false

cp.equals(p)がtrueを返すよう、以下のようにColorPointクラスとPointクラスの比較の際は色情報を無視するよう実装すると、推移性が守られない。

// ColorPointの誤ったequals()の例1
// 推移性が守られていない
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) {
        return false;
    }
    // oがPointクラスの場合は、Pointクラスのequals()で色情報を無視した比較を行う
    if (!(o instanceof ColorPoint)) {
        return o.equals(this);
    }
    return super.equals(o) && ((ColorPoint) o).color == color;
}

これは以下のように比較を行った場合、推移性が守られないため不適切。

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1.equals(p2); // => true
p2.equals(p3); // => true
p1.equals(p3); // => false

以下のようにコンポジションを用いることで、equals契約を守ったままPointクラスに色情報を付加できる。 (ColorPointクラスはPointのサブクラスではないためcp.equals(p)p.equals(cp)もfalseになる)

public class ColorPoint {
    private Point point;
    private Color color;
    public ColorPoint(int x, int y, Color color) {
        if (color == null) {
            throw new NullPointerException();
        }
        point = new Point(x, y);
        this.color = color;
    }
    // ビューを返す
    public Point asPoint() {
        return point;
    }
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint)) {
            return false;
        }
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

ただし抽象クラスのサブクラスには、equals契約を守ったまま値要素の追加が可能。 (スーパークラスインスタンス化されないため、サブクラスと比較されて推移性に違反することがないため)

整合性

nullでない任意の参照値xとyに対して、オブジェクトに対するequals比較に使用される情報が変更されなければ、 x.equals(y)は常に同じ値を返し続けなければならない。

  • 信頼できない資源に依存するequalsメソッドを書いてはならない
    • 例) java.net.URLのequalsメソッドは、比較対象URLに関連づけられたホストのIPに依存している
      • ホストをIPに変換する時ネットワークへのアクセスが必要であり、常に同じ結果が保証されないため不適切
    • メモリ中のオブジェクトのみに依存する処理を行うべき

非null性

nullでない任意の参照値xに対して、x.equals(null)はfalseを返さなければならない。

  • nullが渡されたときにNullPointerExceptionがスローされるのは契約違反なので注意(falseを返すようにする)

高品質なequalsメソッドの書き方

  1. 引数が自分自身のオブェクトであるかどうかを検査する場合は == を利用する
  2. 引数が正しい型であるかを調べるためには instanceof を使う。
    • 正しい型とは、普通はメソッドが定義されているクラスである。場合によっては、このクラスによって実装されているインターフェース。
  3. 引数を正しい型にキャストする
  4. そのインスタンスのフィールドと、与えられたオブジェクトのフィールドを検査する
    • 基本データ型には==を、オブジェクトには再帰的にequalsを用いる
    • ただし、floatとdoubleにはFloat.compareDouble.compareを利用する。
    • 配列のフィールドはArray.equalsが利用できる。
  5. 「対称性」、「推移性」、「整合性」の性質を満たしたかどうかのテストを書く
    • 「反射性」と「非 null 性」は心配しなくても大抵の場合満たされる。

項目9. equalsをオーバーライドする時は、常にhashCodeをオーバライドする

equalsをオーバーライドしているすべてのクラスで、hashCodeをオーバーライドしなければならない。

  • 上記を守っていないクラスがHashmap、HashSet、Hashtableを含む、すべてのハッシュに基づくコレクションで用いられると適切に機能しない

守らなければならない契約は以下。

  1. equals比較で使用されるオブジェクトの情報が変更されない限り、hashCodeメソッドは同じオブジェクトに対して常に同じ整数を返さなければならない
  2. 2つのオブジェクトがequals比較により等しければ、2つのオブジェクトの個々に体するhashCodeメソッド呼び出しは、同じ整数を返さなければならない
  3. 2つのオブジェクトがequals比較により等しくない場合、2つのオブジェクトの個々に体するhashCodeメソッド呼び出しが別の整数を返さなければならない、ということは要求されない。しかし別の整数を返す場合、ハッシュテーブルのパフォーマンスが改善される可能性がある。

hashCodeをオーバーライドするのを忘れた場合、上記2が破られる。

例) hashCodeをオーバーライドしていないPhoneNumberClass

public class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "areaCode");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    private static void rangeCheck(int arg, int max, String name) {
        if (arg < 0 || max < arg) {
            throw new IllegalArgumentException(name + ": " + arg);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;

        PhoneNumber phoneNumber = (PhoneNumber) o;
        return phoneNumber.lineNumber == this.lineNumber
                && phoneNumber.prefix == prefix
                && phoneNumber.areaCode == areaCode;
    }

    // hashCode がない!
}

putgetで利用される2つのPhoneNumberクラスのハッシュコードが一致しないため、下記のようにHashMapが正しく動作しない。

Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
m.get(new PhoneNumber(707, 867, 5309)); // => null

以下の手順に従うことで、理想的なハッシュ関数を計算することが可能。

  • 何らかのゼロでない定数、例えば 17 をresultという int 変数に保存する。
  • オブジェクト内のequalsメソッドで考慮されるフィールドfに対して以下のことを行う。
    • 各フィールドに対する intのハッシュコードcを計算する。
      1. boolean ならば、(f ? 0: 1)を計算する。
      2. byte, char, short, int ならば(int) fを計算する。
      3. long ならば、(int)(field ^ (f >>> 32))を計算する。
      4. float ならば、Float.floatToIntBits(f)を計算する。
      5. double ならばDouble.doublToLongBits(f)を計算し、結果の long を6と同様にハッシュする。
      6. オブジェクト参照で、equals内でフィールドをequals再帰的呼び出しによって比較しているならば、再帰的にhashCodeを呼び出す。フィールドがnullなら0を返す。
      7. 配列ならば各要素を別々のフィールドとして取り扱い、各意味のある要素に対して再帰的にこれらの規則を適用してハッシュを計算する。要素全てに意味がある場合、Arrays.hashCodeメソッドの利用が可能。
    • 上記で計算したcresult = 31 * result + c;の式に代入する。
  • resultを返す。
  • 等しいインスタンスが等しいハッシュコードを持つかどうかを自問し、単体テストを書く。

もしクラスが不変でハッシュコードを計算するコストが高いならば、オブジェクト内にハッシュコードをキャッシュしておくことを検討すべきである。

もしそのクラスのオブジェクトのほとんどがハッシュキーとして利用される場合、以下のいずれかの対策をすべきである。

  • インスタンス生成時にハッシュコードを計算する
  • 最初にhashCodeが呼び出された時に、遅延初期化をする(項目71)

項目10. toStringを常にオーバーライドする

toStringメソッドのデフォルト実装は、"クラス名@ハッシュコードの符号なし16進数表現"の文字列を返す。

→ 分かりづらいので、オーバーライドしてオブジェクトに含まれる有益な(ユーザーが興味があるであろう)情報を全て返すようにすべき。

toStringをオーバーライドする時に考慮すべき点として以下が挙げられる。

  • ドキュメンテーションjavadoc)で戻り値の形式を明示するかどうか
    • 明示することでユーザーに正確な表現形式を伝えることができる
    • 一方で、明示した表現形式に依存した利用がされている可能性を考えると、その後の仕様変更がしづらくなるというデメリットもある
  • 明示する、しないに関わらず、その意図は正確にjavadocに記載すべき
  • toStringの戻り値に含まれる全ての情報への、プログラミングによるアクセス手段を提供すべき

項目11. cloneを注意してオーバーライドする

cloneの一般契約は以下の通り。

  • 「コピー」の正確な意味はオブジェクトのクラスに依存する
  • 大まかな意図は以下が成り立つことだが、絶対的な要件ではない
    • 任意のオブジェクト x に対してx.clone() != xが true であること
    • またx.clone().getClass() == x.getClass()も true であること
    • x.clone().equals(x)が true であること
    • そのクラスの新たなインスタンスを生成することに加え、内部データ構造のコピーも必要な場合がある
    • コンストラクタは呼び出されない

cloneメソッドをオーバーライドするならば、super.cloneにより得られたオブジェクトを返すべき

以下の例を考える。

public class Data1 implements Cloneable {
    ...
    @Override
    public Object clone() throws CloneNotSupportedException {
        return new Data1(x);
    }
    ...
}
 
public class Data2 extends Data1 {
    ...
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    ...
}

上記の場合、Data2.cloneを呼び出すとsuper.cloneによりData1のコンストラクタが返却される。 したがって「x.clone().getClass() == x.getClass()も true であること」が成り立たない。 これはData1クラスでも同様に、clonesuper.cloneを返すようにすることで解決する。

また上記でcloneメソッドのアクセス修飾子が public となっていることも大切である。 本書に書いてある通り、Cloneableを実装しているクラスは、適切に機能している public のcloneメソッドを提供することが期待されている。

これらを踏まえて、java1.5からは下記のようにcloneメソッドを修正することができる。

public class Data1 implements Cloneable {
    ...
    @Override
    public Data1 clone() {
        try {
            return (Data1)super.clone();
        } catch(CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
    ...
}
 
public class Data2 extends Data1 {
    ...
    @Override
    public Data2 clone() {
        try {
            return (Data2)super.clone();
        } catch(CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
    ...
}

上記の修正したcloneメソッドにおいて、注目すべき点が3点ある。

  • 修正したcloneの戻り値型がData1/Data2になっている
    • java1.5から導入された共変戻り値型によるもの(オーバーライドしたメソッドの戻り値型は元のメソッドの戻り値型のサブタイプであってもよい)
  • super.cloneの戻り値は親クラスであるため、キャストを忘れずに行う(キャストはクライアントでなくライブラリーですべき処理)
  • CloneNotSupportedExceptionのスロー宣言が削除されている
    • チェックされる例外をスローしない方が使い勝手が良い(publicのcloneメソッドはスローしないべき)
    • 継承されることを想定したクラスでcloneをオーバーライドする場合、cloneを protected と宣言し、CloneNotSupportedExceptionのスロー宣言をし、Cloneableは実装しないようにする(Object.cloneをまねるべき)

フィールドに可変オブジェクトが含まれる場合は、再帰的にcloneを呼び出す

下記の例を考える。

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    @Override
    public Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
    ...
}

上記のcloneメソッド内のsuper.cloneでは、elementsフィールドの参照がコピーされるだけである。 (浅いコピー。元のオブジェクトとcloneオブジェクトで同一の配列が共有されてしまう。) したがってelements.clone()のように、再帰的にcloneを呼び出す必要がある。

しかしelementsフィールドがfinalの場合、新たな値を代入することができないため正しく機能しない。 この場合は、フィールドのfinal修飾子を取り除く必要がある。(cloneアーキテクチャは可変オブジェクトを参照している final のフィールドと両立しない)

内部クラスを保持するなど、クラスの構造が複雑な場合、上記のように再帰的にcloneを呼び出すだけでは不十分な場合もある。 その時はsuper.cloneを呼び出し、返されたオブジェクトのすべてのフィールドを初期化した上で、オブジェクトの状態を再現するために 「深いコピー」を実現するための処理を呼び出すべき。

一般的に、クラスが基本データ型のフィールドか不変オブジェクトへの参照しか保持していない場合、上記のような「深いコピー」のための処理は不要。

cloneが本当に必要かよく検討する

これまで述べたようにcloneの実装は気をつけるべき点が多く複雑であり、本当に必要なケースは稀である。 (配列のコピー用途以外ではcloneを利用しないという人も多い)

以下のいずれかの手段で代替できないかよく検討する。

  • コピーコンストラクタかコピーファクトリーを提供する java public Yum(Yum yum); //コピーコンストラクタ public static Yum newInstance(Yum yum); //コピーファクトリー
  • そもそもオブジェクトの複製機能を提供しない

項目12. Comparableの実装を検討する

Comparableを実装することで、インスタンスが自然な順序を持っていることを明示する。

→アルファベット順、数値順、年代順など、明らかに自然な順序を持つ値クラスを書く場合、Comparableを実装しcompareToメソッドを提供する。

compareToメソッドの一般契約は以下。これを守らないと、compareToに依存しているTreeSetTreeMap、検索とソートアルゴリズムを含む CollectionsArraysが正しく機能しない。

sgn(expression)は、expressionが負の場合 -1、ゼロの場合 0、正の場合 1を返す関数)

  • すべての x と y に関してsgn(x.compareTo(y)) == -sgn(y.compareTo(x))を保証しなければならない
    • x.compareTo(y)が例外をスローする( y のクラスは x と比較できない)場合のみ、y.compareTo(x)は例外をスローしなければならない
  • (x.compareTo(y) > 0 && y.compareTo(z) > 0)がtrueならばx.compareTo(z) > 0もtrueでなければならない
  • すべての z に関してx.compareTo(y) == 0ならばsgn(x.compareTo(z)) == sgn(y.compareTo(z))でなければならない
  • (x.compareTo(y) == 0) == x.equals(y)は強く推奨されるが必須ではない(破る場合は明示すべき)

上記制限はequals制限と同じく、反射性、対称性、推移性に従わなければならないことを示している。 したがってequalsと同様に、インスタンス化可能なクラスを拡張してcompareTo契約を守ったまま新たな値要素を追加する方法はなく、コンポジションが推奨される。

compareToは以下にしたがって実装すると良い。

  • 基本データ型フィールドは関係演算子><で比較する
  • 浮動小数点の比較ではDouble.compareFloat.compareを使用する
  • 配列は個々の要素に対して上記を適用する
  • 比較するフィールドが複数ある場合、最も意味のあるフィールドから順に比較を行う
    • 例えば電話番号の場合、市外局番を先に比較すれば、残りの番号の比較が不要となる可能性がある

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

入社3年目になりましたが、全然Javaの基礎できてないなと思いまして、 effective javaの学習を始めました。まず第1章から。

項目1. コンストラクタの代わりにstaticファクトリーメソッドを検討する

staticファクトリーメソッドとは

クラスのインスタンスを返す単なるstaticのメソッド。

  • 例) 基本データ型booleanに対応するボクシングされた基本データクラスBooleanを返す
    • ボクシング:基本データ型の変数をそのラッパークラスのインスタンスに変換(逆はアンボクシング)
public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}

長所

コンストラクタと異なり名前を持つ

同じシグニチャを持つコンストラクタが複数必要な時は、シグニチャの順番を変えるしかない。

  • Hoge(int num, String str)とHoge(String str, int num)
    • 外部からはコードが何をしているのか分からない
  • Hoge.methodA(int num, String str)とHoge.methodB(int num, String str)
    • メソッド名で内部処理の違いを区別できる

メソッドが呼び出されるごとに新たなオブジェクトを生成する必要がない

あらかじめメンバーとして生成しておいたインスタンスや、オブジェクト生成時にキャッシュしておいたインスタンスを返すことで 不必要に重複したオブジェクトの生成を回避可能。

→オブジェクト生成のコストが高く、かつ頻繁に要求される場合は、パフォーマンスの大幅な向上につながる

メソッドの戻り値型の任意のサブタイプのオブジェクトを返却可能

  • staticファクトリーメソッドの戻り値型を、とか抽象インタフェースとかにする
  • 上記メソッドから返すクラスの実装を内部クラスとして持つ
    • 実装の隠蔽が可能
    • 複数のクラスを返却可能なので、柔軟なAPIの設計が可能になる
// java.util.Collectionsの抜粋
...
public class Collections {
    // インスタンス化不可能
    private Collections {
    ...
    // staticファクトリ〜メソッド
    public static <K,V> Map<K,V> More ...unmodifiableMap(Map<? extends K, ? extends V> m) {
        return new UnmodifiableMap<K,V>(m);
    }
    // 返却するクラスの実装
    private static class More ...UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
}

パラメータ化された型のインスタンス生成の面倒さを低減する

Java7からはダイヤモンド構文が導入されたので、おそらく解決した

// ダイヤモンド構文
Map<String, List<String>> m = newHasMap<>();

以前は以下。

// 冗長で面倒
Map<String, List<String>> m = newHasMap<String, List<String>>();
public static <K, V> HashMap<K, V> newInstance() {
    return new HashMap<K, V>();
}
// 簡潔になる
Map<String, List<String>> m = HashMap.newInstance();

短所

publicあるいはprotectedのコンストラクタを持たないクラスのサブクラスを作れない

容易に他のstaticメソッドと区別がつかない

  • コンストラクタほど目立たない(その他のメソッドと同列扱い)
    • valueOfなどstaticファクトリーメソッドでよく利用される命名法に従うことで軽減する

項目2. 数多くのコンストラクタパラメータに直面した時にはビルダーを検討する

数多くのコンストラクタパラメータが必要な時、実現方法としては以下の3つが考えられるが、 読みやすさ、書きやすさ、安全性の観点からビルダーパターンを検討すべきである。

  • テレスコーピングコンストラクタ・パターン
  • JavaBeansパターン
  • ビルダーパターン

テレスコーピングコンストラクタ・パターン

いくつかの必須パラメータとn個のオプションパラメータが存在する時 以下のように複数のコンストラクタをオーバーロードする。

  • 必須パラメータだけを受け取るコンストラクタ
  • 必須パラメータ+0個のオプションパラメータを受け取るコンストラクタ
  • 必須パラメータ+1個のオプションパラメータを受け取るコンストラクタ
  • 必須パラメータ+n個のオプションパラメータを受け取るコンストラクタ

上記のうち、設定したいパラメータを全て含む最も短いパラメータリストを持つ コンストラクタを利用していくことになるが、大抵不要なパラメータも含まれることになる。

→可読性の低下、パラメータの順序を間違える恐れ

JavaBeansパターン

パラメータなしのコンストラクタを呼び出し、その後必要なパラメータと対応したセッターを呼び出し パラメータを設定する。

→生成が複数の呼び出しに分割されるので、その生成過程の途中で不整合な状態にある恐れ

ビルダーパターン

  1. 対象のオブジェクトを直接生成する代わりに、必須パラメータをすべてもつビルダーオブジェクトを生成する。
    • ビルダーは生成するクラスのstaticメンバークラス
  2. オプションパラメータはビルダーオブジェクトの持つセッターのようなメソッドから追加する。
  3. ビルダーオブジェクトの持つbuild()メソッドから生成するクラスのコンストラクタを呼び出し、対象のオブジェクトを生成する。
// 例: Member member = new Member.Builder("Taro").age(25).build();
public class Member {
    private final String name;
    private final int age;
    public static class Builder {
        private final String name; //必須パラメータ
        private int age = 20; //オプションパラメータはデフォルト値で初期化
        public Builder(String name) {
            this.name = name;
        }
        public Builder age(int age) {
            this.age = age;
            return this;
        }
        public Member build() {
            return new Member(this);
        }
    }
    private Member(Builder builder) {
        this.name = Builder.name;
        this.age = Builder.age;
    }
}

欠点

  • オブジェクト生成時に必ずビルダーオブジェクトの生成が必要
    • シビアなパフォーマンスが要求される場合は注意する
  • テレスコーピングコンストラクタ・パターンより長くなりがち
    • 例えばパラメータ数4以上など、パラメータが多い場合にだけ利用すべき
    • 途中からビルダーパターンへ変更は難しいため、将来的にパラメータが増えそうな場合は 最初からビルダーパターンを検討すべき

項目3. privateのコンストラクタかenum型でシングルトンを強制する

シングルトンとは

一度しかインスタンスが作成されないクラス

→コード中の同じクラスのインスタンスは全て同一であることが保証される

実現方法1:privateなコンストラクタ + finalのフィールド (+ staticファクトリーメソッド)

以下ではfinalのフィールド初期化時にのみ、コンストラクタが呼び出される

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; }
    ...
}

上記はAccessibleObject.setAccessibleメソッドを使用して、リフレクションにより privateなコンストラクタを呼び出すことが可能。

  • コンストラクタを修正して2つ目のインスタンス生成時は例外を返すなどして回避

また、シリアライズ可能にする場合、ディシリアライズ毎に新たなインスタンスが生成されることを防ぐために 次のメソッドを追加する。

private Object readResolve() throws ObjectStreamException {
    // ディシリアライズ時にINSTANCEを返却して、重複生成を防ぐ
    return INSTANCE;
}

リフレクションとは

「実行時にプログラム自身の情報を取得/振る舞いを変更する」仕組み。 (リフレクションがどうしても必要なケースがいまいち分からない)

Class cl = Class.forName("Foo"); //オブジェクト取得
Method method = cl.getMethod("hello"); //メソッド取得
method.invoke(cl.newInstance()); //メソッド実行
  • JPCERTのセキュアコーディングスタンダードには 「リフレクションを使ってクラス、メソッド、フィールドのアクセス範囲を広げない」という項目がある

実現方法2:Enum

実現方法1よりこちらを選ぶべき。

public enum Elvis {
    INSTANCE;
    ...
}

項目4. privateのコンストラクタでインスタンス化不可能を強制する

1つの明示的なprivateなコンストラクタを含むことでインスタンス化を防ぐ。

  • 明示的にコンストラクタを書かないと、コンパイラによりデフォルトコンストラクタが 追加されてしまう
public class UtilityClass {
    private UtilityClass() {
        throw new AssertionError();
    }
    ...
}

項目5. 不必要なオブジェクトの生成を避ける

機能的に同じオブジェクトが必要なときは、1つのオブジェクトを再利用するほうが大抵適切である。

極端にだめな例

String s = new String("stringette") //bad
String s = "stringette"

bad:

“stringette"自体がStringオブジェクトであり、Stringコンストラクタで生成されるオブジェクトと同等

→二重にオブジェクトが生成されてしまう

good:

言語仕様上、同じ内容の文字列リテラルで生成されるインスタンスは再利用することが保証されている。(同一JVM上のみ)

→"stringette"は一度しか生成されず、再利用される

不変クラスの再利用

  • staticファクトリーメソッドを利用すれば大抵、不必要なオブジェクト生成は回避可能
    • 例) Boolean.valeOf(String)

変更されない可変オブジェクトの再利用

  • static初期化子で回避可能

オートボクシングに注意

オートボクシングにより、基本データ型が自動で対応するラッパークラスに変換される

→不要なオブジェクトが増える可能性

Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
    sum += i; //iがLong型にオートボクシングされる
}

防御的コピーは別

何でも再利用すればよいわけではない。 防御的コピーが求められる場合、オブジェクトの再利用が悪質なバグやセキュリティホールにつながる可能性があるので注意。(項目39)

項目6. 廃れたオブジェクト参照を取り除く

廃れた参照

今後それを通してオブジェクトが参照されることのない単なる参照

  • 本文中の例ではelements配列のsize外にある要素

→いらなくなったらnullを代入することで、スタックの利用クライアントで参照がなくなればGCされるようになる - 参照が間違っている場合、NullPointerExceptionでエラーを伝えることもできる

メモリリークを気をつけるべきとき

とにかくnullを入れればよいわけではない。廃れた参照に対する最善の方法は、参照が含まれていた変数を スコープの外に出すこと。

以下のような場合にメモリリークを注意すべき。

  • クラスが独自のメモリを管理しているとき
    • 本文中コードだとStackのelements
  • キャッシュ機構をもつとき
    • WeakHashMapでキャッシュを表現するのが望ましい
      • エントリーが廃れると(キーへの外部からの参照がなくなると)自動的に取り除かれる
  • リスナーやコールバックを持つクラス
    • キャッシュと同じくWeakHashMapで解決可能

項目7. ファイナライザを避ける

ファイナライザは予測不可能で、大抵危険であり、一般には必要ない

  • JPCERTのセキュアコーディングスタンダードには 「ファイナライザは使わない」という項目がある

ファイナライザ: GCによりインスタンスが破棄されるタイミングで実行されるメソッド

  • 即座にファイナライザが実行される保証がない
    • 時間的に制約のあることをファイナライザで行うべきではない
    • 実行タイミングはJVMごとのGCアルゴリズムに依存する
  • 実行されるかどうかも分からない
    • 重要な処理をファイナライザに頼ると実行されない可能性がある
    • 例1) データベースの共有ロックの解除
    • 例2) ファイナライザ中の例外は無視されて、そのままファイナライズは終了する
      • 他のオブジェクトへの悪影響を与える可能性もある
  • パフォーマンスも悪い
  • サブクラスのファイナライザは手作業でスーパークラスのファイナライザを呼び出す必要あり

→リソースの開放はファイナライザに頼らず、try-finallyのfinallyブロックで明示的に終了させる

ファイナライザが有効な場合

以下に対しては有効ではあるが、明示的終了のほうがよりよい。

  • 安全ネットとして
    • 明示的な終了を忘れた場合に備えて、保険的にファイナライザを用いる
      • ただし明示的終了がされずファイナライザが呼ばれたときは警告ログを出力すべき
  • ネイティブピアに対して

ファイナライズ連鎖とファイナライザガーディアン

サブクラスから、スーパークラスのファイナライザの呼び出しは手動で実施する必要がある。

→継承される可能性のあるクラスでファイナライズを用いる場合、 ファイナライザガーディアンと呼ばれる無名クラスを検討したほうがよい。

ファイナライザガーディアンとは

  • ファイナライザ処理を含み、スーパークラスとなりうるクラスの内部クラスとして記述する
  • ファイナライザガーディアンの中にスーパークラス自身のfinalize処理を書いておく
  • サブクラスでスーパークラスのfinalize処理を忘れたとしても、内部クラスとして定義されている ファイナライザガーディアンによってスーパークラスはfinalize処理される

macでbashからzshへ乗り換えた話

今までずっとbashを使ってきましたが、補完が強力とか作業がいろいろ捗るとか聞くので、重い腰を持ち上げてzshに乗り換えることを決めました。

環境

  • MacOS Sierra 10.12.3
  • Homebrew 0.9.9

zshインストール

Homebrewは入れている前提です。

以下のコマンドでzshをインストールします。zsh-completionsは、zshの補完機能を強化するプラグインです。

$ brew install zsh
$ brew install zsh-completions

ログインシェルの変更

/etc/shellにログインシェルにできるプログラムがフルパスで記述されています。インストールしたzshのパスを追記します。

$ sudo sh -c "echo '/usr/local/bin/zsh' >> /etc/shells"

シェルを変更します。パスワードを入力後、ターミナルを再起動します。

$ chsh -s /usr/local/bin/zsh

初期設定

再起動後、以下のようなメッセージが表示されます。

This is the Z Shell configuration function for new users,
zsh-newuser-install.
You are seeing this message because you have no zsh startup files
(the files .zshenv, .zprofile, .zshrc, .zlogin in the directory
~).  This function can help you with a few settings that should
make your use of the shell easier.

You can:

(q)  Quit and do nothing.  The function will be run again next time.

(0)  Exit, creating the file ~/.zshrc containing just a comment.
     That will prevent this function being run again.

(1)  Continue to the main menu.

--- Type one of the keys in parentheses ---
  • (q): 何もしません。次回ターミナル起動時に同じメッセージが再度表示されます。
  • (0): ホームディレクトリ以下に.zshrcが作成されます。ここに設定を自分で書き込む方式です。
  • (1): 対話式メニューで.zshrcの設定を決定していく方式です。

0を選択します。

zplugのインストール

zshプラグインマネージャであるzplugをインストールします。

zshプラグインマネージャといえばoh-my-zshやantigenも有名みたいですが、 参考にもあるようにパフォーマンス等の面でzplugが最近は流行っているよう。

$ brew install zplug

zplugのREADMEに従って~/.zshrcに設定を追加します。

# brewのインストールパスを設定する
export ZPLUG_HOME=/usr/local/opt/zplug
source $ZPLUG_HOME/init.zsh
# pluginの追加
## zsh-completions
zplug "zsh-users/zsh-completions"¬
fpath=(/usr/local/opt/zplug/repos/zsh-users/zsh-completions $fpath)¬
## git
zplug "plugins/git", from:oh-my-zsh
## 未インストール項目をインストールする
if ! zplug check --verbose; then
  printf "Install? [y/N]: "
  if read -q; then
       echo; zplug install
  fi
fi
## コマンドをリンクして、PATH に追加し、プラグインは読み込む
zplug load --verbose

上記で追加しているのは、git-completionsとoh-my-zshのgitプラグインです。

git-completionsは、vagrantコマンドやrailsコマンドなど、zshの補完を強化するプラグインです。 上記のみで補完が有効にならない場合は以下のコマンドも実行します。

$ rm -f ~/.zcompdump; compinit

oh-my-zshのgitプラグインは大量のgitのaliasや関数を読み込みます。(例:git status -s > gss) 全部はなかなか覚えられませんが、仕様頻度の高いaliasを使うだけでも意外と快適です。

vcs_infoの設定

vcsとはVersion Control Systemsの略です。 Gitとかバージョン管理システムの情報をターミナルに表示することが可能になります。

今回はシンプルにgitレポジトリ内ではブランチ名を表示するようにします。

# vcs_info
autoload -Uz vcs_info
## branchの表示
zstyle ':vcs_info:*' formats '(%b)'
precmd() { vcs_info }

#prompt
PROMPT='%n@%m: %~ ${vcs_info_msg_0}'

その他設定

上記に加えて、lessやgrepのグローバルエイリアス、historyの端末間での共有など 基本的な設定を追加したものが以下になります。

uu64/.zshrc

参考

RailsでBootstrapのmodalが開いてすぐ閉じてしまう問題

開発していて地味にハマったのでメモ。

概要

削除・更新系の処理実行前に確認画面を表示するために、bootstrapのmodalを利用。 ボタンを押すとmodalが一瞬表示されるが、何もUI操作をしていないのにmodalダイアログが閉じてしまう。

原因

異なる2つのgemの中でbootstrapを読み込んでいたことが原因。私の場合は”sass-rails”に加えて、"jquery-datatables-rails"のbootstrapオプションを利用していたからでした。

似たような事象が以下で報告されているので共有。どうやらmodal起動時に2回toggle()が呼ばれて、1回目の呼び出しでmodal起動、即座に2回目が呼び出されて閉じているっぽいですね。

Modal Disappears Immediately · Issue #1611 · twbs/bootstrap · GitHub

Bootstrap Modal immediately disappearing - Stack Overflow

解決方法

bootstrapを呼び出しているGemのうち、片方の代替となるGemを探してそれを利用するとかでしょうか。もしくは、片方のGemでbootstrapを呼び出さないように、何かしら工夫をするとか(Gemによって対処方法は違うと思うので具体的な解決策を示すことができなくて申し訳ないのですが)

私の場合は"dataconfirm-modal"という別のgemを利用することで正常に動作するようになったので、そのまま放置してあります。結局、sass-railsjquery-datatables-railsの両方を利用しているので、また再発するかもしれないです。が、現状は問題ないので、しばらくこのままでいこうと思います・・・。