Hatena::ブログ(Diary)

偏見プログラマの語り!

2011-12-22

Java 的オブジェクト指向を 90 分で理解する

| 21:30

1. 分からない。いくら説明を読んでも分からない。

● 1.1. 未だに分からない Javaオブジェクト指向

 今日び Javaオブジェクト指向の説明なんて星の数ほどあるような気がしますが、それでもなお「これで分かった!」という説明に辿りつけない不運な人がいるようですね。まぁこんだけ色々な説明が溢れていたら逆にどれを読めば良いのかワケ分からなくなってしまうのかもしれません。じっくり読んでも理解できなかったのであれば、きっとその説明と読者の相性が悪かったんでしょう。… というわけで、僕も Javaオブジェクト指向が全っっっっ然これっぽっちも分からないという人に向けて説明する記事を書こうと思います。そうでない人には無価値な記事ですのでブラウザの「戻る」をクリックしましょう。

● 1.2. 「オブジェクト指向」という名の南の島がある

 オブジェクト指向にはいくつもの専門用語があって、学習するのが大変ですね。専門用語の説明に専門用語が使われてたりして、ワケ分からなくなる事もあるかと思います。専門用語の説明に専門用語が使われるというのはどういうことかというと、その界隈には色んな文脈があるということです。そしてそれぞれについて語るべきことがあり、それぞれがそれぞれの知識背景を抱えて語っているということです。その深さや広さに唖然としてしまうかもしれませんが、一部分でも分かることができれば、あとはちょっとずつ分かる領域を広げていくことができるはずです。Javaオブジェクト指向が分からないというのは、どこから学べば良いのかが分からないだけだと思います。

 まぁそんなわけで、もしかしたらこの記事を読んで分かるようになるかもしれませんし、やっぱり分からないかもしれません。が、本当に Javaオブジェクト指向が全然分からなくて、分かりたいと思っていて、この記事を読んでいる人は、僕が分かってる範囲で噛み砕いて説明するので、これを読むために 90 分投資してください。「もっと短くてわかりやすい説明ぐらいいくらでもあるしwww アホかwww」と思うならブラウザの「戻る」をクリックしましょう。この説明を最後まで読んでもやっぱり分からなかった場合も、他を当たりましょう。その際にはこの記事の説明はきれいさっぱり忘れて読むと、もしかしたら今度こそ分かるかもしれません。例えば 『いまさらながらだけど、オブジェクトとクラスの関係を究めてみようよ』 檜山正幸のキマイラ飼育記 とか 『Javaのオブジェクト指向入門』とかの説明は分かりやすいように思います。


2. Javaオブジェクト指向を理解するためにどうしても理解してほしいこと。

● 2.1. Java で書いたプログラムは上から下へ実行される

 Java では上から書いた順に実行されます。

・A を実行する。
・B を実行する。
・C を実行する。

 という感じでプログラムを書いたら A を実行して、終わったら B を実行して、終わったら C を実行します。

● 2.2. Java では「上から下へ実行する」部品を組み合わせて書く

 ↓ Java では、「上から下へ実行する」を部品にすることができます。

部品「買い物に行ってくる」 {
 ・店に行く。
 ・商品を選ぶ。
 ・お金を払う。
 ・帰る。
}
部品「洗濯する」 {
 ・洗濯物を集める。
 ・洗濯機に入れてスイッチオン。
 ・終わったら干す。
}

 ↓これを組み合わせて新しい部品を書くことができます。

部品「今日すべきことをする」 {
 ・「洗濯する」
 ・「買い物に行ってくる」
 ・ご飯食べる。
 ・寝る。
}

 「今日すべきことをする」を実行すると、洗濯して買い物行ってご飯たべて寝る、というわけです。

● 2.3. なぜ Javaオブジェクト指向を理解するために部品を組み合わせるということを理解しないといけないのか

 部品にすると、良いことがたくさんあります。そうした良いことを積み上げていくことで、ようやくアプリケーションが出来上がります。何がどう良いのか、その理由を知ることが、Javaオブジェクト指向を理解するための最短距離です。

 1. 部品を利用するプログラムの見通しが良くなる。

   ↓こんな感じ。

  f:id:kura-replace:20111222210744p:image

   左の方が読みやすいですね。

   なぜ見通しが良くなったように感じるかというと、部品に名前がついたからです。また、短くなったからです。

   見通しが良いということは、間違いに気づきやすいということです。これは良いことです。

   また、部品を交換するだけで処理そのものを変えることができるということです。変更に強いというのは良いことです。

 2. 同じ処理をいくつも書かなくて良くなる。

   例えばある処理を 3 箇所で行いたいとき、その処理内容を 3 箇所に書かなくても、部品の実行を 3 箇所に書けば済むようになります。プログラムを書く量が短くなるのは良いことです。

   また、その処理に誤りがあったとき、その修正を 3 箇所全てで行わなくても、部品の内容を修正すれば済むようになります。修正漏れを見つける手間が軽減されます。これも良いことですね。

 3. 部品を利用するプログラムは、その部品の詳細を深く知る必要がなくなる。

   部品の名前さえ知っていれば、その内容について詳しく知らなくても利用することができます。だから部品の名前と部品の機能概要をざっと決めておけば、部品の詳細は他のプログラマが作っても良いわけです。その代わりもっと大事なプログラミングに集中することができるようになります。複雑さが軽減されるのは良いことです。作業量が減るのも良いことですね。

 Javaプログラムを作るということは、複雑な情報処理をするものを作るということです。Javaプログラムを書くということは、複雑な情報を Java でどう処理するのかを書くということです。それをプログラマという人間が読み書きするわけなので、複雑さを緩和できれば緩和できるほど、それは良い Java プログラムだということです。

 Java には良いプログラムにするための仕組みがたくさん詰まっています。Javaオブジェクト指向を学ぶということは、そうした仕組みを学ぶということです。Javaオブジェクト指向を駆使してプログラムを書くということは、そうした仕組みの良いところを生かしたプログラミングをするということです。

3. 処理のグループとデータのグループを見つめる。

● 3.1. 処理のグループ

 処理をグループにする話は前節で書いたとおりです。グループと言っても、実行順序が決まっていますのでグループという言葉は適切ではないですね。これはグループではなく、メソッドと呼びます。メソッドは用語ですので憶えて下さい。メソッドを組み合わせて Java を書くのです。

 ↓ではメソッドJavaプログラムにしてみます。

class Main
{
    // f1 という名前のメソッド
    public void f1()
    {
        System.out.println( "あーいーうー" );
        System.out.println( "えーおー" );
    }
    // f2 という名前のメソッド
    public void f2()
    {
        System.out.println( "A〜B〜C〜" );
        System.out.println( "D〜E〜" );
    }
    // f3 という名前のメソッド
    public void f3()
    {
        f1();
        f2();
    }
}

● 3.2. データのグループ

 多くのメソッドはデータを扱います。データをグループにすることは良いことです。どう良いのかというと、それは先ほどと全く同じですので 2.3. を読み返しましょう。

 データをグループにする仕組みが Java に搭載されています。

 ↓では「データのグループ」を Javaプログラムにしてみます。

