Skip to content
This repository was archived by the owner on Jun 20, 2023. It is now read-only.

A.04. Java の活用

Keishin Yokomaku edited this page Feb 16, 2014 · 38 revisions

この章では、より発展的な Java の活用と実践について簡単に解説します。
数々の API の使い方と合わせて、様々なプラクティスやイディオムについても含まれます。

参考:Effective Java

目次

  • マルチスレッド
    • スレッドプール
    • 原子性と可視性
    • スレッドセーフ
    • 遅延初期化
      • スレッドセーフでない実装
      • Double Checked Locking
      • Initialization-on-demand Holder
    • 同期化を支援する仕組み
      • CountDownLatch
      • CyclicBarrier
      • Semaphore
  • データ構造
    • ミュータブルとイミュータブル
    • Defensive Copying
    • Builder パターン
  • 参照の管理
    • 内部クラス
    • static フィールド
    • WeakReference
    • WeakHashMap
  • 列挙型の活用
    • Singleton パターン
    • Strategy パターン
    • Enum Factory パターン
    • 列挙型とコレクションフレームワーク
    • 列挙型とパフォーマンス
  • アノテーション
  • [New I/O](#New I/O)
    • バッファ
    • チャネル
  • [New I/O2](#New I/O2)
    • 非同期チャネル

マルチスレッド

スレッドプール

Android を始めとして、各種の GUI を構築するアプリケーションでは、UI Thread(Main Thread) をブロックする各種の処理のためのスレッド(Worker Thread)を用いた非同期処理を実装する。

UI のイベントに応じてネットワークやデータベース等へアクセスする頻度が高いアプリケーションの場合、イベントが発生するごとに新規にThreadを立ち上げて動作させると、パフォーマンスの問題が発生する。

そこで、ある程度Threadのインスタンスをプールしておき、適宜インスタンスを使いまわしていく仕組みとして、ThreadPoolExecutorを用いる。

この仕組は Android の標準の非同期処理フレームワークであるAsyncTaskでも用いられている。

TODO: Sample code here

原子性と可視性

原子性(アトミック性)とは、あるスレッド上での、あるデータへの複数の操作が、他のスレッドからみて単一の操作に見えること。データの状態遷移の過渡的な不整合な状態が見えない性質とも言う。

Java のプリミティブ型のうち、long と double 以外の型の操作は原子性が保証されている。long と double は 64bit のデータで、その読み書きの際に複数の操作が発生するため原子性が保証されない。

一方で、int 等の整数のインクリメント操作やデクリメント操作の記法(count++;count--;)は、その操作の中に複数の操作(読み取り、加算or減算、書き込み)を含むため、原子性が保証されない。

64 bit データの操作やインクリメント・デクリメント操作の原子性を保証するには、synchronized による同期化をするか、AtomicIntegerクラスなどの原子性を保証する操作を実現するラッパークラスを使用する。

可視性とは、どのスレッドからでも同じ値が見えること。

通常、スレッドを複数立ちあげると、変数の値はスレッドごとにキャッシュされる仕組みになっている。このため、スレッドごと値の更新と参照に不整合が起こることがある。

同期化では、この原子性と可視性の両方を保証する必要がある。

volatile修飾子は可視性を保証し、どのスレッドからでも同じ値を見えるように、スレッドごとのキャッシュを使わないようにする。

スレッドセーフ

スレッドセーフであるとは、以下の条件を満たすこと。

  • インスタンスに対する操作をどんな順番で実行しても正しく振る舞う。
  • 複数のスレッドからの操作も同様に、どんな順番で実行しても正しく振る舞う。

順番が入れ替わると破綻したり、複数スレッドから操作を行う際、順番が狂うと破綻する操作をスレッドセーフでない操作という。

スレッドセーフにはレベルが有り、クラスの性質から幾つかのレベルに分類され、レベルによって使う側の同期の必要性の有無を判断する。

  • 不変 状態を持たないもの。イミュータブルなオブジェクトは使う側で同期化する必要がない。
  • 無条件スレッドセーフ 使う側で特別同期化をしなくてもよいもの。
  • 条件付きスレッドセーフ 一部に、使う側で同期化が必要な操作を含むもの。
  • スレッドセーフでない 同期化をしていないもの。
  • 敵対 マルチスレッドで使えないもの。

遅延初期化

通常、メンバ変数の初期化はコンストラクタで行う。しかし、コンストラクタでの処理がパフォーマンスに影響をおよぼす場合、メンバ変数を、それが必要になった時に初めて初期化をするようにすることで、コンストラクタのパフォーマンスを向上させることが出来る。このようなチューニングのノウハウを遅延初期化と言う。

遅延初期化は、シングルトンパターンの実装にも見られる。

このイディオムは、マルチスレッドで正しく動作させるために工夫が必要になるため、特に理由のない限り、必要なければ使わないことが推奨されている。

スレッドセーフでない実装

メンバ変数を、必要になったタイミングで初期化する単純な実装は以下のとおり。

public class LazyInitializationSample {
    private Map<String, Object> mMap;

    public LazyInitializationSample() {} // コンストラクタで初期化しない

    public void add(String key, Object data) {
        if (mMap == null) { // mMap を使う直前で初期化する
            mMap = new HashMap<String, Object>();
        }
        mMap.put(key, data);
    }
}

この実装は、単一のスレッドで使用する場合には問題ないが、複数のスレッドからLazyInitializationSampleのインスタンスを操作しようとするときに問題を発生させる可能性がある。

ひとつには、HashMapの中のデータの操作が同期化されないため、結果が不定となること。
もうひとつは、mMapの初期化処理が同期化されないため、これも結果が不定となること。

いずれにしても、スレッドセーフでない操作が含まれるため、予期せぬ動作を招くことがある。

HashMap の中のデータの操作を同期化するには、HashMapではなくConcurrentHashMapを使用することで解決できる。
スレッドセーフなコレクションはjava.util.concurrentパッケージにいくつかの実装があるほか、Collectionsクラスのユーティリティメソッドを用いてスレッドセーフなコレクションを生成することも出来る。

public class LazyInitializationSample {
    private Map<String, Object> mMap;

    public LazyInitializationSample() {} // コンストラクタで初期化しない

    public void add(String key, Object data) {
        if (mMap == null) { // mMap を使う直前で初期化する
            mMap = new ConcurrentHashMap<String, Object>();
        }
        mMap.put(key, data);
    }
}

上記の場合も、mMapの初期化がスレッドセーフではない。

よって、以下のようなイディオムを使用して、スレッドセーフな実装とする。

Double Checked Locking

null チェックをsynchronizedブロックの外と内で二度行うことから、Double Checked と呼ばれる。

public class LazyInitializationSample {
    private volatile Map<String, Object> mMap; // どのスレッドからも常に同じ値を見る(可視性)ことを保証

    public LazyInitializationSample() {}

    public void add(String key, Object data) {
        if (mMap == null) { // 既に初期化が終わっている場合はロックを取らず処理を継続
            synchronized (this) { // 自身のオブジェクトをミューテックスとしてロックを取得し、クリティカルセクションに突入
                if (mMap == null) {  // ロック解放待ちの間に mMap が初期化された場合は何もしないようにするためのチェック
                    mMap = new HashMap<String, Object>();
                }
            }
        }
        mMap.put(key, data);
    }
}

このイディオムを正しく動作させるためには、volatileの役割が欠かせない。 ただし、Java 1.4 と Java 1.5 でvolatileの保証する範囲が異なり、Java 1.4 の volatile修飾子で保証する範囲では不足があるため、Java 1.4 以前でこのイディオムは正しく動作しない。

以下のように、シングルトンパターンの実装にも用いられる。

public class Singleton {
    private static volatile Singleton sInstance;

    protected Singleton() {}

    public static void getInstance() {
        if (sInstance == null) {
            synchronized(Singleton.class) {
                if (sInstance == null) {
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}

Initialization-on-demand Holder

staticなフィールドが、クラスをロードしたタイミングで初期化されることと、staticな内部クラスが、使用されるタイミングで初めてロードされることを利用したイディオム。
クラスのロードは VM 上で逐次実行されることと、staticなフィールドの初期化も逐次実行されることから、同期化のコードを書かなくてもよい。これにより、同期化に掛かるオーバヘッドも削減できるほか、Java のバージョンに依らず正しく動作する。

public class LazyInitializedObject {
	private LazyInitializedObject() {}
 
	private static class LazyHolder {
		private static final LazyInitializedObject INSTANCE = new LazyInitializedObject();
	}
 
	public static LazyInitializedObject getInstance() {
		return LazyHolder.INSTANCE; // LazyHolder がロードされた時のみコンストラクタが呼ばれる
	}
}

同期化を支援する仕組み

ここでは、言語仕様以外に用意されている同期化のためのユーティリティの概要を説明する。

CountDownLatch

カウンタが 0 になるまで待ち合わせをするためのオブジェクト。初期値に与えた数から開始し、CountDownLatch#countDown()を呼び出すことでカウンタをデクリメントする。待ち合わせはCountDownLatch#await()で行い、カウンタが 0 になったタイミングですべてのスレッドが待機状態から実行状態となる。

CountDownLatchは再利用しない。再利用を前提とする場合はCyclicBarrierを使う。

TODO: sample code here

CyclicBarrier

指定した数のスレッドが待ち合わせ箇所に到達するまですべてのスレッドを待機させるためのオブジェクト。初期値として、待ち合わせをするスレッド数を与え、CyclicBarrier#await()で待機する。指定した数のスレッドがCyclicBarrier#await()に到達した時点で、すべてのスレッドが待機状態から実行状態となる。

TODO: sample code here

Semaphore

指定した数のスレッドのみが進入できるクリティカルセクションを作る仕組み。 synchronizedブロックは同時に 1 つのスレッドのみが進入できるが、Semaphore は指定した数の進入を許可する。数の指定を 1 とすることでsynchronizedブロックと同等の排他制御が可能。

Semaphore#acquire()によってロックを取得し、指定した回数このメソッドが呼ばれた段階で、以後の呼び出しがブロックされる。Semaphore#release()によってロックを開放する。

TODO: sample code here

データ構造

ミュータブルとイミュータブル

ミュータブルとは、オブジェクトの生成後にその状態を変更可能であることで、イミュータブルはその反対に、状態を変更できないこと。

final修飾子は、参照を変更不可能にすることを保証するが、オブジェクトの状態を変更不可能にすることは保証しないことに注意する。

イミュータブルなオブジェクトは、生成後に状態の変更ができないため、複数のスレッドで同じイミュータブルなオブジェクトを使用している場合でも安全に使用できる。

Defensive Copying

Java ではオブジェクトは参照によって共有されるため、ミュータブルなオブジェクトの参照を共有する場合、誰かがそのオブジェクトに変更を加えた時点で、すべての参照を共有している箇所にその変更の影響が波及してしまう。

以下の例では、初期化や状態の取得時にこの問題を誘発する。

public class SomethingMutable {
    private final List<Object> mList; // 参照は書き換えられないが…

    public SomethingMutable(List<Object> list) {
        mList = list; // 参照をそのまま渡すので、コンストラクタの呼び出し側で、渡したリストを変更すると、その影響を受けてしまう
    }

    public List<Object> getList() {
        return mList; // 参照をそのまま返すので、このメソッドの呼び出し側で、返って来たリストを変更すると、その影響を受けてしまう
    }
}

フィールドが保持する参照が変更不可能であること意外にも、参照を通じた変更を許容しないことがイミュータブルであることの条件となるので、以下のように防御的コピーの手法を用いる。

public class SomethingImmutable {
    private final List<Object> mList;

    public SomethingImmutable(List<Object> list) {
        mList = new ArrayList<Object>(list); // 別のオブジェクトの参照を保持することで、呼び出し側の変更の影響を受けないようにする(防御的コピー)
        // mList = Arrays.asList(list.toArray()); 配列に変換して再度 List 化する方法もある
    }

    public List<Object> getList() {
        return new ArrayList<Object>(mList); // 別のオブジェクトの参照を返すことで、呼び出し側の変更の影響を受けないようにする(防御的コピー)
        // return Collections.unmodifiableList(mList); // 変更不可能なコレクションを生成するユーティリティを使うことも可
    }
}

Builder パターン

多数のメンバ変数を持つオブジェクトを生成する際には、コンストラクタよりも Builder パターンを用いることが推奨されている。
このパターンを用いて、イミュータブルなオブジェクトを生成する。

public class Something {
    private final String mName;
    private final String mLocation;
    private final int mAge;
    private final Date mBirthday;

    private Something(Builder builder) {
        mName = builder.name;
        mLocation = builder.location;
        mAge = builder.age;
        mBirthday = builder.birthday;
    }

    public String getName() {
        return mName; // String はイミュータブルなので参照をそのまま返しても問題ない
    }

    public int getAge() {
        return mAge;
    }

    public Date getBirthday() {
        return new Date(mBirthday.getTime()); // Date はミュータブルなので、防御的コピーをする
    }

    // Something のビルダー
    public static class Builder {
        private final String name;
        private String location;
        private int age;
        private Date birthday;

        public Builder(String name) {
            this.name = name;
        }

        public Builder setLocation(final String value) {
            location = value;
            return this;
        }

        public Builder setAge(final int value) {
            age = value;
            return this;
        }

        public Builder setBirthday(final Date value) {
            birthday = new Date(value.getTime());
            return this;
        }

        public Something create() {
            return new Something(this);
        }
    }
}

参照の管理

内部クラス

Java には、内部クラスの定義の仕方によって 2 種類の内部クラスがある。

class OuterClass {
    private String mSomeData;

    public void doSomething(NestedClass nested) {
        System.out.println(nested.mAnotherData); // static な内部クラスのインスタンスのフィールドは private でもアクセスできる
    }

    public class InnerClass {
        public void doSomething() {
            System.out.println(mSomeData); // 内部クラスの外側のクラスのフィールドは内部クラスからアクセスできる
        }
    }

    public static class NestedClass {
        private OuterClass mInstance;
        private String mAnotherData;

        public void doSomething() {
            System.out.println(mInstance.mSomeData); // 内部クラスの外側のクラスのフィールドは、インスタンスを経由すれば private でもアクセスできる
        }
    }
}

public class Consumer {
    public static void main(String[] args) {
        OuterClass.InnerClass inner = new OuterClass().new InnerClass();
        OuterClass.NestedClass nested = new OuterClass.NestedClass();
    }
}

staticでない内部クラス(インナークラス)の場合、暗黙のうちに外側のクラスへの参照を持つ。 この特徴から、あるインスタンスを複数のインスタンスが共有する場合に有用なものとなる。
一方で、そのインナークラスのインスタンスへの参照があるかぎり、その外側クラスのインスタンスも開放されなくなるため、メモリリークを誘発しやすくもある。

static フィールド

staticなフィールドは、ガーベジコレクションの対象とはならず、初期化後はメモリ上に存在し続ける。

この時、以下の様に、コレクションをstaticなフィールドとして保持すると、メモリリークの原因となり得る。

public class LeakCollectionHolder {
    // 保持するデータがずっと参照され続けるため、コレクションの中身をクリアするか、コレクションそのものを null としないかぎり GC されなくなる
    private static List<String> LIST = new ArrayList<String>();

    public void addToList(String data) {
        LIST.add(data);
    }
}

WeakReference

Java の参照には強さがあり、通常なにもしないと強参照となり、参照が切れた時点ではじめて GC の対象となる。

しかし、オブジェクトの循環参照を起こすと、互いに参照が保持されるために GC の対象とならず、メモリリークの原因となる。
このような状況を防ぐため、参照の強さを弱め、弱い参照からのみ参照されるオブジェクトは GC による回収の対象とする仕組みが用意された。

このような、弱い参照を実現するためのクラスがWeakReferenceである。

public class Something {
    private NestedClass mNested;

    public Something() {
        mNested = new NestedClass(this);
    }

    public static class NestedClass {
        private WeakReference<Something> mSomething; // Something 型のオブジェクトへの弱参照を保持するメンバ変数

        public NestedClass(Something something) {
            mSomething = new WeakReference<Something>(something); // 弱参照の初期化
        }

        public void doWithSomething() {
            Something something = mSomething.get(); // 弱参照から実際の強参照を得る
            if (something == null) { // Something オブジェクトへの参照が回収済みの場合
                return; // 既に GC されている場合は何もしない
            }
            // 以後スコープを抜けるまでは something は生存が保証される
        }
    }
}

特に、Android では、Activity への参照をstaticな内部クラスで保持するときに用いられる。

public class SomeActivity extends Activity {
    private NestedClass mNestedClass;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // ...

        mNestedClass = new NestedClass(this);
    }

    private static class NestedClass {
        private WeakReference<Activity> mActivity;

        public NestedClass(Activity activity) {
            mActivity = new WeakReference<Activity>(activity);
        }

        public void doWithActivityContext() {
            Activity activity = mActivity.get();
            if (activity == null) {
                return;
            }
            // ...
        }
    }
}

WeakHashMap

HashMapの key の参照がWeakReferenceとなっており、key が回収された時点で対応する value も削除される。value は強参照で保持されるが、key と value のペアの削除の基準は key の弱参照が回収された時点なので、キャッシュのような使い方には適さない。

使い方は通常のHashMapと変わらない。注意すべきは、値を取り出すときに既に key が回収されていることを考慮する必要がある点。

列挙型の活用

列挙型の特性を活かした各種のデザインパターンの実装がある。

Singleton パターン

列挙型は、列挙したオブジェクトがpublic static finalな扱いとなるため、そのままでも Singleton パターンをできる。

public enum Hogehoge {
    SINGLETON; // Hogehoge クラスの public static final なフィールドとして扱われる

    public void doSomething() {}
}

Strategy パターン

abstractなメソッドを作ることが出来るので、以下のような実装をすると Strategy が組める。

public enum Hogehoge {
    TYPE_A {
        @Override
        public void doSomething() {
        }
    },
    TYPE_B {
        @Override
        public void doSomething() {
        }
    };

    public abstract void doSomething();
}

列挙オブジェクトが増えると煩雑になりがちなので、以下のように戦略を委譲する。

public enum Hogehoge {
    TYPE_A(Strategy.A),
    TYPE_B(Strategy.B); // 戦略が同じであれば、この列挙を増やすだけでよい

    private final Strategy mStrategy;

    private Hogehoge(Strategy strategy) {
        mStrategy = strategy;
    }

    public void doSomething() {
        mStrategy.do();
    }

    private enum Strategy {
        A {
            @Override
            public void do() {
            }
        },
        B {
            @Override
            public void do() {
            }
        };

        public abstract void do();
    }
}

Enum Factory パターン

enumのフィールドに、生成したいクラスのインスタンスを持たせることで、Factory を実現する。

enum Factory {
    HOGE(0, new HogeObject()),
    FUGA(1, new FugaObject());
    // Null Object パターンを実装するならば、UNKNOWN(-1, new UnknownObject());

    private final int mTypeId;
    private final SomeInterface mInstance;

    private Factory(int typeId, SomeInterface instance) {
        mTypeId = typeId;
        mInstance = instance;
    }

    public static Factory valueOf(int typeId) { // Factory の実体を、id から逆引きする
        for (Factory factory : values()) {
            if (factory.getTypeId() == typeId) {
                return factory;
            }
        }
        throw new IllegalArgumentException("unknown type id");
        // あるいは、Null Object パターンに基いた実装をするのも OK
        // return UNKNOWN;
    }

    public static int getTypeCount() {
        return values().length;
    }

    public int getTypeId() {
        return mTypeId;
    }

    public SomeInterface getInstance() {
        return mInstance;
    }
}

interface SomeInterface {
    public void doSomething();
}

class HogeObject implements SomeInterface {
    @Override
    public void doSomething() {}
}

class FugaObject implements SomeInterface {
    @Override
    public void doSomething() {}
}

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < Factory.getTypeCount(); i++) {
            Factory factory = factory.valueOf(i);
            factory.getInstance().doSomething();
        }
    }
}

インスタンスではなく、型トークンを持たせることで、DI フレームワークによる On-Demand-Injection が可能。

enum Factory {
    HOGE(0, HogeObject.class),
    FUGA(1, FugaObject.class);
    // Null Object パターンを実装するならば、UNKNOWN(-1, UnknownObject.class);

    private final int mTypeId;
    private final Class<? extends SomeInterface> mClass;

    private Factory(int typeId, Class<? extends SomeInterface> clazz) {
        mTypeId = typeId;
        mClass = clazz;
    }

    public static Factory valueOf(int typeId) { // Factory の実体を、id から逆引きする
        for (Factory factory : values()) {
            if (factory.getTypeId() == typeId) {
                return factory;
            }
        }
        throw new IllegalArgumentException("unknown type id");
        // あるいは、Null Object パターンに基いた実装をするのも OK
        // return UNKNOWN;
    }

    public static int getTypeCount() {
        return values().length;
    }

    public int getTypeId() {
        return mTypeId;
    }

    public SomeInterface getInstance(Context context) {
        return RoboGuice.getInjector(context).getInstance(mClass);
        // return Proton.getInjector(context).getInstance(mClass);
    }
}

列挙型とコレクションフレームワーク

列挙型で列挙したオブジェクトをコレクションの中で取り扱う場合は、通常のコレクションクラスではなく、専用のコレクションクラスを使う。

SetにはEnumSetMapにはEnumMapがあり、それぞれ enum 専用にパフォーマンスのチューニングが施されている。

enum Hoge {
    FOO,BAR,BAZ,QUX,QUUX;
}

public class Main {
    public static void main(String[] args) {
        // enum が含まれる Set の生成
        Set<Hoge> part = EnumSet.of(Hoge.FOO, Hoge.BAR); // (FOO | BAR)
        Set<Hoge> all = EnumSet.allOf(Hoge.class); // (FOO | BAR | BAZ | QUX | QUUX)
        Set<Hoge> empty = EnumSet.noneOf(Hoge.class); // ()
        Set<Hoge> copy = EnumSet.copyOf(part); // (FOO | BAR)
        Set<Hoge> complement = EnumSet.complementOf(part); // (BAZ | QUX | QUUX)
        Set<Hoge> range = EnumSet.range(Hoge.BAR, Hoge.QUX); // (BAR | BAZ | QUX), 引数の順序を逆にすると IllegalArgumentException

        // enum を key とする Map の生成
        Map<Hoge, Object> map = new EnumMap(Hoge.class);
        for (Hoge hoge : Hoge.values()) {
            map.put(hoge, hoge.name());
        }
    }
}

列挙型とパフォーマンス

列挙型は非常に便利なしくみであり、他の言語に見られる単純な数値の列挙とは異なり振る舞いを持つことが出来る点が大きな利点である。

一方で、列挙型は、以下の様なパフォーマンス上の欠点もある。

  • 列挙したオブジェクトの数だけ中間ファイルが生成されるため、列挙が増えるほどアプリケーションの容量が膨れ上がる
  • 列挙型のクラスがロードされるタイミングですべての列挙したオブジェクトの初期化処理が実行されるため、列挙が多いほど、クラスの初期化に時間がかかるようになる
  • 列挙したオブジェクトの数だけメモリを食い続ける
  • int 型などの定数と異なり、コンパイラによるインライン化の恩恵が受けられない

単純なint型定数の列挙の置き換えとして用いると、コストパフォーマンスが悪くなるため、Android のような少メモリ環境では敢えて使用を控える傾向がある。

アノテーション

New I/O

バッファ

チャネル

New I/O2

非同期チャネル

GitHub Pagesへ移行しましたmixi-inc.github.ioへお願いします。

Clone this wiki locally