Hatena::ブログ(Diary)

達人プログラマーを目指して このページをアンテナに追加 RSSフィード

2011-12-17

Javaのクラスとオブジェクトについて再度解説を試みる

オブジェクト指向プログラミングの考え方については、今までこのブログでも何度か取り上げてきました。

[オブジェクト指向] - 達人プログラマーを目指して

オブジェクト指向プログラミングプログラミング技法のすべてではないとはいえ、Javaのようなオブジェクト指向言語で本格的なプログラムを作るには理解を避けて通ることができませんし、また、関数型言語など他のパラダイムの言語を利用するにしても、オブジェクト指向の考え方をまったく理解しないまま使いこなすということは困難でしょう。オブジェクト指向の考え方はデータ構造やアルゴリズムといったことと同様に、プロフェッショナルプログラマーが理解しておくべき基本的な素養といってもよいと思います。実際、海外では募集要項でオブジェクト指向の理解を前提とすると書かれていることが普通ですし、プログラマーの面接試験で、アルゴリズムと並んでオブジェクト指向プログラミングの基本についての正しい理解を問うケースが多いようです。

しかしながら、特殊なケースを除いて、我が国ではいまだになかなか普及していないようですね。数週間の新人研修やOJTのみで短期間に理解できるほど簡単ではないというのは事実かもしれません。それゆえ、SIの開発案件ではJavaVisualBasicといったオブジェクト指向言語を用いながらも、実際にはオブジェクト指向を利用しないで開発できるような規約を作るといったこともかなり一般的に行われているようです。

オブジェクト指向プログラミングの普及を妨げる原因の一つとして、わかりやすい入門向けの解説を作ることが難しく、また、実際にそのような解説が書かれている入門書も非常に少ないということがあると思います。実際に、Javaの入門書の場合、ずっとmainメソッドのみでifやforなどの構文を説明した後、いきなりVehicle(乗り物)クラスやCarクラスなどの説明に飛躍するといったケースも多く、初めてオブジェクト指向プログラミングを学ぶ人にとっては非常に理解しにくいところがあると思います。*1

さらに、一言でオブジェクト指向、(あるいはもっと範囲を狭めてオブジェクト指向プログラミング)と言っても、プログラミング言語によっていろいろな考え方やアプローチがあり、さまざまな人々がいろいろな解説を試みているので、学習する側の立場としてはどの考え方が正しいのか混乱に拍車がかかるというところがあるかもしれません。

私自身は「オブジェクト指向とはどう考えるべきか」という哲学的な研究をしているわけではなく、単に仕事をするうえで便利な考え方の枠組みの一つとして考えています。要するに、

  • 共通のロジックを流用しやすくする(再利用)
  • 大規模なプログラムを分割して扱い易くする(モジュール化)
  • 変化や拡張に強いプログラムにする(拡張性)
  • 設計のアイデアを共有する(パターン)
  • ビジネスルールをプログラムとしてわかりやすい形で表現する(モデル化)

といった目的を達成するための手段一つとして、欠かせないものとして考えています。

ここでは、Java言語のオブジェクト指向プログラミングの基礎となるクラスとオブジェクトについて、再度、自分なりの解説を試みてみることにしたいと思います。

staticおじさんの世界におけるクラスの役割

一昔前のJavaの解説では、「Javaは純粋なオブジェクト指向言語である」という説明のされ方をされることが多くありました。ご存じのように、Javaではどんなに簡単なプログラムでもクラスを作成することが必須です。実際、HelloWorldは以下のようにクラス内に定義する必要があります。

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

最初から、クラスというオブジェクト指向の概念を使う必要があるから、純粋なオブジェクト指向言語なのであるということです。これは、「純粋なオブジェクト指向」という言葉の定義にもよりますが、一般にはちょっと不適切で勘違いしやすい用法だと思います。確かに、このプログラムに登場する文字列配列オブジェクトですが、ユーザー定義のオブジェクトは利用していません。ただし、HelloWorldというユーザー定義のクラスは使っているので、クラス指向といった方がよいかもしれません。

さて、このHelloWorldプログラムと同様の形式を拡張して、すべてのクラスのメンバー(フィールド、メソッド)にstaticキーワードをつけることで、完全にstaticな世界で*2プログラムを作成することができます。staticフィールドやstaticメソッドは、Smalltalkなどの純粋なオブジェクト指向言語の用語をまねてクラス変数、クラスメソッドなどと呼ばれることもありますが、本来staticというのは静的、すなわち、動的(dynamic)と反対の意味を持つ用語です。Java言語において、staticキーワードの持つ意味はあまり理解されていないようですが、staticフィールドについては、一旦クラスローダーによってクラス定義がJVMに読み込まれたら、クラスごとに固定的に変数領域が割り当てられてずっと存在しているということを示しています。また、staticメソッドはそのようなフィールドを操作するためのメソッドとして、やはりクラスごとに定義が読み込まれます。*3

この点を理解するためには、以下のごく簡単な例を考えてみてください。

public class ClassA {
    public static int field;

    public static void method() {
        System.out.pirntln("ClassAのメソッド");
    };
}

public class ClassB {
    public static int field;

    public static void method() {
        System.out.pirntln("ClassBのメソッド");
    };
}

public void MainClass {
    public static void main(String[] args) {
        ClassA.field = 100;
        ClassB.field = 200;

        System.out.println(ClassA.field); // 100
        System.out.println(ClassB.field); // 200

        ClassA.method(); // ClassAのメソッド
        ClassB.method(); // ClassBのメソッド
    }
}

ClassA、ClassBのそれぞれに独立した変数の定義、メソッドの定義が存在し、クラスが読み込まれるとともに静的にメモリーに読み込まれます。このことは、フィールド名やメソッド名が以上の例のように同じであっても影響を受けません。特に、この例で、int型のstaticフィールドに対応するメモリー領域は、クラスがロードされると共にクラスごとに用意されることに注意してください。

このように、staticな世界におけるクラスの役割とは、単にたくさんあるフィールドやメソッドを種類ごとに分類して、適切な部品の単位に分割するというモジュール化の役割があるに過ぎません。つまり、クラスは単にstaticフィールドやstaticメソッドの入れ物として機能しています。また、Javaの場合、基本的には一つのクラスを一つのソースファイルに記述しますから、クラスというのは物理的なソースコードの分割単位としての役割もあります。英語のclassifyに分類分けするという意味があるように、クラスには分類されたものという意味もありますから、大規模なプログラムで必要な変数メソッドの定義を適切な単位に分割して扱うというのは、オブジェクト指向以前にクラスの持つ重要な役割であることがわかります。

抽象データ型の扱えることの便利さを理解することがstaticな世界を卒業する最初の一歩*4

このように、staticおじさんの世界でも適切にクラスを定義して、フィールドやメソッドをしかるべきクラスに定義することで、大規模なプログラムを適切な単位に分割して管理することができるようになります。しかしながら、この場合の問題は、プログラムが扱える変数パラメーターの型が基本型、文字列型、またその配列といった非常に限定された型しか扱えないということです。FORTRANCOBOLなどの昔の言語しかしらないstaticおじさん*5の問題点はプログラム言語が扱えるのはこのような限定された型のデータのみであると決めつけてしまっているところにあると思います。