// Apple という名前の「データのグループ」
class Apple
{
    public int weight;
    public int price;
};

● 3.3. 「メソッド」と「データのグループ」を組み合わせる

 良いことだらけの「メソッド」と、良いことだらけの「データのグループ」を組み合わせましょう。

 ↓ Javaプログラムにしてみます。

class Apple
{
    public int weight;
    public int price;
}

class Main
{
    // Apple を初期化するメソッド
    public static void initApple( Apple a, int price )
    {
        a.weight = 100;
        a.price  = price;
    }
    // 重さを表示するメソッド
    public static void printWeight( Apple a )
    {
        System.out.println( a.weight );
    }
    // 価格を表示するメソッド
    public static void printPrice( Apple a )
    {
        int price = ( int )( a.price * 1.05 );
        System.out.println( price );
    }
    public static void main( String[] args )
    {
        Apple a1 = new Apple();
        Apple a2 = new Apple();
        
        initApple( a1, 200 );
        initApple( a2, 300 );
        
        printPrice( a1 );
        printWeight( a2 ); // ※
    }
}

 Java プログラムならではの良い仕組みを活かせているのを確認しますね。

メソッド「main」は、データのグループ Apple を利用しているが、Apple の詳細を知らなくても済んでいる。
・メソッド「main」は、Apple を2個利用しているが、Apple という名前が付けられているので 2 個目も名前だけで利用できている。
・メソッド「main」は、メソッド「initApple」と「printPrice」を利用しているが、その詳細を知らなくても済んでいる。
・メソッド「main」は見通しが良いので、 a1 の価格を表示して a2 の重さを表示していることがよくわかる。
・メソッド「main」の機能を、a2 の重さではなく価格を表示するように変更する場合は ※ のところを printPrice( a2 ) に交換するだけで済む。

 これらが同意できないのであれば、もう一度最初から読んでください。そうしないとこの先の文章が絶望的に理解できませんので。

● 3.4. 「メソッド」と「データのグループ」が増えたときのことを想像する

 プログラムの量が増えると、まずデータのグループの種類が増えます。Apple, Lemon, Peach, …と増えてゆきます。

 また、メソッドの種類が増えます。initApple, initLemon, initPeach, …と増えてゆきます。

 ということは、グループ化することによって複雑さを緩和したのにも関わらず、どんどんそれだけでは足りないくらい複雑になって手に負えなってゆくということです。もっと強力に複雑さを緩和する仕組みが欲しくなるんです。2.3. で書いた「良いプログラムのためのコンセプト」をもっと推し進めると良いはずです。

 例えば Apple 用に initApple メソッドを、Lemon 用に initLemon メソッドを、 Peach 用に initPeach メソッドをそれぞれ用意するのなら、それぞれをまとめて部品にした方が良いはずです。

 なお、部品にした方が良い理由はまたも 2.3. で書いたのと全く同じですので読み返してください。「メソッド」と「データのグループ」をひとまとめにしたものをオブジェクトと言います。オブジェクトは用語です。覚えましょう。

 ↓ Java プログラムにしてみます。

class Apple
{
    public int price;
    
    public void init( int p )
    {
        price = p;
    }
    public void printPrice()
    {
        int p = ( int )( price * 1.05 );
        System.out.println( p );
    }
}
class Lemon
{
    public int price;
    
    public void init( int p )
    {
        price = p;
    }
    public void printPrice()
    {
        int p = ( int )( price * 1.05 );
        System.out.println( p );
    }
}
class Main
{
    public static void main( String[] args )
    {
        Apple a1 = new Apple(); // Apple のオブジェクトを作る。
        Apple a2 = new Apple(); // Apple のオブジェクトをもう一つ作る。
        Lemon l  = new Lemon(); // Lemon のオブジェクトを作る。
        
        a1.init( 200 );
        a2.init( 250 );
        l .init( 150 );
        
        a1.printPrice();
        a2.printPrice();
        l .printPrice();
    }
}

 メソッド「main」では、Apple や Lemon の詳細を知らずとも書くことができる、見通しが良い、… など、さっき書いた通りの良い性質が維持されています。さらに、オブジェクトにしたことで「データのグループ」と「メソッド」の関係が掴みやすくなりました。

 さて、オブジェクト a1 と a2 は同じデータグループを持ち、同じメソッドを持っています。オブジェクトの構造が全く同じですね。このように構造が同じオブジェクトがあるとき、それがどういうどういうデータを持ち、どういうメソッドと関連付けられるのか、ということを記述するのはプログラム中で一箇所にすべきです。(なぜ一箇所にすべきかという理由は再度 2.3. を読み返して考えましょう。)

 この記述をクラスといいます。クラスは用語です。覚えましょう。

 クラスを一つ作れば、それと全く同じ構造のオブジェクトを量産できるし、そのクラスの利用者に内部の詳細を知らせぬままプログラムを書かせることができるようになります。このクラスという仕組みは非常に強力に複雑さの緩和に役立つため、Java ではクラスをプログラムを構成する単位として据えています。

4. 「どうしたら良いクラスを作れるかを理解する」ために理解すべきこと。

● 4.1. クラスを組み合わせてプログラムを書くということ

 Java では、プログラムをクラスの組み合わせで記述します。そしてあらゆるメソッドはクラスに所属します。ということは、メソッドを書く前には必ず、それがどのクラスに所属するのがふさわしいかを考えないといけないということです。

 

● 4.2. 見えないものをクラスにする

 例えば『データ「price」から税率を含めた価格を求める』メソッドは、どのクラスに所属すべきか?を考えます。さっき書いたプログラムでは Apple と Lemon に所属していましたがこれは妥当かどうか?もっと良い場所は無いでしょうか。

 僕は、こういうギモンを持つようになることが、Javaオブジェクト指向を学ぶための最初のジャンプだろうと思っています。

 Javaオブジェクト指向を「モノをクラスに対応付けて…」と説明しているのを目にすることがありますが、その説明では物足りないような気がします。その説明では、Javaオブジェクト指向の高みへ登ることはできないし、深くまで潜ることもできないんです、きっと。Java プログラムを構成するほとんど全てのクラスは、存在しないものに対応付けられるのが実際のところです。『データ「price」から税率を含めた価格を求める』メソッドにしても、実際に存在しないモノを表現したクラスに所属させないと良い設計に近づけないんです。

 この設問でいうと例えば

プログラム中の税関係の計算をひと通り担当する TaxCalculator クラスを定義してそこにメソッドを所属させる

 というのもあり得るだろうし、

・顧客の会員ランクに応じた価格を提示する PriceProvider クラスを定義してそこにメソッドを所属させる

 というのもあり得ると思います。

