【Android】【Applet】【Java】interface と abstract の使い方・違いとは~簡単なフレームワークを作って、体感してみる 
2014/06/26 Thu [edit]
さて、これまで「テキストファイルを読み込む」でたくさんの関数を作ってきたが、それを更に使い回しするために、インターフェイスと抽象クラスを利用した、簡単なフレームワークを作成してみる。
「インターフェイスって何?」「抽象クラスとどう違うの?」「何となく概念はわかるが、使い分けがわからない」という感じの人にピッタリな講座(笑)になると思う。それぞれの説明や概念的なものはググればいくらでも出てくるので割愛するが、いくつか参考資料を載せておくので、サラっと見ておくと良いかも知れない。
(参考) インターフェイス
(参考) 抽象クラス
(参考) クラスの継承
(参考) ポリモーフィズム
(参考) 多重継承とインターフェイス
ここでは文章で説明するより、実際にコードが出来上がっていく過程で、その違いや使い途を考えてみよう。今回作ってみる「簡易フレームワーク」というのは、そのインターフェイスと抽象クラスの使い方の1つに過ぎないが、一度理解できたなら、自然に interface と abstract を利用するようになるだろう。また自作ライブラリなどの資産を、長く使い回しできる手段にもなると思う。もちろんこれが正しい使い方というわけではない。解説に出てくる言葉もあくまで比喩的な表現なので、正式な用語というわけでもない。プログラムの書き方なんて千差万別なので、こうでないといけない、という例でもない。そういった理屈で理解するのではなく、それぞれの概念や用法を、ある条件下において、実際に使用することによって、体感的に捉えてくれれば十分だと思う。
テーマとして、2つのプラットフォーム - Android と Applet でメインコードにおいて、共通に使えるコード表記というものを考えてみよう。
そのどちらも言語としては Java で開発できるわけだが、やってみると色々と使用するオブジェクトが違い、そのままでは2種類のコードを書かざるを得ない事がわかる。
実際にコンポーネントの違いで、どうしても同じコードは書けない部分があるのだが、なるべくなら同じコードで動くようにしたいものだ。画面サイズの違いやユーザーインターフェイス(Android はタッチ、Applet は PC なのでマウスとか)の違いなどあって、なかなか同じコードで書けないと思うわけだが、こういったものも、自分である程度仕様を決め、抽象化してしまえば、共通の動作が見えてくる。それらを上手くまとめて使うために interface と abstract などを利用すると良い。
せっかくなので今回は、「テキストファイルを読み込む」で作ったファイル読み込みを、両プラットフォームで共通利用できるコードを考えてみよう。
例えば、あるアプリがあったとして、その Android 版と Applet 版を作るとする。それぞれのアプリは内部にリソースを持っていて、そこからテキストファイルを読み込む動作をすると仮定しよう。具体的な仕様は Android 版では assets から、Applet 版では自身の jar のリソースの中からファイルを読み込むとする。これをそれぞれコードを書いてみたらどんな感じになるだろうか?以前作ったライブラリをそのまま使って表現してみよう。
●Android 版(Activity クラスにて)
//リソースからテキストファイルを読み込む
String fileName = "res/data/sample.txt";
String text = loadTextAsset(fileName, this);
●Applet 版(Applet クラスにて)
//リソースからテキストファイルを読み込む
String fileName = "res/data/sample.txt";
String text = loadTextJar(fileName, this);
コードの違いは何だろうか?ファイルのパス名はたまたま同じに書けたから良いが、メソッド名がどうにもならない。
Applet 版では loadTextAsset() は使えないし、かと言って、Android 版で loadTextJar() も使えない。
もし、このファイル読み込みのコードが100箇所あったらどうなるだろうか?移植しようと思ったら、当然100箇所書き換えの必要性が出てくる。もちろんエディタの置換機能を使っても良いのだが、例えばバージョンアップなど、コードを更新するたびに書き換えでは骨が折れる。そういう時、 interface を使ってメソッドを共通化して置くと便利だ。
では、実際にやってみよう。インターフェイスの概念はともかく、簡単な仕様さえわかってればいい。参考ページにも載ってるね。インターフェイスとは「抽象メソッドしか持たないクラスのようなもの」とあるけど、そんな感じで良い。ちょっと違う視点で用途を考えてみよう。例えば「これからあるクラスを作りたいのだが、あらかじめメソッドを決めておきたい」みたいな感じだ。そうつまり、「インターフェイスというものはメソッドを"予約"できる」と考えるわけだ。
というわけで「メソッドを予約」してみよう(笑)。とりあえずファイルを読み込むようなクラスをいずれ作りたいので、名前は適当に付けて、定義してみる。こんな感じだろうか?
●ファイル操作用のインターフェイス (XFiler)
public interface XFiler {
public String loadTextFile(String fileName);
}
interface を定義する場合、よく慣例的に頭に"I"(Interface の頭文字)を付けることも多いので、"IFiler"等でも良いかも知れない。名前はあとで自分でわかれば良いので、何でも良い。
メソッドはとりあえず、テキストファイルを読み込んでくれそうな名前(笑)にしておく。引数はある程度、慣れでわかってくると思うが、なるべくどのプラットフォームでも要求されそうな、共通の値が良い。String fileName となっているが、実際には String 型であれば、ファイル名でもリソース名でも、何でも良い。
準備は整ったので、今度は実装クラスを作ってみよう。実装というのは言わば、空のメソッドやクラスに中身を入れる事だ。インターフェイスは仕様上、中身は空っぽなので、もちろんそのままでは使えない。だから、implements して、インスタンス化できるクラスが必要なのだ。実装はそれぞれのプラットフォームに依存するので、わかり易い名前でクラスを定義してみよう。とりあえず、Android 版は AndroidFiler に、Applet 版は AppletFiler にしてみた。
●Android 版 XFiler インターフェイス実装(仮)
public final class AndroidFiler implements XFiler {
public AndroidFiler() {
// TODO 自動生成されたメソッド・スタブ
}
@Override
public String loadTextFile(String fileName) {
// TODO 自動生成されたメソッド・スタブ
return null;
}
}
●Applet 版 XFiler インターフェイス実装(仮)
public final class AppletFiler implements XFiler {
public AppletFiler() {
// TODO 自動生成されたメソッド・スタブ
}
@Override
public String loadTextFile(String fileName) {
// TODO 自動生成されたメソッド・スタブ
return null;
}
}
先ほど作った「XFiler」というインターフェイスを新規クラスに噛ませる(implements)と、自動的に loadTextFile() が作られたと思う。そう、実際"予約だ"(笑)。作る前から決まっている。もう少しちゃんと説明すると、「継承」というのは、親クラスから子クラスにもメソッドが引き継がれるだろう?インターフェイスもコンパイラから見ればただのクラスなのだ。だからそれを取り込んだら(extends と同じ)、そのメソッドが無ければつじつまが合わない。インターフェイス自身はシグニチャだけという感じなので、コンパイラが「きちんとメソッドを定義しろ」みたいに言ってくるわけだ。つまり逆に考えれば、「あるインターフェイスを implements していれば、その定義メソッドを持ってることは保証される」ということになる。それは抽象クラスも同じだ。要するにクラスの「型」というのは「どんなプロパティがあって、どんなメソッドがあるのか?」みたいなものだ。このインターフェイスはそのメソッドだけを保証したものとなる。
「じゃあ、抽象クラスでも良いの?」と思うだろうが、それも別に構わない。実際に抽象クラスというのはある程度実装を持っているので、中身がある分、実装コードをショートカットできる利点もあるだろう。しかしその実装部分がかえって邪魔になることもある。どういう事かと言うと、それはこの例のように Android 版では Applet 版の機能は使えず、Applet 版では Android 版の機能は使えないのなら、どちらかの機能を実装していた場合、片方は邪魔になる。完全な定型処理ならともかく、実装クラスごとに書き換える可能性が高い場合は、無駄にしかならない。だから中身が無い方が良い場合もある。もっと言えば、「中身がないというのは、中身を自由に書き換えられる」という事だ。そしてコンパイラから見ても、「名前などシグネチャが合ってれば、中身なんて何でも良い」って感じで動作する。だからケースバイケースだ。
そして今度は実際に使えるように、先ほどの XFiler 実装クラスを定義し直そう。ファイル処理の場合、よくアプリケーションコンテキストのようなものを要求されるので、それを引数としたコンストラクタで作って置く。
●Android 版 XFiler インターフェイス実装 (ファイル操作用クラスの定義)
public final class AppletFiler implements XFiler {
private final Applet context;
//コンストラクタ
public AppletFiler(Applet context) {
this.context = context;
}
//テキストファイルを読み込む
@Override
public String loadTextFile(String fileName) {
try {
return loadTextJar(fileName, context);
} catch (Exception e) {
return null;
}
}
}
●Applet 版 XFiler インターフェイス実装 (ファイル操作用クラスの定義)
public final class AndroidFiler implements XFiler {
private final Context context;
//コンストラクタ
public AndroidFiler(Context context) {
this.context = context;
}
//テキストファイルを読み込む
@Override
public String loadTextFile(String fileName) {
try {
return loadTextAsset(fileName, context);
} catch (Exception e) {
return null;
}
}
}
例外処理は手抜きした。読み込み失敗したときは null が返ってくる。クラスやメンバ: context は final にしてるが、別に final でなくても構わない。
これを、それぞれのプラットフォームでインスタンス化して使ってみよう。その部分だけを抜粋してみる。
●Android 版(Activity クラスにて)
//インスタンス生成
XFiler xFiler = new AndroidFiler(this);
//リソースからテキストファイルを読み込む
String fileName = "res/data/sample.txt";
String text = xFiler.loadTextFile(fileName);
●Applet 版(Applet クラスにて)
//インスタンス生成
XFiler xFiler = new AppletFiler(this);
//リソースからテキストファイルを読み込む
String fileName = "res/data/sample.txt";
String text = xFiler.loadTextFile(fileName);
はじめのコードと比較すると、メソッド部分は共通化され、XFiler のインスタンス化部分以外は同じコードになったと思う。つまり、Android 版の loadTextAsset() メソッドと、Applet 版の loadTextJar() メソッドの名前を、表舞台から裏方に隠したみたいな感じになる(笑)。これは AndroidFiler でも AppletFiler でも同じ XFiler というインターフェイスを持っているため、loadTextFile() というメソッドも持っていることが保証されているからだ。そしてこれは継承の機能そのものである。つまり、XFiler というのはスーパークラス(親クラス)と同じなので、その子クラス AndroidFiler でも AppletFiler でも左辺に代入できる。loadTextFile() の中身はそれぞれ違うものだが、コンパイラからすれば、XFiler という型さえわかっていれば、実行できるので問題ないということだ。
ここまでくれば、あともうひと息。インスタンスを生成する部分以外は、まったく同じコードになったわけだから、コードの共通化はできたとも言える。実際にこういった考えで、よく使う機能を共通化して置けば、他のプラットフォームに移植するときなど、かなり楽になるだろう。ファイル名なども通常は別の定義ファイルなどにして置けば、メインコードを書き換えないで済む。インターフェイスの使い途としても申し分ない。
あとはインスタンス生成部分か?これもインターフェイスを使って、そのコードを裏方に隠す方法があるのだが、それは次の機会に譲るとして、今回は別の方法でインスタンス生成をメインコードから追い出す事とする。この手法がちょっとしたフレームワークになる。
実際に色々共通化してみればわかる事だが、インスタンス生成というのはオブジェクトの型(クラス名)が必要なわけで、これが共通化の邪魔になる。だからある程度は諦めるしかないのだが、おおよそよく使う機能などなら、それらをまとめて扱う抽象クラスを作って置くと良い。それを毎回利用することによって、使い回しと同じような事ができる。
では簡単なフレームワークを定義しよう。実際にはちゃんとしたものなら、グラフィック描画やユーザーインターフェイス、サウンド機能など、一般的によく使われるものをまとめて置くのだが、今回はいままで作ったファイル操作用のインターフェイス(XFiler)だけを定義例にしておく。名前はまた適当なものだ。何でも良い。
●フレームワーク用のインターフェイス (XFramework)
public interface XFramework {
public XFiler getFiler();
}
定義した内容は、使いたい機能を呼び出すためのものだ。今度は「機能を予約」して置くとでも考えれば良い(笑)。
今度はこれを各プラットフォームの起動クラスで使えるように工夫しよう。つまり Android 版なら Activity クラス、Applet 版なら Applet クラスからアプリが開始されるわけだが、それらに機能を持たせたい。しかし毎回同じコードを書くのは面倒だ。だからそういう場合、ある程度実装を持たせた抽象クラスを使うのが手っ取り早い。もちろん、最終的に継承して使うのが目的なので、スーパークラスは起動に必要なクラスと同じにする。実際にそこまでやってみよう。名前は適当に、Android 版では Activity クラスを継承した XActivity に、Applet 版は Applet クラスを継承した XApplet にした。
●Android 版 起動用 抽象クラス (XActivity)
public abstract class XActivity extends Activity implements XFramework {
private final XFiler xFiler;
//コンストラクタ
public XActivity() {
xFiler = new AndroidFiler(this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public XFiler getFiler() {
return xFiler;
}
}
●Applet 版 起動用 抽象クラス (XApplet)
public abstract class XApplet extends Applet implements XFramework {
private final XFiler xFiler;
//コンストラクタ
public XApplet() {
xFiler = new AppletFiler(this);
}
@Override
public void init() {
super.init();
}
@Override
public XFiler getFiler() {
return xFiler;
}
}
注目すべき点は、それぞれの抽象クラスに XFramework インターフェイスを implements してる事だ。すると当然、インターフェイス定義の getFiler() が必要になるわけで、それを xFiler メンバに、コンストラクタ起動時にインスタンス化してたものを、メソッドで返している。つまり、このクラスにはファイル操作の機能(XFiler)が追加されたことと同じになる。
そしてこのクラスは abstract で宣言されているので、そのままでは使えない。最後に起動クラスに継承して完成するわけだ。本来、このクラスの場合、abstract でなくても別に構わないのだが、直接このクラスのインスタンスを生成したくないので、わざと抽象クラスにしている。あくまでサブクラス利用を前提とするためだ。「ヘルパークラス」と呼ばれるものも、同じような使い方をするものが多い。ここではコンストラクタを定義してるが、逆にサブクラスでコンストラクタを定義させたり、またはスーパークラスであらかじめ必要なものが定義してあって、サブクラスでいくつかのメソッドをオーバーライドすることを前提とした抽象クラスなどだ。これもケースバイケース。
ちなみにコンストラクタでインスタンスを生成しているが、別に各初期化メソッド onCreate()、init() でも構わない(ただし、final 宣言はできなくなる)。画面に関するオブジェクトなら、onCreate()、init() の方が良いだろう(コンストラクタ起動時点では、まだ画面に関するプロパティなどが正常に取得できなかったりするため)。コンストラクタ→初期化メソッドの順に実行される事だけ注意しよう。メソッドを使う前にインスタンス化しないと、当然、NullPointerException となる。
また余談だが、なぜ私がやたら final 宣言にこだわってるかと言うと、interface や abstract を多く使うようになると、コードが getter/setter (getXXX(), setXXX() メソッドのこと)で多く占められることが容易に想像付くだろう?実は getter/setter 表記(a = getXXX() のような)というのは、ドットシンタクス表記のプロパティ直接アクセス(a = object.data のような)よりは、若干実行速度が落ちる。リアルタイムな処理を必要とするアプリの場合、これが痛手になる事もある。やってみるとわかる事だが、Java という言語はクラスでもメソッドでもプロパティでも、final 宣言を多くすると実行速度が格段に上がる。これはコンパイラが final 宣言されたものに関して、優先的に最適化を行うためだ。だから1つでも多く final 宣言する事で、実行速度を上げようとする、筆者のクセになってるためである。だから必ずしも final にしなければならないと言う事はない。
さて、今度はこれらを実際に起動クラスに実装する。ついでにメインコードの「テキストファイルを読み込む」部分も書いてみよう。名前は、Android 版では MainActivity に、Applet 版は MainApplet にした。
●Android 版 起動クラス (Activity クラス)
public final class MainActivity extends XActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//リソースからテキストファイルを読み込む
String fileName = "res/data/sample.txt";
String text = getFiler().loadTextFile(fileName);
}
}
●Applet 版 起動クラス (Applet クラス)
public final class MainApplet extends XApplet {
@Override
public void init() {
super.init();
//リソースからテキストファイルを読み込む
String fileName = "res/data/sample.txt";
String text = getFiler().loadTextFile(fileName);
}
}
どちらのクラスも XFramework というインターフェイスを持っているので(抽象クラスから継承しているので)、当然 getFiler() メソッドを使って xFiler インスタンスを呼び出す事ができる。これでめでたく、メインのコード「テキストファイルを読み込む」部分は共通化できた。これは抽象クラスを使って、インスタンス生成コード部分を、メインから裏方に追い出したことになる。ちょっとしたズルだが、こういうのは抽象クラスの方がやり易い。つまり「型」を必要とするオブジェクトをなるべくメインから裏方に隠し、メソッドで表現することによって、プラットフォームによるコード依存を軽減していることになる。実際、この「ファイル操作用のインターフェイス(XFiler)」の機能を使うコードに関しては、どちらのプラットフォームでも、コードを書き換えずにそのまま使えることがわかるだろう。
それにバージョンアップするときなども、抽象クラスのオブジェクトを入れ替えるだけで、メインのコードはいじらなくても済むという利点もある。更に言えば、画面を作成するレンダリングエンジンなども分離して、起動クラスからそちらへ制御を移してしまえば、Activity や Applet クラスはただ起動するだけのものにしてしまえる。定型処理で済むようなものは、こういう風にインターフェイスと抽象クラスを使って、あらかじめ雛形を作って置くと、それを利用するだけで、機能が使え、共通化しやすいコードで構成できるようになるわけだ(これが簡単なフレームワークとなる)。
そして他のクラスにその機能を渡す場合も簡単だ。例えば Android 版で View クラス内でもその機能を使いたい場合は、XFramework をそのまま渡してやれば良い。
●Android 版 で Activity クラスから View クラスにXFiler 機能を渡してみる
//起動 Activity
public final class MainActivity extends XActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new MainView(this, this)); //View のコンストラクタに XFramework を渡す
}
}
//Activity から 生成された View
@SuppressLint("ViewConstructor") //← View のコンストラクタと違うので警告が出るが気にしない(笑)
public final class MainView extends View {
private final XFiler xFiler;
//コンストラクタ
public MainView(Context context, XFramework fw) { //ここで XFramework を受け取る
super(context);
xFiler = fw.getFiler(); //XFiler 機能(オブジェクト)呼び出し
//リソースからテキストファイルを読み込む
String fileName = "res/data/sample.txt";
String text = xFiler.loadTextFile(fileName);
}
}
このような手法で、色々な機能や定型処理を抽象化してしまえば、機能入れ替えやプラットフォーム間のメインコードの移植等はもの凄く簡単だ。interface や abstract はこういったことを容易に実現してくれる。実行手続き(メインコード)と実装部分(定義とか)を切り離したものだとも言えよう。こういった構造で作って置くと、後から定義だけをそっくりそのまま入れ替えることも可能である。今回の一部機能だけ取り出したり、実装を隠したり、これが正しい使い方とかいう話は別にして、「こういう風にも使える」と覚えて置くだけでも、使い途を色々考えられるだろう。
他には例えば Object クラスの clone() を使うためには Cloneable インターフェイスを implements する必要があるが、実は定義は空だったりする。あれはメソッド定義のためでなく、実行前に「Cloneableインターフェイスを実装してるか?」だけをチェックするためにあるのだという。実装してない場合、CloneNotSupportedException を発生させて、clone() を使えなくする。こういうインターフェイスの使い方もある(マーキングという)。あとよく使われるのはコールバック(リスナー)機能だね。
その辺は慣れたら、自分で色々やってみると良いだろう。最後に、この簡易フレームワークの考え方で作ったサンプルデモを載せて置こう。Applet 版と Android 版の2種類作ってあるが、どちらもメイン部分は同じコードで書かれている(というより同じコードを参照している)。処理速度や見た目に多少の違いは出るが(内蔵フォントの違い等)、動作的にはほとんど変わらない。簡易フレームワークも詰めれば、マルチプラットフォーム機能のように振る舞うことも可能である。
サンプルデモ:Rule Generator Demo
Applet 版のサンプル (PC[IE]でアクセス)
Android 版のサンプル (Android2.2以降でアクセス)
(解説) Android と Applet の互換ってできるもんだね
(関連記事)
【Android】【Applet】【Java】interface を使って独自のラッパークラスを作る
【Android】【Applet】【Java】interface を使ってインスタンスを生成する
【Java】interface を使って簡単なコールバック機能を作る
【Java】clone() を使ったオブジェクト複製
【Android】【Applet】【Java】テキストファイルの読み込み・保存 まとめ
- 関連記事
トラックバック
トラックバックURL
→http://fantom1x.blog130.fc2.com/tb.php/127-40297c4a
この記事にトラックバックする(FC2ブログユーザー)
| h o m e |