クラスとインスタンス (復習)
クラスとインスタンスの関係
「クラス」と「インスタンス」の関係は「設計図」と「製品」の関係に近い。設計図であるクラスには製品であるインスタンスの特徴が書いてあり、インスタンスはそのクラスを元に作成される。
例えば、「自動車」というクラス Car があったとする。しかし、自動車というクラスはあくまで「自動車の設計図」であり、そのままでは運転することができない。もし、自動車を使いたい場合は new Car という構文によってインスタンス (製品) を作成し、そのインスタンスを使う必要がある。
コンストラクタの役割
クラスとインスタンスを設計図と製品の関係と捉えると、コンストラクタは「設計図を元に製品を作る工場」であると考えることができる。new Car() という構文によって、Car という設計図を工場に渡し、その結果として Car のインスタンスである製品を受け取ることができる。
// 工場に Car という設計図を渡すことによって myCar を作ってもらう Car myCar = new Car();
コンストラクタに引数を渡す場合、これは工場に「設計図と一緒に注文をつける」という行為に近い。例えば new Car(Color.WHITE) とした場合、おそらく「Car という設計図を元に、白い自動車を作れ」という注文をしていると推測できる。
コンストラクタを定義する場合、製品を作成する際にどのような注文をつけられるかということが重要になる。先の例では「自動車を作成する際に、その色を定義できる」ということを考えたため、色を受け取るコンストラクタを作成すればよいことになる。
// 色を指定して、インスタンスを作成する public Car(Color bodyColor) { ... }
インスタンスフィールドの役割
インスタンスフィールドは、各製品が持っている製品の性質 (属性) と捉えることができる。例えば、ある自動車 a が時速 60km/h で走っている場合、その a の「現在の速度」という性質は 60km/h である。
これは各製品が別々に持っているため、もし他に Car のインスタンス b があったとして、a の現在の速度が 60km/h でも b には影響しない。また、b が事故により大破したとしても a が壊れるということはない。
インスタンスフィールドを定義する場合、その製品がどのような性質を持つかということ (has-a 関係) が重要になる。例えば、自動車が「現在の速度」という性質を持つ場合、以下のように書ける。
- 自動車 has-a 現在の速度
このような関係にあるものを、インスタンスフィールドとして定義すればよい。
public class Car { double currentSpeed; ... }
インスタンスメソッドの役割
インスタンスメソッドは、各製品が使える機能 (振る舞い) と捉えることができる。例えば自動車は「加速する」という機能を持っている。
この機能も各製品にそれぞれ与えられているため、a の「加速する」という機能を使用したところで b が加速するということは普通ない。
また、この機能を使うには製品に対して「メッセージ」を送る必要がある。このメッセージこそがメソッド呼び出しの本質であり、自動車の場合はアクセルペダルを踏むことによって「加速する」という機能を使いたいというメッセージを自動車に伝える。そのメッセージを受け取った自動車は「加速する」という振る舞いをし、結果的に自動車が加速することになる。
Car a = new Car(); a.pressAccelerator();
また、メソッドに渡す引数は「注文を付けて機能を呼び出す」というイメージが近い。例えば、アクセルを50%だけ開きたい場合には次のように書くのが普通だと思う。
Car a = new Car(); a.pressAccelerator(50.0);
注意すべき点は、そのメッセージを受け取ったからといって、そのメッセージに沿った動きをしてくれるとは限らない。これは、メッセージを受け取ったメソッドの振る舞いによって決まる。たとえば、アクセルペダルとブレーキペダルを逆に付けた場合、アクセルを踏んでもブレーキがかかるかもしれない。
インスタンスメソッドを定義する場合、その製品にどのような機能があるか (どのようなことができるか)ということが重要になる。例えば、自動車が「加速する」という機能を持つ場合、以下のように書ける。
- 自動車 can 加速する
また、メソッドの中身は受け取ったメッセージ (メソッドの呼び出し) に合うような振る舞いをすればよい。例えば「加速する」というメッセージを受け取った場合は、自動車自身を加速させればよい。
public class Car { double currentSpeed; // 1% アクセルを踏むごとに 0.1 だけ加速する public void pressAccelerator(double percent) { this.currentSpeed += 10.0 + percent / 100.0; } }
クラスメンバの役割
static の付けられたフィールドやメソッドは、それぞれ「クラスフィールド」「クラスメソッド」と呼ばれ、これらを総称して「クラスメンバ」と呼ばれる。
クラスメンバが何かということを考えるには、「設計図」という物体の性質や機能について考えればよい。例えば、Math というクラスに sqrt という平方根を計算するクラスメソッドがあるが、これは「数学」という設計図に「平方根を計算する」という機能があるということになる。「数学」は「製品」というものがないため、数学という設計図 (=概念) のうえで機能を使うことになる。
パッケージ
パッケージとは
パッケージとはいくつか関連のあるクラスをグループ化するためのもので、これまでにも使用してきた。授業では「各回の授業で使用するクラス」という単位でパッケージを作成し、その中にクラスを作成した。
この図にある、j2.lesson01, j2.lesson02, j2.lesson03 がパッケージである。各パッケージにはクラスが格納されている。例えば、「Rectangle」というクラスは j2.lesson01 に格納されているため、第01週で使用したということが分かる。
パッケージという考え方は日本人の姓名という考え方と似ている。例えば、「田中一郎」と「鈴木一郎」という人物がいたとして、どちらもファーストネームは「一郎」である。これではどちらがどちらであるか判別ができないため、「田中家の」「鈴木家の」という修飾をして「田中家の一郎」と「鈴木家の一郎」と呼んでやれば各個が限定される。「田中家の」「鈴木家の」にあたるものがパッケージで、それぞれの一郎を「田中家」「鈴木家」というグループで分類している。
他に例を挙げると、前回の演習で作成した Point クラスは、java.awt というパッケージ内にも同じ名前のクラスが存在する。それを区別するには「j2.lesson02 の Point」と「java.awt の Point」とパッケージ名で修飾してやればよい。
Javaには無数のクラスがすでに用意されている。もし、パッケージという考え方がなければこれら全てと異なる名前を用意しなければならない。C言語などの古い言語ではパッケージという概念がなかったため、組み込みの関数 (Java でいうメソッド) と同じ名前を使用しないように注意する必要があった。
Java クラスライブラリ
クラスライブラリとは、特定の機能を持ったプログラムを他のプログラムから利用できるようにクラスとして部品化したものの集まりである。
例えば、コンソールへ文字を出力する際には System クラスを用いて System.out.println メソッドを使用しているし、sqrt や sin などの数学関数を使用するには Math クラスの Math.sqrt や Math.sin メソッドを使用している。
それぞれのクラスは関連のある機能群からなり、System クラスはコンソール画面やコンピュータのシステム時計などのシステムに関するものを使用する際などに利用でき、Math クラスは数学関数を使用する際などに利用できる。
このクラスライブラリの各クラスもパッケージに分かれて格納されている。Java の標準クラスライブラリにあるパッケージをいくつか抜粋してみる。
パッケージ名 | このパッケージに含まれるクラス | 例 |
---|---|---|
java.lang | プログラムを作成する際に使用される、基本的なクラス | System, String, Math |
java.io | 入出力 (I/O) に関するクラス | File, FileReader, FileWriter |
java.net | ネットワークに関するクラス | URL, HttpURLConnection |
java.util | ユーティリティクラス | Calendar, Random, List |
java.awt | Abstract Window Toolkit。GUIを作る際に使われるクラス | Window, Dialog, Graphics |
上記のように、パッケージごとに関連性のあるクラスがまとめて格納されている。これまでは主に java.lang パッケージのクラス (System, String, Math) や java.io パッケージのクラス (IOException, BufferedReader, InputStreamReader) を扱ってきた。
特に、java.io のパッケージにあるクラスを使用する場面は、コンソール入力を取得する という、入出力に関するものであった。
他にどのようなクラスが存在するか知りたい場合は、以下のページを参照するとよい。
- J2SDK 1.4 API 仕様: http://java.sun.com/j2se/1.4/ja/docs/ja/api/index.html
- JDK 5.0 API 仕様: http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/index.html
このリファレンスマニュアルの読み方は、07回目以降で紹介する予定である。
パッケージの宣言を行う
パッケージの宣言を行うと、「クラスがどのパッケージに含まれるか」ということをコンピュータに知らせることができる。パッケージの宣言はソースファイルの先頭で次のように行う。
package パッケージ名;
例えば、今週使用するパッケージは「j2.lesson03」である。それに対するパッケージ宣言は次のように行えばよい。
package j2.lesson03;
今まで、Eclipse が自動的にこの package 宣言を行ってきてくれたため、あまり意識することはなかったかもしれない。これからは、クラスを作成するたびにこの宣言を確認すること。
他のパッケージにあるクラスを使用する
これまで、自分で作成したクラスを他のパッケージにあるクラス (他の週で作成したクラス) から使用することはしなかった。このようなことをする場合、クラス名を'「パッケージ名.クラス名」で指定してやる必要がある。
// このクラスのパッケージは j2.lesson03 package j2.lesson03; public class OtherPackage { public static void main(String[] args) { // j2.lesson02 パッケージにあるクラスを使用する j2.lesson02.Point p1 = new j2.lesson02.Point(1.0, 2.0); j2.lesson02.Point p2 = new j2.lesson02.Point(4.0, 6.0); System.out.println(p1.distance(p2)); } }
このように、「パッケージ名.クラス名」の形で指定したクラス名を完全限定名 (または完全修飾名) と呼ぶ。また、パッケージ名を省略した「クラス名」の形で指定したクラス名を単純名と呼ぶ。
ただし、完全限定名は冗長で読みにくい。上記の例では二次元座標を表す Point という単語が最も重要なのに、j2.lesson02 というパッケージ名のせいでそれが埋もれてしまっている。
Java では import という機能を使って、他のパッケージ内にあるクラスを単純名で書くことができる。
// このクラスのパッケージは j2.lesson03 package j2.lesson03; // j2.lesson02.Point は単純名を用いる import j2.lesson02.Point; public class ImportPackage { public static void main(String[] args) { // j2.lesson02 パッケージにあるクラスを使用する Point p1 = new Point(1.0, 2.0); Point p2 = new Point(4.0, 6.0); System.out.println(p1.distance(p2)); } }
上記のように、package 宣言の下に
import 完全限定名;
と import 宣言を行うことによって、対象のクラスを単純名でアクセスできるようになる。
また、「import パッケージ名.*;」と書くことによって、対象のパッケージに含むクラスを全て単純名でアクセスできるようにする機能もある。
package j2.lesson03; public class ConsoleInput { public static void main(String[] args) throws java.io.IOException { java.io.BufferedReader reader = new java.io.BufferedReader( new java.io.InputStreamReader(System.in) ); System.out.print("input>"); String input = reader.readLine(); System.out.println("input = " + input); } }
上記のプログラムは、今まで暗黙に使用していた「import java.io.*;」を使用せずに完全限定名でクラス名を記述した例である。以下のように書いてもよい。
package j2.lesson03; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class ImportConsoleInput { public static void main(String[] args) throws IOException { BufferedReader reader = new BufferedReader( new InputStreamReader(System.in) ); System.out.print("input>"); String input = reader.readLine(); System.out.println("input = " + input); } }
暗黙の import
先ほど、Systemクラス, Stringクラス, Mathクラスはそれぞれ java.lang パッケージ内に格納されていると紹介した。しかし、これまでのプログラムには java.lang パッケージに対する import 文も完全限定名も使用していない。
java.lang パッケージ内のクラスはよく使用されるため、import しなくても単純名を使用できることになっている。逆に、完全限定名を使用しても問題はない。
package j2.lesson03; // import java.lang.*; は不必要 public class FullyQualifiedJavaLang { public static void main(java.lang.String[] args) { java.lang.System.out.println(java.lang.Math.sqrt(2.0)); } }
java.lang. を取り除くと、以下のようなプログラムになる。
public static void main(String[] args) { System.out.println(Math.sqrt(2.0)); }
完全限定名を使用しても確かに問題はないが、特別な理由がない限りは避けるべきである。
アクセス制御
今まで、public という単語をおまじないのように、プログラムのいたるところに記述してきた。
public class Hello { ~~~~~~ public static void main(String[] args) { ~~~~~~ System.out.println("Hello, world!"); } }
この public 意味や、その他同じように使える単語を紹介する。
可視性
public という修飾子は、それを付けた要素 (クラス、フィールド、メソッド、コンストラクタ) の可視性を決定するものである。public は「公開の」という意味を持つように、public を付けた要素はどこからでも (全てのクラスから) 参照することができる。
それに対して、private という修飾子がある。これはフィールド、メソッド、コンストラクタに付けられるものであるが、これを付けた要素は他のクラスから参照できなくなる。
例えば、次のようなクラスを考える。
public class Rectangle { private double width; private double height; public Rectangle(double w, double h) { this.width = w; this.height = h; } public double area() { return this.width * this.height; } }
上記のようなクラスを作った際には、public のついているコンストラクタ、area メソッドには他のクラスからでもアクセスできる。
// 他のクラス public class RectangleAction1 { public static void main(String[] args) { Rectangle r = new Rectangle(1.0, 2.0); // ~~~~~~~~~~~~~~~~~~~~~~~ ok System.out.println("area = " + r.area()); // ~~~~~~~~ ok } }
ただし、private のついているフィールド double、height には他のクラスからはアクセスできない。
// 他のクラス public class RectangleAction2 { public static void main(String[] args) { Rectangle r = new Rectangle(1.0, 2.0); // ~~~~~~~~~~~~~~~~~~~~~~~ ok // private なのでどちらもコンパイルエラー r.width = 3.0; r.height = 4.0 System.out.println("area = " + r.area()); // ~~~~~~~~ ok } }
このように private を使用すると、「書き換えてほしくない情報を書き換えられないで済む」という利点がある。例えば、上記の r というインスタンスはどんなことがあっても width=1.0, height=2.0 であるような長方形であってほしい場合、private でこれらのフィールドを宣言しておくことによって、外部から書き換えることができなくなる。また、間違えて書き換えようとしてもコンパイルエラーとなるので、わざわざ実行しなくてもプログラムミスであることが分かる。
private は「他のクラスから参照できない」だけであって、同じクラスからならば読み出すことも書き換えることができる。
public class Rectangle { private double width; private double height; public Rectangle(double w, double h) { // 自分のクラスからは書き込める this.width = w; this.height = h; } public double area() { // 自分のクラスからは読み出せる return this.width * this.height; } }
他には、public も private も指定しない「パッケージアクセス」と呼ばれる可視性も存在する。これは「同じパッケージ内のクラスからのみ参照できる」といったものである。
public class Rectangle { double width; double height; ...
通常、パッケージは「ある特定の機能群を持ったクラスの集まり」で構成される。そのため、その機能群の中 (同一パッケージ内) からはアクセス可能で、そのパッケージを外側から使用するユーザからはアクセスできないといった可視性にも需要がある。たとえば、今までインスタンスフィールドはこのパッケージアクセスで宣言してきた。このようなフィールドは、他のパッケージにあるクラスからは参照することができない。
他にも protected といった修飾子も存在するが、これは他の知識が必要であるため、次回以降に紹介することにする。
final
final という修飾子は、「二度と書き換えられない」という意味を持ち、主にフィールドに付けられる。
インスタンスフィールドに final を付けた場合、このインスタンスフィールドは「コンストラクタ以外で書き換え不可能」になる。つまり、インスタンスを作成するときに一度だけ設定され、それ以降は書き換えることができなくなる。
public class Rectangle { private final double width; private final double height; public Rectangle(double w, double h) { this.width = w; this.height = h; } public double area() { return this.width * this.height; } }
上記の例では、width と height をコンストラクタで一度だけ設定し、以後書き換えられなくしている。この final によって、変更していけないフィールドを不用意に変更してしまう、というミスをなくすことができる。
また、クラスフィールドに final を付けた場合、基本的に「宣言と同時にしか代入不可能」となる。他にも代入する方法が一応存在するが、ここでは割愛する。
主に、クラスフィールドに final を指定する場面として「定数の宣言」等が挙げられる。
// 自動販売機 public class VendingMachine { // 飲み物の標準価格 = 110円 public static final int DEFAULT_PRICE = 110; ... }
上記のように、標準価格を定数として設定しておくと、標準価格を変更する際に DEFAULT_PRICE の値を変えるだけで済む。もし、定数を使わずにコード内に 110 という値が散乱していた場合、これらの値を全て同時に変える必要があり、バグの元となる。
static
static はフィールド、メソッドに付けることができ、対象のフィールド、メソッドが「インスタンスではなくクラスに属する」ようになる。つまり、static を付けた場合には製品個々の性質や機能ではなくなり、設計図自体の性質や機能になる。
例えば、次のようなクラスを考える。
public class Rectangle { private double width; private double height; // 指定した長さを持つ正方形を作る public static Rectangle createQuadrate(double edge) { return new Rectangle(edge, edge); } public Rectangle(double w, double h) { this.width = w; this.height = h; } public double area() { return this.width * this.height; } }
上記は、長方形クラスの中に「正方形を作成する」というメソッドを用意した。正方形を作るという機能を持つのは、長方形インスタンスの役目ではなく設計図の役目である。
また、設計図はどの製品からでもアクセス可能である。そのため、全てのインスタンスで共通に用いられる定数を用意するときには static を付けるのが普通である。
public class VendingMachine { // どの自動販売機でも標準価格は110円 public static final int DEFAULT_PRICE = 110; ... }
カプセル化
「一種類のジュースを売っている自動販売機」というものをプログラムで表してみる。
自動販売機の性質と機能を簡単に抽出する。
- 自動販売機の性質
- 今までに投入された金額を保持する
- 自動販売機の機能
- お金を投入する
- ジュースを買う
- おつりを返す
これを元に VendingMachine クラスを作成する。
package j2.lesson03; public class VendingMachine { /** ジュースの標準価格. */ static final int DEFAULT_PRICE = 110; /** 投入金額 */ int amount; /** * お金を投入する。 * @param yen 投入金額 */ public void charge(int yen) { if (yen < 0) { return; } this.amount += yen; } /** * ジュースを買う。 * @return ジュース。お金が足りなければ null */ public Juice buy() { // お金が足りているか? if (this.amount >= DEFAULT_PRICE) { // 足りていれば投入金額を減らしてジュースを出す this.amount -= DEFAULT_PRICE; return new Juice(); // Juice クラスは別に定義 } else { // 足りていなければ null (無意味なもの) を返す return null; } } /** * おつりを返す。 * @return おつり */ public int getChange() { int change = this.amount; this.amount = 0; return change; } }
このプログラムには大きな問題がある。amount が private で指定されていないため、次のようなプログラムを書くことができてしまう。
VendingMachine vm = new VendingMachine(); // 不正に投入金額を調整 vm.amout = 10000; // おつりをもらう int money = vm.getChange();
現実の世界では、ふたでも開けない限り上記のような行動をすることはできない。なぜなら、投入金額は直接操作することができないからである。もし投入金額を操作しようとする場合、次のような手続きを踏む必要がある。
- お金を投入する
- ジュースを買って合計金額を引く
- おつりをもらう
現実世界に近づけるためには、amount を他から変更できないように private を指定する必要がある。
package j2.lesson03; public class VendingMachine { /** ジュースの標準価格. */ static final int DEFAULT_PRICE = 110; /** 投入金額 */ private int amount; ...
これで、「vm.amount = 10000」といったような不正な変更はできなくなったが、今度は現在いくら投入したのか、他のクラスから確認できなくなってしまった。そこで、確認するためのメソッドを作成する。
package j2.lesson03; public class VendingMachine { /** ジュースの標準価格. */ static final int DEFAULT_PRICE = 110; /** 投入金額 */ private int amount; /** * 合計金額を表示する。 * @return 合計金額 */ public int getAmount() { return this.amount; } ...
上記のように、amount という内部情報を隠蔽し、それを外部からのメッセージ (メソッド呼び出し) のみによって変更されるように設計することをカプセル化と呼ぶ。カプセル化によって、インスタンスの内部の性質を意識することなく、機能やそれを扱うためのメッセージだけを意識してプログラミングできるようになる。
そういえば、最近の缶ジュースは標準価格が120円になっていた。もしこれを修正したい場合、
/** ジュースの標準価格. */ static final int DEFAULT_PRICE = 120;
と、1文字変えるだけで全て修正できる。
カプセル化の例
カプセル化という概念は薬のカプセルに似ている。例えば、医者から処方された風邪薬が、以下の内容だったとする。
- 薬品A: 10mg
- 薬品B: 7mg
- 薬品C: 3mg
カプセルがない状態では、薬を飲む側はこれらの薬品を適切に計量して摂取する必要がある。薬品の分量を間違えると人体に悪影響を及ぼす可能性もあり、この方法は危険である。
カプセルに計量済みのこれらの薬品が入っていれば、薬を飲む側は「カプセルを飲む」という行為を行うだけで、適切な量を摂取することができるようになる。
上図のように、薬の調合部分 (カプセルを作成する部分) を使用者に隠蔽することにより、提供者は望んだ通りの分量を確実に服用者に与えることができるし、服用者は簡単に薬を摂取することができるようになる。カプセルの中身を調べたい場合は、医者に効能について問うか、処方箋の内容について調べればよい。
また、薬の内容を同じ効果のより良い薬品に差し替える場合も容易である。例えば、薬品Aの代わりに薬品Dを2mg摂取することになったとしても、薬を飲む側は「カプセルを飲む」という行為に変更はない。自分で計量する場合は、間違えて薬品Dを10mg摂取してしまうこともあるだろう。
このようなカプセル化を行うことにより、使用者 (この例では薬を飲む人) はカプセルの内部を意識することなく適切な行為をとることができるようになる。
これをプログラミングの世界で考えてみると、一つ一つのフィールドを互いの影響を考えながら操作するより、いくつかのフィールドをまとめて適切な値に設定してくれるメソッドを一つだけ使用するほうが単純で、間違いも起こりにくい。また、内部表現をより良いものに変更する場合にも、使用者は使用方法を変えることなくプログラムを作成することができる。
上記の例は、内部表現 (フィールド) のカプセル化であるが、機能 (メソッド) のカプセル化という考え方もある。
機能をカプセル化しているものとしては、テレビなどが良い例である。テレビは地上波を受け取る際に放送局が発している周波数と共振させて映像を取得し、変換して画面に表示している。しかし我々はそのようなことを意識することもなく、リモコンでチャンネルを指定することによって映像を見ることができる。
これはリモコンという道具を使って「チャンネルを変更」というメッセージをテレビに送信し、それを受け取ったテレビが (自動で周波数を設定し変換し) 映像を表示してくれる。このように「チャンネルを変更」というメッセージだけ (実際にはより多くの機能を持つ) を受け取るように製品を設計し、使用者に余計な情報を与えないことによって使いやすくなっている。
インスタンスフィールドやユーザに公開すべきでないメソッドは、よほどの理由がない限りは private で宣言し、それを操作する public なメソッドを別に宣言するように心がけるのが良い。実際に、多くのプログラムはそのように設計されているし、現実世界の製品でも同じような考え方がなされている。