● 4.3. そもそもクラスを決めることは架空の構造を決めることに過ぎない

 例えば Apple というクラスを定義するとして、その内部にどういうデータを抱え、どういうメソッドを所属させるのが良いか、という問いに答えはありません。価格、重さ、色、糖度、出荷日、… データを増やそうと思えばいくらでも増やせてしまいます。これではキリがない。

 クラスは Java プログラムの部品です。

 クラスの構造が肥大したり複雑怪奇だったら、2.3. で書いた Javaオブジェクト指向の良いところを潰してしまうことになります。「あらゆる情報を兼ね備えた多機能な Apple クラス」を定義することは Javaオブジェクト指向が目指す場所ではないんですね。クラスは、あくまでプログラムに必要な情報と操作を完結に表現するだけのために定義されるべきでしょう。このように、プログラムにとって都合が良いような架空の構造を紡ぎだす事をモデリングといいます。モデリングJava 以外でも通用する用語です。覚えましょう。

● 4.4. Javaオブジェクト指向はほとんど「クラスをどう組み合わせるか」という話に終始する

 Java は、ソースコードの記述が文法的に正しいことが確認されないと実行することはできません。このチェックはクラスについても行われます。例えば、あるオブジェクトがクラスに所属していないメソッドを呼んでいたらコンパイルエラーになるし、存在しないデータ変数の値を表示しようとしてもコンパイルエラーになります。クラスを記述するということは、コンピュータに明らかな間違いを検出させるということも兼ねているんです。コンピュータに間違いを検出させる機構があるということは、それを使って検出可能な間違いが多ければ多いほど安全にプログラムが書けるということです。クラスをどう記述するとより多くの間違いを検出できるのか…、クラスをどう組み合わせるとより柔軟なプログラム設計ができるのか…、それが Javaオブジェクト指向で語られていることのほとんどを占めます。プログラムを、上から下へのフローを軸にして考えるのではなく、クラスの組み合わせ方を軸にして考えるのが Javaオブジェクト指向では大切です。

 というわけで、ここからはクラスの話をします。

5. 「あからさまに出来が悪いクラス」を作らないために理解すべきこと

● 5.1. 情報を隠蔽することは大切だと理解する。

 クラスが部品であるということは、クラスの内側とクラス外側のプログラムを分離するということです。密接に絡み合ってしまうと、クラスを利用するプログラムが変更に弱くなる可能性があります。それを避けるための、手っ取り早い作法を紹介します。

 ↓まずは先程のコードを再掲。

class Apple
{
    public int price;
    
    public void init( int p )
    {
        price = p;
    }
    public void printPrice()
    {
        int p = ( int )( price * 1.05 );
        System.out.println( p );
    }
}

 これを↓こう書き換えます。

class Apple
{
    // public → private
    private int price;
    
    public void init( int p )
    {
        price = p;
    }
    public void printPrice()
    {
        int p = ( int )( price * 1.05 );
        System.out.println( p );
    }
    
    // 追加
    public int getPrice()
    {
        return price;
    }
    // 追加
    public void setPrice( int p )
    {
        price = p;
    }
}

 price という変数の読み書きを、getPrice(), setPrice() メソッドを通じてしか許可しないようにしました。これで、このクラスの利用者が price という名前に依存したコードを書けなくなりました。従って getPrice(), setPrice() メソッドが機能する限りは、Apple クラスの構造の変化に利用者のプログラムが引っ張られることが無くなります。例えば price という変数の名前が変わっても利用者には全く影響しないし、例えば Apple クラスから値段を保持する変数が消え去ってしまっても構わなくなりました。「Apple が price という変数を抱えている」という事実を隠蔽したわけです。

 これは Java で良いクラスを作るために使う常套的なテクニックです。

● 5.2. 情報を隠蔽するために初期化が大切だと理解する。

 上の例では、必ず初期化時に init() を呼ぶ決まりになっていました。もし利用者が init() を呼ばなかったら price は 0 になります。しかし、あらゆるクラスの全てのデータが必ず 0 で初期化されるというのはなかなか都合の悪い話です。絶対に 100 以上の値じゃないとダメ、というデータもあるでしょう。正常な値がセットされていないと他のメソッドの動作に著しい悪影響を及ぼす、というケースも出てくるかもしれません。こうした問題に対応するため、price の値が正常な値であるかどうかを確認するための checkPrice() メソッドを追加することができますね。しかし、そんなのをうじゃうじゃ足していったら情報を隠蔽する価値が目減りしてしまうし、クラスが複雑になります。

 そこで、Java には初期化のための仕組みが用意されています。これをコンストラクタといいます。コンストラクタはクラスに所属します。

 ↓ Apple クラスを修正しました。

class Apple
{
    private int price;
    
    // init() → Apple()
    public Apple( int p )
    {
        price = p;
    }
    public void printPrice()
    {
        int p = ( int )( price * 1.05 );
        System.out.println( p );
    }
    
    public int getPrice()
    {
        return price;
    }
    public void setPrice( int p )
    {
        price = p;
    }
}

 コンストラクタを記述すると、そもそもコンストラクタを利用しないと new ができないよう、制限がかけられます。従って、情報隠蔽を安全に開始するための処理をコンストラクタの中に書けば、必ずその通りの初期化を経たオブジェクトしか存在しないことを保証できるようになるということです。

● 5.3. 依存関係が少ないクラスは良いクラスであることを理解する。

 何かクラスを記述するとき、他のクラスの名前が出てくる場合があります。

 ↓ 例えばこんなの。

class Apple
{
    private int price;
    
     // 引数に Lemon クラスの名前が使われてる
    public Apple( Lemon l )
    {
        price = l.getPrice();
    }
}

 初期化の際に必ず Lemon が必要になることが分かるようになっています。これを、「Apple は Lemon に依存している」といいます。依存しているということは、例えば Lemon クラスのメソッドとかに何らかの変更があったとき、Apple クラスの内部のプログラムが影響を受ける可能性があるということです。不用意に変更箇所が増えてしまうのは悪いことですね。これを避けるためには、できるだけ「余計な依存」を避けるようにクラスを記述することが大切です。

 Java プログラムは、クラスを組み合わせて作る、と書きました。そしてクラスは、何度も使いまわしたり見通しを良くするための部品としての役目を持っていることも書きました。ということはつまり、クラス間の「余計な依存」は、「プログラム全体の変更しやすさ」を著しく悪化させる原因になり得るということです。あらゆる依存は排除すればするほど良いということです。

● 5.4. インスタンスという言葉を理解する。

 次のコードを考えます。

class Apple
{
    private int price;
    
    public Apple( int p )
    {
        price = p;
    }
    // 税込価格を表示する
    public void printPrice()
    {
        int p = ( int )( price * 1.05 );
        System.out.println( p );
    }
    // 税率を取得する
    public double getTaxRate()
    {
        return 1.05;
    }
}

class Main
{
    public static void main( String[] args )
    {
        Apple a1 = new Apple( 100 );
        Apple a2 = new Apple( 200 );
        
        a1.printPrice();
        a2.printPrice();
    }
}

 このとき、一度目の new によって作られたオブジェクトと二度目の new によって作られたオブジェクトはその構造は同じですが別物です。このとき、それぞれをひとつのインスタンスと言います。インスタンスは用語です。覚えましょう。インスタンスが異なるから printPrice() の実行結果も異なるというわけです。

 「オブジェクト」と「インスタンス」の言葉の意味は似ていますが、オブジェクトはクラスとインスタンスを含んだ総称です。a1, a2 はそれぞれインスタンスを参照する変数であって、オブジェクトを参照しているわけではありません。

 

