Android
AndroidDay 16

定数を型安全に利用する -詳解 IntDef , StringDef-

More than 3 years have passed since last update.


概要

enumの代わりにstatic finalな定数を使うと、メモリを節約できるが型安全でなくなるというデメリットがある。

これを解決する手段として@IntDef@StringDefがある。

単純な使い方はネットにあふれているが、RetentionPolicyの設定やflagsについては情報がまとまっていなかったりするのでここで体系的に記載しておきたい。


導入(enum vs static final定数)

メモリの削減のためにenumじゃなくてstatic finalな定数を使おうぜ!っていう話をよく耳にする。

公式には、二倍以上もメモリの使用量が違うとのこと。

Managing Your App's Memory


Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.


これ、冷静に考えたら確かにその通りで、例えば以下のようなクラスを定義すると、

public enum TestType {

TEST_TYPE_1,
TEST_TYPE_2,
TEST_TYPE_3,
TEST_TYPE_4,
TEST_TYPE_5,
}

内部的にはこんなクラスが生成されていたりする。

継承してるEnumクラスは結構なボリュームなので、まあそりゃあ32bitぽっちのintで定義するよかメモリ食うよね、っていう話。(気にするべきか)


public final class com.taishonet.memory_test.TestType extends java.lang.Enum<com.taishonet.memory_test.TestType> {
public static final com.taishonet.memory_test.TestType TEST_TYPE_1;
public static final com.taishonet.memory_test.TestType TEST_TYPE_2;
public static final com.taishonet.memory_test.TestType TEST_TYPE_3;
public static final com.taishonet.memory_test.TestType TEST_TYPE_4;
public static final com.taishonet.memory_test.TestType TEST_TYPE_5;
public static com.taishonet.memory_test.TestType[] values();
public static com.taishonet.memory_test.TestType valueOf(java.lang.String);
static {};
}


IntDefとStringDef

enumを使わないとなるとstatic finalを使うことになる。

enumの利点だった型の保証はどうするんだ?っていうのにこたえるのがIntDefとStringDef。

IntDefについて

StringDefについて


なにこれ?

ざっくり言えば、自分で定義するアノテーションに取りうる値の制限を付加するためのメタアノテーション。

なにかというと、

    @IntDef({TEST_TYPE_1, TEST_TYPE_2})

public @interface TEST_TYPE {
}
public static final int TEST_TYPE_1 = 1;
public static final int TEST_TYPE_2 = 2;

こうやって書いておいて、

    private void test(@TestType2.TEST_TYPE int a) {

}

こうすると、引数はTEST_TYPE_1, TEST_TYPE_2のどちらかしか取れなくなるし(コンパイルが通らなくなる)、

    @TestType2.TEST_TYPE

private int getTest() {
return TestType2.TEST_TYPE_1;
}

こうすると返り値がTEST_TYPE_1, TEST_TYPE_2のどちらかしか取れなくなる。StringDefはこれがStringになったバージョン。


flag指定

定数をflagとして扱うこともできる。

    public static final int FLAG_A = 0x1;

public static final int FLAG_B = 0x2;
public static final int FLAG_C = 0x4;
public static final int FLAG_D = 0x0;

@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {FLAG_A, FLAG_B, FLAG_C, FLAG_D}, flag = true)
public @interface TEST_FLAG {
}

private boolean isDeathFlag(@TEST_FLAG int flag) {
return (flag & (FLAG_A | FLAG_B)) != 0;
}


使用手順

使用手順をまとめると


  1. 定数を何か定義する

  2. 独自アノテーションを定義する

  3. そのアノテーションに対して@IntDef(もしくは@StringDef)アノテーションをつける

これだけ。


Retentionについて


RetentionPolicyの定義

アノテーションにはRetentionといって、アノテーション情報をどこまで保持しておくの?というのを設定しておくことができる。

Retentionは以下の3つの定義がある。

定数
意味

CLASS
クラスファイルにはアノテーション情報を記載するが、実行時にVMには読み込まれない(デフォルトはこれ)

RUNTIME
クラスファイルにアノテーション情報を記載し、実行時にもVMに読み込む

SOURCE
コンパイル時に破棄される

    @Retention(RetentionPolicy.SOURCE)

@IntDef({TEST_TYPE_1, TEST_TYPE_2})
public @interface TEST_TYPE {
}

このように指定する。

正直この定義だとどう使い分けていいのか何が違うのかが直感的に分からない。


実行時にVMに読み込まれないとどうなるのか

SOURCEにして定義してても、実行時にはアノテーション情報がなくなるのでリフレクションで意図しない引数で実行できてしまいます。


CLASSっていつ使うんだ・・・?

クラスファイルにアノテーション情報があってもランタイム実行時には消されるので、SOURCEと同じようにリフレクションに対しては効果を発揮しない。

Javaバイトコードレベルで何かする時はいる・・・のかな・・・


結論

開発してるプロダクトがリフレクションとか使ってて、かつ型安全を担保したいのであればRUNTIMEを使いましょう。

そうでないなら基本SOURCEで良さそう。


余談

試しにenumとstatic finalで定義したコード書いて、

adb shell dumpsys meminfo 

でメモリのぞいてみたりしたけどあんまかわんなかった。。。。

enumを避けることでどのくらい効果が見込めるかはふめい、、、、

ごめんなさい、、、