2
3

More than 3 years have passed since last update.

非staticネストクラスが握っちゃう、エンクロージングオブジェクトの暗黙的参照

Last updated at Posted at 2020-03-27

この記事は、Javaにおける「非staticネストクラスが、エンクロージングオブジェクトを暗黙的参照として握っちゃうから、気を付けましょう」について述べます。

なぜ、気を付けなければならないのか、と言えば、「メモリーリークを引き起こす可能性を高めてしまうから」です。

そして結論は、「ネストクラスは、static修飾子を付けた方がイイ(さすれば、エンクロージングオブジェクトを暗黙的参照として握ることはないから)」です。エンクロージングオブジェクトは短命で、ネストクラスのインスタンスの方が長命である場合は特にです!

事のきっかけはAndroidアプリ開発時

(この記事は、Androidアプリ開発に特化したものではありません。が、読み進めていけばJavaに関わることになっていきますので、当初は我慢してお読みください)

ある日のことでございます。AndroidアプリをJavaで作っていた時に、Android Studio1が、こんな警告を出してきました。

thisasynctaskclassshouldbestaticorleaksmightoccur.png

This 'AsyncTask' class should be static or leaks might occur

CouponAsyncTaskと名付けたこのクラスは、ネストクラス(インナークラス・内部クラス)として定義しているのですが、「static修飾子を付けなさいよ、さもなければメモリリークを引き起こすかもよ」と吹き出しに書かれています。まあ、怖い。

Android Studioが備えているコードチェック機能「Lint」が教えてくれる詳しい説明を見ますと、

Static Field Leaks
A static field will leak contexts.
Non-static inner classes have an implicit reference to their outer class. If that outer class is for example a Fragment or Activity, then this reference means that the long-running handler/loader/task will hold a reference to the activity which prevents it from getting garbage collected.
Similarly, direct field references to activities and fragments from these longer running instances can cause leaks.
ViewModel classes should never point to Views or non-application Contexts.
Issue id: StaticFieldLeak

Google翻訳に頼んでみます。

静的フィールドリーク
静的フィールドはコンテキストをリークします。
非静的内部クラスには、外部クラスへの暗黙的な参照があります。 その外部クラスがたとえばフラグメントまたはアクティビティである場合、この参照は、長時間実行されるハンドラー/ローダー/タスクがガベージコレクションの取得を妨げるアクティビティへの参照を保持することを意味します。
同様に、これらのより長時間実行されているインスタンスからのアクティビティおよびフラグメントへの直接フィールド参照は、リークを引き起こす可能性があります。
ViewModelクラスは、ビューまたは非アプリケーションコンテキストを指すことはありません。
問題ID:StaticFieldLeak

難しい英文を、難しい日本語に翻訳されてもねえ...チンプンカンプンです。

開発者サイトに書いてありました

ちゃんとAndroidアプリ開発者向け公式サイトに「スレッド化されたオブジェクトを使ったコード設計によく見られる欠陥の例」として掲載されていました。あるあるなんでしょうね。
スレッド化によるパフォーマンスの向上[非明示的参照]

この公式サイトの日本語ページでは「非明示的参照」と訳されていますが、原文は「Implicit references」と書かれています。
当記事では「暗黙的参照」と五字熟語で語っていきます。

ネストクラス

いきなり「非staticネストクラスが握っちゃう、エンクロージングオブジェクトの暗黙的参照」について語ろうにも、このお題目に乗っかっている情報量が多いので、前提知識を一つ一つ説明していきます。
急がば回れ、です。ただ、ちょっともどかしいかもしれませんが。

「ネストクラス(nested class)」は、「インナークラス(inner class)」とも呼ばれます。当記事は「ネストクラス」で統一します。

Fooクラスには2つのネストクラスがある
public class Foo {
    class Bar {
        void methodBar() {
            System.out.println("methodBar");
        }
    }

    static class Baz {
        void methodBaz() {
            System.out.println("methodBaz");
        }
    }