● 5.5. インスタンスに依存すべきものとそれ以外のものを区別する大切さを理解する。

 さて、Apple クラスには 1.05 という数値が 2 箇所に書かれています。これは良くないので(良くない理由が分からないなら 2.3. を読みましょう)、↓改善したコードを書いてみます。

class Apple
{
    private int    price;
    private double taxRate; // 追加
    
    public Apple( int p )
    {
        price = p;
        taxRate = 1.05;
    }
    
    public void printPrice()
    {
        int p = ( int )( price * taxRate );
        System.out.println( p );
    }
    
    public double getTaxRate()
    {
        return taxRate;
    }
}

 この結果、taxRate は全ての Appleインスタンスについて 1.05 で固定になります。もし 1.05 以外の値にする必要があるなら void setTaxRate( double r ) を追加する必要があるでしょう。ただしその場合、全てのインスタンスについて setTaxRate( ?? ) を呼ばないといけないということです。全てのインスタンスを追跡する仕組みを用意する必要があるということです。しかしそうすると今度はそこらじゅうのクラスに Apple クラスへの依存が発生してしまいそうです。

 それもこれも taxRate が Appleインスタンスに依存しているからです。Java には、クラスに所属しつつもインスタンスに依存しないデータを記述する仕組みが用意されています。↓コードにするとこんな感じです。

class Apple
{
    private int price;
    private static double taxRate = 1.05; // static 指定
    
    public Apple( int p )
    {
        price = p;
    }
    public void printPrice()
    {
        int p = ( int )( price * taxRate );
        System.out.println( p );
    }
    
    // static 指定
    public static double getTaxRate()
    {
        return taxRate;
    }
    // static 指定されたメソッドを追加
    public static void setTaxRate( double r )
    {
        taxRate = r;
    }
}

 ↑このように static 指定するとインスタンスに依存しなくなります。taxRate がインスタンスに依存しないので getTaxRate も setTaxRate もインスタンスに依存しないように static 指定しました。ところでインスタンスに依存しないということは、インスタンスを作らなくても利用できるということです。例えば↓こういうコードが書けるようになります。

class Main
{
    public static void main( String[] args )
    {
        // インスタンスを作っていなくてもメソッドが呼べる。
        System.out.println( Apple.getTaxRate() );
        
        Apple a1 = new Apple( 100 );
        Apple a2 = new Apple( 200 );
        
        a1.printPrice();
        a2.printPrice();
        
        // 税率を変更
        Apple.setTaxRate( 1.10 );
        
        a1.printPrice();
        a2.printPrice();
    }
}

 こうしてまたひとつ、余計な依存を排除する方法が手に入りましたね。

6. クラスとクラスをより良く関係づけさせるために理解すべきこと

● 6.1. 異なるクラスに共通の構造を同一視して扱うことの大切さを理解する。

 ようやく継承の紹介です。同じクラスのインスタンスは同じ構造をしているということはこれまで説明してきた通りですが、異なるクラスのインスタンスであっても部分的に同じ構造になるのが妥当である場合があります。それを記述するための仕組みが継承です。

 例えば Apple にデータ「price」とメソッド「printPrice」があって Lemon にもデータ「price」と「printPrice」メソッドがあるとしたなら、その部分については全く同じ構造であるといえます。全く同じものが複数のクラスに点在しているのであれば、それは改良の余地があるということです。

 この節では Apple、Lemon クラスの構造の共通部分を新しくモデリングします。たかが構造の共通部分であったとしても、逆にいえば Apple, Lemon といった部品を作るための部品ですので 2.3. で書いたような価値は相変わらず持ち得ます。また部品を作るための部品、を作るための部品、を作るための部品、… というように、階層づけられた部品化はそれぞれの階層ごとに 2.3. で書いたような価値を持つことにも留意しましょう。

 ここでは共通部分をくくりだした部品に「Fruit」という名前を付けるものとします。継承を使う目的は、Apple を Fruit と同一視すること、Lemon を Fruit と同一視すること、それによって Apple と Lemon に共通する構造のみに依存したコードを書くことです。これによって、Apple に固有の部分、Lemon に固有の部分に依存したプログラムを排除します。

↓ ではコードにしてみます。

// 共通部分のクラス
class Fruit
{
    private int price;
    
    public int getPrice()
    {
        return price;
    }
    public void setPrice( int p )
    {
        price = p;
    }
    public void printPrice()
    {
        System.out.println( price );
    }
}

class Apple extends Fruit
{
    public Apple( int p )
    {
        setPrice( p );
    }
}
class Lemon extends Fruit
{
    public Lemon( int p )
    {
        setPrice( p );
    }
}

class Main
{
    // 値段に 100 を足すメソッド。
    public static void add100( Fruit f )
    {
        int newPrice = f.getPrice() + 100;
        f.setPrice( newPrice );
        f.printPrice();
    }
    public static void main( String[] args )
    {
        Apple a = new Apple( 100 );
        Lemon l = new Lemon( 200 );
        add100( a );
        add100( l );
    }
}

 ↑ Apple クラスも Lemon クラスもすっきりして見通しが良くなりました。また add100 メソッドは、 Apple にも Lemon にも依存しないまま、双方のインスタンスの共通部分にのみ依存して処理を記述できました。さらに、データ price は Fruit の中に完全に隠蔽されました。Apple や Lemon ですら、price という名前のデータ変数に依存しない構造になったわけです。

 プログラムに新しく Fruit という名前が定義されたことで、Fruit の機能を拡張すれば Apple の利用者も Lemon の利用者もその恩恵を受けることができるようになりました。一方、Apple, Lemon に続いて Peach クラスを記述することを考えると、Fruit の構造をそのまま使いまわすことが出来るうえ、Fruit にのみ依存したあらゆるプログラム部品を利用することも出来るわけです。

 このように継承は、クラス間の関係を記述するうえでかなり強力な仕組みであることを理解しましょう。

● 6.2. インスタンスを作ることが許されていることの大切さを理解する。

 前節で Fruit を定義しましたが Apple, Lemon と同様にこれもまたクラスですのでインスタンス化できます。

 ↓ ほらこんな感じで。

class Main
{
    public static void add100( Fruit f ) { /*省略*/ }
    
    public static void main( String[] args )
    {
        Fruit f = new Fruit();
        Apple a = new Apple( 100 );
        Lemon l = new Lemon( 200 );
        add100( f );
        add100( a );
        add100( l );
    }
}

 しかしこれは妙です。Fruit, Apple, Lemon が同列に並んでいます。Apple や Lemon を Fruit と同一視することはできましたが、Fruit を Apple や Lemon と同階層に扱うことは望んでいません。ここで有効な手立ては、Fruit だけはインスタンス化を禁止することです。これも Java に備わっている機能です。

 ↓ Fruit クラスをこう書き変えます。

// ↓ abstract 指定を追加
abstract class Fruit
{
    private int price;
    
