新人エンジニア研修で知っておきたいインタフェースの使い方
なぜ、インタフェースの理解が重要なのか、その理由。
この記事では、弊社の新人エンジニア研修の参考にJavaを解説します。
前回はカプセル化と情報隠蔽について解説しました。今回はインタフェースについて解説します。インタフェース型はこのテキストで扱う最後のJavaの型です。全て揃いましたので以下に再掲します。
インタフェースが何のためにあるのか?を一言で言えばポリモーフィズムのためです。つまり、インタフェースにより異なるクラスを同じ仲間として扱うことができるようになります。まずはそのことを念頭においてください。
実は当社の新人エンジニア研修でインタフェースを作ることは想定していません。ただし、利用することはありますのでその仕組を知ることは重要です。インタフェースも抽象クラス同様インスタンス化できません。しかし、抽象クラスとはまた違った意味で巧妙な役割が与えられています。
インタフェースを考える際にはJavaでは多重継承が禁じられていたことを思い出す必要があります。Javaではスーパークラスを継承するサブクラスの数に制限はありませんでした。しかし、サブクラスから見て直接のスーパークラスは一つだけと決まっていました。これがJavaでは多重継承が禁じられているということの意味です。では、継承関係にかかわらずポリモーフィズムを使いたいときにはどうしたらいいのでしょうか?
そうです。
インタフェースを使います。
そもそもinterfaceとは接点のことです。人間とコンピュータとの接点を【man - machine interface】と言ったりしますね。あるいは、パソコンと周辺装置のインタフェースという表現もあります。
例えば、USBというインタフェースを例にとりましょう。USBの接続口がある製品は何でも、パソコンでもプリンタでもスキャナーでも相互に接続できます。なぜなら、お互いに一定のルールを内包して共有しているからです。(下図参照)
Javaのインターフェースのイメージもこのルールを内包して共有するというところにあります。同じインタフェースを組み込むことでクラスを共通ルールのもとに扱うことができるようになるのです。
1.Comparableインタフェース
例によりJavaの標準APIに使用例の範を求めましょう。
ここでは、まずIntegerクラスをご紹介します。このクラスはラッパークラスといってint型をIntegerというクラスにラップするクラスです。英語の【wrap】には包むという意味があります。
Integerクラスは、int値とIntegerオブジェクトの間でボクシング(intからIntegerへの変換)とアンボクシング(Integerからintへの変換)を行うメソッドを提供します。これにより、int値をIntegerオブジェクトに変換して扱ったり、逆にIntegerオブジェクトからint値を取り出したりすることができます。
また、Integerクラスは、文字列からint値への変換(parseInt()メソッド)や、int値から文字列への変換(toString()メソッド)を行うメソッドを提供します。
例えば、以下のようなことができますので確かめてみましょう。
String num1 = "1024";
String num2 = "512";
System.out.println(Integer.parseInt(num1) + Integer.parseInt(num2));
また、以下のコードを実行するとNumberFormatExceptionという例外が出ますのでこれも試してみてください。
System.out.println(Integer.parseInt("1byte"));
話をComparableインタフェースに戻します。
IDEでIntegerクラスの52行目の宣言部分を見てみます。
java17標準APIpublic final class Integer extends Number implements Comparable<Integer> { 以下省略
「implements Comparable」という文字列が見えます。この部分がインタフェースを実装している部分です。
インタフェースを実装するクラス宣言にはimplementsと書きます。
英語のimplementsには実装という意味があります。ちょうどアタッチメントなどを付ける感覚です。
Comparableの文字列の先頭文字が大文字になっていますね。クラス同様インタフェースも先頭は大文字です。【compare:比較】に【able:できる】という意味の接尾語がついています。比較できるためのインタフェースを実装しているわけですね。比較できるというルールを内包して、共有しているわけです。後ろの<Integer>というのはジェネリックス(Generics:総称型)というものです。詳しくはあとで章を改めてお話しますが、「比較の対象はIntegerクラスのインスタンスだけですよ」とあらかじめ断っているのです。
※ちなみにクラスとインタフェースを名前だけで判別する際に接尾語に注目してableがついていたらインタフェースと考えると良いでしょう。
インタフェースを実装するということは、能力を組み込むということだと考えるとナイスなネーミングですね。
ただし、すべてのインタフェースにableがついているわけではないので誤解のないように。
では、次にComparable(標準API)から検索してみます。するとこのように書かれています。
このインタフェースを実装する各クラスのオブジェクトに全体順序付けを強制します。
「全体順序付けを強制します」とあります。
整数値にせよ、文字列にせよ、インスタンスを順序付けできないと並べ替え等ができませんからこれは大切なインタフェースですね。しかし、equals()メソッドのところでも似たような議論がありましたが、何をもって順序を前、後ろと決めるのかという問題があります。例えば、この新入社員研修のクラス一つとってみても、順序付けの基準は無限に考えられます。名前のあいうえお順、身長順、年齢順、成績順、などなど。
そこで、Comparableインタフェースでは、「比較する」という抽象メソッドのみを決めて、実装は個々のクラスに任せるのです。
IDEを使ってComparableインタフェースの中身を見ていくと一番下、136行目に以下のような記述があります。
Java17標準APIpublic int compareTo(T o);
※()の中のT oもジェネリックスです。Tはタイプ(型)の頭文字で任意のクラスの型を意味しています。
いつものメソッドについている()の後ろの{}がありません。このような中身のないメソッドは抽象メソッドと呼ばれます。インタフェースでは、メソッドは基本、抽象メソッドになります。
※実は、Java8からdefaultメソッドというものも使えるようになったのですが、本研修では触れません。
また、仮にpublicがついていなくてもpublicなメソッドになります。インタフェースに定める抽象メソッドは暗黙的にpublicで修飾されるのです。なぜなら、インタフェースは外部に公開することを目的としているものなので、そもそも公開を制限するのは論理矛盾なのです。
Comparableインタフェースを実装したクラスでは比較できるという性質を具体的に記述することが必要です。
では、このcompareTo()メソッドをオーバーライドしている個所をIntegerクラスの1476行目に見つけてください。
Java17標準APIpublic int compareTo(Integer anotherInteger) { return compare(this.value, anotherInteger.value); }
compareTo()メソッドの中でcompare()メソッドを呼んでいます。
次はこのcompare()メソッドに飛んでください。すぐ下の1494行目にあります。
Java17標準APIpublic static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); }
compare()メソッドの中身の三項演算子はxとyを比較して
yが大きければ-1を返す
同じであれば0を返す
xが大きければ1を返す
ということをしているだけです。(私は便宜的に負けたらマイナスと覚えています)
では、そのことを確認するサンプルプログラムを書いてみましょう。
以下のExample01はcompareTo()メソッドの使い方を説明するプログラムです。
package chap12;
public class Example01 {
public static void main(String[] args) {
Integer one = 1;
Integer two = 2;
Integer three = 3;
System.out.println(two.compareTo(one));
System.out.println(two.compareTo(two));
System.out.println(two.compareTo(three));
}
}
<実行結果>
1 0 -1 |
確かにそうなっていますね。ですので、(実用性はともかく)以下のように最大値を求めるプログラムが書けます。cという変数名で宣言されている配列の型に注目してください。
package chap12;
public class Example02 {
public static void main(String[] args) {
Comparable[] c = {new Integer(10), new Integer(20), new Integer(1500), new Integer(202)};
Comparable max = c[0];
for (int i = 0; i < c.length - 1; i++) {
if (max.compareTo(c[i + 1]) < 0) {
max = c[i + 1];
}
}
System.out.println(max);
}
}
Comparable型の配列を宣言しています。つまり、この配列にはComparableインタフェースを実装しているクラスのインスタンスであればなんでも入れることができます。
インタフェースとそのインタフェースを実装したクラスの関係は“is-a”関係ではありません。
「パソコン is a コンピュータ」、「サブクラス is a スーパークラス」といったような論理的な意味関係は以下のように失われてしまっています。
「Integer is a Comparable.」、「実装クラス is a インタフェース.」とは言えませんからね。
しかし、こうすることで後で説明するようにポリモーフィズムを働かせることが可能になります。
2.インタフェースのメソッドをオーバーライドする
Comparableインタフェースを活用したプログラムの実例として、以下のようなStudentの年齢の最大値を求めるコードを作成してみました。オーバーライドしたcompareTo()メソッドの実装方法に気をつけて以下のExample03を読んでみてください。
package chap12;
public class Example03 {
public static void main(String[] args) {
Comparable[] c = {new Student(29, 173), new Student(33, 169), new Student(30, 180), new Student(25, 175)};
Student max = (Student) c[0];
for (int i = 0; i < c.length - 1; i++) {
if (max.compareTo(c[i + 1]) < 0) {
max = (Student) c[i + 1];
}
}
System.out.println(max);
}
}
package chap12;
public class Student implements Comparable {
int age;
int height;
public Student(int age, int height) {
this.age = age;
this.height = height;
}
@Override
public int compareTo(Object anotherStudent) {
Student as = (Student) anotherStudent;
return this.age - as.age;
}
@Override
public String toString() {
return "Student{" + "年齢=" + age + ", 身長=" + height + "}";
}
}
<実行結果>
Student{年齢=33, 身長=169} |
戻り値は-1,0,1に限定されず、2つの値の大小がプラスマイナスで戻ればよいため、
return this.age - as.age;
とシンプルに書いていますが、これでOKです。
上記プログラムで配列0番目の人のcompareTo()メソッドを使って配列1番目の人と年齢を比較、maxが入れ替わって、配列1番目の人のcompareTo()メソッドを使って配列2番目の人と年齢を比較・・・ということをしていることが理解できればオブジェクト指向の基本は理解していると言えるでしょう。
インタフェースを実装したときのクラス図も下図に載せておきます。
継承のクラス図との相違点を意識してください。親クラスの継承では実線でしたが、インタフェースの実装は点線になります。また、インタフェースには、<<interface>>というステレオタイプ表記が付きます。
抽象メソッドは、クラスとの“契約”であるという表現をこれから皆さんも耳にすることがあると思います。クラスがインタフェースを実装する場合、そのクラスはインタフェースで定義された全ての抽象メソッドを実装しなければなりません。このように、抽象メソッドはクラスとインタフェースの間で定義される契約であり、クラスがインタフェースの要件を満たすことを保証するので“契約”というわけです。
ちなみに、抽象メソッドをオーバーライドする利点として、全てのメソッドの名前が同じになるので、あえてドキュメントを読まなくてもそのメソッドの挙動が推測できるというものがあります。この点は研修が進むとだんだん実感できると思います。
※ちなみに、equals()メソッドがObjectクラスのメソッドをオーバーライドして使うものだったのに対して、CompareTo()メソッドはComparableインタフェースを実装して使うというところにこの2つの処理の適用範囲が表れていて興味深いですね。つまり、等しいかどうかというのはすべてのクラスに共通の問題ですが、順序付けに関してはそれを必要としないクラスもあるという。
3.インタフェースを実装したクラスを仲間にできる
インタフェースを使ってポリモーフィズムを実現することができます。サブクラスは一種のスーパークラスだったので、スーパークラスの変数にサブクラスのインスタンスを参照させることができました。
同じように、インタフェース型の参照変数にインタフェースを実装したクラスのインスタンスを代入することができるのです。
つまり、インターフェースを実装したクラスを仲間にできるのです。目的は継承の時と同様に、ポリモーフィズムです。変数の仮引数で大は小を兼ねたいときに使います。例えば、人間クラスと猫クラスのインスタンスが混在している配列から年齢の一番高いインスタンスを見つけます。ただし、直接人間と猫のようにクラスが違うものをcompareTo()メソッドで比較するとキャストが頻発して複雑で変更に弱いプログラムになってしまいます。
以下のExample04はインタフェースを実装したクラスを仲間として扱っている例です。人間と猫の共通のクラスAnimalを用意してそこにcompareTo()メソッドを実装しています。比較の対象はクラスAnimalに限るためComparable<Animal>のようにジェネリクスを使用しています。
package chap12;
public class Example04 {
public static void main(String[] args) {
Animal[] c = { new Cat(21, 63), new Human(33, 169), new Cat(29, 55), new Human(25, 175) };
Animal max = c[0];
for (int i = 0; i < c.length - 1; i++) {
if (max.compareTo(c[i + 1]) < 0) {
max = c[i + 1];
}
}
System.out.println(max);
}
}
class Animal implements Comparable<Animal> {
int age;
int height;
public Animal(int age, int height) {
this.age = age;
this.height = height;
}
@Override
public int compareTo(Animal anotherAnimal) {
return (this.age < anotherAnimal.age) ? -1 : ((this.age == anotherAnimal.age) ? 0 : 1);
}
@Override
public String toString() {
return this.getClass() + "{ 年齢=" + age + ", 身長=" + height + "}";
}
}
class Cat extends Animal {
public Cat(int age, int height) {
super(age, height);
}
public void speak() {
System.out.println("mew mew mew");
}
}
class Human extends Animal {
public Human(int age, int height) {
super(age, height);
}
public void speak() {
System.out.println("blah blah blah");
}
}
※getClass()メソッドはクラス名を取得するメソッドです。
※繰り返しになりますが、複数のクラスを1つのファイルに書く書き方は実務では推奨しません。
<実行結果>
class chap12.Human{ 年齢=33, 身長=169} |
下図にクラス図を示します。
こうしておけば犬クラスやキリンクラスが追加されても同様に扱うことができます。
4.複数インタフェースの実装
複数インタフェースを実装している例を見てみましょう。JavaのソースコードでStringクラスを見てみます。
すると140行目に次のように定義されています。
Java17標準APIpublic final class String implements java.io.Serializable, Comparable<String>, CharSequence (中略)
このようにimplementsの後に,(カンマ)で区切って複数のインタフェースを実装することができます。
Comparableインタフェースは既にみました。Serializable, CharSequenceというインタフェースは初出ですね。Serializableインタフェースはマーカーインタフェースといって、【marker:目印】としての意味だけの特殊なインタフェースです。
Serializable インタフェース のソースコード193行目を見ても中身は空です。
Java17標準APIpublic interface Serializable { }
Serializable は中身は空ですが、ObjectOutputStream に利用できる、つまりディスクに保存したりネットワーク経由で送受信できるという特性を持っていることを示しています。この場合の【serial:連続】とは直列化ともいい、 0,1の2進数の形にすることを意味します。この後、Webアプリケーションを学ぶ方は、後でJavaBeansというテーマで再会しますので心に留めておいてください。
今回はもう一つのインタフェース、CharSequenceを標準APIで追いかけてみます。
既知のすべての実装クラス:CharBuffer, Segment, String, StringBuffer, StringBuilder
文字と文字列のところで少し紹介したStringBuilderクラスがCharSequenceインタフェースを実装しているようです。
どのような意図があるのでしょうか?
StringクラスもStringBuilderクラスもCharSequenceインタフェースを実装しているということは、どちらのクラスもCharSequenceインタフェース型の変数で扱えるということです。ですから、メソッドを作成するときに仮引数にCharSequence型を宣言しておけば、実引数としてはStringのインスタンスもStringBuilderのインスタンスも受け取ることができるのです。わざわざオーバーロードしなくても良いのです。
サンプルコードを掲載しておきます。
package chap12;
public class Example05 {
static void show(CharSequence cs) {
System.out.println(cs);
}
public static void main(String[] args) {
String str = "Hello World.";
StringBuilder strb = new StringBuilder("Goodby World.");
show(str);
show(strb);
}
}
<実行結果>
Hello World. Goodby World. |
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
以上、今回は「インタフェースで直接関係のないクラスもグループとして扱う」方法について見てきました。
インタフェースを使うことで、Javaでも実質的には多重継承が可能となります。(多重実現と表現することがあります)より、柔軟なクラス体系を作り出すとができるということが理解できたのではないでしょうか?
また、抽象クラスかインタフェースかどちらを選択するかという点でいうと、インタフェースに軍配が上がることはこれまでお読みいただいた皆さんには理解いただけるものと思います。
次回は、「例外処理で想定外の事態に強いシステムにする」です。
例外処理を使って、途中で落ちてしまわない頑強なプログラムを作っていきましょう。