    void methodFoo() {
        System.out.println("methodFoo");
    }
}

この場合、

  • BarクラスとBazクラスは、「Fooクラスのネストクラス」と言います。
  • Fooのことは、「Barクラスのエンクロージングクラス」と言います。

「エンクロージングクラス(enclosing class)」は、「アウタークラス(outer class)」とも呼ばれます。当記事は「エンクロージングクラス」で統一します。

BarとBazの違いは、static修飾子が付いているか否かなので、

  • Barクラスは、「Fooクラスの非staticネストクラス」と言います。面倒くさいと思う人は「Fooクラスのネストクラス」で済ませてしまいますが。
  • Bazクラスは、「Fooクラスのstaticネストクラス」と言います。

ネストクラスのインスタンスを生成するには

Fooの2つのネストクラスのインスタンスを生成する
class Main {
    public static void main(String[] args) {
        Foo foo = new Foo();
        Foo.Bar bar = foo.new Bar();

        Foo.Baz baz = new Foo.Baz();
    }
}

非staticネストクラスのインスタンスを生成する場合

Barクラスのインスタンスを生成するには、2行を要しました。どうしても1行で済ませたい場合は、以下のコードとなります。

1行でBarクラスのインスタンスを生成
Foo.Bar bar = new Foo().new Bar();

いずれにせよ、非staticネストクラスのインスタンスを生成する場合は、エンクロージングのインスタンスが必要です。
そして、new演算子の前に.が付くのも特徴的です。

staticネストクラスのインスタンスを生成する場合

事前にエンクロージングのインスタンスなんて要りません。いきなりnewできます。

特徴的なのは、クラス名が「Baz」ではなく、「Foo.Baz」という名前だということです。クラス名の合間に.が含まれているのが特徴的です。

書籍「Effective Java」に当たる

Effective javaは、第1版(2001年刊行)から「非staticのメンバークラスよりもstaticのメンバークラスを選ぶ」べしとのたまっております。それは2018年に刊行された第3版ですら残っている御成敗式目です。

メモリリークは壊滅的になり得ます。

とか 

メモリリークの検出がたいてい難しいです。

とか

余分な参照を保持しメモリと時間を無駄に使います。

など、怖いことが書かれています。なぜなら、

static修飾子を省略すると、個々のインスタンスはエンクロージングオブジェクトへの関係のない参照を持ってしまいます。その参照を保存することは時間とメモリを必要とします。深刻なのは、その参照がなければガベージコレクションの対象になる場合、そのエンクロージングインスタンスが残ってしまう可能性があることです。

と書かれてあります。

エンクロージングのインスタンスはもう不要なのに、天国に召されない

前出のプログラムをちょっと改変してみました。staticネストクラスであるBazはちょっと退かせました。

public class Main {
    public static void main(String[] args) {
        Foo foo = new Foo();
        Foo.Bar bar = foo.new Bar();

        foo = null;
        System.gc();
//        foo.methodFoo(); // NullPointerException発生
        bar.methodBar();
    }
}

エンクロージングクラスのFooのインスタンスにnullを代入してガベージコレクション(GC)を実行させてから、非staticネストクラスのBarインスタンスにメソッドを呼び出してみました。実行してみますと、以下の通りです。

実行結果
methodBar

へえ、ちゃんと動くんだぁ!と私は瞠目してしまいました。

Javaの仕様としては、私がいくらエンクロージングクラスのFooのインスタンスにnullを代入しても、その非staticネストクラスのBarインスタンスが動く限り、fooと名付けたオブジェクトはGCされない(GC対象と見做されず華麗にスルーされた)のです。なぜなら、 barfooを暗黙的参照として保持しているから です。

ファニーな例えとしてイラストにすると、こんなかんじでしょうか。

joubutudekinai.png

階段を一歩ずつ登っていってる女性が、barで、元気いっぱいです。