    public int getPrice()
    {
        return price;
    }
    public void setPrice( int p )
    {
        price = p;
    }
    public void printPrice()
    {
        System.out.println( price );
    }
}

 このように abstract をつけたクラスはインスタンス化が禁止されます。new Fruit() するとコンパイルエラーになって実行できません。従って、Fruit を Apple や Lemon と同程度に具体的で堅牢なクラスに仕立て上げる必要はなくなりました。このようなクラスを抽象クラスと呼びます。これは用語ですので覚えましょう。抽象クラスであると明示的に記述することで「それを継承したクラスのインスタンスを活用することの必然性」を利用者に伝えることにもなります。

 さてこのように new を禁止しましたが、別の意味で new を禁止したい場合があります。例えばクラス A があったとして、クラス A はその内部で一度だけ A をインスタンス化したいけれど A 以外の誰によってもインスタンス化して欲しくないというような場合です。これは外部からコンストラクタへのアクセスを禁止することで対応します。

 ↓例えばそう、こんな風に。

class A
{
    // コンストラクタは外部からアクセス禁止
    private A() {}
    
    private static A a = new A();
    
    public static A getUniqueInstance()
    {
        return a;
    }
}

 インスタンスが存在し得ないこととコンストラクタへのアクセスが許されないことは、意味も性質も記述も違うので、より適切な方を選択しましょう。

● 6.3. 前節のような同一視は、同一視という行為の一面に過ぎないことを理解する。

 継承によって Fruit の構造が Apple や Lemon に引き継がれるのはここまで説明した通りですが、それは Fruit を継承したクラスにのみ引き継がれます。この継承の性質によってプログラムの再利用が強力に推し進められるということは、クラスを継承すればするほど再利用の幅が広がるということです。しかしそれは、Apple や Lemon が多数の継承元クラスに依存するということでもあります。継承はすればするほど良いというわけでは無いのです。

 もっとクラスの継承階層に対して横断的な同一視ができると良いはずです。その仕組も Java に用意されています。それがインターフェイスです。

 f:id:kura-replace:20111222210855p:image:w640

↓コードにしますね。

// インターフェイスの定義
interface Discountable
{
    int getDiscountedPrice();
}

// Discountable ではないクラス
class Apple extends Fruit
{
    public Apple( int p )
    {
        setPrice( p );
    }
}

// Discountable なクラス
class Melon extends Fruit
            implements Discountable
{
    public Melon( int p )
    {
        setPrice( p );
    }
    
    public int getDiscountedPrice()
    {
        return ( int )( getPrice() * 0.7 );
    }
}

class Main
{
    // Discountable 指定したものだけを同一視する。
    public static void printDiscountedPrice( Discountable d )
    {
         System.out.println( d.getDiscountedPrice() );
    }
    
    public static void main( String[] args )
    {
        Melon m = new Melon( 1000 );
        Apple a = new Apple( 100 );
        printDiscountedPrice( m );
        printDiscountedPrice( a ); // コンパイルエラー。a は Discountable ではない。
    }
}

 printDiscountedPrice メソッドは、引数 d が Discountable であることだけを要求しています。これは、Fruit を継承しているか否かには関与しないので、このメソッドは Fruit に依存しないという利点があります。Fruit と全く関係無いクラスであっても Discountable であれば printDiscountedPrice メソッド引数 d を同一視できるのです。 

● 6.4. クラスの良い組み合わせ方はプログラマたちの間で広く知られ、共有されていることを理解する。

 クラスをどう組み合わせると余計な依存が排除できて、かつ情報隠蔽が徹底され、かつ柔軟であるか、ということを考えてプログラムを書いていると、よく似たパターンがあることに気づきます。そうしたいくつかのパターンに名前を付けてカタログにしたものをデザインパターンと言います。Javaオブジェクト指向を意識してプログラムを書き続けるということは、「ここは◯◯パターンを使って設計すると良いな」という風にぴったりはまることがたくさんあるということです。

 そこで、チームで開発するときなどは「ここは◯◯パターンを使っているよ」とプログラマ間で意思疎通をはかれば、クラスの組み合わせ方それ自体の見通しが良くなることが期待できます。見通しが良いことのメリットは 2.3. で書いたのと全く同じですので読み返しましょう。

7. Javaオブジェクト指向が持つ本質的な問題を認識することの大切さを理解する

● 7.1. Javaオブジェクト指向が問題を抱えているとしたなら…?

 Javaオブジェクト指向の良いことばかりをここまで書いてきましたが、クラスを組み合わせてプログラムを構築することに問題があるとしたらどうしましょうか。Java が提供するあらゆる仕組みが、実はその問題を推し進めてしまっているとしたら…。

 Javaプログラムを書く以上、Java が提供する枠組みの外側へは出られません。したがって Javaオブジェクト指向が根本的な問題をはらんでいるのであれば、まずそれを認識して、Java の範囲内で可能な限りの対応を検討することが大切です。

● 7.2. プログラムをクラスの組み合わせで記述することの是非

 Javaオブジェクト指向ではクラスを組み合わせて記述すること、及びクラスの構造はモデリングによって決定づけられることはここまで書いたとおりです。しかしそうやって定義したクラスというものは本当にプログラマにとって必要なものなのでしょうか。あるメソッドを利用したいだけなのに、無駄なクラスを量産していないでしょうか。過剰な抽象化、過剰な共通化、過剰な明示化によってクラスが量産されてしまいがちな性質が Javaオブジェクト指向には潜んでいるのではないでしょうか?

 良いプログラムを書くために Javaオブジェクト指向を活用するというのに、煩雑にクラスが量産されてプログラム全体の見通しが悪くなるのであれば、それは本末転倒ですよね。例えば ◯◯Manager クラスとか、Abstract◯◯ クラスとかが多くなりすぎてしまっていませんか。

● 7.3. Java継承という仕組みを活用することの是非

 Javaオブジェクト指向では継承の利用を前提にいくつものクラスを同一視するための共通部品をつくり、再利用を促進するなどしてプログラムを構築するということについて説明しました。継承は強力ですが、それゆえに「どこで継承元と継承先の構造を分離するのか」という判断を誤るとプログラムの質に甚大な悪影響を与える場合があります。また、Javaオブジェクト指向では継承元は高々ひとつしか選べないという制限があります。例えば『「Apple を Fruit と同一視することが効果的な場面」と「Apple を Stock と同一視することが効果的な場面」』が共存するようなプログラムを書かないといけないことだってあるのです。

 どういう手立てが良いかはケースバイケースですが、共通の部品であってもあえて継承をしない、あえて同じコードを何度も書く、そういう判断が将来のリスクを軽減する場面もあるのです。

● 7.4. 内部の構造を隠蔽するという考え方の是非

 隠蔽することが何故良いのかは 5.1. で書きました。しかしもっと戻って 2.1. で「Javaプログラムは上から下へ実行する」ことも書きました。これらを組み合わせると、実装が隠蔽されたメソッドから別のメソッドを呼び、それがさらに別のメソッドを呼び… という風に永遠にメソッドを呼びつづけてしまうような事態を生む可能性があるということです。そういう誤ったプログラム構成になってしまうことを、クラスを組み合わせて動かすまで(つまり開発の終盤まで)気づかない可能性があるということです。

 また一方、データを隠蔽していることは来るべきマルチスレッド環境下でのプログラミングと相性が悪いことにも留意すべきです。

