Javaのクラスとオブジェクトについて再度解説を試みる
オブジェクト指向プログラミングの考え方については、今までこのブログでも何度か取り上げてきました。
[オブジェクト指向] - 達人プログラマーを目指して
オブジェクト指向プログラミングはプログラミング技法のすべてではないとはいえ、Javaのようなオブジェクト指向言語で本格的なプログラムを作るには理解を避けて通ることができませんし、また、関数型言語など他のパラダイムの言語を利用するにしても、オブジェクト指向の考え方をまったく理解しないまま使いこなすということは困難でしょう。オブジェクト指向の考え方はデータ構造やアルゴリズムといったことと同様に、プロフェッショナルなプログラマーが理解しておくべき基本的な素養といってもよいと思います。実際、海外では募集要項でオブジェクト指向の理解を前提とすると書かれていることが普通ですし、プログラマーの面接試験で、アルゴリズムと並んでオブジェクト指向プログラミングの基本についての正しい理解を問うケースが多いようです。
しかしながら、特殊なケースを除いて、我が国ではいまだになかなか普及していないようですね。数週間の新人研修やOJTのみで短期間に理解できるほど簡単ではないというのは事実かもしれません。それゆえ、SIの開発案件ではJavaやVisualBasicといったオブジェクト指向言語を用いながらも、実際にはオブジェクト指向を利用しないで開発できるような規約を作るといったこともかなり一般的に行われているようです。
オブジェクト指向プログラミングの普及を妨げる原因の一つとして、わかりやすい入門向けの解説を作ることが難しく、また、実際にそのような解説が書かれている入門書も非常に少ないということがあると思います。実際に、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おじさんの世界でも適切にクラスを定義して、フィールドやメソッドをしかるべきクラスに定義することで、大規模なプログラムを適切な単位に分割して管理することができるようになります。しかしながら、この場合の問題は、プログラムが扱える変数やパラメーターの型が基本型、文字列型、またその配列といった非常に限定された型しか扱えないということです。FORTRANやCOBOLなどの昔の言語しかしらないstaticおじさん*5の問題点はプログラム言語が扱えるのはこのような限定された型のデータのみであると決めつけてしまっているところにあると思います。
ところが、実際の複雑なプログラムでは、
- 注文をデータベースに登録する
- 入力フォームを開く
- カタログ一覧を検索する
といったように、プログラムで処理したい単位はintやfloatなどといった基本的な型のデータなのではなく、「注文」「入力フォーム」「カタログ項目」といった人間にとってもっと自然な単位でデータを扱いたいということがあります。JVMの仕組みを考えてみればわかるように、本当はコンピューターが扱いやすいデータというのは有限のビットの集まりとして表現できる数値や文字といったデータなのですが、そういう詳細のことは無視して、データをより抽象化して扱えるようになれば、プログラムを実際のユースケース(仕様)に近い形で記述でき、よりわかりやすく記述できるようになります。実際にJavaの場合は、intやfloatといった基本型の値だけでなく、文字列、日付、注文書といったさまざまなオブジェクトを利用することで目的に応じてあらゆるデータを変数から参照したり、メソッドのパラメータや戻り値としてやり取りできるようになります。
もちろん、オブジェクトを真に使いこなせるようになるには、カプセル化やポリモーフィズムといったさまざまな事柄を理解する必要があるのですが、いきなりそのような概念を理解しようとせず、まずはこのようなさまざまなデータを扱えることの便利さを理解することが、staticな世界を卒業する最初の一歩として重要なのではないでしょうか。
一般のJavaのオブジェクトを理解する前に、まず配列オブジェクトの性質を理解するとよい
ところで、一般のオブジェクトはクラスを使って自分で定義し、newを使ってメモリーに割り当てて使うわけですが、最初に勉強する際に、オブジェクトの性質と同時にクラスの定義方法を学習するのは、一気にいろいろな概念を覚えなくてはならずなかなか難しいかもしれません。そこでお勧めなのは、最初に配列の性質をきちんと理解するという学習手順です。配列はクラスで定義する必要がないため特殊ですが、Javaの配列は
という性質を考えると、一般のオブジェクトと共通の性質を備えていることがわかります。
一般的な入門書の説明では、配列は複数の値を読み書きする箱が並んだ絵が説明に使われることが多くあります。一個の変数が値を格納する箱なので、これが連続して横に並んだものが配列というわけです。これは、昔の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のオブジェクト指向プログラミングを理解する前提としては、
- 変数の宣言とは独立して実行時にオブジェクトが生成される
- newされるたびに新しいオブジェクトが生成されるので、同じ型でも複数のオブジェクトが存在する
- 変数は生成したオブジェクトを参照する
- 変数が何も参照しない場合はnullという値を持つ
- 変数の数とヒープメモリー上のオブジェクトの数は一般に一致しない
- 代入によってオブジェクトのコピーは作成されず、複数の変数が同一のオブジェクトを参照するようになる。
という性質をしっかりと理解することが重要だと思います。クラスがロードされたタイミングで最初から用意されるstaticなメモリ領域とは異なり、newされるたびに新しいオブジェクトが動的にヒープに割り当てられていきます。
ところで、どうしてJavaのオブジェクトには、このような性質があるのでしょうか。これも、配列の性質を考えてみれば納得ができると思います。配列は一般にはたくさんの要素を含むので、あらかじめ静的に変数の領域を確保したり、メソッドが呼び出されるつどメモリーを毎回スタックに割り当てるのはあまり効率的とは言えません。実行時に必要なサイズで動的にメモリーを割り当てて、必要がなくなるまで(ガーベッジコレクションされる)ヒープに確保して使うというのは、一般的なプログラムでは効率的と考えられます。さらに、変数への代入時やパラメーターの受け渡し時にすべての値のコピーを行うよりも、単に既にメモリー上にあるオブジェクトを変数が参照するようにした方が、プログラムの実行性能からも有利です。他の言語では、さまざまな方法でオブジェクトを割り当てることができるものがありますが、Javaの場合は常に動的にヒープに割り当てて、変数から参照するというという決まりになっています。常に、最適というわけではないかもしれませんが、JVMのガーベッジコレクションの仕組みと合わせて、この割り切った仕様により、Javaでは効率的にオブジェクトを扱えるようになっていると考えることができます。
オブジェクト指向プログラミングは、オブジェクトを定義するクラスとモジュールとしてのクラスを同一視することから始まる
前節で説明したクラスは、他の言語では構造体、レコード、あるいはユーザ定義型と呼ばれるものに過ぎず、複数の値から構成されるデータ構造を定義していました。
しかし、ここで本当に理解すべき重要なことは、Javaではオブジェクトの構造を定義して、そこから生成されたオブジェクトを参照する変数の型となるためのクラスと、「staticおじさんの世界におけるクラスの役割」の節で説明した、大きなプログラムを分割するモジュールとしての役割を持つクラスを同一視しているということにあります。
まず、Javaにおいて、この事実が当たり前と思えるようになれば、相当オブジェクト指向で考えられるようになっていると言えます。これは、Javaで適切な単位でクラスを設計し、オブジェクト指向でプログラミングできるようになるための大前提であり、オブ脳の基本回路が脳に形成されたといっていいでしょう。
この同一視により、カプセル化という考え方も自然に理解できると思います。つまり、モジュールとして何を一緒に含めるのが自然であるかということを考えれば、オブジェクトに含まれるデータに関連する処理をメソッドとして一緒にクラスに定義し、フィールド自体はprivateにして外部から勝手に操作されないようにする考え方も納得がいくのではないでしょうか。String型のオブジェクトに含まれる文字列を加工するためのメソッドはStringクラスに定義されているので、文字列オブジェクトに対してそのまま呼び出すことができます。一般的にはこのように関連するデータと処理を同じクラス内に定義することが基本となります。
どうすれば、オブ脳が鍛えられるのでしょうか。こればかりは、JDKやオープンソースのライブラリーをお手本として、また自分なりに似たような設計を試しながらプログラミングの経験を積むことに尽きると思います。
まとめ
ここでは、一般の入門書にあるような説明とは違ったアプローチで、Javaのクラスとオブジェクトについて理解するための説明を試みてみました。
- オブジェクトを考えない世界(staticな世界)ではクラスは単にモジュール化の手段
- オブジェクトとは動的にメモリーに割り当てられて変数から参照される値の集合
- Javaの配列はオブジェクトの一種。一般のオブジェクトを理解する前に、メモリーへの割り当てや変数からの参照など配列の性質を完全に理解するのが近道
- オブジェクトは変数から参照したり、メソッドのパラメーターや戻り値として簡単に受け渡すことができる
- 一般のオブジェクトを定義して、生成するためにはクラスを利用する
- オブジェクトを定義して生成する手段してのクラスとモジュール作成手段としてのクラスを同一視することがJavaにおけるOOPの始まり
実際には、最後の項目はかなり内容を端折っているので、クラスを有効なモジュールとして設計するためには、カプセル化の他に継承やポリモーフィズムなどさまざまなことを理解する必要があります。
いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について - 達人プログラマーを目指して
さらに、実際の問題を上手に分割してクラスに割り当てるということができるためには、やはり、それなりの経験を積んで勘を養う必要があります。*7そして、クラスをオブジェクトと結びつけて自然な単位として扱うのがなぜよいのかといったことを納得するには時間がかかるでしょう。しかし、プログラミング言語としてのオブジェクトやクラスの意味や存在意義はここに書いたように非常に単純なものに過ぎないということもできます。
もちろん、オブジェクト指向をプログラミング言語で実現する方法はさまざまですし、Rubyなど、より純粋な言語を使って学ぶべきであるという意見もあるかもしれません。しかし、大部分のJavaプログラマーにとっては、モジュール、配列、構造体といった言語の仕組みの自然な拡張として、クラスやオブジェクトを理解するというパスがわかりやすいのではないかと思いますが、いかがでしょうか。乗り物や動物を使った説明や、逆に専門的すぎて難しい解説を読んでよく理解できなかったという人は、まずは、ここに書いたようにプログラミング上の当たり前の考え方との比較から徐々に理解するのがよいのではないかと思います。そして、この基本が理解できたら、お手本となるソースはいくらでも転がっているのですから、オブジェクト指向の良いクラスライブラリーを読んだり、拡張したりすることに是非挑戦してみてください。
*1:ただし、オブジェクト指向プログラミングを動物や乗り物で解説するのは和書だけでなく、世界的にも一般的な傾向のようです。
*2:ただし、厳密には文字列や配列オブジェクトは除く
*3:勘違いしやすいところですが、メソッド定義はstaticでもそうでなくても静的にクラスごとに読み込まれます。ただし、staticでないメソッドは暗黙のthisオブジェクトを受け取ることで、動的なインスタンス変数にアクセスできる点が違います。
*4:抽象データ型や参照はオブジェクト指向言語に限らず重要だと思います。スクリプト言語のPerlでも、バージョン5から参照が使えるようになって随分と高度なプログラミングが可能になっていますし、このあたりは本格的なプログラミングには欠かせないツールなのだと思います。
*5:本家のstaticおじさんはC#やVisualBasic使いなので、適切でなかったかもしれません。ここではオブジェクト嫌いなおじさんという意味の一般用語として使っています。