一方、成仏したがっているお爺ちゃんがfooなんですが、barが握ってるのでね、なかなか天国へ召されずにいるのですよ。地縛霊になりかねません。

  • 握っている=参照している
  • 天国へ召される=GCによりメモリから解放される
  • 地縛霊=メモリリーク

で読み替えてください。

では、短絡的にネストクラスにstaticを付けるだけで万事解決!といくのか?

FooとかBarとかBazの題材をやめて、以下のプログラムを見てください。

ビフォアー:ネストクラスが非staticだ
class Fizz {
    private Integer x = 12;
    private static Integer y = 34;

    class Buzz {
        void m() {
            x = 56;
            y = 78;
            System.out.println(x + y);
        }
    }
}

class FizzBuzzMain {
    public static void main(String[] args) {
        Fizz.Buzz fb = new Fizz().new Buzz();
        fb.m();
    }
}
ビフォアーの実行結果
134

(わざとらしく)やるなっつってんのに非staticネストクラスですよ。では、安直にこのBuzzと名付けたネストクラスにstatic修飾子を付けてあげましょう。

アフター
class Fizz {
    private Integer x = 12;
    private static Integer y = 34;

    static class Buzz {
        void m() {
            x = 56; // xがstaticフィールドではないのでコンパイル不可
            y = 78;
            System.out.println(x + y); // xがstaticフィールドではないのでコンパイル不可
        }
    }
}

class FizzBuzzMain {
    public static void main(String[] args) {
        Fizz.Buzz fb = new Fizz.Buzz();
        fb.m();
    }
}

途端にコンパイルできなくなりました。xがstaticフィールドではないために、ダメだとIDEが怒り出します。

なぜなのか。それは、 staticネストクラスは、エンクロージングのstaticメンバーにのみアクセス可能 だからです。

また再びのAndroidアプリ開発の話に戻ります

作りたかったAndroidアプリは、このアニメーションGIFをご覧ください。

countup.gif

起動したとたんに、勝手にカウントアップが始まります。このアニメーションGIFは、3まで至ったらまたSTART!を繰り返しているアニメーションですが、本当はこのアプリはSTART!から始まったら「9」まで数え上げたらお終い(「9」を出力したままキープ)なんです。

そしてそのプログラムは以下の通りです。Androidアプリ開発未経験者の方も我慢しておつきあいください。

画面のクラス
public class MainActivity extends AppCompatActivity {

    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.text_view);
        new MyTask().execute();
    }

    class MyTask extends AsyncTask<Void, String, Void> {
        @Override
        protected Void doInBackground(Void... voids) {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                    publishProgress(String.valueOf(i));
                } catch (InterruptedException e) {
                    Log.e("MyTask", e.getMessage(), e);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(String... values) {
            textView.setText(values[0]);
        }
    }
}

Androidで「画面のクラス」を作りたければ、AppCompatActivityを継承したクラスをコーディングしはじめます。

TextViewというクラスは、画面中央に配置して「START!」か「0」とか「1」とか表示している、文字列を表示する(ことしかできない)ビュー(部品)です。
このTextViewを非同期処理で「0」を「1」に、そして「2」へと1000ミリ秒きざみで変えさせたい。これがこのアプリの要件です。

非同期処理をしたければ、AsyncTaskのサブクラスを定義して、それにその処理をコーディングしておきます。

MainActivity_MyTask.png

その非同期処理をするMyTaskexecute()メソッドを呼び出すと、MyTaskが処理を始めます。
この画像の赤い囲みがTextViewです。MyTaskがこのTextViewに、"0"とか"1"とか"2"とか文字列を上書きします。これでカウントアップするように見せかけています。

それでは、MyTaskクラスにstaticを付けてみます。

Non-static field.png

IDEが真っ赤に怒り出しました。なぜならそのtextViewと名付けたフィールド、static修飾子を付けてませんからね!

MainActivityのインスタンスはもう不要なのに、天国に召されない