8. まとめ

 本稿で扱おうと思っていた説明は以上です。少しでも理解の足しになっていれば幸いです。

 しつこいくらいに「Javaオブジェクト指向」と書いてきましたが、もっと別のかたちのオブジェクト指向プログラミングも世の中にあるのです。本稿に書いてあることだけでオブジェクト指向を語るのは "井の中の蛙大海を知らず" 状態を晒すことですので控えましょうね。

 でわでわ、お疲れ様でした。

匿名匿名 2011/12/26 11:49 ちょっとstaticの使い方が危険じゃない?

kura-replacekura-replace 2011/12/26 18:08 コメントありがとうございます。
危険といってもいろんなのがあると思うんですが、どういう危険をご指摘でしょうか?

初心者初心者 2013/02/15 02:28 3.4のオブジェクトの説明文の中にあったコードで
メソッドについては理解できたのですが、データーグループを示す記述がどの部分になるのか教えていただけませんか?

kura-replacekura-replace 2013/02/15 11:07 コメントありがとうございます。ここでデータグループと言っているのは Apple の中の price や Lemon の中の price を指します。該当ソースでは price だけしか無いですが、例えば重さを示す weight や入荷日を示す date といったデータを追加することができます。

class Apple
{
  public int price;
  public int weight;

  public void init( int p, int w )
  {
    price = p;
    weight = w;
  }
  public void printPrice()
  {
    int p = ( int )( price * 1.05 );
    System.out.println( p );
  }
  public void printWeight()
  {
    System.out.println( weight );
  }
}

class Lemon
{
  public int price;
  public Date date;

  public void init( int p, Date d )
  {
    price = p;
    date = d;
  }
  public void printPrice()
  {
    int p = ( int )( price * 1.05 );
    System.out.println( p );
  }
  public void printDate()
  {
    System.out.println( date );
  }
}

class Main
{
  public static void main( String[] args )
  {
    Apple a1 = new Apple(); // Apple のオブジェクトを作る。
    Apple a2 = new Apple(); // Apple のオブジェクトをもう一つ作る。
    Lemon l = new Lemon(); // Lemon のオブジェクトを作る。

    a1.init( 200, 100 );
    a2.init( 250, 200 );
    l .init( 150, new Date() );

    a1.printPrice();
    a2.printPrice();
    a1.printWeight();
    l.printPrice();
    l.printDate();
  }
}

Apple は Apple で、Lemon は Lemon でグルーピングすることで、Apple の中では Lemon に date があろうが無かろうが気にしなくて良いし、Lemon の中では Apple に weight ががあろうが無かろうが気にしなくて良いため、それぞれのクラスを設計するときに考慮すべきことが少なくて済みます。他にも、例えば main() の中で誤って a1.printDate() と書いてしまった場合(=データとメソッドの組み合わせを間違ってしまった場合)にコンパイルエラーにすることができます。またさらに、Apple の init() の中で、weight が大きいのに price が安すぎるコトを(if 文とかで)検出するような機能をつけることだってできるハズです。こうしたメリットを、Apple や Lemon を使う人が、その内部にどういうデータグループを持っているかを知らなくても享受できるようになります。

2010-09-01

Chapter 2. 継承というものがあるがこれは一体全体、何がしたいのか

| 21:07

Java という言語の記事です。

 継承を使うと、例えばこういうことができます。

 

 ・値段を「確実に」表示する関数を作る。

 

 もしそんな関数が作れるのであれば、Chapter 1. で例示したような、Melon でも Apple でもどっちを受け取っても良いような、そんな機能を作れますよね。

class Main {
    
    // 値段を「確実に」表示する関数
    public void printPrice( Apple または Melon f ) {
        System.out.println( f.price );
    }
    
   public void main( String[] args ) {
        Apple a = new Apple( 100 );
        printPrice( a );
        
        Melon m = new Melon( 500, 100 );
        printPrice( m );
    }
    
}

まぁ、↑このコードは「Apple または Melon なんて都合の良い解釈してられるかボケ!」と怒られてコンパイルエラーになるのですが、それを表現する方法が Java には用意されています。それが継承です。

 「Apple または Melon」とは何でしょうか。AppleMelon はそれぞれモデリングした定義でした。ここからさらに AppleMelon の共通項をモデリングします。

class Fruit {
    
    public Fruit( int price ) {
        this.price = price;
    }
    
    public int price;
    
}

 Fruit クラスの定義です。Fruit を利用して、「AppleMelon も Fruit の一種だ」という意味に定義し直せば良いのです。では AppleMelon を書き直します。文法の紹介なのでさらっといきましょうか。

class Apple extends Fruit {
    
    public Apple( int price_a ) {
        super( price_a );
    }
    
}

class Melon extends Fruit {
    
    public Melon( int price_m, int weight ) {
        super( price_m );
        this.weight = weight;
    }
    
    public int weight;
    
}

「A extends B」は「A は B である」という意味です。Apple は Fruit であり、Melon も Fruit である、という意味になります。これを使うとこう書けます。

class Main {
    
    // 値段を「確実に」表示する関数
    public void printPrice( Fruit f ) {
        System.out.println( f.price );
    }
    
    public void main( String[] args ) {
        Apple a = new Apple( 100 );
        printPrice( a );
        
        Melon m = new Melon( 500, 100 );
        printPrice( m );
    }
    
}

念押しになりますが、『「Apple は Fruit」なので Fruit に代入できます』し、『「Melon は Fruit」なので Fruit に代入できます』ね。

 これで、「確実に」値段を表示する関数をつくれました。めでたしめでたし。

 ここで、なぜ Java は、「Apple または Melon」という書き方ではなくて「Fruit」という共通の名前を必要とするような文法になっているのか、を考えます。だいたい、新しく Fruit クラスを定義するなんて面倒くさいだけだと思いますよねぇ。



 では例を。

 

 Apple, Melon と来て、この流れで行くと、新しく Lemon クラスが必要になっちゃったりしそうです。そのとき、

 

printPrice( Apple または Melon または Lemon f )

にするほうが優れているのか、

 

printPrice( Fruit f )

にするほうが優れているのか、というと printPrice の書き換えが発生しないという点で後者の方が優れているのです。

 何故か。

 Apple, Melon, Lemon ときたら、このあといくらでも増えそうです。Fruit っていう名前を用意したことで、Grape, Banana, Strawberry, ... と増やしていく作業の見通しが立ちます。つまり

「共通の何か(Fruit)を作る、ということは、それを踏まえた具体的な何か(Apple, Melon, ...)を新しく作っていく上でのガイドラインになる」

のです。

 また一方で、printPrice だけではなく、もっと多彩な関数を作りたくなりそうです。例えば、かごに入っている Fruit の数を数えたり、その値段の合計を計算したり...。そうした関数を書く際に、もしもいちいち Apple ,.... Strawberry と書かないといけないのであれば非常に面倒だし、ヒューマンエラーが入りやすそうです。つまり、

