メソッドやクラスなどを実例で説明してきましたが、それらについてきちんとした説明をここでしておきましょう。このことは、プログラムをどう作るかということに密接に関わってきます。
プログラムは手順書きであることは第1章に説明をしましたが、単なる手順書きであるのなら、ともかく手順をずらずらと書くというのが基本のように思えるかもしれません。昔のBASIC言語はそうですし、今のVisual Basicもそうしたやり方がある意味では残っていると言えます。また、アプリケーションに組み込まれているマクロ言語は、一般にずらずらとプログラムを書きます。
そうやってプログラムを書いているうちに、だんだん長いプログラムをつくってしまうことになります。また、同じような処理がプログラムのあちこちに出てきて過剰に長くなったり、変更があったときのメンテナンス性も悪くなります。そこで、「サブルーチン」としてプログラムの一部分を別に独立させるということができるようになっていました。サブルーチンは、プログラムから呼び出されます。サブルーチンの処理で必要なデータを引数という形式で引き渡し、サブルーチンでの処理結果は戻り値として呼び出し元に伝えられるというのが一般的です。こうしたサブルーチンは、言語によっては関数やプロシージャなどで実現しています。
サブルーチンに分けることで、プログラムのメンテナンス性がよくなり、規模の大きなプログラムも作りやすくはなります。プログラムを読むときにはトップダウン式にサブルーチンは特定の機能の固まりとして見ればよいわけですし、逆に設計するにはボトムアップ的な手法も可能となったわけです。
さらにこうした考え方を発展させると、サブルーチンのような一連のプログラムを、あたかも1つのソフトウェアのパーツのように扱って、プログラムを次第に組み立てるという考え方に移っていきました。サブルーチンがあるのなら、メインルーチンがあるわけですが、サブルーチンもメインルーチンも、たとえばC言語のようにどちらも「関数」として同一のものとして定義するという方向になりました。メインかサブかは、命名規則で区別する場合もあるのですが、要はプログラマ自身が管理するようになったのです。
こうした中、プログラム作成において重要な考え方が生まれてきました。サブルーチンに分けるという考え方の背景には、そのプログラムの手続きそのものに重点があるという見方がありました。一方で、プログラムの本質は個々の手続きではなく、データがあって、そのデータを処理するということに集約されるという見方をしようということになります。まずデータがあって、それを処理するプログラムがあるというような見方になるでしょう。こうした考え方を総称して「オブジェクト指向」と呼んでいます。
つまり、それまではデータがあって、プログラムがあってと独立していたのですが、オブジェクト指向では、まずデータというものがあり、そのデータに処理プログラムが組み込まれているという見方をします。あるいは処理プログラムを組み込むわけです。言い方を変えれば、データそのものに機能が組み込まれているというような考え方をするのです。
データと一般的に言うと難しいのですが、オブジェクト指向では、いろいろな形式のデータを均一に扱えるようになっています。たとえば、人間というデータがあったとしましょう。人間には、名前、生年月日、身長、体重、出身校など、いろいろな属性があります。人間というオブジェクトには、こうした属性を持つという定義を行います。もちろん、人間だと完全にオブジェクトの性質を属性で記述しきることは不可能ですが、「色」というオブジェクトなら、一般には、Rの強さ、Gの強さ、Bの強さで属性は完全に記述できます。このような属性は「プロパティ」などと呼ばれます。
ここでは「人間」と「色」を例に出しましたが、その属性は違うものの、なにかしら属性を持ったひとかたまりのデータである点は「人間」も「色」も同じです。ちょっと強引な例かもしれませんが、みなさんがさっき会った人の特徴をささっとメモとして書いたとします。そのメモを1つのカードに書いたとしましょう。そして、街を歩いていたら変わった色使いのお店があったとします。今度は別のカードに書いたとしましょう。こうしたことをメモする場合、「人間」というものに対してどうだったのかを書き記し、「色使い」というものに対してどうだったのかを書き記します。いずれのメモ書きについても、内容や属性は違っても、同じカードとして扱えるということになります。プログラムでの「オブジェクト」はまさにこうした特徴を持ったものです。
さらに、オブジェクトにオブジェクトの処理機能を持たせておくということで、データと処理プログラムを密接に関連付けることになります。「人間」だったら「歩く」という処理プログラムがあるかもしれません。外部から「歩け!」というと、その人の歩くためのプログラムが呼び出されて実際に歩きます。「犬」に「歩け!」と命令すれば、その犬の歩くためのプログラムが動きだします。「歩くプログラム」というのは仮想的ではありますが、人間の歩き方と犬の歩き方は違います。こうした機能をオブジェクトごとに組み込むというのがオブジェクト指向の考え方というわけです。
では、こうしたオブジェクト指向のプログラムをどう作成するかという具体的な問題になります。「オブジェクト」としては、実在の人間や、あるいは犬や、実際に壁に塗られた「色」などを想像してください。これに対して、オブジェクトの設計図というものを定義します。人間だったらどういう属性を持っているか、色だったらRGBの各色がどのくらいの強さかを記録すればよいというようなことを、設計図として定義します。それを「クラス」と呼びます。
それでは、実際にJavaという言語の規則にのっとって、プログラムをオブジェクト指向的に考えるとどうなるかということを具体的に説明しながら、考えましょう。この章の最後で、規則はまとめておきますので、ここでは物語風に話を進めることにします。
ここでは、個人個人のデータを管理するということを考えます。つまり、一人一人が1つのデータであるとして、プログラムを作っていきます。人間にはいろいろな属性があることはすでに説明しましたが、その中のプログラムで知りたい属性を管理するためのクラスを最終的に定義しますが、まず最初に、どの属性に注目するかを考えます。もちろん、たくさんあってもいいのですが、「名前」と「年齢」に注目することにします。名前はもちろん文字列ですし、年齢は正の数でせいぜい150以内の整数です。であれば、String型で名前を管理し、shortあるいはint型で年齢を管理すればいいことになります。それぞれ、属性の名前として「firstName」「familyName」「age」という単語を割り当てることにしましょう。これは任意ですが、名前がa、年齢がbというよりも、具体的にfamilyNameやageとした方がイメージしやすいことは理解していただけるかと思います。
これらの属性を覚えるための変数を定義するので、まず、次のような変数定義が必要になります。プログラムの中のどこに書くのかということは後で説明します。まずは名前と年齢を記録するためにはこうした変数定義がどこかになければならないということになります。
String firstName; String familyName; int age;
ここでの例では3つの変数だけですが、もちろん、用途によってはたくさんの変数が必要になります。たとえば、戸籍なら「生年月日」も必要になるかもしれません。そうした必要な属性は用途によってかわるものです。
以上で必要な属性がそろったとして、この属性の集まりを1つのまとまったデータとして定義したいと考えます。つまり、これらの属性は常にセットになって使われるからです。こうした属性の集まりが「クラス」の基本的な考え方になります。クラスは必ず名前をつけて区別をします。名前は「Personality」にしましょう。Javaでは、これらの属性を持ったクラスを次のように定義します。
class Personality{ String firstName; String familyName; int age; }
Personalityクラスでは名をfirstName、姓をfamilyName、年齢をageという変数で管理をします。このようなクラスの属性として記録される変数のことを「インスタンス変数」と呼びます。「インスタンス」の意味は改めて説明します。
さらに、ここでは姓と名を分けていますが、リスト表示などでは、姓名を半角スペースでつなげた文字列が欲しいかもしれません。そうした、このデータに対して後からこんなことがやりたいということをあらかじめ想定することはよくあります。もちろん、プログラムを作りながら要求が出てくることもありますが、いずれにしても、この「個人データ」に対する処理が欲しくなります。
では、「姓名がつながった文字列を求める」機能を組み込みましょう。こうしたクラスに組み込む機能のことを「メソッド」と呼んでいます。メソッドも名前で区別する必要があるので、名前をまずは決めます。姓名の文字列を得るメソッドは「getFullName」という名前にしましょう。このメソッドは、値をもらい、結果を値として返すことは可能です。実行前にもらう値を「引数」と呼び、複数指定することも可能です。返される結果は1つだけです。返される値は「返り値」や「戻り値」と呼ばれます。なお、クラスのインスタンス変数についてはメソッドの中で随時使えます。getFullNameの仕様としては次のようなものを想定します。姓名を求めるための情報はすべてインスタンス変数にあるので、引数は必要なくなります。
メソッド名 | getFullName |
---|---|
返り値 | 姓と名を半角スペースでつなげた文字列 |
引数 | なし |
このメソッドをPersonalityクラスに組み込みます。次のようになります。
class Personality{ String firstName; String familyName; int age; String getFullName() { String fullName = firstName + " " + familyName; return fullName; } }
メソッドは、返り値の型、メソッド名、()でくくって引数リスト、{}でくくって実際のプログラムとなります。もし、返り値がない場合は型の代わりにvoidと書きます。引数がない場合でも()だけは書いておく必要があります。
メソッドの中で、「値を返す」という処理はメソッドの最後の行のように、「return 値;」と記述します。メソッドの中でreturnがあると、そのプログラムのメソッドはそこで終了します。その後にプログラムを書いても実際には実行されません。
ここでString型の変数fullNameを定義しました。もちろん、姓名をつなげた文字列を求めて一気に代入までしていますが、このfullNameという変数がメソッドの中で定義されていることに注目してください。こうした変数は何かのタイミングでgetFullNameというメソッドを稼働させたその瞬間にだけ存在し、メソッドを終了するとあとかたもなく消えてしまいます。こうした変数を「ローカル変数」などと呼ぶこともありますが、Javaの世界では単に「変数」とだけの記載で参照されます。
一方、firstNameなど、メソッドの外で定義された変数は前にも説明したようにインスタンス変数と呼ばれます。この変数は実際にプログラムが動いている間のある程度の期間は存在し続けます。つまり、getFullNameを呼び出す前にすでにインスタンス変数firstNameは存在しているのです。そして、変数名だけで変数の値を参照できるというわけです。変数fullNameとその他の変数は位置づけが違うのだということをまず理解してください。しかしながら、プログラムの中では同等のように記述できます。
classの定義をここまで説明してきましたが、最初に説明したように、このclassによる定義内容は、ソフトウエアの設計図です。この設計図をもとにして、実際に動くものを作るということが必要になります。設計図としてのクラスから、実際にデータを記憶しプログラムした通りに処理を行う「オブジェクト」を生成します。生成したものはデータの中身を実際に持つことから、インスタンスと呼ばれます。このときに使うのは、newというキーワードです。一般的には、次のように記述しますが、この式によって得られた結果は、クラス名と同じ名前の型名の変数に代入することができます。
new クラス名(引数)
引数を指定できますが、ここでは、まず引数がない場合を説明します。すべてのクラスが引数なしでインスタンス化できるわけではありませんが、少なくともここで説明しているPersonalityクラスは可能ですので、まずはその方法で説明しましょう。Personalityクラスの定義がどこかにあるとして、たとえば、次のようなプログラムを実行することで、インスタンスを得ることができます。
Personality person1 = new Personality();
これで、いわば、一組の姓、名、年齢を記憶する変数person1を確保し、実際に記憶ができるようになったといえるでしょう。つまり、一人分の情報を記憶できるオブジェクトが、プログラムの世界に登場したわけです。一般にはこのようなインスタンス化を経てクラスがオブジェクトとして利用できるようになります。しかしながら、後で説明するように、こうした記述をしなくてもいいという場合もあり一概にはいえませんが、ここではnewで1つのインスタンスが生成されることをまずは理解してください。
ところで、ここでのperson1という変数の中身は何でしょうか? コンピュータの内部では何らかのデータではあるのですが、プログラムを作るときには、こうした変数は「参照している」ものとして抽象的に理解してください。インスタンス化されたオブジェクトがあるということも抽象的に理解する必要がありますが、さらにそのインスタンスを参照しているという抽象的な変数があるということです。これらは実際にどんな数値なのかといったことなどはあまり気にしないで、特徴だけを理解することを心がけましょう。
インスタンスから、その中のインスタンス変数を利用するには、ドット(.)を利用します。一般には次のように記述します。この結果は、インスタンス変数の型と同じ型の値を持ちます。つまり、インスタンス変数の参照ということです。
インスタンスへの参照.インスタンス変数
また、クラスのメソッドを利用するのも同じように、ドット(.)を使います。この場合、メソッドを呼び出すということになります。
インスタンスへの参照.メソッド名(引数)
たとえば、前に定義したクラス、Personalityのインスタンスを生成して、そのインスタンス変数に値を代入したり取り出したりということをするには、次のようなプログラムになるでしょう。このプログラムをどこに書くのかという具体例は後で説明します。クラスPersonalityに対してこのような処理が可能だという例です。
Personality person1 = new Personality(); person1.firstName = "Masayuki"; person1.familyName = "Nii"; person1.age = 43; System.out.println( "フルネームは" + person1.getFullName() );
図を見ながら改めてクラスとインスタンスの関係をまとめておきましょう。クラスはプログラムとして書かれますが、実際にデータを記録したり処理をしたりするというよりも、どんな振る舞いをさせるかという設計図です。設計図をもとに、newキーワードで「インスタンス化」することによって、インスタンス変数にデータを記録できるようになります。このインスタンス化したものが「実際に動くプログラム」でもあり、「実際に記録可能なデータ群」でもあります。
インスタンス変数やメソッドは必ずインスタンスへの参照とセットになっていないといけないかというとそうではありません。この場合「変数person1」はインスタンスが確定しているから書けるのですが、クラスの内部では実際に参照がどんな変数に入れられるかはわかりません。そこで、「自分自身のインスタンス」を参照する「this」というキーワードを利用することができます。これはクラスの内部で、インスタンス化された自分自身を意味します。このthisを用いて、Personalityクラスを改めて記述すると次のようになります。
class Personality{ String firstName; String familyName; int age; String getFullName() { String fullName = this.firstName + " " + this.familyName; return fullName; } }
しかしながら、このthisは省略可能であり、書かれないことが多くなっています。thisを省略すると以前の通りのプログラムになることも確認してください。
Javaでは、ある変数名が出てきたとき、その変数がローカル変数として定義されているのならローカル変数を利用し、ローカル変数として定義されていなければそれがインスタンス変数として定義されているかを調べます。そして、インスタンス変数として定義されているのなら、それを使い、それでも指定した名前の変数が見つからない場合は何らかのエラーだと判断します。
newでクラスからインスタンスが生成されることを説明しましたが、クラスの中には「インスタンスを作るときに呼び出されるメソッド」を定義することができます。そうしたクラスをコンストラクタと呼んで、特殊なメソッドとして区別をしています。これまでPersonalityクラスにはコンストラクタに相当するものは一見するとないように思えるかもしれませんが、実は暗黙に存在していました。この件は後でまとめるとして、まずはコンストラクタの作り方です。通常は、返り値のないメソッドで、クラス名と同一名のメソッドであるということになります。引数はあってもなくてもかまいません。また、コンストラクタの中では明示的にreturnを書くことはなく、自動的に生成したインスタンスを返すと考えておいてください。
よくあるのは、値の初期設定を含めたコンストラクタを定義するということです。今までの方法だと、いちいち、インスタンス変数を参照していましたが、初期設定機能をクラスの方に持たせることで、プログラムが見通しよくなるかもしれません。なお、コンストラクタというと「作る人」という意味合いになってしまいますが、インスタンスそのものはコンストラクタのプログラムを実行する段階ですでに作られています。つまり、コンストラクタが呼び出された段階ではすでにインスタンス変数は利用可能な状態になっています。
class Personality{ //インスタンス変数の定義 String firstName; String familyName; int age; //以下のメソッドが「コンストラクタ」 Personality( String initFirstName, String initFamilyName, int initAge ) { this.firstName = initFirstName; this.familyName = initFamilyName; this.age = initAge; } //通常のメソッド String getFullName() { String fullName = this.firstName + " " + this.familyName; return fullName; } }
ここで付け加えたコンストラクタを呼び出すには、newで生成するとき、コンストラクタの引数と同じパターンの引数をつけて呼び出します。このコンストラクタの引数は、順にString、String、intです。このような引数のパターンで呼び出されると、このコンストラクタが呼び出されます。たとえば、次のようなプログラムで、Personalityクラスの新たに定義したコンストラクタが呼び出されて、インスタンス化されるので、すでにインスタンス変数にデータが入った状態でインスタンスが使えます。
Personality person1 = new Personality("Masayuki", "Nii", 43); System.out.println( person1.getFullName() + "さんの年齢は" + person1.age );
では、このコンストラクタが定義されていない場合はどうなるかというと、これはJavaの規則で規定されています。基本的にすべてのクラスは、引数のないコンストラクタは書かなくても存在することになっています。ですので、単に引数のないコンストラクタ「Personality() { }」を定義する必要はないのです。
しかしながら、クラスの中に1つでもコンストラクタが定義されると、引数のないコンストラクタ「Personality() { }」の暗黙の定義はなくなってしまいます。そのため、何も記述することのないコンストラクタでも使うのであれば、記述しないといけなくなってしまいます。
コンストラクタの中で、インスタンス変数をすでに利用しています。コンストラクタは、インスタンス化のためにあるともいえるのですが、より正確にいえば、インスタンス生成を行った後にコンストラクタは呼び出されます。そのため、その中ですでに処理を書き始めることができるのです。
コンストラクタ定義の引数として、「initFirstName」という書き方をしています。ここでは、コンストラクタの中で使う変数で、つまりは呼び出されたときに、呼び出す側で指定した引数が、コンストラクタの中で変数「initFirstName」として参照できるものです。文法的には、aでもbでもいいのですが、何のデータが入ってくるかが分かりやすいように、英文的に変数名を記述しています。さしずめ、initial first nameなのですが、変数名に空白が使えないため空白を詰めるものの単語の区切りが分かりやすいように、単語の頭は大文字にします。そして、「変数名は小文字で始める」というJavaの世界でのしきたりがあるため、initFirstNameというふうに記載しています。
ここで改めて、メソッドを呼び出すということを考えてみます。引数と戻り値を両方とも持っているメソッドのgetDecoratedNameを以下のように新たに定義してみました。呼び出し元と、呼び出された側で、引数を通じてデータを送っていいます。引数が、呼び出した先では変数にセットして利用できるようになります。さらにメソッドの処理が終わると、戻り値が呼び出し元で得られます。こうした流れをよく整理してみてください。
class Personality{ //インスタンス変数の定義 String firstName; String familyName; int age; //以下のメソッドが「コンストラクタ」 Personality( String initFirstName, String initFamilyName, int initAge ) { this.firstName = initFirstName; this.familyName = initFamilyName; this.age = initAge; } //通常のメソッド String getFullName() { String fullName = this.firstName + " " + this.familyName; return fullName; } //前後や中央になにか文字列を付け加えた名前を戻すメソッド String getDecoratedName( String prefix, String middle, String suffix ) { String decoName = prefix + this.firstName + middle + this.familyName + suffix; return decoName; } }
メソッド名 | getDecoratedName | |
---|---|---|
返り値 | 名と姓に加えて、引数の文字列を付加した文字列 | |
引数 | prefix | 名の前に付ける文字列 |
middle | 名と姓の間に付ける文字列 | |
suffix | 姓の後に付ける文字列 |
次のように、Personalityクラスをインスタンス化し、そのインスタンスに対してgetDecoratedNameを呼び出しました。
Personality person1 = new Personality("Macedonia", "Alexander", 31); System.out.println( person1.getDecoratedName( "King of ", " ", " the Great" ) );
1行目のnew Personality(...)の部分でコンストラクタを呼び出しています。このとき、"Macedonia"という文字列が変数initFirstNameにセットされ、"Alexander"という文字列が変数initFamilyNameにセットされ、31という数字が変数initAgeにセットされて、Personalityクラスのコンストラクタ(つまり、Personality(...) {...}の部分)が実行されます。つまり、インスタンス変数のfirstName、familyName、ageがそれぞれ、"Macedonia", "Alexander", 31という値になるということです。
次のgetDecoratedNameメソッドの呼び出しで、"King of "という文字列が変数prefix、" "という文字列が変数middle、" the Great"という文字列が変数suffixにセットされ、getDecoratedNameメソッドの中の処理に移ります。prefix、firstName、middle、familyName、suffixがそれぞれ順番に結合された文字列"King of Macedonia Alexander the Great"が変数decoNameに代入され、その値を戻します。そして、元のプログラムでは、System.out.printlnということで、戻された文字列を標準出力に書き出すということです。
これまでは、インスタンス化をしてクラスを実際に使えるオブジェクトにしていましたが、特殊なクラスあるいはメソッド、インスタンスの存在形態として、staticというキーワードをつけて定義したものがあります。staticなクラスは、1つしかインスタンスは存在しないクラスを管理する方法です。1つしかないということで決められているので、プログラムの中で明示的にインスタンス化しなくても利用できます。そして、クラス名そのもので、1つだけのインスタンスは参照できます。クラスそのものがstaticな動作だけでいい場合は、コンストラクタは何の意味も持ちませんが、staticクラスのインスタンスを生成する時に行う処理を記述するための方法もJavaでは用意されています。
staticなクラスは1つしかインスタンスを持たないかというと、一概にいえないのが説明の難しいところです。この「1つしか」というのは、「クラス名で参照できる1つしかない特別なインスタンスを確保できる」と理解してください。
staticなクラスだけでなく、staticなメソッドやstaticなインスタンス変数という定義もできます。たとえば、staticとは特に明示しないクラスにおいても、staticなメソッドは定義できます。このstaticなメソッドは、インスタンス化しなくても利用できるのです。
staticなクラスの例としては、「System」です。Systemという名前のクラスがJavaのシステムで定義されているので、Javaの世界では、プログラマはいきなり「System」と書くことができます。そして、Systemクラスのインスタンス変数であるoutは、「標準出力先」を参照しているのです。つまり、いきなり「System.out」と書くことで、アプリケーションの標準出力を利用できます。このoutで参照されるインスタンスのクラス(正確には、java.io.PrintStreamクラス)では、printlnというメソッドが利用でき、引数の文字列を標準出力に書き出します。ここまで、とにかく「System.out.println」と書いてくださいと言っていたこの記述にはこうした意味があります。
ここでやっとアプリケーションの成り立ちが説明できるようになりました。javaでは、ある種のコマンドを使って、特定のクラスに対して実行を要求するということを行います。プログラムの形式を問わず、この規則は有効なのですが、Windowsから実行の指令を出すと通常はアプリケーションとして稼働しようとします。このとき、Javaの規則として、指定したクラスのstaticなmainという名前のメソッドを呼び出すという動作を行います。
ここで、呼び出したクラスのインスタンス化は行っていません。そのため、ちょっと理解しづらいプログラムの書き方をしなければなりません。たとえば、Personalityクラスにmainメソッドを追加して、アプリケーションとして稼働させるには、次のように記述します。mainメソッドの引数や返り値についても規定がなされていて、定義はその通りに書かないといけません。
class Personality{ //インスタンス変数 String firstName; String familyName; int age; //コンストラクタ Personality( String initFirstName, String initFamilyName, int initAge ) { this.firstName = initFirstName; this.familyName = initFamilyName; this.age = initAge; } //メソッド String getFullName() { String fullName = this.firstName + " " + this.familyName; return fullName; } //staticなメソッドで、自分自身をインスタンス化している public static void main( String arg[] ) { Personality person1 = new Personality("Masayuki", "Nii", 43); System.out.println( person1.getFullName() + "さんの年齢は" + person1.age ); } }
この最後のmainメソッドのプログラムは理解しづらいと思います。Personalityクラスの中で、なぜPersonalityのインスタンスを生成しないといけないのかということは理解しづらいでしょう。ここでは、mainは特別な存在だと思ってください。ここが、Javaのアプリケーションとオペレーティングシステムの架け橋になるのです。最初に呼ばれたmainメソッドだけ、ある意味、妙な記述をしないといけないということです。この気持ち悪さを解消するには、アプリケーションの入り口クラスを作り、それとは別に注目しているデータを管理するクラスを定義するとよいでしょう(後述のコラムを参照)。mainメソッドは、String型の配列を引数に定義する必要があります。この変数argには、アプリケーション起動時に指定したパラメータを渡すことができますが、使わないことも多く、その場合でも定義だけは必要になります。
ここまで作ってきたPersonalityクラスを実際に動かしてみます。単にオブジェクトを作るだけなので、今までと同様見栄えがしませんが、基礎的なところなのでそのあたりは納得してください。次のように作業をします。
1「ファイル」メニューから「新規プロジェクト」を選択して、新しいプロジェクトを作ります。ここではプロジェクト名を「ClassStudy」としています。
2プロジェクトを作った後、「ファイル」メニューから「新規クラス」を選択します。クラスウィザードが表示されます。ここで、パッケージ名はそのままにし、クラス名にこれから作るクラスの名前である「Personality」を入力します。ベースクラスは「java.lang.Object」になっているかを確認してください。そうなっていない場合は正確にキータイプします。オプションのチェックボックスはすべてオフにしてください。そして、OKボタンをクリックします。
3Personality.javaが1つだけ含まれているプロジェクトができました。Personality.javaはほんの少しだけです。(図のようになっていない場合は、このように修正してください。)
4Personality.javaに次のようなプログラムを入力します。mainメソッドの1行目、new Personality(...)の部分は自分の名前と年齢にしましょう(年齢は嘘でもいいです)。
package classstudy; class Personality { String firstName; String familyName; int age; Personality( String initFirstName, String initFamilyName, int initAge ) { this.firstName = initFirstName; this.familyName = initFamilyName; this.age = initAge; } String getFullName() { String fullName = this.firstName + " " + this.familyName; return fullName; } public static void main( String arg[] ) { Personality person1 = new Personality("Masayuki", "Nii", 43); System.out.println( person1.getFullName() + "さんの年齢は" + person1.age ); } }
5実行するためには、左側のプロジェクトの内容を表示しているところで、「Personality.java」を右ボタンでクリックして、そして、「デフォルト実行時設定を使用して実行」を選択します。
6実行しましたが、特に何もウインドウなどは表示されません。メッセージペインにフルネームが表示されているかを確認し、表示されていれば実行されたことになります。
この状態では、F9キーでの実行ができません。F9キーで実行をするには次のように、アプリケーションとして実行をさせることをプロジェクトに設定を追加します。これはJavaというよりもJBuilderの機能です。以下の設定をしておくと、その後はF9キーでの実行ができるようになるので便利です。
1F9キーを押して実行しようとすると、次のような「プロジェクトプロパティ」というダイアログボックスが表示されます。(「プロジェクト」メニューの「プロジェクトプロパティ」を選択するのが本来の呼び出し方法です。)「実行」のタブが選択されていることを確認して、「新規」ボタンをクリックします。
2「実行」「アプリケーション」のタブが選択されていることを確認して、メインクラスの右の「…」ボタンをクリックします。
3ダイアログボックスで、mainメソッドが存在するクラスを指定します。ここでは、これまでに作ったPersonalityクラスを指定しますが、classstudyというところに分類されています。
4メインクラスに「classstudy.Personality」が指定されていることを確認して、OKボタンをクリックします。
5新たに設定が1行加わることを確認します。「デフォルト」のチェックを入れてOKボタンをクリックして、ダイアログボックスを閉じます。
publicとprivateというキーワードが時々出てきます。これらは、変数やメソッド、クラスそのものをどの範囲から利用可能にするのかということを定義するキーワードです。この2つ以外に「なにもなし」という記述も可能です。おおむね、広く利用できる順からpublic→なにもなし→privateとなります。publicだと、他のクラスから利用できますが、privateにすると同じクラスからしか利用できなくなります。つまり、変数やメソッドの存在を隠すことができます。これらのキーワードを何も指定しないと同じファイルからのアクセスが可能となるのですが、公開か隠蔽かということがあいまいになります。このテキストのサンプルプログラムの範囲ではこうした設定はあまり意味はありませんが、Javaのライブラリのようにたくさんの機能を提供する場合、クラスの中身を全部見せるのではなく必要な変数とメソッドだけにして余計な情報を見せないようにしたり、あるいは想定しないことを利用者がすることを避けるために公開する範囲は詳細に定義したくなります。そのため、普通はprivateかpublicかを考えながらどちらかをつけて定義するのが一般的です。
Javaの作法では、1つのクラスは1つのファイルで定義をします。文法上は、publicなクラスは1つのファイルには1つしか定義できないのですが、そうではないクラスは1つのファイルに複数異なるものを定義できます。ただ、そうするとどこに何があるかかなり分かりづらくなるので、ファイルごとにクラスを分ける方法は一般に広く使われています。このとき、ファイル名とクラス名は一致しなければなりません。そうでないとコンパイルエラーが出ます。たとえば、クラス名が「Personality」なら、それが定義されているソースファイル名は「Personality.java」でなくてはならないのです。
ここでのPersonalityクラスは、アプリケーションとしての入り口であるmainメソッドも入れていますが、こうした記述はあまり気持ちのいいものではありません。Personalityクラスからmainメソッド削除して、「個人のデータを管理する」という機能だけに絞り込みたいと考えるでしょう。そうしたい場合、クラスごとにファイルを分離し、次のようにクラスごとにファイルを作ると良いでしょう。そして、Starterクラスを実行するように指定を行います。
//以下のプログラムはファイル名「Personality.java」で作成する package classstudy; class Personality { String firstName; String familyName; int age; Personality( String initFirstName, String initFamilyName, int initAge ) { this.firstName = initFirstName; this.familyName = initFamilyName; this.age = initAge; } String getFullName() { String fullName = this.firstName + " " + this.familyName; return fullName; } }
//以下のプログラムはファイル名「Starter.java」で作成する package classstudy; class Starter { public static void main( String arg[] ) { Personality person1 = new Personality("Masayuki", "Nii", 43); System.out.println( person1.getFullName() + "さんの年齢は" + person1.age ); } }
次に、「複数の人間の情報を管理する」ということを行っています。たとえば、3人の人がいれば、それぞれ普通は名前も年齢も違うので、それぞれ別々の記憶管理するものが必要です。つまり、3人の人間を管理するには、3つのインスタンスが必要です。3つのインスタンスを作るには、たとえば次のようなプログラムになります。
Personality person1 = new Personality(); Personality person2 = new Personality(); Personality person3 = new Personality();
これにより、それぞれ別々のPersonalityクラスの3つのインスタンスが生成されます。=の右側はいつも同じです。同じ「new Personality()」ですが、このプログラムによって3回実行され、Personalityクラスのインスタンスは3つ作成されます。
3つ作成されたインスタンスを単にそのまま作ったままにすると、プログラムの中で利用できません。そこで、Personality型の変数person1、person2、person3を作って、その変数に、それぞれのインスタンスへの参照を記録しておくということです。
ここで、クラスにインスタンス変数を定義したことを記憶してください。ここでは、person1が参照するfirstNameとperson2が参照するfirstNameは、同じ「firstName」という名前でも、異なる文字列記憶変数になります。たとえば、上記のようにインスタンスを生成し、さらにインスタンス変数に値を入れるとすると、次のようになります。
person1.firstName = "Masayuki"; person1.famlyName = "Nii"; person2.firstName = "George"; person2.famlyName = "Washington"; person3.firstName = "Abraham"; person3.famlyName = "Lincoln";
ここまでに作ったPersonality.javaのプログラムを以下のように修正して、Personalityクラスのインスタンスを3つ作ってみます。そして、それぞれのインスタンスから、同じfamilyNameのインスタンス変数を標準出力に出力します。同じインスタンス変数ではありますが、参照するインスタンスが違っていると変数の値が違っていることを確認しましょう。なお、ここでは、引数のないコンストラクタと、引数のあるコンストラクタの両方を使いますので、引数のないコンストラクタ「Personality( )」も明示的にプログラムの中で作る必要があります。プログラムとして記述する内容がなくても、「Personality( )」は必要になります。
package classstudy; class Personality { String firstName; String familyName; int age; Personality( ){ } Personality( String initFirstName, String initFamilyName, int initAge ) { this.firstName = initFirstName; this.familyName = initFamilyName; this.age = initAge; } String getFullName() { String fullName = this.firstName + " " + this.familyName; return fullName; } public static void main( String arg[] ) { Personality person1 = new Personality("Masayuki", "Nii", 43); System.out.println( person1.getFullName() + "さんの年齢は" + person1.age ); Personality person2 = new Personality("Jiro", "Motohashi", 4); Personality person3 = new Personality(); person3.familyName = "Sawada"; person3.firstName = "Taro"; System.out.println( "person1のfamilyName:::" + person1.familyName ); System.out.println( "person2のfamilyName:::" + person2.familyName ); System.out.println( "person3のfamilyName:::" + person3.familyName ); } }
ここまでプログラムを入力すると、JBuilderの自動入力機能が効いていることが確認できると思います。ここでは自動的に定義したインスタンス変数が選択入力できるようになっていることも確認できます。このように、Javaの文法に従った定義はJBuilder側にも取り込まれるので、こうした機能をうまく使うと、キータイプをミスする確率も低くなるでしょう。
オブジェクト指向の大きな特徴として、既存のクラスから新たなクラスを作ることができるということです。こうした特徴を「継承」と呼んでいます。継承のやり方もいろいろあります。まず、1つは既存の完結したクラスから新しいクラスを定義するということです。このとき、既存のクラスの特徴はすべて引き継ぐので、新しいクラスでは、すべてを新たに定義する必要はありません。
まず、以下のようなPersonalityクラスがあるとします。人間一人を管理するわけですが、フルネームを戻すメソッドが定義されています。
class Personality{ String firstName; String familyName; int age; Personality( String initFirstName, String initFamilyName, int initAge ) { this.firstName = initFirstName; this.familyName = initFamilyName; this.age = initAge; } String getFullName() { String fullName = this.firstName + " " + this.familyName; return fullName; } }
アメリカ人と日本人では、姓名の順序が違います。そこで、日本人のデータを管理するためのクラスJapanesePersonalityと、アメリカ人のデータを管理するためのAmericanPersonalityというクラスを定義したいとします。日本人の名前は漢字およびカナで記録するとし、アメリカ人の名前はアルファベットで記録するとしましょう。次のように、extendsというキーワードを使って定義をします。
class JapanesePersonality extends Personality { } class AmericanPersonality extends Personality { }
クラスの中身は何もありませんが、インスタンス変数としてfirstName、familyName、ageは定義されているものとして扱えます。つまり、Personalityに存在しているインスタンス変数は、改めて書かなくてもPersonalityクラスを継承しているAmericanPersonalityでは利用できるのです。
続いて、getFullNameメソッドについて考えますが、アメリカ人の名前については、問題なくフルネームを返すといえるかと思います。しかし、日本人は姓名は逆にして、空白も全角の空白がいいように思います。となると、次のように、元になっているクラスにあるものと同じ名前のメソッドを定義します。
class JapanesePersonality extends Personality { String getFullName() { String fullName = this.familyName + " " + this.firstName; return fullName; } } class AmericanPersonality extends Personality { }
このように定義すれば、AmericanPersonalityクラスでgetFullNameメソッドを呼び出すと、PersonalityクラスのgetFullNameメソッドが使われます。一方、JapanesePersonalityクラスでgetFullNameメソッドを呼び出すと、JapanesePersonalityクラスで定義したgetFullNameメソッドが呼び出されます。このように、元となっているクラスに定義されたメソッドを上書きして置き換えることができる性質を「オーバーライド」と呼びます。
ここで、英語で名前を入力するとしてAmericanPersonalityクラスだと、イニシャルを取得できそうだということが考えられます。AmericanPersonalityクラスにそのメソッドを追加しましょう。
class JapanesePersonality extends Personality { String getFullName() { String fullName = this.familyName + " " + this.firstName; return fullName; } } class AmericanPersonality extends Personality { String getInitial() { String initial = this.firstName.substring(0, 1) + "." + this.familyName.substring(0, 1); return initial; } }
getInitialメソッドは、AmericanPersonalityクラスで利用可能です。しかしながら、JapanesePersonalityクラスやPersonalityクラスでは利用できません。
継承して定義したクラスを使ったサンプルを紹介しましょう。ここからも、今まで同様に、これまで作っているPersonalityクラスを以下のように修正して実際に稼働するプログラムにします。
package classstudy; class Personality { String firstName; String familyName; int age; Personality( ){ } Personality( String initFirstName, String initFamilyName, int initAge ) { this.firstName = initFirstName; this.familyName = initFamilyName; this.age = initAge; } String getFullName() { String fullName = this.firstName + " " + this.familyName; return fullName; } public static void main( String arg[] ) { Personality person1 = new Personality("Masayuki", "Nii", 43); System.out.println( person1.getFullName() + "さんの年齢は" + person1.age ); Personality person2 = new Personality("Jiro", "Motohashi", 4); Personality person3 = new Personality(); person3.familyName = "Sawada"; person3.firstName = "Taro"; System.out.println( "person1のfamilyName:::" + person1.familyName ); System.out.println( "person2のfamilyName:::" + person2.familyName ); System.out.println( "person3のfamilyName:::" + person3.familyName ); JapanesePersonality jperson1 = new JapanesePersonality("園田","健二",20); AmericanPersonality aperson1 = new AmericanPersonality("John","Dow",22); System.out.println( "jperson1のgetFullName:::" + jperson1.getFullName() ); // System.out.println( "jperson1のgetInitial:::" + jperson1.getInitial() ); System.out.println( "aperson1のgetFullName:::" + aperson1.getFullName() ); System.out.println( "aperson1のgetInitial:::" + aperson1.getInitial() ); } } class JapanesePersonality extends Personality{ JapanesePersonality(String initFamilyName, String initFirstName, int initAge) { super(initFirstName, initFamilyName, initAge); } String getFullName() { String fullName = this.familyName + " " + this.firstName; return fullName; } } class AmericanPersonality extends Personality{ AmericanPersonality(String initFirstName, String initFamilyName, int initAge) { super(initFirstName, initFamilyName, initAge); } String getInitial(){ String initialString = this.firstName.substring(0,1) + "." + this.familyName.substring(0,1); return initialString; } }
Personalityクラスは、前の実行例のときのまま変更はありません。同じPersonality.javaというファイルの中に、新たにAmericanPersonalityとJapanesePersonalityの2つのクラスを定義しました。なお、ここでは手順を単純にするため、同じファイルに3つのクラスを定義しましたが、通常、Javaの世界では、クラスごとにファイルを別々にするのが作法となっています。
前に説明したように、継承したクラスでは、インスタンス変数は引き継がれますし、メソッドも引き継がれるので、「変更したいメソッド」だけを再定義すればいい訳です。ただし、コンストラクタについては引き継がれません。姓と名と年齢を与えてインスタンスを作るコンストラクタは、AmericanPersonalityとJapanesePersonalityの両方で、改めて定義をする必要があるのです。ここで、これらのコンストラクタの中にあるsuperの意味ですが、これは継承している元になっているクラス(ここではPersonalityクラス)のコンストラクタを呼び出すことを意味します。つまり、「super(initFirstName, initFamilyName, initAge);」によって、Personalityクラスのコンストラクタ「Personality(String initFirstName, String initFamilyName, int initAge){ ... }」を呼び出します。継承元のクラスは1つしか指定できないので、superというキーワードで参照することで、どのコンストラクタは確定します。
なお、ここで、PersonalityとAmericanPersonalityの引数を3つ持つコンストラクタは、ファーストネーム、ファミリーネームの順に指定しましたが、JapaneseNameは日本的に逆順の姓名の順でコンストラクタの引数を指定できるようにしました。
プログラムでは、JapanesePersonalityクラスのインスタンスjperson1に対してgetInitial()メソッドを呼び出している部分をコメントにしていますが、このコメントをはずしてみてください。もちろん、エラーになります。JapanesePersonalityクラスにはgetInitial()メソッドが定義されていないので、実行前からエラーが出ており、正しく実行ができないことが分かります。
ここでは完成されたクラスを継承して新たなクラスを作りましたが、それ以外に2つの継承の方法があります。まず1つは、半完成品のクラスを継承する方法です。たとえばあるクラスで汎用的な機能は組み込まれていても、いくつかの機能を実際の場面に合わせて作らないといけないものもあるかもしれません。その場合、実際の場面に合わせてプログラムを組み込まないといけないメソッドを「abstruct」というキーワードで定義します。このキーワードがあれば、クラス内ではプログラムを書くことはできません。一方、継承したクラスでは必ず同じメソッドを定義してプログラムを書かないといけません。こうしたチェックがコンパイル時に行われます。ライブラリにあるクラスではこうしたabstructなメソッドを持ったものがあります。
もう1つの継承は、ある特定の名前のメソッドを必ず定義させるためのものです。忘れないように定義しておくべきメソッドがある場合、メソッドの仕様だけを記述したクラスを適用します。これもコンパイル時にエラーは発見されるので、忘れたらプログラム自体が動かなくなってしまいます。機能そのものとは直接関係ありませんが、ライブラリに定義した機能を正確に利用させるための継承の使い方もあります。
Javaでは、オブジェクト指向というやり方をはずしたプログラムは基本的に存在しません。不可能ではないものの、かえって難しくなります。プログラムの作成を見通しよくし、さらに大規模なプログラム作成も系統的にできるようにと考えられた手法がオブジェクト指向なのです。その雰囲気をぜひとも感じて、会得していただきたいと思います。
このコースで作っているような一連のプログラムはごくごく短いものですが、現実のソフト開発でははてしなく長いプログラムを書くのが一般的です。それだけ長いものになると、共通に利用できるようなプログラムも出てきます。そうしたプログラムを「サブルーチン」として、1回だけどこかで記述し、それを別のところで使い回すということはすでに説明したとおりです。
ただ、サブルーチンと実際のデータが分離している場合、そのサブルーチンを再利用するということにある種のハードルが存在します。自分がサブルーチンを作ってすぐに使う分には、サブルーチンの使い方を忘れることもなくスムーズかもしれません。しかし、1年前に作ったサブルーチンとなると、きっと忘れていることでしょう。もちろん、ソースプログラムがあればそれを解析して、使い方を納得するということも可能かもしれませんが、複雑な処理を組み込んでいる場合だとそれも大変そうです。ましてや他人が作ったサブルーチンだと余計に不明な点は多くなりますし、ソースプログラムは得られないこともあるかもしれません。こうしたことを補うために、ドキュメントをきちんと作ることは鉄則とされてはいるもの、サブルーチン群というのは再利用にはおのずと限度があるものです。
オブジェクト指向では、文字列ならStringというオブジェクトがあって、そのオブジェクトに処理プログラムであるメソッドが所属していました。その所属するメソッドを利用することで、文字列というデータの処理ができるようになっています。つまり、データと処理プログラムがより強い結びつきで存在しているというわけです。
サブルーチン群でプログラムを作るとなると、ターゲットに対するデータを処理するサブルーチンを集めたり、あるいは作ったりしながらプログラム作りを進めることになります。こうしたサブルーチンの管理までもプログラマが行わないといけません。
オブジェクト指向では、既存のソフトウェアを利用する方法で、特徴的なやり方を備えています。それは、既存のクラス定義を拡張して、新しいクラスを定義するというやり方なのです。オブジェクト指向プログラミングの世界では、この機能のことを「継承」とも呼んでいます。この継承は、サンプルの中ですでに何度も出てきています。拡張という言葉遣いからピンとくるかもしれませんが、クラス定義に記述したextendsという記述が、まさに、既存のクラスを拡張して新しいクラスを作成するということを意味しています。つまり、Appletというクラスを拡張して、ここまでに説明してきたサンプルのアプレットを作っています。
この拡張というやり方により、拡張前の元のクラス(スーパークラスや親クラスと呼ばれる)の機能をそのままに、新しいクラスを定義できます。平たく言えば、元のクラスのインスタンス変数やメソッドが、そっくりそのまま新しいクラスで利用できるのです。つまり、新しいクラスは、元のクラスに存在しない機能だけを組み込めばよいということにもつながります。
こうしたしくみがあるので、たとえば独自にボタンを作ろうとした場合、1からボタンを作るのではなく、既存のButtonを拡張して独自のボタンクラスを作るということができるようになります。ボタンのように、グレーで盛り上がったように表示したり、ラベルを表示したり、クリックを受け付けるというような機能は、Buttonを拡張して作ったクラスでは、そっくりそのまま使えます。自分自身で定義したい機能だけ付け加えればよいのです。これより後の章のサンプルで実例を示しましょう。
8-1:AmericanPersonalityクラスのイニシャルを求めるメソッドは、現在は「M.N」のような文字列になるが、「M.N.」や「M N」などいろいろな形式のイニシャルが欲しいと考えたとする。そのため、新たに引数を持つgetInitialメソッドを定義して対応すること。引数はint型として、数値によって形式を指定できるものとする。引数と形式の対応は自分で考えて、プログラム内にコメントで記述すること。新しいgetInitialメソッドを使ったサンプルもmainメソッドに追加すること。(同じクラス内で同じ名前ながら引数のパターンが違うメソッドを定義することを「オーバーロード」と言います。すでに出ている例では、Personalityクラスのコンストラクタはオーバーロードされています。)
8-2:「円」という図形をプログラムで扱うとしたい。この円に相当するクラスを定義しよう。インスタンス変数として何が必要かを考えること。そして、面積を求めるメソッドを定義して、実際に面積を求めてみる。クラスを定義してmainメソッドを定義し実際に動くサンプルプログラムを作ること。さらに、直径4cmの円と直径4mの円の2つのクラスを作り、その面積が何倍違うかをプログラムで求めてみよう。