オブジェクトとクラスの関係について、次のような説明を見かけました(文言の引用ではなくて、檜山による要約)。
オブジェクトとクラスは全体としてツリー構造をしていて、ツリーの末端をオブジェクト、末端以外のノードをクラスという。末端であるオブジェクトは、その親ノードであるクラスのインスタンスと呼び、クラスどおしの親子関係を継承関係と呼ぶ。
うーむ、この説明、ある意味「簡潔でわかりやすい」とも言えるのだけど、ちょっと単純化し過ぎでしょ。
オブジェクトやクラスの概念て、そんなに美しくもなきゃ、整合的でもありません。実用性やら実装上の都合やらでゴチャゴチャですがね。しかし、そのゴチャゴチャが悪いともいえません。ゴチャゴチャを無理に単純化することなく、必然性を持った(幾分は偶発的だけど(苦笑))複雑さとして理解すべきかと思います。
というわけで、メタクラスやレイフィケーション(reification)なんて少し珍しい概念も含めて、オブジェクトとクラスの関係を整理してみましょう。今回の目標は、「Classクラスオブジェクト」(書き間違ってないぞ!)という概念を、実感を伴って明確に理解することです。
内容:
- まずは、クラスの継承関係を絵に描く
- オブジェクトの生成とインスタンス概念
- オブジェクト・プレーンとクラス・プレーン
- クラスオブジェクトとレイフィケーション
- クラスオブジェクトが所属するクラス
- すべてを一階に押し込めてみる
- もっと上の階の住人達
- メタタワーと繰り返しレイフィケーション
[追記]ちなみに、犬、猫、ポチとタマとかは出てきませんよ。ましてや牛は出てきません。[追記]
まずは、クラスの継承関係を絵に描く
クラスを四角で描いて、継承関係は二重の矢印(⇒)で示しましょう。二重矢印を使うのは、後で色々な矢印が出てくるので、それらと区別するためです。矢印の方向は、「派生クラス⇒基底クラス」の向きとします。この向きに必然性があるわけじゃないけど、UMLなどの習慣に従いました。例えばこんな:
毎回絵で示すのも面倒なんで、B ⇒ A のとき B is-derived-from A とも書きます。Javaなら B extends A
、Rubyなら B < A
、C++なら B : public A
てな書き方をしますね*1。is-derived-fromを使って上の図を書き下せば:
- String is-derived-from Object
- Person is-derived-from Object
- Employee is-derived-from Person
B is-derived-from A は、BがAから直接的に派生したことを示します。あっ、「派生した」なんて言葉を使いましたが、「継承により定義された」と同じことです。
is-derived-from をもとに、サブクラス/スーパークラスの関係 B is-subclass-of A を定義するなら、
- [ルールsub-1] B is-derived-from A ならば、B is-subclass-of A
- [ルールsub-2] とあるXに対して、B is-subclass-of X かつ X is-subclass-of A ならば、B is-subclass-of A
絵で考えると、何本かの二重線矢印'⇒'をたどってBからAに行けるなら B is-subclass-of A ってこと。先の図では、「Employee ⇒ Person」「Person⇒Object」だったので、Employee is-subclass-of Object ですね。
ところで、A is-subclass-of A (つまり、自分自身はサブクラス)と考えるのかな? is-subclass-of を数学的な順序関係として定式化したいなら、自分もサブクラスとしたほうが都合がいいんですけど、自分もサブクラスとする習慣はあんまりない気がするな。
あ、そうそう、「単一継承か多重継承か」なんて話はいまの文脈ではまったくどうでもいい事なので、単に絵を描く手間を省くために単一継承を例とします。
オブジェクトの生成とインスタンス概念
さて次に、「オブジェクトaが、クラスAから生成された」という主張(あるいは記述)を a is-created-by A と書くことにします。これはつまり、a = new A();
のようなコードが実行された、ってことです。「aが、クラスAのnewで作られた」なんてことは、実行時のある時点を特定しないと意味を持たない言明であることに注意してください。
a is-created-by A であることを絵で描くときは、クラスAからオブジェクトaに向かって波線矢印を引きます。クラスは四角、オブジェクトは丸で描くことにします。
それで、a is-instance-of X ということを、とりあえず次のように定義します(この定義は後で修正します)。
- [ルールinst-1] a is-created-by X ならば、a is-instance-of X である。(Xは生みの親)
- [ルールinst-2] a is-instance-of A かつ A is-subclass-of X ならば、a is-instance-of X である。(Xは生みの親のスーパークラス)
この定義は別に珍しいものじゃないですね。例えば次の状況を考えましょう。
/* (1)クラスAの定義 */ class A extends X { // ... } /* (2)オブジェクトaの生成 */ A a = new A();
(1)の部分から A is-derived-from X です。(2)の部分からは a is-created-by A がわかります。このことから、a is-instance-of X は次のように導き出せます。(下図の、'-'を並べた横棒は、ルールによる“推論”を表します。)
a is-created-by A A is-derived-from X -----------------[ルールinst-1] -------------------[ルールsub-1] a is-instace-of A A is-subclass-of X --------------------------------------------------[ルールinst-2] a is-instace-of X (結論)
まー、普通はいちいちこんなことを考えなくても、「aはXのインスタンス」と直感的に判断できるでしょう。
a is-instance-of A であることを絵に描くときは、aからAに向かって実線の矢印を引くことにします。上に挙げたis-instance-ofの定義を図示すれば次のようになります。
オブジェクト・プレーンとクラス・プレーン
Object, String, Personという3つのクラス、それとStringのインスタンスs1, s2, Personのインスタンスpを絵に描いてみましょう(is-instace-ofは、直接の所属関係だけを描いた)。
結局のところ、冒頭で述べた「クラスとオブジェクトは全体としてツリー構造をしていて、ツリーの末端がオブジェクト、末端以外のノードがクラス」になっちゃったね。いや、違うんですよ。ちゃんと絵を描くと、実は次のようです。
立体的なんです! 下の面には丸で描かれるオブジェクトだけを配置します。この面をオブジェクト・プレーンと呼びましょう。二階にあたる上の面には四角で描かれるクラスだけを配置します。二階のフロアはクラス・プレーンです。
クラス間の継承関係(is-derived-from)やサブクラス/スーパークラスの関係(is-subclass-of)は、クラス・プレーン上に描かれる矢印で示されます。一方、生成関係(is-created-by)、所属関係(is-instance-of)は、一階のオブジェクト・プレーンと二階のクラス・プレーンをつなぐ矢印になっています。
一階と二階を区別して、水平方向(プレーン内)の関係と垂直方向(プレーンをまたぐ)関係の違いも意識すべきなんです。これ、大事。
クラスオブジェクトとレイフィケーション
プログラムの実行中にはオブジェクトが生成されたり捨てられたりしますが、先の図に即して言えば、それらはオブジェクト・プレーンで生じる変化です。クラス・プレーンは、プログラムの実行中も何ら変化しません。
実行時には、そもそもクラスなんて存在してないプログラミング言語もあります。例えば、実行時型情報(RTTI)が導入される以前のC++では、クラスはプログラマの頭の中とソースコード上にあるだけで、実行時のメモリ内を探してもクラスのかけらも見つかりません。
一方Javaなどでは、実行時のメモリ内にクラスが存在し、プログラムからクラスへアクセスすることもできます。このような“実行時に存在するクラス”をどう解釈すべきでしょうか? 以下、オブジェクト・プレーンとクラス・プレーンの絵で考えてみます。
オブジェクト・プレーン内に描かれたオブジェクトは、実行時のメモリ内存在物に対応するとしましょう。しかし、クラス・プレーンに描かれたクラスは、概念、あるいはソースコードに記述されたものとしてのクラスです(そう約束します)。となると、“実行時のクラス”はオブジェクト・プレーン(一階)に配置されるべきものです。
しかし、クラスは本来クラス・プレーン(二階)にいるものなのに、オブジェクト・プレーンに配置されたクラスってなんなのそれ? この点を説明するために、クラスオブジェクトという言葉を使うことにします。クラスオブジェクトは、クラスではなくてオブジェクトです。ただし、クラスを表現するためのオブジェクトです。
図でいえば、二階のクラス・プレーンに載っているほんとのクラスに対して、その“真下”に、そのクラスに対応するオブジェクトがあると思ってください。二階にいるほんとのクラスに対して、代理となるオブジェクト(それがクラスオブジェクト)を作り出すことをレイフィケーションといいます。
レイフィケーションのレイファイ(reify)とは、「抽象物を具現化する」といった意味です。もともとは概念的な存在であるクラスを、メモリ内実体であるオブジェクトに具現化したものがクラスオブジェクトです。図に描くときは、レイフィケーションを、クラス・プレーン(二階)からオブジェクト・プレーン(一階)に落ちる点線矢印で示すことにします。
クラスオブジェクトが所属するクラス
今扱っている例では、二階のクラス・プレーンに Object, String, Personという3つのクラスが載っています。レイフィケーションにより、二階から一階への写像が行われると、一階のオブジェクト・プレーンに対応する3つのクラスオブジェクトが出来ます。これらのクラスオブジェクトを、Javaの記法を借りて、Object.class, String.class, Person.classと書きましょう。お尻に'.class'が付いてますが、クラスそのものではなくてオブジェクトですからね! 構文(書き方)としては、Javaクラスファイル名と同じだけど、もちろんファイルではなくて、実行時のメモリ内にあるものです。
さて、オブジェクト・プレーンに存在するオブジェクトは、何らかのクラスのインスタンスになるべきです*2。ですから、例えば Object.class is-instance-of X となるクラス(ほんとのクラス)Xが必要です。そこで、クラス・プレーンに Class というクラスを設けましょう。
目論見としては、Object.class, String.class, Person.class(これらはオブジェクトです!)を、クラスClassのインスタンスと考えたいのです。しかし、is-instance-ofの定義には、「生みの親」が条件に入っていました。クラスオブジェクトが、実行時に「Classからnewで生成された」わけではありません。Object.class, String.class, Person.classは、実行時ではなくてコンパイル時に作り込んでおく必要があります(レイフィケーションはコンパイラの仕事です)。
is-created-byの解釈をゆるくする(createの意味を拡大解釈する)手もありますが、ここでは、混乱を避けるためにis-prepared-asという関係を導入しましょう。Object.class is-prepared-as Class などが成立している、とします。そして、is-instance-ofの定義を少し修正します。
- [ルールinst-1'] a is-created-by X または a is-prepared-as X ならば、a is-instance-of X である。
すべてを一階に押し込めてみる
立体的な図を描くのは面倒なので、プレーンを直線、クラスやオブジェクトを点で描くことにします。その描き方で図示すると:
ん? クラス・プレーンに新設されたクラスClassに対するレイフィケーション・イメージ(レイファイの結果)が欠けていますね。そう、Class.classというクラスオブジェクトも追加しないとね。これで、一階のオブジェクト・プレーンも随分にぎやかになりました。
- 普通のインスタンスオブジェクト:s1, s2, p
- クラスオブジェクト:Object.class, String.class, Person.class, Class.class
二階のクラス・プレーンの代理が一階に揃っているので、本来はクラス・プレーン内の関係である継承関係(is-derived-from)や、オブジェクト・プレーンとクラス・プレーンを結ぶ関係である所属関係(is-instace-of)も、オブジェクト・プレーン内に描くことができるようになります(下図; is-instace-ofは、直接の所属関係だけを描いた)。
図の矢印に、1から10の番号を付けておきました。次のJavaプログラムは、各番号に対応するアサーションを書き並べたものです。実行すると単にOKと表示されるので、アサーションの等式はすべて正しいことが確認できます。
/* ObjectsAndClasses.java */ class Person extends Object { public final String name; public Person(String name) { this.name = name; } } public class ObjectsAndClasses { public static void main(String[] args) { String s1 = new String("Hello"); String s2 = new String("Bye-bye"); Person p = new Person("tonkichi"); /* 1 */ assert (String.class).getSuperclass() == Object.class; /* 2 */ assert (Person.class).getSuperclass() == Object.class; /* 3 */ assert (Class.class).getSuperclass() == Object.class; /* 4 */ assert s1.getClass() == String.class; /* 5 */ assert s2.getClass() == String.class; /* 6 */ assert p.getClass() == Person.class; /* 7 */ assert (String.class).getClass() == Class.class; /* 8 */ assert (Person.class).getClass() == Class.class; /* 9 */ assert (Object.class).getClass() == Class.class; /*10 */ assert (Class.class).getClass() == Class.class; System.out.println("OK"); } }
もっと上の階の住人達
どうでしょう、納得できましたか? 僕は納得できないですね(オイオイ)。Class.classというクラスオブジェクトの扱いが不徹底で気持ち悪いんです。
クラスオブジェクトClass.classは、クラス・プレーンにあるクラスClassのレイフィケーション・イメージでした。が、Classというクラスがクラス・プレーンに在るのはおかしいのですよ。
僕たちは、「Ojbect, String, Personは“クラス”です」という言い方をしますよね。引用符でかこまれた“クラス”をCLASSと書くことにすると、この言明の意味は次のようなことでしょう。
- Object is-instance-of CLASS (Objectは“クラス”です)
- String is-instance-of CLASS (Stringは“クラス”です)
- Person is-instance-of CLASS (Personは“クラス”です)
既に指摘したように、is-instance-ofは、2枚のプレーンにまたがる“垂直な”関係です。そしてクラス達は、二階のクラス・プレーン上に住んでいます。となると、CLASSは三階のプレーン上の住人でなくてはなりません。
三階のプレーンはメタクラス・プレーンと呼びます。Ojbect, String, Personなどクラスが所属する分類カテゴリであるCLASSは、メタクラスと呼ばれるものなのです。
この図では、メタクラス・プレーンにはCLASSしかいませんが、クラスをBUILTIN_CLASS(組み込みクラス)とUSER_DEFINED_CLASS(ユーザー定義クラス)に分ければ、メタクラス・プレーン内に次のような関係が生じます。
そして、四階であるメタメタクラス・プレーンを設けて、そこにMETACLASSというメタ分類カテゴリーを置いてもいいかもしれません。
- CLASS is-instance-of METACLASS
- BUILTIN_CLASS is-instance-of METACLASS
- USER_DEFINED_CLASS is-instance-of METACLASS
メタタワーと繰り返しレイフィケーション
通常は、オブジェクト・プレーンとクラス・プレーンの二階建てで十分でしょう。しかし、プログラミング言語や応用によっては三階建て、四階建ての建物が必要になります。原理的には、「メタ、メタメタ、メタメタメタ、…」とより高いレベルの概念を作り出せるので、メタレベルを昇るタワーは高層建築になり得ます。
レイフィケーションも、二階から一階だけでなく、三階から二階、四階から三階へのレイフィケーションも考えられます。もちろん、レイフィケーションを繰り返してメタレベルを落としていくことができます。三階にいるメタクラスであるCLASSは、まず二階のClassへとレイファイされ、さらに一階のClass.classにレイファイされたのです。
Class.class is-instance-of Class という関係は、もともとは Class is-instance-of CLASS という二階と三階の関係をレイファイして、一階と二階の関係しにしたものです。その関係をさらに一階に(ちょっと無理やり)押し込めたので、(Class.class).getClass() == Class.class というループが生じたのです。
概念的なメタタワーの何階までがオブジェクト・プレーンにレイファイされているかにより、プログラムの実行時に自分自身の言語環境/実行環境をどこまで見られるか/触れるかが決まります。極端な話ですけど、メタタワーへの操作が十分に許されていれば、実行中にプログラミング言語の言語仕様や実行方式を変更しながら走るプログラムなんてのも実現可能です。
とととと、話がそれてきたのでこのへんにしましょう。オブジェクトとクラスの関係はそれほど単純ではないことを感じていただけたでしょうか。最後に Class.classですが、これは、Classというクラスの代理であるクラスオブジェクトですから、Classクラスオブジェクトと呼べますよね、さらに正確には、CLASSというメタクラスの代理であるメタクラスクラスClassのクラスオブジェクトですから、メタクラスクラスClassクラスオブジェクトです :-)