「共通の何か(Fruit)であることだけを踏まえたコードが書ける、ということは、実際の具象物(Apple, Melon, ...)にどんなものがあるかを考慮しないというのに正しく書ける」

ということです。継承ってものの価値は、この2点を両立していることにあるのです。

Chapter 1. そもそも class って何だコラ

| 21:02

Java という言語 の記事です。

 プログラミングってのは割と複雑です。プログラミングってのはコンピュータに「あれして、これして、こういうときはこれやって、こんな手順でこういうことしてね」と指示を与えることです。コンピュータを動かすための指示を紡ぐことをプログラミングといいます。

 ところが、コンピュータが受け付けられる指示の種類ってたかが知れているのです。その一方でユーザーからは高い要求が来るわけで、どうしてもプログラミングっていう作業は複雑な作業になってしまいます。ということは、プログラマによるヒューマンエラーが入り込みやすいということです。いわば指示の間違いです。これがバグです。

 そこで、できるだけ複雑さを軽減するためにプログラミング言語というものがいくつも作られたわけです。そして、その中でも Java っていうのは、「コンピュータに指示間違いを見つける作業をお願いする」ことでバグの発生を防いでくれる言語なわけです。

 この記事を読んでいる人は恐らく Java のソースをちょっとぐらい見たことがあるかと思います。

さてこれ↓は何でしょうか?

class Apple {
    
    public Apple( int price ) {
        this.price = price;
    }
    
    // 値段
    public int price;
}

 これは、りんごを表現するクラスです。しかしこのりんご、名前こそ Apple ですが、値段しかパラメータがありません。どんなりんごか想像できませんね。なぜかというと、必要ない要素は含めていないからです。もっと糖度とか赤みとか産地とか、いろいろ情報を足すことはできますが、不要なので入れていません。これがすごく大切な事で、こうやって必要なパラメータだけを必要なぶんだけ取り出して適切な形にすることを「モデリング」といいます。作ろうとしているプログラムに適応させるために、りんごっていうのはこういうものですよ、と定義したわけです。

 では同じようにメロンを定義します。

class Melon {
    
    public Melon( int price, int weight ) {
        this.price = price;
        this.weight = weight;
    }
    
    // 値段
    public int price;
    // 重さ
    public int weight;
}

 メロンモデリングが完了し、Melon クラスを定義しました。さて、これを使ってみます。

class Main {
    public static void main( String[] args ) {
        Apple a = new Apple( 100 );
        Melon m = new Melon( 500, 1000 );
        
        System.out.println( m.price );
    }
}

 コードが増えてきました。実行するとメロンの値段が表示されるプログラムです。ところで、もしも、こう書いていたらどうでしょうか。

class Main {
    public static void main( String[] args ) {
        Melon m = new Apple( 100 );
        System.out.println( m.price );
    }
}

これはコンパイルエラーになります。コンパイルエラーが発生したということは、「コンピュータにどういう指示を出したらいいかを決められないようなことを書くなボケ!」ということです。

 どうしてでしょうか。Apple にも Melon にも price があります。なのにエラーになるのです。コンパイラは恐らく、「Melon変数 m に Apple を代入しちゃダメだ」、という旨のメッセージを出しているはずです。なぜダメかというと、変数 m は Melon だからです。Apple ではありません。もし仮にこれができてしまうと、例えば m.weight の値がいくつになるのか決めようがないのです。

 「じゃあとりあえず weight は 0 でいーじゃん」と思いがちですが、だったらなぜ「とりあえず 100 でいーじゃん」ではないのでしょうか。100 だと重さが 100 のものと区別がつきません。同じように 0 が何か意味を持つかもしれません。「メロンの重さだからここは 0 にしておけば、値をセットしていない、ってことは分かる」のは人間だからであって、コンピュータはそこまで関与しないわけです。

 従って、このように「異なる型への代入を許してしまうということは、意味的な不整合が起こることである」ってことが分かります。実行してみなくても、明確に間違いなのです。こういう、不整合を徹底的に排除して意地でも実行させない、まさにヒューマンエラーの介入を防ぐ機能が Java の強みなわけです。

 Java を扱うということは、コンピュータの助けを利用して、正しく実行するプログラムを書くということです。Java には、こうやって安全に意味を履き違えずに開発を行うためのしくみが、大量に備わっています。それらのうち、知っておくと良い(であろうと僕が思っている)ことを、いくつか噛み砕いていくつか紹介しようと思います。

Java という言語

| 20:24

 Java っていうプログラミング言語が流行って、そろそろ次の言語へのシフトが進みつつある感じですが、Java はまだまだ×3現役の言語として生き延びるでしょう。

 先日、ニコ生で"俺々プログラミング言語"を考える放送を見ていたのですが、そこで気づいたことがあります。意外にも Java が抑えている、(僕が、プログラマ一般に)周知されていると思っていたような事が理解されていなかったりするのです。それは悪いことではないのですが、多分、これまで十分に語り尽くされたが故に、今更説明されることも少なく、最近になってプログラミングを始める人にはなかなか理解されないからなんじゃなかろーか、と思います。

 ・・・というわけで、そこら辺のことを一度書き留めておこうと思います。いわば自称中級者(という害悪的存在の僕)が書く、初心者向けの記事というわけです。エキスパートの方から見たら「最近はちょっと事情が違うんだよ」というところもあるかも知れませんが、まぁそれはちょっと横に置いておいて下さい。

Chapter 1. そもそも class って何だコラ

Chapter 2. 継承というものがあるがこれは一体全体、何がしたいのか

Chapter 3. 気が向いたら書きます。

2010-05-07

「例外」というもの

| 04:11

 プログラミング言語には「例外」というものがたいがい、備わっているわけですが、それについていろいろ思うところがあるのでまとめておこうと思います。

Java の例外は何が素晴らしいか

 まず Java の例外のメリットを並べておきます。

a. プログラムのメイン処理の見通しが良くなります。

 継続不可能なエラーが発生したときに処理本体から"脱出"させることで、メイン処理の記述にエラー処理が紛れ込む、ということを抑止出来ます。

b. エラー内容を適切に呼び出し側に伝えられます。

 エラーが発生したとき、どういうエラーが発生したのか、というのはエラー対処方法を決定する根拠になります。「どういうエラーなのか」ってことを例外オブジェクトに意味付けしておくことで、呼び出し側に責任転嫁出来ます。ライブラリ設計者がエラーにどう対処するか、ということまで責任持たなくても良くなるわけです。

c. エラー対処を適切な場所で行えます。

 エラーが発生したとき、そのエラーに対応できるかどうか、というのはクラスの粒度に依存します。従って、エラーの種類によっては呼び出し側が対処できないこともあるわけです。そういう場合に、例外を使えば無理に責任を取る必要はなく、さらに上位の呼び出し側に対応をお願いすることができます。