ところが、実際の複雑なプログラムでは、

  • 注文をデータベースに登録する
  • 入力フォームを開く
  • カタログ一覧を検索する

といったように、プログラムで処理したい単位はintやfloatなどといった基本的な型のデータなのではなく、「注文」「入力フォーム」「カタログ項目」といった人間にとってもっと自然な単位でデータを扱いたいということがあります。JVMの仕組みを考えてみればわかるように、本当はコンピューターが扱いやすいデータというのは有限のビットの集まりとして表現できる数値や文字といったデータなのですが、そういう詳細のことは無視して、データをより抽象化して扱えるようになれば、プログラムを実際のユースケース(仕様)に近い形で記述でき、よりわかりやすく記述できるようになります。実際にJavaの場合は、intやfloatといった基本型の値だけでなく、文字列、日付、注文書といったさまざまなオブジェクトを利用することで目的に応じてあらゆるデータを変数から参照したり、メソッドパラメータ戻り値としてやり取りできるようになります。

もちろん、オブジェクトを真に使いこなせるようになるには、カプセル化ポリモーフィズムといったさまざまな事柄を理解する必要があるのですが、いきなりそのような概念を理解しようとせず、まずはこのようなさまざまなデータを扱えることの便利さを理解することが、staticな世界を卒業する最初の一歩として重要なのではないでしょうか。

一般のJavaオブジェクトを理解する前に、まず配列オブジェクトの性質を理解するとよい

ところで、一般のオブジェクトはクラスを使って自分で定義し、newを使ってメモリーに割り当てて使うわけですが、最初に勉強する際に、オブジェクトの性質と同時にクラスの定義方法を学習するのは、一気にいろいろな概念を覚えなくてはならずなかなか難しいかもしれません。そこでお勧めなのは、最初に配列の性質をきちんと理解するという学習手順です。配列はクラスで定義する必要がないため特殊ですが、Java配列

  • new演算子を使ってオブジェクトJVMのヒープと呼ばれるメモリー領域に動的に生成される。
  • 配列変数は参照型の変数である。
  • 多数の値をまとめて扱う抽象データ型の一種である。

という性質を考えると、一般のオブジェクトと共通の性質を備えていることがわかります。

一般的な入門書の説明では、配列は複数の値を読み書きする箱が並んだ絵が説明に使われることが多くあります。一個の変数が値を格納する箱なので、これが連続して横に並んだものが配列というわけです。これは、昔のBASICやC、COBOLなど多くの言語の配列の説明には最適ですし、この説明自身は間違っていないのですが、これだけではJava配列が持つ以上の重要な性質を見逃してしまうことになります。


int[] x = new int[3];
int[] y = x;

x[0] = 1;
x[1] = 2;
x[2] = 3;

System.out.println(y[0]); // 1
System.out.println(y[1]); // 2
System.out.println(y[2]); // 3

以上のコードは理解している人にとっては当然のことですが、配列オブジェクトは一個しか生成されておらず、xとyという二つの別々の変数によって同一オブジェクトが参照される状態となっているため、変数xの要素を変更すると、連動してyの参照する値も変わって見えるということです。これは、独立した値の箱として扱われる普通のint型変数の振る舞いと大きく違っています。


int x = 1;
int y = x;

x = 2;

System.out.println(y); // 1のまま

もちろん、参照型の変数オブジェクトを参照するためのものなので、nullという特別な値が代入されることで何もオブジェクトを指していない状態にもなれます。この状態の変数にアクセスすると、有名なNullPointerExceptionが発生することになります。

なお、C言語そっくりにするため、Java配列を宣言する際には中括弧の初期化子を使って宣言することも可能です。

int[] x = {1, 2, 3};

あるいは、Javaでは推奨されませんが、以下のように書くこともできて、これだと本当にC言語のように見えます。

int x[] = {1, 2, 3};

ただし、似ているようにみえるのは見かけだけで、C言語配列とはメモリーの割り当て方が全く異なることに気を付けてください。このように、初期化子を使った場合でも、実際にはnewを使って動的に生成した場合と同様に配列オブジェクトは常に実行時に動的に生成されてメモリーに確保されます。*6

独自のメソッドを定義できないなど制約もありますが、配列は実際にオブジェクトの一種です。まずは、この配列の意味や配列変数の使い方に習熟することで、動的にオブジェクトを生成して使うというstaticでないプログラミングの世界への第一歩を踏み出すことができます。

ユーザー定義のオブジェクトを定義して生成する手段としてのクラス

以上で説明した、動的な配列オブジェクトの生成と配列変数によるオブジェクトの参照ということが理解できたら、一般のオブジェクトを理解することも難しくありません。配列との違いは、以下の点です。

配列の場合は、同一の型の値の集合しか表現できませんが、クラスを使えば、任意の値の組み合わせからなる構造のオブジェクトを生成できるようになります。

public class Person {
    public int age;
    public String name;
}

Person personX = new Person();
Person personY = personX; // 二つの変数が同じオブジェクトを参照するようになる

personX.age = 20;
personX.name = "test";

System.out.println(personY.age); // 20
System.out.println(personY.name); // test

フィールドを自由に定義できることと、要素にアクセスする際の演算子がピリオドである点を除けば、配列オブジェクトとまったく同様の性質を持っていることがわかります。要するに、Java言語においてオブジェクトとは、ヒープに割り当てられたメモリ領域で、オブジェクト生成元のクラスを型として持つ変数によって参照されるものということになります。まずは、データの塊がヒープに生成されて、変数を使ってそれを参照して使うということをイメージできるようになることが何よりも大切です。つまり、Javaオブジェクト指向プログラミングを理解する前提としては、

という性質をしっかりと理解することが重要だと思います。クラスがロードされたタイミングで最初から用意されるstaticなメモリ領域とは異なり、newされるたびに新しいオブジェクトが動的にヒープに割り当てられていきます。

ところで、どうしてJavaオブジェクトには、このような性質があるのでしょうか。これも、配列の性質を考えてみれば納得ができると思います。配列は一般にはたくさんの要素を含むので、あらかじめ静的に変数の領域を確保したり、メソッドが呼び出されるつどメモリーを毎回スタックに割り当てるのはあまり効率的とは言えません。実行時に必要なサイズで動的にメモリーを割り当てて、必要がなくなるまで(ガーベッジコレクションされる)ヒープに確保して使うというのは、一般的なプログラムでは効率的と考えられます。さらに、変数への代入時やパラメーターの受け渡し時にすべての値のコピーを行うよりも、単に既にメモリー上にあるオブジェクト変数が参照するようにした方が、プログラムの実行性能からも有利です。他の言語では、さまざまな方法でオブジェクトを割り当てることができるものがありますが、Javaの場合は常に動的にヒープに割り当てて、変数から参照するというという決まりになっています。常に、最適というわけではないかもしれませんが、JVMガーベッジコレクションの仕組みと合わせて、この割り切った仕様により、Javaでは効率的にオブジェクトを扱えるようになっていると考えることができます。

オブジェクト指向プログラミングは、オブジェクトを定義するクラスとモジュールとしてのクラスを同一視することから始まる

前節で説明したクラスは、他の言語では構造体、レコード、あるいはユーザ定義型と呼ばれるものに過ぎず、複数の値から構成されるデータ構造を定義していました。

