対象読者
- Javaの継承/インターフェースを理解している方
- Androidアプリ開発経験がある方
検証環境
この記事では、以下の環境でサンプルの動作を確認しています。
- Android Studio 1.5
- AVD(Nexus 5 API23)
Androidアプリ開発でよく見かける構文
Androidアプリ開発で使われる言語はJavaです。Javaは、ご存知のように業務Webシステムで主に使われている言語です。業務Webシステムでは、JSP/サーブレットを使って開発を行われることが多いですが、Android開発では、これらJSP/サーブレット開発を行っている限りではほとんど使われない構文を使います。
具体的には、以下の例を見てみましょう。
public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // ボタンを取得 Button button = (Button) findViewById(R.id.btClick); // クリック時にメッセージを表示 button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { EditText input = (EditText) findViewById(R.id.etName); TextView output = (TextView) findViewById(R.id.tvOutput); String inputStr = input.getText().toString(); output.setText(inputStr + "さん、こんにちは!"); } }); } }
これは、画面に配置した入力ボックスに名前を入力し、ボタンをタップしたら「■○さん、こんにちは!」と表示されるアプリです。
このソースコードで注目すべき記述は、以下の部分です。
赤色で示したような記述はJSP/サーブレットではほとんど出てこない一方で、Android開発では頻出する定石のコードです。実は、この記述がどういう構文で書かれているのかを理解せずとも、onClick()メソッドの中身を書き換えれば、ボタン処理の内容を修正することも可能です。しかし、それでは少し複雑なものを作る場合や、不具合が起きたときに対応できません。
そこで本稿では、上のような構文を押さえることで、一段上位のAndroidプログラマへの脱皮を目指します。
クラスのメンバとして、クラスを定義する — メンバクラス
まずは、先ほどのリスト1をよく見かけるようなクラス定義で書いてみましょう。
public class HelloListener implements View.OnClickListener { //(1) /** * 入力画面部品。 */ private EditText _input; /** * 出力画面部品。 */ private TextView _output; /** * コンストラクタ。 * 画面部品をあらかじめもらい、フィールドに保持しておかないと、onClick()メソッドで利用できない。 * * @param input 入力画面部品。 * @param output 出力画面部品。 */ public HelloListener(EditText input, TextView output) { _input = input; _output = output; } @Override public void onClick(View v) { String inputStr = _input.getText().toString(); _output.setText(inputStr + "さん、こんにちは!"); } }
public class MainActivity2 extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); EditText input = (EditText) findViewById(R.id.etName); TextView output = (TextView) findViewById(R.id.tvOutput); Button button = (Button) findViewById(R.id.btClick); HelloListener listener = new HelloListener(input, output); // (2) button.setOnClickListener(listener); } }
通常、OnClickListenerのようなインターフェースを使うとき、それをimplementsしたクラスを作成し、それを実行クラスで使用します。
たとえば、(1)のようなリスナークラスを作成し、実行クラスの側では(2)のように、インスタンス化して使います。
しかし、ここで定義しているHelloListenerクラスは、MainActivity2クラスの中でしか利用しないクラスです。このようなクラスを、別々のクラスとして定義するのは管理上望ましくありません。そこで、以下のようにコードを書き換えます。
public class MainActivity3 extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = (Button) findViewById(R.id.btClick); HelloListener listener = new HelloListener(); button.setOnClickListener(listener); } /** * ボタンをクリックしたときのリスナークラス。 */ private class HelloListener implements View.OnClickListener { @Override public void onClick(View v) { EditText input = (EditText) findViewById(R.id.etName); TextView output = (TextView) findViewById(R.id.tvOutput); String inputStr = input.getText().toString(); output.setText(inputStr + "さん、こんにちは!"); } } }
このクラス内には、MainActivity3クラスのメンバとしてHelloListenerがあります。これは、classキーワードがあることからわかるように、MainActivity3クラス内で入れ子に定義されたクラスであり、ネスト(入れ子)クラスの一種です。あるいは、クラスのメンバであることから、より限定的にメンバクラスとも呼ばれます。
ではなぜ、このようなものが存在するのでしょうか。それは、クラス再利用の防止と管理のためです。
先ほども述べたように、HelloListenerクラスは、MainActivity3クラス以外では使いようがないクラスです。クラスを設計するうえで、あるクラスだけにしか使われないクラスを作る必要がでてくる場合があります。その際に、独立したクラスとして定義した場合、想定されずに他から利用される可能性が高くなり、それはすなわち、バグにつながります。また、別ファイルで作ることで、どのクラスとどのクラスが結びついているのかがわからなくなります。
これらを防止するためには、クラス内でクラスを作成した方がよく、そのためにネストクラス(メンバクラス)があるのです。
厳密に言えば、メンバクラスの場合は、フィールドやメソッドといった他のメンバ同様、public宣言にすることで、外部からも参照できます。しかし、それではわざわざメンバクラス化する意味がありません。そこで、private宣言にし、完全に隠蔽します。
今回のサンプルでは、HelloListenerクラスはprivate宣言にしてあります。
名前を持たないクラスを定義する — 無名クラス
ただし、先ほどのような例では、メンバクラスを利用するだけでは不十分です。というのも、HelloListenerクラスはsetOnClickListener()メソッドでしか利用しない使い捨てのクラスです。そのような場合には、最初から名前付けすらせずに、クラスの定義からインスタンス化までを一気にできてしまった方が便利です。これが以下のコードです。
View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View view) { …中略… } }; button.setOnClickListener(listener);
ここでは、OnClickListenerクラスを定義し、インスタンス化する代わりに、インターフェースを実装したクラス定義を「new View.OnClickListener() {…}」として、直接記述しています。定義したクラスに対してHelloListenerのようなクラス名がありません。名前がないことから、このように直接クラス定義を書いたクラスを無名クラス、あるいは匿名クラスといいます。無名クラスは、他で再利用できない代わりに、定義からインスタンス化までをまとめて表せることからシンプルであるのが特長です。
もっとシンプルにするならば、無名クラスをsetOnClickListener()メソッドの引数として直接に引き渡すことも可能です。これが冒頭のコードです。
button.setOnClickListener(new View.OnClickListener { @Override public void onClick(View view) { …中略… } });
[Note]ネストクラス
ネストクラスは、正確にはメンバクラス(staticメンバクラス、非staticメンバクラス)、無名クラス、ローカルクラスに分類できます。以下に、分類と簡単な説明の図を記載しておきます。
クラスメンバの位置に定義されたインターフェース −メンバインターフェース
最後に、以下のコードで表された
「button.setOnClickListener(new View.OnClickListener {…});」
について解説します。クラス名.クラス名のように見えますが、その通り、これはクラスの中で定義されたクラス(インターフェース)です。このようなインターフェースは、いわゆるメンバクラスの一種で、メンバインターフェースと呼びます。疑似的なコードで表すならば、以下のように定義されています。
public class View { interface OnClickListener { void onClick(View v); } …中略… }
なぜこのような記述をするのでしょうか。インターフェースの場合は外部ファイルとして定義されていても、クラスメンバとして定義されていてもpublicです。したがって、前節で解説した再利用の防止には当たりません。ここでは、もうひとつの管理の意味合いに注目してみましょう。
注目するのは、setOnClickListener()メソッドの引数の型です。このメソッドの引数はOnClickListenerインターフェースを受け取ります。ここで、もしOnClickListenerインターフェースがあくまでsetOnClickListener()メソッドの引数専用で使うことを想定しているのであれば、外部ファイルとして定義するのではなく、Buttonクラス(正しくはその親クラスであるViewクラス)内部に記述したほうが管理がしやすく、メソッドとの結びつきが強いことがより明確に把握できます。そのために、メンバインターフェースがあります。
メンバインターフェースを利用する方法
では、これらメソッドとメンバインターフェースを使う場合、どのような記述が便利なのでしょうか。先ほどのコードが一つの答えです。
button.setOnClickListener(new View.OnClickListener { … });
メンバインターフェース型を指定する場合は、「.」でつなぎ、View.OnClickListenerのようにします。ただし、import宣言でView.OnClickListenerまでインポートしたならば、以下のようにも書けます。
button.setOnClickListener(new OnClickListener { … });
まとめ
本稿では、Androidアプリ開発で頻出の無名クラス、メンバクラス、メンバインターフェースについて解説してきました。実際のAndroidアプリサンプルではリスト1 MainActivity.javaで示した無名クラスだけでなく、リスト4 MainActivity3.javaのようなメンバクラスのものもあります。ところが、この両者は記述方法が違うだけで同じ処理を実現できます。開発するアプリに応じて、無名クラス、メンバクラスを自由に使い分けられるようにして、一段上のプログラマを目指してください。