d. 発生し得るエラー群に対応漏れが無いかコンパイラがチェックしてくれます。

 いわゆる検査例外というやつです。"こんな例外が発生するよ" と明示しておく代わりに、その例外への対処を記述しないとコンパイルが通らないようになっています。これは、いちいちプログラマが発生し得るエラー一覧を確認して網羅しないといけない、というヒューマンエラー入りまくりのリスクを回避してくれます。

 さしあたりこんなところでしょうか。

 ここでまずポイントになるのは b。エラー内容を呼び出し側に伝える方法として、例外を使わない方法も十分にあり得ます。例えばエラーの種類を戻り値として返したり、エラー受け取り用のオブジェクトを渡したり。もしくはグローバルオブジェクトにエラーを格納するようにしておいて、呼び出し側でそれを確認してもらったりすることも考えられます。

 しかし、そうした方法はそもそもエラー処理のための機構ではないので、どういう手法を使っているのかを利用者が確認しないと痛い目を見ます。また、ありがちな例としてエラーの種類を enum にしておくという手がありますが、そうすると複数の処理でその enum を共有しがちになり、その結果処理によっては発生するはずのない列挙子というものが出てきます。すると、処理ごとに発生し得る列挙子と発生し得ない列挙子をプログラマがいちいちチェックすることになり、結局ヒューマンエラー入りまくり、という事態になります。

 で、ここまで読んでいただければとりあえず分かるかと思いますが、例外の素晴らしいところは、混沌としがちな「エラー対処」をサポートするために上記4つのメリットを"セットで提供"してくれていることです。この機構があるおかげで、エラー処理は例外機構へ集約されました。「俺々エラー機構」からの脱却が成されたわけです。

Java の例外の何がまずいのか

 さてさて。

 Java の例外の大事なトコロをかじったところで、デメリットについて説明しようかと思います。ある機構を使いこなすには、その機構のデメリットを知ることが近道ですしね。

A. 検査例外である故に、エラー対処が非常に面倒になる。

 処理の都合上"どうやったって発生し得ない例外"であっても、catch 節を用意しないとコンパイルが通りません。また、エラー処理は後回しでとりあえずプログラムを動かしたい、という場合にもコンパイル通らないことが足かせになります。

B. オブジェクト指向プログラミングの「継承」と相性が悪い。

 オブジェクト指向を謳った Java という言語で、オブジェクト指向プログラミングと相性が悪いなんて本末転倒もいいとこです。これは検査例外とも絡むのですが、基底クラスの例外仕様を外れた例外仕様は定義できません。

 大きく言うとこの2つでしょうか。

 A の弊害は非常に大きいです。コンパイルエラーが欝陶しいので必殺「握りつぶし」が発動します。何でも catch( Exception e ) にしてしまうということですね。また、Java には RuntimeException というクラスがあり、こちらは検査されない例外となっています。検査されないようにするために利用されちゃったりするわけですね。

 続いて B について補足説明します。

 例えば基底クラスのメソッド func が IOException 投げる場合、派生クラスの func では IOException を継承した例外以外の例外は投げられません。なぜなら、func の呼び出しを含むライブラリに派生クラスのインスタンスを渡したとき、想定外の例外を投げるとそれに対処する catch 節が記述されている保証が無いからです。派生クラスが「どんな実装をするか」は基底クラスが知る由もないので、派生クラスが「どのような例外を発生させるべき処理を行うか」もまた基底クラスの知るところではありません。しかしそれを許してくれない、という矛盾があります。

C++ の例外事情

 では、こうしたデメリットを克服するような例外機構はないものでしょうか。Java 以外のプログラミング言語の例として、C++ の事情を紹介します。(まぁその、異なる言語間で比べちゃだめだろうという突っ込みは置いといて)

 C++ でも Java と同様の例外機構が備わっています。大きく違うのは非検査例外であるということです。検査されないので、メリット d がなくなってしまいます。しかしデメリット A, B は解消されます。例外を catch するかどうかは利用者の責任になるわけです。もし、実際に例外が throw されて catch されなかった場合、プログラムは終了します。まぁ、C++ では不正メモリアクセス発生させて簡単にプログラム終了させられるので、あまり驚くことでは無いです。

 しかし一方で C++ には型安全性を確保する言語仕様が厳密に規定されています。また、C++ には template を使ったメタプログラミングがあります。これは型安全性を保ったままでプログラムを増産する仕組みで、非常に高い生産性を確保することができる機構です。C++ では型というものについて厳密に安全性を確保してくれるわけです。しかし例外については甘甘な安全性しか確保できないというのはなんともバランスが悪いと思うのです。その結果、C++ のプログラミングスタイルはどうなるかというと、例外が発生し得る処理があったら、せめて利用者側の処理を破綻させないように、「例外が発生してもなんとかするコードを書くしくみ」が発達しました (デストラクタを finally のように使ってリソースの破棄を確実に行う、等のテクニックのことです)。

 これはこれで価値のあることなのですが、例外機構が本来解消すべき(だと僕が勝手に思っている)、「エラー対処の安全性向上」の決定打にはなっていません。

例外の責任は誰のものか

 ではここでいったんオブジェクト指向との絡みについて再考してみたいと思います。先の説明で、オブジェクト指向の「継承」と検査例外は相性が悪い、ということを述べました。

 では例外機構がオブジェクト指向と相性良くなるためには、どうあるべきでしょうか?

 オブジェクト指向では実装隠蔽が是とされます。そして、あらゆる場面において誰が責任を持つのか、ということを最大限考慮します。となると「派生クラス独自の処理で発生する例外にどんなものがあるのか」は派生クラスが一番知っているはずですから、派生クラスが責任を持つような仕組みになっているべきだろうと思うのです。これが一つ目のテーマです。この考え方は総称性プログラミングを使った静的多態においても同じことが言えます。

例外はどれくらい例外的か

 さて、今度は C++ の話に飛んで二つ目のテーマを述べます。いやぁ、あちこち飛び回って申し訳ない。

 ポイントは例外に対する考え方の違いです。C++ では Java ほど頻繁には例外を投げない傾向があります。「例外は本当に例外的な場合にのみ投げるべし」という考え方ですね。これはこれで正しいのですが、はてさて、どうなんでしょうか。

 「本当に例外的」というのが曲者なのです。二つ目のテーマは『「本当に例外的」かどうかは、設計粒度に依る』という点です。つまり、ライブラリ側が「これは本当に例外的だから throw する」「これはそれほど例外的ではないから throw しない」という判断をすることがそもそも間違っているのではないか、ということです。Java のやりかただと厳しすぎて、C++ のやりかただと甘すぎる、という実態の原因はここにあるのではないでしょうか。

まとめ

 ずいぶんと長くなってしまいました。本稿でも僕の中でも結論が出ていないのですが、最後にまとめておきます。

 まとめ:例外機構はこうあるべき(だと思う)

  

  1. メイン処理の見通しをよくするものであること

  2. 発生したエラーの内容を的確に表現できるものであること

  3. 発生したエラーへの対処を的確な場所で行えるようなものであること

  4. 発生し得るエラーにどんなバリエーションがあるかは、抽象部・共通部が関与しないこと

  5. 個々のエラーがどの程度の危険性を持つかは、その処理の利用者側が決められるようなものであること

 そんな例外機構をまだ見たことが無いのですが、もしご存知でしたら教えて欲しいです。非常に興味があります。

 まーそんなわけで、なーんかご意見いただけると幸いです。