しかし、ここで本当に理解すべき重要なことは、Javaではオブジェクトの構造を定義して、そこから生成されたオブジェクトを参照する変数の型となるためのクラスと、「staticおじさんの世界におけるクラスの役割」の節で説明した、大きなプログラムを分割するモジュールとしての役割を持つクラスを同一視しているということにあります。

まず、Javaにおいて、この事実が当たり前と思えるようになれば、相当オブジェクト指向で考えられるようになっていると言えます。これは、Javaで適切な単位でクラスを設計し、オブジェクト指向プログラミングできるようになるための大前提であり、オブ脳の基本回路が脳に形成されたといっていいでしょう。

この同一視により、カプセル化という考え方も自然に理解できると思います。つまり、モジュールとして何を一緒に含めるのが自然であるかということを考えれば、オブジェクトに含まれるデータに関連する処理をメソッドとして一緒にクラスに定義し、フィールド自体はprivateにして外部から勝手に操作されないようにする考え方も納得がいくのではないでしょうか。String型のオブジェクトに含まれる文字列を加工するためのメソッドはStringクラスに定義されているので、文字列オブジェクトに対してそのまま呼び出すことができます。一般的にはこのように関連するデータと処理を同じクラス内に定義することが基本となります。

どうすれば、オブ脳が鍛えられるのでしょうか。こればかりは、JDKオープンソースのライブラリーをお手本として、また自分なりに似たような設計を試しながらプログラミングの経験を積むことに尽きると思います。

まとめ

ここでは、一般の入門書にあるような説明とは違ったアプローチで、Javaのクラスとオブジェクトについて理解するための説明を試みてみました。

実際には、最後の項目はかなり内容を端折っているので、クラスを有効なモジュールとして設計するためには、カプセル化の他に継承ポリモーフィズムなどさまざまなことを理解する必要があります。

いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について - 達人プログラマーを目指して

さらに、実際の問題を上手に分割してクラスに割り当てるということができるためには、やはり、それなりの経験を積んで勘を養う必要があります。*7そして、クラスをオブジェクトと結びつけて自然な単位として扱うのがなぜよいのかといったことを納得するには時間がかかるでしょう。しかし、プログラミング言語としてのオブジェクトやクラスの意味や存在意義はここに書いたように非常に単純なものに過ぎないということもできます。

もちろん、オブジェクト指向プログラミング言語で実現する方法はさまざまですし、Rubyなど、より純粋な言語を使って学ぶべきであるという意見もあるかもしれません。しかし、大部分のJavaプログラマーにとっては、モジュール配列、構造体といった言語の仕組みの自然な拡張として、クラスやオブジェクトを理解するというパスがわかりやすいのではないかと思いますが、いかがでしょうか。乗り物や動物を使った説明や、逆に専門的すぎて難しい解説を読んでよく理解できなかったという人は、まずは、ここに書いたようにプログラミング上の当たり前の考え方との比較から徐々に理解するのがよいのではないかと思います。そして、この基本が理解できたら、お手本となるソースはいくらでも転がっているのですから、オブジェクト指向の良いクラスライブラリーを読んだり、拡張したりすることに是非挑戦してみてください。

*1:ただし、オブジェクト指向プログラミングを動物や乗り物で解説するのは和書だけでなく、世界的にも一般的な傾向のようです。

*2:ただし、厳密には文字列配列オブジェクトは除く

*3:勘違いしやすいところですが、メソッド定義はstaticでもそうでなくても静的にクラスごとに読み込まれます。ただし、staticでないメソッドは暗黙のthisオブジェクトを受け取ることで、動的なインスタンス変数にアクセスできる点が違います。

*4:抽象データ型や参照はオブジェクト指向言語に限らず重要だと思います。スクリプト言語Perlでも、バージョン5から参照が使えるようになって随分と高度なプログラミングが可能になっていますし、このあたりは本格的なプログラミングには欠かせないツールなのだと思います。

*5:本家のstaticおじさんはC#VisualBasic使いなので、適切でなかったかもしれません。ここではオブジェクト嫌いなおじさんという意味の一般用語として使っています。

*6:重要な違いとして、C言語配列サイズはコンパイル時に決定されますが、Java配列は実行時に決められます。

*7:経験を補うものとして、デザインパターンなどがあるわけですが。

2011-12-05

ここぞというときの集中力Upのために、ヘッドフォンで音楽を聴きながらプログラミングしましょう

仕事や趣味でプログラミングしたり技術書を読みながら勉強したりする際には、言うまでもなく集中力を高めて維持することが大切ですね。職業プログラマーに必要な集中力ということには少なくとも二つの意味があるとは思いますが、

  • 単純に正確に繰り返しキーをタイプしたり、Excelシートをひたすら埋めるような単純作業を一定時間以上継続する
  • 新しいアルゴリズムの設計やリファクタリングのアイデアを構想する

いずれにしても、自分の場合は寝不足だったり、周りの雑音で気が散ったりしてコンディションが悪い時にはあまり集中できずに、圧倒的に作業効率が下がってしまいます。逆に、調子よく集中できた時には時間がたつのも忘れて一気に仕事を片付けることができます。

もちろん、プログラミングで集中力を高めるためには、日頃から規則正しい睡眠や食事などが欠かせませんが、ここぞという時に一人で集中するにはヘッドフォンで音楽を聴くのが個人的には非常に効果的な手段だと思っています。

客先常駐の場合は、常駐先の会社の文化によってはヘッドフォンで音楽を聴きながら作業をすることを禁止しているところもあるかもしれませんが、幸運なことに自分のチームではOKなので、本当に一人で集中して考えたいときにはヘッドフォンを利用しています。もちろん、ヘッドフォンにはチーム間のコミュニケーションを阻害するという弊害もあって、周りの人に質問しづらくなってしまうという問題があるのですが、一日の作業のうちで集中する時間を区切って利用すればよいのではないでしょうか。

音楽を聴きながらプログラミングする際の問題は、どのような音楽が集中に適するかということですね。一般的には歌詞のない音楽でモーツァルトなどのクラシック音楽などがよいとよく言われますが、もちろん、これには好みによって個人差があると思いますし、人によってはクラシックはかえって心地よくて眠くなるという人もいるかもしれません。

私の場合はずいぶん以前からいわゆる「集中力Upのための音楽」というちょっと怪しげなジャンルの音楽に非常に興味があって、実際仕事や勉強でいろいろと活用しています。

Focus CD (フォーカス CD)

Focus CD (フォーカス CD)

たとえば、以上のCDは雨の音がひたすら聞こえるだけなのですが、左右の耳から周波数の異なる音を同時に聴くことでいわゆるバイノーラルビートと呼ばれる効果で脳の集中力が活性化するそうです。Amazonのレビューでも高評価なので試してみてはいかがでしょうか。このCDの製造元の説明は以下にあります。

Boost your concentration at the touch of a button

そのほか、同様のバイノーラルビートを活用した音楽でヘミシンクと呼ばれるシリーズのタイトルはいくつか利用していますが、個人的にはかなりおすすめです。(以下で部分的に視聴できます。)

D

D

インディゴ : Indigo for Quantum Focus [ヘミシンク]

インディゴ : Indigo for Quantum Focus [ヘミシンク]

コンセントレーション : Concentration [ヘミシンク]

コンセントレーション : Concentration [ヘミシンク]

