Home: Up

Java言語について


1.オブジェクト指向設計について

Javaはオブジェクト指向言語の一つであり、C++等と文法的にも似た部分が多く見られます.

当資料ではオブジェクト指向設計の方法やその必要性等について説明していくつもりはなかった(長くなるし)のですが、せっかくJavaでプログラムを書くならオブジェクト指向らしいプログラムを書いてもらいたいので、第1章はオブジェクト指向設計について説明してみようと思います.

なお、元々第1章は1頁ぐらいで終わる予定だったのが、後で気が変わって書き足している分、用語説明などを以降のJavaの説明の方でやっていたり、Javaの説明と重複するような表記もあるのでご容赦ください.

※当資料はレビュー結果に応じて説明を書き足していく事になるので、説明が必要と思われる用語等があった場合はshin@sk-jp.comに要求してください.


1−1.オブジェクト指向の特徴

オブジェクト指向プログラミングの特徴は以下の点に集約されます.

これらを総合した上で結局なんの役に立つのかというと個人的には以下のような点なのかなと思います.

何が重要かといえば、プログラムをオブジェクトという名の部品の集合で表し、それぞれの部品に明示的に独立性を持たせる(他のオブジェクトへの依存を極力なくす)ことで、部品単位での修正(バージョンアップ)や再利用を楽に行えるようになるということです.

結局、これは「生産性」の向上につながることになります.

これだけでは良く分からないということで、以降にそれぞれの特徴について簡単な説明を書いてみます.

(余計ややこしくなるかも)


1−1−1.データ抽象化

非オブジェクト指向言語ではルーチン(routine:いわゆる関数)によってプログラミングを行ってきました.
しかし、このルーチンというやつを再利用しようとした場合、かなり面倒な作業が必要になる事が多くなります.

たとえば、ルーチンの中でモジュール内の共通領域を操作するようなコードが書かれていた場合、それを他のモジュールで動作させるのはルーチン自体の書き換えが必要であり、そんな事をして元のモジュールでの動作に支障をきたすぐらいなら、一から作ったほうがマシと言った事が頻繁に起こったでしょう.

オブジェクト指向においては、「抽象データ型」を用いてプログラミングを行います.

抽象データ型とはintやlongのようなプリミティブな型と異なり、データ(★1)+操作(★2)を一纏めにして型として定義します.
一般的にはこの型の名称はclass(クラス)と呼び、その実体をオブジェクトと呼びます.

※Cの感覚で言い換えればクラスはtypedef structに相当し、その型の変数を定義したときの実領域がオブジェクトに相当します.
※特定のクラスからオブジェクトを生成する事をインスタンス化するといいます.

★1:一般的にフィールドと呼びます.C++ではメンバ変数、Javaではメンバと呼ばれます.
Javaの世界でもフィールドで通じるのでなるべくこちらの表記を使います.
★2:一般的にオペレーションと呼びます.C++ではメンバ関数、Javaではメソッドと呼ばれます.
Javaの世界でもオペレーションで通じるのでなるべくこちらの表記を使います.
(どちらかと言うとメソッドと呼ぶ事の方が多いですが)

もっと簡潔にいえば、全ての世界をオブジェクトとオブジェクトに対するメッセージのみで表現しようとすることがオブジェクト指向であり、それをプログラミングの世界に適用しようとしたものがオブジェクト指向プログラミングというわけです.

話を戻して、データを抽象化する事の理由は、データを値としてしか持たない非オブジェクト指向言語の場合、データに対する操作を記述する場所が分離してしまっている上、そのデータをどのように操作するかはプログラマの自由になってしまうため、特定のルーチンの再利用が(上記に挙げたように)難しくなってしまうのを回避する事にあります.
抽象化されたデータ(抽象データ型)では、フィールドを操作できるのはその抽象データ型の中で定義したオペレーションのみであるという約束(★3)を守っていれば、いつのまにかデータが不本意な値に書き換わるような事が無くなり、保守性/可読性が向上します.
オブジェクト指向設計では、設計の段階で、ある事象に対して行える動作(インターフェイス)を抽出し、その動作によって変化する状態を抽出するといった形で抽象データ型の設計を行っていくようにします.

(先に状態から決めていくと問題が発生しやすくなります)

※「実装に対してプログラミングを行うのではなくインターフェイスに対してプログラミングする」とは有名な言葉です.

★3:「カプセル化」の概念と呼ばれます


1−1−2.継承

抽象データ型があれば再利用が楽になるかというとそれだけではパンチ不足です.
なぜなら、抽象データ型によってデータとそれに対する処理をカプセル化したからといって、再利用の際にその型の持つ機能がそのままで使えるとは限らないからです.

抽象データ型「配列」クラスを作ったとしましょう.
「配列」クラスの機能には要素数の設定、指定Indexに対するデータのSet/Getを定義しました.

※以降、コードについてはすべてJava言語で記述しています.
※Java言語については第2章以降を参照.

