Java の「インタフェース」の使用方法は、継承と似ています。
継承の目的の一つに、 スーパクラスを起点としてサブクラス群をグループ化するということがありました。 継承によってクラスをグループ化するということは、 同じ型として扱えるようにするということと、 同じオブジェクトとして振る舞うための要素を持たせる、 すなわち、オブジェクトのインタフェースを統一するという意味がありました。
継承がインタフェースを定める機能とクラスの拡張を行う機能があるのに対して、 Java の「インタフェース」は、 純粋にクラスのインタフェースだけを定義するための機能です。
インタフェースの目的は、 クラスのインタフェースを定義することにより、 同じインタフェースを持つクラス群をグループ化することです。 つまり、異なったクラスのインスタンスでも、 そのインタフェースが同じであれば、 同じグループの一員であるとみなし、同じように扱うことができるのです。
円を示すクラス Circle と、 矩形を表すクラス Rectangle についての例を示します。
Circle と Rectangle は、 図形であるという性質は共通だと考えられます。 図形には、面積を求めるメソッドと、 外周の長さを求めるメソッドが存在するべきだと考えることができます。
このような場合、図形に共通するクラスの利用方法を定義しておき、 Circle と Rectangle を図形共通の方式で使うことができれば便利です。 図形共通の方式を決めておくためにインタフェースという機能を用います。
interface Shape { public int area(); public int lengthOfOutline(); } class Circle implements Shape { private int radius; private static final int PI = 3; Circle(int r) { radius = r; } public int area() { return radius*radius*PI; } public int lengthOfOutline() { return 2*radius*PI; } } class Rectangle implements Shape { private int width; private int height; Rectangle(int w, int h) { width = w; height = h; } public int area() { return width * height; } public int lengthOfOutline() { return 2*width + 2*height; } } class SomeShapes { public static void main(String[] args) { Circle c1 = new Circle(10); Circle c2 = new Circle(20); Rectangle r1 = new Rectangle(10, 10); Rectangle r2 = new Rectangle(10, 20); Shape variousShapes[] = new Shape[4]; variousShapes[0] = c1; variousShapes[1] = c2; variousShapes[2] = r1; variousShapes[3] = r2; for (int i = 0; i < variousShapes.length; i++) { System.out.println("shape " + i); System.out.println(" Area: " + variousShapes[i].area()); System.out.println(" Outline: " + variousShapes[i].lengthOfOutline()); } } }
最初の interface で始まるブロックに、 図形として備えているべきインタフェース Shape が定義されています。 ここでは、面積を求めるメソッド area と、 外周の長さを求めるメソッド lengthOfOutline を宣言しています。
インタフェースの定義の文法は次のようになります。
interface インタフェース名 { 属性の宣言; ... メソッドの宣言; ... }
ここでのメソッドの宣言は、中身のないメソッドの宣言です。 メソッドの返り値の型、メソッド名、引数のリストの宣言文のみを書き、 メソッドの中身を記述しません。
インタフェースにおけるメソッドの宣言は、 それ自体を実行して何か仕事をするものではありません。 その役割は、 クラスがグループに属するために必要なメソッドの枠組みを定義することです。 インタフェースのメソッド宣言で定義するのは、 メソッドの枠組みだけで、その中身の内容は書きません。
実際のメソッドの仕事内容は、 クラスの定義の中で記述します。 インタフェースで定められたメソッドの内容をクラスの中で定義することを、 インタフェースを実装すると言います。
インタフェース定義は、クラス定義の文法と似ていますが、 インタフェースからインスタンスを生成することはできません。 インタフェースは単にクラスの枠組みを決めているのに過ぎないのです。
この例題では、インタフェース Shape の定義によって、 クラスが Shape というグループに属する要件として、 メソッド area と lengthOfOutline を備えている必要があると決めています。
あるインタフェースを実装するクラスを定義する場合、 文法的には次のように書きます。
class クラス名 implements インタフェース名 { ...... クラス定義の内容 }
インタフェースを実装するクラスには、 インタフェース内で定義されている全てのメソッドを宣言する必要があります。
この例題では、 Shape を実装するクラスとして Circle と Rectangle を定義しています。 Circle と Rectangle は Shape インタフェースを持つグループの一員として、 同じように扱うことができるのです。
一つのクラスで複数のインタフェースを実装することも可能です。 その場合、次のように implements に続けて実装するインタフェース名を コンマで区切って並べます。
class クラス名 implements インタフェース名1, インタフェース名2, ... { ...... クラス定義の内容 }
また、あるクラスを継承し、 なおかつインタフェースを実装するということもできます。 したがって、クラス定義の一般的な文法は次のようになります。
class クラス名 extends スーパクラス名 implements インタフェース名1, インタフェース名2, ... { ...... クラス定義の内容 }
実は、Java では継承できるスーパクラスは一つのみである (単一継承, single inheritance) という決まりがあります。 しかし、インタフェースは多重に実装することが可能なのです。
スーパクラスが一つしか許されない単一継承に対して、 スーパクラスが複数存在する場合を 多重継承(multiple inheritance)と言います。 Java では多重継承を行うことはできませんが、 C++ などの他の言語で行うことができるものもあります。 多重継承では、 クラス階層の設計を正しく行わないと、 複数のスーパクラスから受け継いだ性質に矛盾や不整合を生じる場合があります。 Java では、多重継承によって複数のクラスの性質を受け継ぐことを禁止し、 その代わり「クラスの使い方」であるインタフェースを複数実装することで、 実装上の矛盾や不整合を防いでいます。
異なったクラスのインスタンスでも、 同じインタフェースを実装していれば、 同じグループの一員として扱うことができます。 これは継承のグループ化の機能とほぼ同一のものです。
この例では、クラス Circle のインスタンスも、 クラス Rectangle のインスタンスも、 インタフェース Shape を実装しているため、 Shape 型の変数に代入することが可能です。
Circle c1 = new Circle(10); Circle c2 = new Circle(20); Rectangle r1 = new Rectangle(10, 10); Rectangle r2 = new Rectangle(10, 20); Shape variousShapes[] = new Shape[4]; variousShapes[0] = c1; variousShapes[1] = c2; variousShapes[2] = r1; variousShapes[3] = r2;
このようにすることで、 Circle のインスタンスと Rectangle のインスタンスを Shape の一員として同一に扱うことが可能になります。 インタフェースが実装されていることから、 計算方法は異なっても、面積と外周長を計算する メソッドは備わっていることになります。
例えば、 次のように繰り返しを用いて、 異なるクラスのインスタンスに対するメソッドの実行を、 インタフェースで宣言されているメソッドを用いて一括して行うことができるのです。
for (int i = 0; i < variousShapes.length; i++) { System.out.println("shape " + i); System.out.println(" Area: " + variousShapes[i].area()); System.out.println(" Outline: " + variousShapes[i].lengthOfOutline());
このようにインタフェースによってクラスをグループ化するということは、 同じ型として扱えるようにするということと、 同じオブジェクトとして振る舞うための要素を持たせるという意味があります。
今回の二つめの話題はメンバの種類についてです。
オブジェクトの属性とメソッドをメンバと呼びます。 メンバにはインスタンスメンバと静的メンバの二種類があります。
これまでのプログラムで用いてきたクラスの属性やメソッドは、 インスタンスを生成することで使用することができます。 これらのメンバをインスタンスメンバと呼びます。 一方、インスタンスを作成せずに使用できる属性やメソッドもあります。 これを静的メンバと呼びます。
これまで、用いてきた属性やメソッドは、 メソッド main を除き、 全てインスタンスメンバに分類されます。
インスタンスは、クラスをもとに生成される「実体」のことでした。 インスタンスはいくつでも生成することができ、 個々のインスタンスはそれぞれが別個のものとして振る舞います。 また、その属性もインスタンスごとに別個の値を保持します。
このように個々のインスタンスが独立してもつ属性やメソッドのことを インスタンスメンバ (instance member) と呼びます。
クラスをもとにインスタンスを一つ生成すれば、 インスタンスメンバも各一つ存在することになり、 インスタンスを n 個生成すれば、 インスタンスメンバも n 個存在することになります。
インスタンスメンバは個々のインスタンスで独立したメンバであり、 複数のインスタンスで共有することはできません。 また、インスタンスメンバにアクセスするには、 あらかじめ、インスタンスを生成しておく必要があります。
静的メンバはクラスに属する属性やメソッドです。 オブジェクトの有無とは無関係に使うことができます。 したがって、静的メンバを使うためにはインスタンスを生成する必要はありません。
静的属性 (static field, 静的変数) は、インスタンスごとに独立した値とはならず、 クラス単位で共通に使える属性です。 静的属性はクラスに対して一つだけ割り当てられる変数であり、 同じクラスを型とする全てのインスタンスで共有されます。 また、静的属性の値はオブジェクトの生成、消滅とは無関係に、 プログラムの実行を開始してから終了するまで値が保持されます。
静的属性を宣言するには、 次のように、クラス定義の中の属性の宣言部分にキーワード static を用います。
static 型名 変数名;
静的属性の使用例として定数があげられます。 定数はクラスに共通して使用される値であり、 途中で変更することがありません。 そこで、定数であることを示すキーワード final と、 アクセス修飾子 private を用いて定数を宣言すると以下のようになります。
private static final double PI = 3.14;
静的属性にアクセスするには、 クラス名と属性名をドットでつなぎ、以下のように記述します。
クラス名 . 属性名
なお、 同じクラス定義の中では「クラス名 . 」の部分を省略することができます。
静的メソッド (static method) は、 インスタンスの有無に関わらず利用することができるメソッドです。 静的メソッドを宣言するには、キーワード static を用いて以下のように書きます。
static 返り値の型 メソッド名 (引数1, 引数2, ...)
静的メソッドを呼び出すには、 クラス名とメソッド名をドットでつなぎ、以下のように記述します。
クラス名 . メソッド名 ( 引数1, 引数2, ... )
静的メンバを使う場面は限られています。 例として、 クラスに関係する定数を静的属性として宣言する場合や、 オブジェクトを生成する必要がない処理のまとまりを 静的メソッドとして宣言すること等が考えられます。
次のプログラムは、四則演算を行うクラス Caluculation の例です (0 で除算した場合の処理は省略)。
class Calculation { public static int add(int a, int b) { return a + b; } public static int subtract(int a, int b) { return a - b; } public static int multiply(int a, int b) { return a * b; } public static int divide(int a, int b) { return a / b; } }
四則演算の機能を実現するメソッドを考えると、 インスタンスを生成する必要がありません。 このような場合は、静的メソッドを用いるのが適していると言えます。 しかし、計算途中の値を覚える等の機能を持つ等、 内部の状態を属性として持つようなクラスは、 静的メソッドを用いず、インスタンスを生成して使うべきです。
一般に、プログラムで行う仕事の内容を考えたときに、 対象 (もの) として考えられるものについては、 インスタンスを生成すべきだと言えます。
今回の三つめの話題は型についてです。
Java では、 数値、文字列やオブジェクトといったデータを扱うために、 データの種類に応じたデータ型を指定する必要があります。 int, double, boolean は数値や真偽を扱うためのデータ型です。 また、オブジェクトの設計図であるクラスもデータ型の一種なのです。
データ型には、大きく分けて 2 つの種類があります。
一つは、基本型 (プリミティブ, primitive) と呼ばれるものであり、 int, char, byte, short, long, double, float, boolean すなわち、 数値や文字、真偽を表す値の型がこれに分類されます。
もう一つは、参照型と呼ばれるものであり、 クラスをもとに生成されたインスタンス (オブジェクト) がこれに分類されます。 後述しますが、文字列も参照型です。
基本型と参照型の違いは、 基本型の変数が値そのものを指すのに対して、 参照型の変数はオブジェクト本体のありかを指し示すということです。
int 型や boolean 型などの基本型のデータを格納する変数には、 値そのものが格納されますが、 参照型の変数に格納されるのはオブジェクトそのものではありません。 変数には、オブジェクトの在処を指し示す矢印のようなものが格納されます。 この矢印は、 オブジェクトがどこにあるかを参照するために使われるということから、 オブジェクトの参照と呼ばれています。
基本型の変数への代入は、 値そのものを代入するという作業が行われます。 例えば、
b = 10; a = b;
上のような代入が実行されると、 変数 a には変数 b に入っている値そのものがコピーされ代入されます。 つまり、結果的に同じ値を格納した変数が 2 つできるということです。
代入後は a と b の値は独立したものとして扱われます。 a の値を変化させても b の値は変わってしまうことはありません。
これに対して、オブジェクトを示す変数への代入は、 オブジェクトへの参照が代入されるという作業が行われます。 例えば、
TDUStudent john = new TDUStudent(); masterStudent = john;
というプログラムについて考えてみます。
一行目の右辺では、 new 演算子によってクラス TDUStudent のインスタンスが生成され、 そのインスタンスへの参照が得られます。 これを左辺の変数 john に代入しています。 つまり、 john に、新たに生成された TDUStudent の インスタンスの在処を指し示す情報を代入する という処理が行われます。
masterStudent = john という代入では、 変数 masterStudent には、john の示すオブジェクトへの参照が代入されます。 john のオブジェクトがコピーされるわけではありません。 元のオブジェクトは一つのまま、 john と masterStudent が同じオブジェクトを 指し示すことになるのです。 つまり、この代入では、オブジェクトの複製ができるのではなく、 オブジェクトの在処を示す「参照」だけがコピーされたということです。
john.setScore(100, 90, 80);
のように、john に対して操作を行うと、 masterStudent の状態も連動して変化していることになります。
メソッドの実行を行う際に、引数を通して メソッドを呼ぶ側から呼ばれる側へデータを渡すことができます。
例えば、 anObject.calculate(x) という形で、 メソッド calculate に基本型の変数 x を渡すことを指示した場合、 メソッドには x から取り出した値だけが渡されます。 変数 x の値であるということは伝わらず、 その値 (例えば 10) だけが渡されます。 このような引数の渡し方を値渡し (call by value) と言います。
しかし、オブジェクトを引数としたメソッド呼び出しの場合は異なります。
例えば、 anObject.process(obj) という形で、 メソッド process にオブジェクト obj を渡すことを指示した場合、 メソッドには obj への参照が渡されます。 obj 本体が渡されるのではなく、 「obj の在処を示す情報」が渡されるのです。 呼び出されたメソッドの中でも、 矢印は同じオブジェクトを指し示しているため、 メソッド内でこのオブジェクトに対して操作を行うと、 obj から見てもオブジェクトの内容が変化していることになります。
引数はオブジェクト自体を渡すわけではありません。 引数として渡されるのは、オブジェクトの在処を示す「参照」であり、 実体は一つである、ということに注意が必要です。
今回の四つめの話題は文字列についてです。
これまでの例題では、 文字列を String というキーワードを用いて、 宣言し利用してきました。 実は Java では、文字列もオブジェクトとして扱っています。 クラス String は、 文字列を表すクラスであり、 あらかじめ Java のクラスライブラリに存在しているクラスです。
クラス String は少し特殊な性質を持っています。
クラスからインスタンスを作成するためには、 new 演算子を用い次のように書くのでした。
String str = new String("Hello");
ただし、クラス String に限り、 上記のように new 演算子を用いる以外に次のようにして インスタンスを生成できるのです。
String str = "Hello";
この式の右辺のように、 文字列をダブルクォーテーション (") で括って記述することで、 String のインスタンスを生成することが可能なのです。 このように Java では、ダブルクォーテーションで括り記述された文字列は、 全て String のインスタンスとして扱われることになっています。
次の例は String のインスタンスを生成する式です。 これらは、いずれも String のインスタンスを新たに生成しています。
new String("Hello, Java.") "Java progamming is easy." "Linux" + " is also easy." "I have studied Java for " + day + " days."
String のインスタンスが表す文字列の内容が同一であるかを調べるには、 比較演算子 == を用いることができません。 クラス String の (正確には String のスーパクラス java.lang.Object で定義されている) メソッド equals を用います。
例えば、String のインスタンス str1 と str2 が同一であるかを 調べるには以下のように書きます。
if (str1.equals(str2)) { ... 等しい場合の処理 } else { ... 異なる場合の処理 }
メソッド equals は、自らのオブジェクト (上の例では str1) の内容と、 引数で与えられたオブジェクト (上の例では str2) の内容を比較し、 等しい場合には true を、異なる場合には false を返します。
文法的には以下のようになります。
基準となるStringのインスタンス . equals ( 比較したいStringのインスタンス )
実は、クラス String にはメソッド equals 以外にも、 様々なメソッドがあらかじめ用意されています。 その中で、主要なものを紹介します。
他にもたくさんのメソッドがあります。 JavaTM 2 Platform, Standard Edition, 1.4.0 API 仕様、 もしくは、 /usr/java 以下に展開した同様のドキュメントを参照してください。
このドキュメントから分かるように、 実は Java には様々なクラスがあらかじめ用意されています。 このように再利用可能なクラス集をクラスライブラリと言います。
また、クラスライブラリの一部は、 プログラムと外部とのインタフェースであることから、 API (Application Program Interface) と呼ばれます。
この講義では、 クラスライブラリの内容全てを取り上げることは不可能です。 しかし、 Java でプログラムを効率的に開発するためには、 クラスライブラリは非常に有効です。 また、ウィンドウ、アイコン、ボタン等の GUI の部品や、 画面入出力やファイル入出力のためのストリームも クラスライブラリに含まれます。
クラスライブラリを使いこなすことが上達への早道です。