Remembrance [ヘミシンク]

Remembrance [ヘミシンク]

ブレークスルー : Breakthrough [ヘミシンク]

ブレークスルー : Breakthrough [ヘミシンク]

以下は、ブレインシンクと呼ばれる別のシリーズで、ヘミシンクと同様に集中力や記憶の向上に役立つとされているタイトルがあります。

High Focus: Activate Lucid Thinking (Brain Sync Audios)

High Focus: Activate Lucid Thinking (Brain Sync Audios)

Brain Power

Brain Power

なお、これらの音楽は人によっては非常に深い集中状態をもたらすため、逆に外界に対する反応が鈍くなってしまうことがあり、自動車で運転中などには絶対に聴かないようにとの注意書きがあります。

2011-09-26

普通の構造化プログラマーがオブジェクト指向の存在意義を理解するコツ

オブジェクト指向言語の存在意義を理解するのは難しい?

id:amapetasさんによる、ちょっと興味深い記事がありました。

オブジェクト指向言語が流行した必然性について考える(1) - Programmer’s Log

そして、その記事の中で説明されているのですが、やはり、C言語など構造化言語のプログラマーにとってはオブジェクト指向の存在意義を理解するのがなかなか難しいところがあるようですね。

初心者向けの書籍を最近読んでいないので、最近の書籍ではうまく説明されているのかもしれませんが、そんな話を聞いたことがないので、たぶん今でもオブジェクト指向に関する説明の始まり方は

「世界は全部オブジェクトで出来ているんじゃぁぁーーー」

という「オブジェクト至高教への洗脳」から始まっているものと推察しますw テンション高めの説明から入るのでドン引きする人多数な感じがかなりアレですね。オブジェクト指向の説明は独特な説明から入るので宗教っぽいんですよね、若干。宗教という表現をさけるなら思想、哲学といえば柔らかいでしょうか。

僕がその手の初心者向け書籍を読んだ時に思った事は「なぜオブジェクト指向言語(Java)で書く必要があるのか? 構造化言語(C言語等)ではダメなのか? 何が違い、何がメリットなのか?」にうまく答えられていない、という疑問でした。オブジェクト指向の世界観の説明で手一杯でそこらへんをうまく説明できていない気がしました。

構造化言語(C言語等)ではできなくて、オブジェクト指向言語(Java、C++等)で出来るようになったものとは何か?

引用元の記事ではオブジェクト指向を自然言語の構造と関連させて論じているのですが、ここでは論点を変えて普通の構造化言語のプログラマーオブジェクト指向のメリットを理解してもらうためにはどのように説明したらよいかという点について考えてみたいと思います。

伝統的なプログラマーの頭の中にある数のイメージ

我々は小さい頃から、まず、ものを数えるということから自然数として数の概念を習得します。そして、成長するにしたがって、金額やテストの点数など、他のところで使われる数も存在することを体験を通して徐々に学んでいきますが、日常生活では基本的には整数は数えたり、大小比較のために使うものという考え方が根強く存在するでしょう。

もちろん、このような普通の整数の概念は(範囲に注意すれば)プログラムを使って表現することができます。しかしながら、コンピューターの内部で表現される整数が特定の個数のビット(あるいはバイト列)からなるデータに過ぎないという発想は本来一般人にはない考え方であると思います。

実際、コンピューターの整数データは32ビットや64ビットなど表現可能な値の範囲という厄介な性質がある一方で、画像を構成する一個一個の画素(ピクセル)の明るさや色、音声データ、通信データなど様々なところで利用されています。ベテランのプログラマーの頭の中にある整数のイメージは、一般人のそれとは違っているのかもしれません。ビットやバイトなどを意識する必要のある(意識できる)比較的低水準な言語を利用することで、プログラマーはコンピューターの構造と近いレベルで考える必要がある反面、効率的なプログラムを記述できます。

ここで重要なのは、コンピューターの構造上の都合で決まっている32ビットや64ビットの情報を使って表現できる値を使う必要があるということであって、これは伝統的なプログラマーの頭では自然な単位なのかもしれませんが、一般人の頭ではまったく自然な単位ではないということです。しかし、コンピューターの処理能力の急激な進歩に従って、最近のアプリケーションはどんどん高度な機能を求められるようになってきています。たとえば、銀行のシステムの金額計算であれば、32ビットの範囲を超えたなどという理由で計算結果が不正になることなどはあってはいけません。

オブジェクトはデータを抽象化したもの

Javaなどのオブジェクト指向言語では、クラスを使ってデータ型を抽象化することで、文字列、誤差なく計算できる金額、日付、リストなどアプリケーションの記述に便利な値(=オブジェクト)を生成して利用できるようになります。実際、Javaでは文字列は基本型に準ずる形で簡単に扱うことができるようになっていますが、単純な文字列結合のような処理であっても、本来のVM上の動作から考えるとかなり複雑な処理が必要なのですが、そういった処理は抽象化されているので普通は考える必要がありません。

コンピューター内部の構造によらない抽象化されたデータ型を利用したり、独自にクラスとして定義したりできることは、このようなオブジェクト指向言語のメリットの一つと考えられます。コンピューター(VM)の作りの制約に縛られた基本型の値を使う代わりに、適切に抽象化されたオブジェクトを使うことで、ビットやバイトといった発想を忘れてよりアプリケーションの要求に近い塊の単位で簡単にロジックを組むことができるようになるのです。

そして、オブジェクトに対して呼び出し可能なメソッドは、intなどの基本データ型に対する+などの演算に近いものであると考えることができます。Javaでは残念ながら独自の演算子を定義することはできませんが、実際にC++などの多くの言語ではクラスに対して演算子を定義することができます。

さらに、基本型の演算に対しても一種のポリモーフィズムは存在しています。*1実際、Javaのint型とfloat型では同じ32ビットでもまったく異なる内部表現を使っているのですが、同じ+演算子を使って足し算を実行することができます。しかし、両者はオーバーフローの有無など、かなり異なる振る舞いをします。

オブジェクト指向の入門書が理解を妨げる?

本来はSmalltalkRubyのように「すべてがオブジェクト」という純粋なオブジェクト指向の世界で考えた方が分かりやすいのですが、普通の構造化言語のプログラマーが最初にオブジェクト指向を学習するには、「社長と社員との関連」「哺乳類も犬もすべてオブジェクト」などということを考えるよりも、intなどの基本型や構造体の値に対する自然な拡張として、段階的にオブジェクトを考える方がしっくりくるのではないでしょうか?私自身も最初にオブジェクト指向を学習した際にはそのようなプロセスで徐々に理解しました。

多くのオブジェクト指向の入門書は、伝統的なプログラミングの考え方とはかけ離れた世界でオブジェクト指向を語ろうとする傾向があるので、かえって構造化プログラミングの知識があると理解を妨げるところがあるかもしれません。つまり、これらの入門書では、ちょっと便利なデータ型を定義できるという段階をスキップして、いきなり実世界の「もの」の関係をモデル化するところから説明しようとするために、かえってプログラミングとの関連性が見えにくくなっているところはあると思います。

つまり、Objectを普通の英語の意味で実世界の「もの」と考えるのではなく、コンピューターのメモリ上に確保された値のことを指す専門用語としてとらえた方が、構造化言語のプログラマーにとってはしっくりくるのではないでしょうか。たまたま、顧客など実世界のエンティティに対応したオブジェクトも必要かもしれませんが、プログラムの都合上で必要な値の塊も立派なオブジェクトになり得ます。