class Array {
    private Object   o[];    //配列のデータ群を格納する領域を指すフィールド
    private int      size;   //配列の要素数を格納する領域を指すフィールド
    public void setSize(int size_){
        //指定数のデータ格納領域の取得処理を記述
        if(o==null){                    //配列が存在しなければ
            o       = new Object[size_];//配列の格納領域の取得
            size    = size_;            //配列の要素数を格納
        }
    }
    public int getSize(){
        return(size);
    }
    public void setData(int index,Object o){
        //指定Indexにデータを書き込む処理を記述
        code・・・
    }
    public Object getData(int index){
        //指定Indexのデータを返す処理を記述
        code・・・
    }
}

余談ですが、クラスArrayを使用する人はo[]のような情報が存在する事について知っている必要はありません.
使う側は公開されたインターフェイスだけを知っていれば良いのです.
それを言語仕様的に明示しているのが「public」(公開)「private」(非公開)のキーワードです.(後述)

それはそうと、ある人がこの配列に似たような機能ですが、実行時に動的に配列の要素数を変更できる物がほしいと思いました.
上のコードを見れば解りますが、このArrayクラスを用いた場合、一度setSize()を呼び出して要素数を決めると、その後 setSizeを呼び出しても何もしないようになっています.
(もちろんこのような仕様である事を明記しておく必要はあります) これではもう再利用できないじゃないかってな事になるんですが、こういう時に継承を用いて機能拡張を行う事ができます.
(うーんあんまりいい例じゃないですね)
とりあえず以下のようなクラスを作って使用すれば良い事になります.

//新たな要素数が以前の要素数より少ない場合
//後半の要素が消滅します
class DynamicArray extends Array {
    public void setSize(int size_) {
        if (o == null) {                //配列が存在しなければ
            o = new Object[size_];      //配列の格納領域の取得
            size = size_;               //配列の要素数を格納
            return;
        }
        Object work = o;                //oを参照する
        int copySize = size < size_ ? size : size_;

        o = new Object[size];           //新しい配列を生成
        for (int i = 0; i < copySize; i++){  //配列の要素のコピー
            o[i] = work[i];
        }
    }
}

これで、動的に配列の要素数を変更できる新たなクラスが出来上がります.つまり、配列の要素のGet/Setは スーパークラスの物がそのまま使用でき、要素数設定オペレーションは 新しく定義された側の物が使用されるといった具合です.(オペレーションオーバーライドと呼びます)
※継承元のクラスをスーパークラス/ 継承先のクラスをサブクラスと呼びます.

この例では継承する部分が少ない為、なんとも言えないですが、継承する部分が多くなれば 一から作るのと比べて大幅に書き足す部分が少なくなるのがわかると思います.

※この例の場合、継承を使わなくてもArray自体を書き換えればいいじゃないかと言う意見もあると思いますが、 Arrayを書き換えられる状態じゃない場合もあるわけです(既に元の機能を前提に使用されていたりする場合).

※継承の欠点としてスーパークラス の実装についてある程度知っていなければならないと言う問題があります.
オブジェクトコンポジション


1−1−3.動的結合

ポリモーフィズムの事です.ってこれだけじゃ良く分かりません.

C言語なんかの場合、関数呼び出し文が記述されていれば、呼び出される関数は決定したと言えます.
これは、リンク時に関数呼び出し文と呼び出される関数との結合が行われる為です(静的結合).
(Cには関数ポインタなんて物も存在しますが)

でオブジェクト指向言語の場合ですが、こちらは、オペレーションへの要求(=メッセージ)が出されたときに どのオペレーションが呼び出されるかと言うのは実行時まで解りません.(絶対に解りませんって)
この言葉が動的結合の全てなんですけど、なんで解らないの?って言うと以下のような重要な機能を 実現する事に理由があります.

クラスは「抽象データ型」と言うくらいだから「型」です.そして、クラスを継承する事は型を継承する事でもあります.
※継承元をスーパータイプ/継承先をサブタイプと呼びます
型を継承すると言う事は、サブクラスの型は自分自身の型でありながらスーパークラスの型でもあると言う事です.

以下のように記述できます

class SuperType {
    void operation(){
        System.out.println("This is SuperType.");
    }
}

class SubType extends SuperType {
    void operation(){
        System.out.println("This is SubType.");
    }
}

class A {
    SuperType   a = new SuperType();            //OK
    SubType     b = new SubType();              //OK
    SuperType   c = new SubType();              //OK
//  SubType     d = new SuperType();            //ERROR
//  SubType     e = (SubType)new SuperType();   //コンパイルOK実行時ERROR
    SubType     f = (SubType)c;                 //OK
    A(){
        a.operation();
        b.operation();
        c.operation();
        f.operation();
    }
    public static void main(String arg[]){
        new A();
    }
}