前出の階段を元気に登っている女性と地縛霊のお爺ちゃんのイラストを今一度見てください。

Androidでは、不要になった画面は、GCされます。「不要になった画面」とは、ユーザが戻るボタンを押すなどしてその画面を非表示にした(消しちゃう)時のことをいいます。
一般的にAndroid端末はPCと比較してメモリが少ないです2。なのでユーザが非表示にした画面=Activityクラスのインスタンスはランタイムにより速やかにGCされます。

ユーザが気軽に戻るボタンを押しちゃう、という所作ひとつで、その画面=Activityインスタンスは不要!んじゃGC!と短命なりにし運命になりがちです。
しかして、バックグラウンドで(否、「バックグラウンド」という用語は正鵠を射てなくて、正確には「ワーカースレッドにおいての非同期処理で」と言った方が適切です)MyTaskは、己の仕事(カウントアップ)を達成しきるまで生きることになります。

この、生存期間の比較で【 エンクロージングインスタンス < ネストクラスのインスタンス 】であることを危惧して警告を出してくれたのがAndroid StudioというIDEなのです。

この私が作ったアプリを起動して、カウントアップがMyTaskの仕業により始まっても、ユーザがもういいやと戻るボタンを押すなどして画面を非表示にする(消す、アプリをやめる)としても、 非staticネストクラスのインスタンスであるMyTaskオブジェクトは、MainActivityインスタンスの暗黙的参照を保持している ので、とっくに表示されてないんだからとっととGCされちゃえばいいのに、GCされない状態に陥ります。
ユーザが見えてないところで、MyTaskは脈々とカウントアップのお仕事をしているのです。
イラストの、女性であるMyTaskは1段1段階段を登って。でもその右手には、成仏できないお爺ちゃんMainActivityをしっかり握っちゃってて。ユーザはこのお爺ちゃんが見えてないのに。

解決したくとも、にっちもさっちも

私はここでジレンマに我が身が嘖(さいな)まれるのです。

  • このMyTaskは、このMainActivityでしか使われないクラスだから、ネストクラスにしたい。
  • しかしstaticネストクラスにせよとIDEが警告を出す。
  • ところがTextTview型のフィールドはstaticフィールドにはしたくない(すべきではないので)。
  • となると、このMyTaskとこのMainActivityは、別クラスに分けるか。
  • でもそうなると、TextTviewの参照の受け渡しが面倒だ(今度はこの受け渡しでヘタこくとTextTviewがメモリリークのタネとなりかねん)。
  • TextTviewを持っているのは(持つべきなのは)MainActivityだし、そのTextTviewの文字列を変えるのはMyTaskだ。
  • だったらやっぱり、このMyTaskは、このMainActivityのネストクラスにしたい。
  • java.lang.ref.WeakReference<T>を使うか...。

ずっとずっとこれに悩んでいます。どこかでなにかを妥協しなければいけないのかもしれません。

以上です。なんだかこれで終わりにしていいのか、良心の呵嘖(かしゃく)に悩む私に、福沢諭吉先生の御言葉でこの記事を綴じたいと思ひます。

されども不良の子に窘(くる)しめらるるの苦痛は、地獄の呵嘖よりも苦しくして、然(しか)も生前現在の身を以てこの呵嘖に当たらざるを得ず。

福沢諭吉「教育の事」1878(明治11)年

ちょっとユキチよ、「地獄の呵責よりも苦しくて」はシビアすぎて引くわ。
やめた。和辻先生に縋(すが)ることにします。

かく道元の説く慈悲の前には、「悪は必ずしも呵嘖すべきものでない」。

和辻哲郎「日本精神史研究」1922(大正11)年

以上です。


  1. IntelliJ IDEAを改造して作られたAndroidアプリ開発専用IDE 

  2. 10GBのRAMを搭載しているAndroid”ゲーミング”スマホとか市販されてますが、まあそれは置いておいて。 

2
3
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3