プログラミングオブジェクト指向を習得するステップ

ところで、C++、Objective C、Object Pascal、Javaといった言語では、比較的低水準な伝統的なプログラマー脳が必要な基本型と演算子を使ってコーディングを行うことも、クラスライブラリーを利用して高水準のオブジェクト指向プログラミングを行うことも可能です。複数のパラダイムが混在しているため、なかなかい理解しにくいというところもあるかもしれません。

まずは、配列文字列、日付といった抽象データ型がオブジェクトの一種なのであるというところから慣れ、次に、

あたりを順番に理解していくようにすれば、理解しやすいのではないかと思います。そして、プログラミングで正しく独自のクラスの作成ができるようになってくれば、自然に業務ドメインなど実世界の問題も、クラスを使ってモデリングする力がついてくるはずです。

*1Javaを含めて、ここで説明する数値演算に対するポリモーフィズムコンパイル時に決定されるので実際にはオーバーロードされたメソッドの呼び分けに近いものですが。

*2:簡単なスタックやリストなどを自作してみるのも学習のためにはよいですね。

2011-07-03

Javaエンジニア必携の「プログラミングGroovy」を献本していただきました

昨日、ポストを確認したところ、執筆陣の皆さんより、献本として送っていただきましたプログラミングGroovyが届いていました。

プログラミングGROOVY

プログラミングGROOVY

私の人生で初の「献本」であったということもありますが、本当に出版を心待ちにしていた本であったので届いていたのをみつけたときは、(まったく季節外れな表現ですが)子供のころにサンタさんからプレゼントが届いていたのを見つけた時と同じような感動を覚えました。私は「Groovyおじさん」と呼ばれてもよい年齢なのですが、新しい技術書が届いた時のわくわく感というものは常にありますね。でも、出版から一足早く「献本」を受け取るというのは生まれて初めての感覚でした。ほんとうに、ありがとうございました。

そして、この本*1に対して、最初に感じた印象は「想像していたよりもずっとコンパクトな本だな」というものでした。一般的にプログラミング言語を解説する書籍は、入門書であってもそれなりのサイズとページ数があるのが普通なのですが、この本はいわゆる単行本サイズで、電車の中でも本当に気軽に鞄にいれて持ち運べるサイズです。しかし、内容が薄いかといえば全然そんなことはなく、今までそれなりにGroovyを勉強して使ってきた私が読んでも十分に読み応えのある内容が詰まっていました。

この時に出た感想は以下の通りです。

そうしたら、さっそく関谷さん(id:ksky、@kazuchika)より、

というコメントが返ってきました。なるほど、そうですね。確かにまったく未知の言語を勉強するというわくわく感は感じられないかもしれませんが、GroovyというのはJavaプログラマーにとっては、自分の「母国語」がいつの間にか最強のツールに生まれ変わっていたといった感じのする言語です。一から文法を暗記し直さなくても、差分を効率よく復習していきなり便利なプログラムを書けるようになるといったところがあります。

この本はそのようなGroovyの特性を最大限に活用し、Javaプログラマーならだれでも知っているような内容はあえてバッサリと省略されています。そのかわり、

など、JGGUGの勉強会などで紹介されるGroovyのエッセンスがわずか320ページの中に凝集されています。基本的にはどこの章から読んでも良い感じですし、時間がなければ、まず、3章までを読み、あとは興味のあるところから読むというスタイルでも役に立つと思います。

今回、献本いただいたからというわけではありませんが(普段お世辞を言うことは苦手なので)表紙に書いてある通りJavaエンジニア必携の良書だと思います。「Pragmatic Groovy for Java Programmers」「Groovy distilled」的な感じで英訳したら世界中で売れるのではないかと思われる出来です。

逆に、まったくJavaのコードを書いたことのない人だと、いきなり本書を学習するのは難しいかもしれません。あと、一つ難点をあえて挙げるとすれば、多少フォントが小さ目(特にサンプルプログラム)なところはあるかもしれません。

普段業務でJavaを使っている人であれば、本書を学習することによって最短で、実用的に、仕事でGroovyを活用できるGroovy使いになることが可能だと思います。つまり、業務ロジックの記述を簡易化したり、ビルドツールとして活用したり、単体試験のツールとして利用するといったことがすぐにできるようになるでしょう。値段の価値が十分にある本で、すべてのJavaプログラマーにお勧めします。

*1:表紙になぜかペンギンのマークが書かれているので、早速ペンギン本という通称も広まっているらしい。なぜペンギンなのかは今のところ不明。Dukeと関連があるという説もある。

2011-06-23

いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について

正しく意味を理解している方にとっては、まったく常識レベルの話であり、何をいまさらと思われる方々も多いかと思いますが、大規模案件のレガシーコードなど、私が仕事で見かけるJavaのコードを読むと、「このコードを書いたSEやPGの方々は、はたして継承の意味を正しく理解していないのではないか」と思われる設計のコードに出会うことが少なからずあります。現在では改良されましたが(Javaプログラミング能力認定試験の問題がかなり改善されていました - 達人プログラマーを目指して)、以前のJavaプログラム認定試験の問題は、そうした不適切な設計がされている典型的な例となっていたのですが、実際、SI業界ではあのような品質のコードのシステムが今でも現役で多数稼動しているというだけでなく、現在でも新たに生み出されているというのは残念ながら紛れもない事実のようなのです。