型の継承の意味を考えると逆の代入は行えない事になります.
キャストを行えばコンパイルは通りますが、実際にサブタイプのオブジェクトをサブタイプにキャストしないと 実行時にエラーが発生します.

そこで、上記のクラスA内のメソッド中にc.opration();とか書いてあった場合に、 SuperType/SubTypeのどちらのoperation()が呼び出されるかはコンパイル時には判別できません.

もっと言えば、どこかのオペレーションの返り値としてSuperTypeのオブジェクトを返すと言って SuperType変数に代入したとしても、それがほんとにSuperTypeのオブジェクトとは限らないから SuperTypeのオペレーションが呼ばれるとは限らないのです.

長くなってしまいましたが、これは断じて欠点ではなく、実際に呼び出されるオペレーションが 何か知らなくてもオペレーションの公開仕様さえ知っていればプログラミングが行えると言う 発想である事を覚えてください.
(その究極の形がJavaのinterfaceですね)


1−2.オブジェクト指向の難点とJava

オブジェクト指向の導入によって被るデメリットが以下のように既に指摘されています.

上記のデメリットにも挙げていますが、オブジェクト指向言語といえば(特に「抽象化」という言葉に関して)概念的な難しさが付きまといます.しかし、Javaはその設計思想の中に「習得のしやすさ」を掲げているため、C++などと比べて短期間でC++と同等のプログラムが作れるようになるほか、オブジェクト指向的記述も行いやすくなっています.

そういう意味でオブジェクト指向言語の中でも特に「生産性」の向上が期待できる言語といえるのではないでしょうか.

たとえば、Javaでは取得した領域を明示的に開放する必要がありません.これらの処理はすべてVM(JVM:Java VIRTUAL MACHINE)が適切なタイミングで行います.

この機能は「ガベージコレクション」と呼ばれるもので、昔のインタプリタは大概備えていた(ガベージコレクタ)のですが、C/C++では実行時に単体で高速に動かすことを目的にメモリの破棄タイミングをプログラマが指示しなければならなくなっていました.

これはプログラムの細部にわたってプログラマが設計できるようにする反面、バグを多発させる危険を持っている事は既にお分かりでしょう.

VMについて

VM(バーチャルマシン)とはその名の通りの「仮想機械」であり、異なるCPUにおいてもVM上では同一のプログラムを実行できます.
実際にはVMはインタプリタであり、Java VM(JVM)が実行可能なのはJavaの「バイトコード」です.
「バイトコード」は言ってみればJVMにおける機械語であり、たとえばWin32用JVMでもUnix用JVMでも同じバイトコードが実行可能です.

各機種用のブラウザにはJVMが同梱されているため、各種(各機種用)ブラウザから同じWebページ上のJavaアプレットを動作させる事ができます.

アプレットについて

アプレットとはHTML上に配置する形式のJavaプログラムのことであり、アプリケーションとは異なるWebページ上の部品プログラムと言ってよいと思います.

また、「多重継承の廃止」もJavaの特徴の一つです.

多重継承は、複数のクラスの機能を持った新たなクラスを生成する機能であり、 これによりそのクラスがどのような機能の組み合わせであるかを推測し易くなるという 利点がありますが、クラス内のどの部分がどのクラスから継承された機能かがわかりにくくなって、 バグの発生源を突き止めづらくなったり、上記問題点に挙げた通り、 「クラス間の依存関係」を深めることになって再利用性の低下を引き起こす要因になっていました.

Javaではクラスの多重継承を廃止し、「インターフェイス」の概念を導入しています.

インターフェイスはクラスとにたようなもの(抽象クラスの一つと言えます)であり、 クラスの「動作」(インターフェイス)のみを記述したものと考えられます(後述).

※多重継承の廃止により直感的な記述ができなくなるか?

多重継承では複数の機能を合成した上に拡張するようなことが可能です.
たとえば、ボタンクラスとイメージクラスを継承した「イメージボタンクラス」を作った場合、 これは直感的であり、イメージボタンクラスのオブジェクトにはイメージの変更やボタンの 入力応答を処理する機能が含まれることになります.

しかし、イメージボタンクラスの「役割」(roleと呼ばれる)を考えると 「ボタンを押せる事」、「ボタンを描画できる事」、「ボタンが押された事を伝える事」さえできればよく、 「ボタン上にImageを描画できる事」はボタンにとっては付加要素でしかありません. この例の場合、ボタンはあくまでもボタンでしかない為、ボタンクラスを継承したものに単独の イメージクラスを管理させれば充分(というよりこちらの方が自然)です.

あまり良い例ではないのですが、要は、継承すべきものは作り出したいオブジェクトの本質的な 機能を持ったオブジェクトのみで良いという事です.

interfaceという概念でこれに機能追加を行う手段を後に示します.
※interfaceは多重継承の代わりではありません.

第2章ではJavaという言語の仕様について記述しますが、第3章以降では実際のプログラムを見てオブジェクト指向設計の優位点を確認していくことができればいいなと思います.