確かに新人研修で「哺乳類を継承して犬クラスと猫クラスができる」といったようなオブジェクト指向の説明を聞いただけで、簡単に理解できるものではありませんが、以降手続き型のレガシーコードしか相手にしていないPGやコードを目にすることもなくExcel方眼紙ばかり描いて(書いて)いるSEの方々は、継承の意味などをきちんと理解する前に忘れてしまっているという方も多いかもしれません。私の経験上、この業界ではこうした基本的な知識が理解されていないという現場が、むしろ、典型的なケースなのではないかという疑いすらあると思えてくるのです。(あの認定試験問題の品質をSI業界の代表的なプログラム品質と考えることの是非 - 達人プログラマーを目指して

オブジェクト指向プログラミングの方法を理解して使いこなせるようになるには、通常はそれなりの努力と期間を要するものですし、きちんと指導してもらえる先輩に巡り合うということも大切ですが、ここでは、「継承」の意味に絞って、最低限知っておいてほしいポイントについて、今更ですがまとめてみたいと思います。あらかじめお断りしておくと、これを読んですぐに理解できるという保証もできないのですが、あと一歩のところで正しい理解に到達できない人にとってヒントになるところもあるかもしれません。そして、もちろん、継承オブジェクト指向プログラミングのほんの一部分の要素でしかありませんが、その意味を理解することで、次のステップに進みやすくなるとも考えられるのです。(ここではJava言語を例として説明していますが、C#やVisual Basicなど、業務で利用する主流のオブジェクト指向プログラミング言語でも基本的なポイントはほぼ同じです。)

クラスの継承の文字通りの意味

Java言語ではクラスを継承するときにはextendsというキーワードを使ってクラスを宣言します。これは、文字通り「拡張する」という意味ですが、親クラスの定義を拡張して新しいクラスを定義するという働きがあります。以下のコード例をみてください。*1

class Parent {
    public String fieldA = "field A";
	
    public String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    public String fieldB = "field B";

    public String methodB() {
	return "method B";
    }
}

この場合、以下のようにChildクラスのインスタンスを生成して、フィールドやメソッドにアクセスすると以下のようになります。

public class Inheritance {

    public static void main(String[] args) {
        Child child = new Child();
        System.out.println(child.fieldA); // field A
        System.out.println(child.fieldB); // field B
        System.out.println(child.methodA()); // method A
        System.out.println(child.methodB()); // method B
    }
}

もともとの「継承する」「拡張する」という意味の通り、ChildクラスはParentクラスのフィールドやメソッド継承元の親から引き継いで自分自身で定義しているような動作となっています。つまり、Childを以下のように定義した場合と同じ動作ということですね。

class Child {
    public String fieldA = "field A";
    public String fieldB = "field B";

    public String methodA() {
	return "method A";
    }

    public String methodB() {
	return "method B";
    }
}

継承を活用することで、このように共通のデータ(フィールド)や処理(メソッド)を親クラスにまとめて定義することができるのです。

子(サブ)クラスで同一の形のメソッドをオーバーライドする

「なんだ、かんたんじゃないか」これで、一人前に継承を理解できたと考える人もいるかもしれません。ですが、残念ながら話はここで終わらないのです。むしろ、実は、ここまでの話はJava言語の継承の働きの中でも本当に20%というか、継承の本当に威力のあるポイントを見逃していることになるのです。話のクライマックスはまだこれからなのです。

話が面白くなるのは、子クラスが親クラスと同じ形(シグネチャ)のメソッドを定義している場合です。以下の定義を見てください。

class Parent {
    public String fieldA = "field A";
	
    public String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    public String fieldB = "field B";

    @Override
    public String methodA() { // 親クラスのメソッドをオーバーライドする。
        return "method A in Child";
    }

    public String methodB() {
	return "method B";
    }
}

ここでは、Childクラスにおいて親クラスで定義されているmethodA()と同じ形のメソッドを再度定義しなおしています。この形で、親クラスのメソッドを子クラスで再定義することをオーバーライドと呼んでいます。なお、Java5以降のバージョンでは、正しくオーバーライドしていることをコンパイラにチェックさせるために@Overrideというアノテーションを明示的につけることが推奨されています。(うっかりスペルミスをしたり、シグネチャが異なっていたりするとコンパイルエラーとなります。)

次に、これを実行してみましょう。

public class Inheritance {

    public static void main(String[] args) {
        Child child = new Child();
        System.out.println(child.fieldA); // field A
        System.out.println(child.fieldB); // field B
        System.out.println(child.methodA()); // method A in Child
        System.out.println(child.methodB()); // method B
    }
}

期待通り、child.methodA()の呼び出しはParentクラス中のメソッドでなく、Chlildクラス中のメソッド呼び出しに置き換えられています。これも、オーバーライドの機構を理解してしまえば、難しいところはないと思います。通常は親クラスのメソッド継承されてくるのに、子クラスでオーバーライドすると、継承元の親クラスの側のメソッドでなく子クラスのメソッドが呼び出されるということです。共通部分を親クラスに定義しておき、必要に応じて、差分があれば子クラスでオーバーライドできるということで、便利ですね。

継承には型の継承というもう一つの重要な側面がある

実は、多くのJavaプログラマーの理解がこの段階で止まってしまっているのではないかと思われるのですが、Java言語の継承には型の継承というもう一つの重要な側面があります。つまり、今までの説明では「継承とは親クラスのフィールドやメソッドを子クラスで再利用するための便利な方法」という意味しかなかったのですが、それに加えて、子クラスのオブジェクトは親クラスの型と代入互換性があるという性質があるのです。Java変数は型をつけて宣言する必要があったことを思い出してください。

int a = 3;
String b = "hello";
b = a; // コンパイルエラー

つまり、以上の例のように基本的には同じ型の変数にしか代入できないように、コンパイラがチェックしてくれます。一方、クラスに親子関係があると、親クラスの型の変数に子クラスの型のインスタンスを代入できるという重要な規則があります。

Child child = new Child();
Parent parent = child; // コンパイルOK

この規則は、冷静になって考えると自然なものであると納得ができます。今まで説明した継承のメカニズムによって、子クラスは親クラスのすべてのデータや振る舞いを保持しているのですから、場合によっては親クラスの型であると抽象化して考えても問題ないということですね。そして、以下の結果を見てください。

public class Inheritance {

    public static void main(String[] args) {
        Parent parent = new Child();
	System.out.println(parent.fieldA); // field A
	System.out.println(parent.methodA()); // method A in Child
    }
}

ここで、非常に大切なポイントはparent.methodA()の呼び出し結果がParentクラスのmethodA()でなく、ChildクラスのmethodA()にバインドされているという事実です。このポイントはレイトバインディングや仮想メソッド呼び出し*2などと説明されることがありますが、とにかく、宣言されている変数の型ではなく、実際に変数に代入されているインスタンスの型によって実行時の振る舞いが決まるということです。

つまり、継承によるメソッドのオーバーライドが、Java言語でポリモーフィズムを実現する手段となっているという事実を理解してください。これが、実はJava言語における継承の威力の中でも最も重要な働きをするポイントとなっているのです。

ポリモーフィズムについては、以前、ドラゴンボールで学ぶオブジェクト指向 改 - 達人プログラマーを目指してで、

共通のメソッド呼び出しで、対象とするオブジェクトの種類に応じてまったく異なるさまざまな処理を実行可能な性質をポリモーフィズム(多態性)と呼びます。

のように説明しています。なお、この説明だけだと、まだ何が嬉しいのかピンと来ないかもしれませんが、親クラスの型の変数に代入できるという性質は変数だけでなく、メソッドパラメーターにも同様に成り立ちます。だから、

public void someMethod(Parent parent) {
   // parentを使って何らかの処理
}

のようなメソッドを一度定義しておくと、Parentクラスの任意の子クラスをパラメーターとして渡して処理をさせることができるというわけです。つまり、このポリモーフィズムの性質を使うことで、拡張性や再利用性の高いライブラリーを作成できるのです。実際、たとえば証券のドメインであれば、ある銘柄の注文を表すOrderクラスを入力として受け取るメソッドを定義しておくと、株式や債券など様々な種類のOrderの子クラスを処理するように拡張させるといった設計が可能になるのです。

型の継承によるポリモーフィズム的な側面にのみ着目したのが抽象メソッドとインターフェース

このようにJava継承の構文が持つ働きには

という二つの側面があるのです。特に、後者の概念はなかなか難しく、(私の説明のまずさもありますが)話を聞いただけですぐに理解できないかもしれません。しかし、コードを実際に写経*3するなどして、実際に試しながら、じっくりと理解することにしましょう。Javaプログラミングでは、この壁を越えらえるかどうかが非常に重要なポイントで、ここをいったんクリアできれば、デザインパターンやフレームワークなど、オブジェクト指向プログラミングの広大な世界を冒険する準備ができたことになります。

さて、この型の継承ポリモーフィズムということに着目すると、もともとの親クラスのメソッドの実装は不要になる場合があります。その場合は、以下のように親クラスをabstractクラスにして、オーバーライドするメソッドをabstractメソッドとして宣言することができます。

abstract class Parent {
    public String fieldA = "field A";
	
    public abstract String methodA(); // 抽象メソッド
}

この場合も、前回と同様にParent型の変数のChildのインスタンスを代入して実行すると、正しくChildのメソッドが実行されるのです。

public class Inheritance {

    public static void main(String[] args) {
        Parent parent = new Child();
	System.out.println(parent.fieldA); // field A
	System.out.println(parent.methodA()); // method A in Child
    }
}

Parentクラスの抽象メソッドは型としてmethodA()が呼び出せるということをコンパイラに知らせている働きがある一方で、実際にメソッドの実装はポリモーフィズムにより、Childクラスのメソッドにバインドされています。

さらに、Javaの場合、継承による型の継承ポリモーフィズムという点を究極に推し進めたものとしてインターフェースが定義できます。上記のParentをインターフェースに置き換えると以下のようになります。

interface Parent {	
    String methodA();
}

class Child implements Parent {
    public String fieldB = "field B";
    
    @Override
    public String methodA() {
        return "method A in Child";
    }

    public String methodB() {
        return "method B";
    }
}

public class Inheritance {

    public static void main(String[] args) {
        Parent parent = new Child(); // Childのインスタンスをインターフェース型に代入
        System.out.println(parent.methodA()); // method A in Child
    }
}

(補足)Java言語ではフィールドはオーバーライドできない*4

本エントリに対して重要なコメントをいただきましたので、子クラスで親クラスと同一名のフィールドを定義した場合にどうなるかについて補足させていただきます。通常は、親クラスと同一名のフィールドを子クラスで定義すべきではなく、このセクションで書いた内容の理解は後回しでもよいと思いますが、うっかりバグの原因となることがあるので、注意が必要であるということは知っておく方がよいかもしれません。

実際、子クラスを以下のように定義して確認してみます。

class Parent {
    String fieldA = "field A";

    public String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    String fieldA = "field A in Child"; //親クラスと同一名称のフィールドを定義
    String fieldB = "field B";

    @Override
    public String methodA() {
        return "method A in Child";
    }

    public String methodB() {
        return "method B";
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Child child = new Child();
        Parent parent = child;

        // フィールドは宣言されている変数の型で決まる
        System.out.println(parent.fieldA); // field A
        System.out.println(child.fieldA); // field A in Child

        // Child型の変数で隠ぺいされているParentのフィールドを参照するにはキャストすることも可
        System.out.println(((Parent)child).fieldA); // field A

        // メソッドはオーバーライドされてポリモーフィックに呼び出される
        System.out.println(parent.methodA()); // method A in Child
        System.out.println(child.methodA()); // method A in Child
    }
}

結果は以上のようになりました。メソッドの場合はすでに説明したように子クラスの実装で親クラスの実装がオーバーライドされており、変数の型によらずに、実際に生成されているオブジェクトの型に応じて子クラスのメソッドが呼び出されるのでした。しかし、同一名のフィールドを定義した場合は変数の型によって、親クラスか子クラスのどちらかのフィールドが参照されています。これはどういうことかというと、フィールドは同一名称であっても決してオーバーライドできないということを意味しています。つまり、たまたま名前が同じ変数が親と子に別々に含まれているだけであって(異なる名前のフィールドの継承とおなじく)子クラスのインスタンスにはどちらの変数も含まれているということになります。ただし、同一名のフィールドをこのように宣言してしまうと変数の型によってどちらかのフィールドが参照できなくなってしまうのです。これは、一般にグローバル変数とローカル変数で同じ名前のものがあると、ローカル変数しか見えなくなってしまうといったことと似ています。それぞれの変数の領域は存在しているのですが、単に名前が隠ぺいされて参照できなくなっているということです。

(補足)staticメソッドもオーバーライドできず、ポリモーフィズムが存在しない

つぎに、staticメソッド継承について調べてみます。以下の例は、以前の例に対して、staticキーワードを各メソッドに追加しています。

class Parent {
    String fieldA = "field A";

    public static String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    String fieldA = "field A in Child";
    String fieldB = "field B";

    public static String methodA() { // 親クラスと同一形のstaticメソッド。普通はやらない。
        return "method A in Child";
    }

    public static String methodB() {
        return "method B";
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Child child = new Child();
        Parent parent = child;

        // staticメソッドはクラスごとに存在しており、変数の型で呼び出し対象が静的に決まる。
        // つまり、ポリモーフィズムが存在しない。
        System.out.println(parent.methodA()); // method A
        System.out.println(child.methodA()); // method A in Child

        System.out.println(((Parent)child).methodA()); // method A

        // 紛らわしいので、普通はインスタンスでなくて、クラスに対して呼び出す。
        System.out.println(Parent.methodA()); // method A
        System.out.println(Child.methodA()); // method A in Child
    }
}

staticでない普通のメソッドと違い、staticメソッドはクラスごとに別々の実装が独立して存在しており、子クラスで同一名のメソッドを定義してもオーバーライドできません。見かけ上インスタンス経由で呼び出した場合、インスタンスの型でなく宣言されている変数の型によって呼び出し先が決まります。前の節で説明したフィールドの場合と似たように解決されています。

このようにstaticメソッドにはポリモーフィズムが存在せず、コンパイル時に呼び出し先が一つに固定されます。staticメソッドの呼び出しにより、コンパイル時に実装が一つに固定されてしまうということです。(一方、普通のメソッドは、ポリモーフィズムにより、オーバーライドしている子クラスの数だけ無数に実装が存在している可能性があります。)staticメソッドが拡張性が低く、また、クラスを分離した単体試験が難しくなるということと関連しています。

(補足)コンストラクタ、初期化ブロック、static初期化ブロックについて

コメント欄にて川久保さんにご指摘いただきましたので、コンストラクタや初期化ブロックに関して簡単に補足させていただきます。初期化ブロックはともかく、コンストラクタに関する理解は重要だと思います。

コンストラクタは継承されない(しかし、子クラスのコンストラクタから呼び出される)
class Parent {
    public Parent() {
        System.out.println("Parent no args constructor");
    }
    
    public Parent(String arg) {
        System.out.println("Parent one arg constructor arg = " + arg);
    }
}

class Child extends Parent {
    public Child() {
//        super(); コメントをはずしても同じ
        System.out.println("Child no args constructor");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Parent parent = new Parent(); //Parent no args constructor
        Parent parent2 = new Parent("test"); //Parent one arg constractor arg = test
        
        Child child = new Child(); // Parent no args constructor, Child no args constructor

  // コメントをはずすとコンパイルが通らない。 
  // コンストラクタはメソッドのようには継承されない
  //      Child child2 = new Child("test");
    }
}

ご指摘のとおり、Java言語の場合にはコンストラクタは通常のメソッドのように自動的に子クラスに継承されません。したがって、以上の例では文字列パラメーターをとるコンストラクタは親クラスには存在しますが、子クラスには存在しません。*5ただし、子クラスのコンストラクタ中から親クラスのコンストラクタが呼び出されます。(呼び出しが省略された場合は親クラスのパラメーターのないコンストラクタの呼び出しが自動的に行われる仕様。)

インスタンス初期化ブロックはnew実行時に自動的に実行され、オーバーライドできない

存在自体知らない人も多いかもしれませんが、コンストラクタの代わりに初期化処理を行うための、インスタンス初期化ブロックという仕掛けがJDK1.1のころからあります。以下の例を実行してみると、各クラスのコンストラクタの実行前に呼び出されることがわかります。親クラスの初期化ブロックは自動的に呼び出されるため、子クラスでオーバーライドできません。

class Parent {

    // インスタンス初期化ブロック
    {
        System.out.println("Parent instance initilizer block");
    }

    public Parent() {
        System.out.println("Parent no args constructor");
    }
}

class Child extends Parent {
    // インスタンス初期化ブロック
    {
        System.out.println("Child instance initilizer block");
    }

    public Child() {
        System.out.println("Child no args constructor");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        // Parent instance initilizer block
        // Parent no args constructor
        Parent parent = new Parent();

        System.out.println();

        // Parent instance initilizer block
        // Parent no args constructor
        // Child instance initilizer block
        // Child no args constructor
        Child child = new Child();
    }
}

この動作は、各フィールドの初期化子に似ています。

static初期化ブロックはクラスロード時に自動的に一度だけ実行され、オーバーライドできない

同様に、static初期化ブロックの例も示します。こちらはインスタンスの生成時ではなく、クラスのロード時に一回だけ実行される点が違います。いずれにしても、サブクラスでオーバーライドするということはできません。

class Parent {
    // static初期化ブロック
    static {
        System.out.println("Parent static initilizer block");
    }

    public Parent() {
        System.out.println("Parent no args constructor");
    }
}

class Child extends Parent {
    // static初期化ブロック
    static {
        System.out.println("Child static initilizer block");
    }

    public Child() {
        System.out.println("Child no args constructor");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        // Parent static initilizer block
        // Parent no args constructor
        Parent parent = new Parent();

        // Parent no args constructor
        Parent parent2 = new Parent();

        System.out.println();

        // Child static initilizer block
        // Parent no args constructor
        // Child no args constructor
        Child child = new Child();

        // Parent no args constructor
        // Child no args constructor
        Child child2 = new Child();
    }
}

まとめ

通常、会社の研修や入門書ではオブジェクト指向の考え方と一緒に継承の説明を一気にされることが多いと思います。そのため、なかなか本質を理解しにくかったり、重要なポイントを見逃してしまうということもあると思います。ここでは、カプセル化や関連などオブジェクト指向に関する他の説明はいったん無視して、Java言語における継承の意味に絞って説明を試みてみました。

  • 継承は親クラスの定義を子クラスで再利用するための手段
  • 親クラス(インターフェース)の型に子クラスを代入することでポリモーフィズムを利用して拡張性の高いライブラリーを作成する手段

という二つの側面があるという点を理解することが大切です。特に、後者はなかなか分かりにくいところがあると思いますので、すぐに理解できなくても悲観することはありません。私も最初そうでしたが、ふとある時突然「そういうことだったのか」という瞬間が来るものです。

SI業界では、プログラミングの研修に十分な時間がとられないことも多く、こうしたごく基本的なことを理解しないまま、設計を行ったり、コードを書いたりするということも多いかもしれません。しかし、より高い生産性のプログラマーを目指すために、こうした基本的な知識を正しく理解しておくことは、プロフェッショナルプログラマーとして最低限の義務なのではないでしょうか。

なお、今回は最短の説明でJava言語の継承の働きを説明するため、オブジェクト指向の説明はあえて省略してしまいましたが、オブジェクト指向については、以下もご参照ください。

ドラゴンボールで学ぶオブジェクト指向 改 - 達人プログラマーを目指して

(追記)

本エントリでは、オブジェクト指向の概念モデルやクラスライブラリーとして意味のないParentやChildといったクラスを説明に使いました。もちろん、入門書などでは普通は図形や実世界の物などを使って継承関係を説明することが多いと思います。今回の説明のアプローチに対して、非難もあるかと思いますが、オブジェクト指向の意味やモデリングということを同時に理解しようとすると、考えることが多すぎて、逆に理解を妨げるということもあると思うのです。また、クラス設計というのは実際にモデル化すべき対象となる問題やプログラム(すなわちコンテキスト)を考えないと、あまり意味がないということもあります。

したがって、まず、初心者の方はオブジェクト指向の考え方はいったん後回しにして、プログラミング言語の機能としての継承の使い方を形から理解するというところから入るというアプローチもあると思います。その後で、JDKやOSSのクラスライブラリーやフレームワークの使い方を理解しながら、デザインパターンリファクタリングなどを学習し、その過程で自然にオブジェクト指向的なモデリングの考え方ができるようになれば、DDDなどの本を勉強して実際の問題を適切にクラスで設計できるようになるという流れもあると思います。

多くのオブジェクト指向の説明ではそういった段階を飛び越していきなり概念モデルに入ろうとするため、なかなか理解しにくいところがあるのではと思うのです。(自分のドラゴンボールの説明はそういうアプローチですが。)特に、インターフェースとポリモーフィズムの威力を理解するには、まずデザインパターンの中で、StrategyパターンかCommandパターンあたりから勉強することをお勧めします。以下の本はデザインパターンについて、初心者にもわかりやすく書かれていると思います。

(追記)

ブクマで

Parent parent = new Child(); の変数名はchildを使いたいなあ。

というコメントをいただいています。確かに、ポリモーフィズムによって呼び出し時のメソッドはオーバーライドされていればChildクラスとして振る舞うので気持ちがわからなくないのですが、あくまでも呼び出し元はParentという型として認識しているという点もポイントかと思います。ポリモーフィズムを説明する時に私がよく使うたとえとして「筆記用具で文字を書く」という文があります。筆記用具は抽象親クラスであり、文字を書く人はあくまでも筆記用具を使っていると考えているのですが、実際に使っているのは筆記用具の子クラスのオブジェクトである鉛筆だったりボールペンだったりするということがあります。文字の太さや色は実際に使っているオブジェクトによって決まるのですが、そのオブジェクトを使う人は特に両者の違いを意識しなくても文字を書けます。変数名にparentを使っているのはそういう気持ちからすれば、一般的に自然だと思います。たとえ話も危険な場合がありますが、ポリモーフィズムは本来は日常我々が行っている抽象化プログラミングで実現する手段としてはすごく自然な考え方だと思います。

*1:ここではアクセス修飾子の問題はあえて考えなくて済むようにすべてpublicとしています。もちろん、通常、フィールドはprivateなどにしてカプセル化すべきです。

*2コンパイル時に呼び出し先のメソッドが解決できないため、実行時に判断する必要があるということ。

*3:単に話を聞いたり本を読んだりするだけでなく、エディタやIDEを使って実際に例題を書き写し、動作を確認しながら理解すること。

*4Java言語はC++など多くのオブジェクト指向言語と同様に、フィールドとメソッドを明確に区別しています。つまり、統一アクセスの原理が言語上はサポートされていません。よって、フィールドをpublicにせず、getterとsetterを定義するということが通常は行われます。C#やVBではプロパティというしくみがあり、また、Scalaではそもそもフィールドとメソッドの区別はよりあいまいです。

*5:この辺りはオブジェクト指向言語によって違いもあるところがあるので、注意が必要です。たとえば、Delphiなどではコンストラクタは継承されます。