この記事では、当社 の新人エンジニア研修の参考にJava8を解説します。
前回は抽象クラスについて解説しました。
今回はインターフェースについて解説します。
インターフェースが何のためにあるのかと言えばポリモーフィズムのためです。
まずはそのことを念頭においてください。
インターフェースも抽象クラス同様インスタンス化できません。
しかし、抽象クラスとは違った意味で巧妙な役割が与えられています。
インターフェースを考える際にはJavaでは多重継承が禁じられていたことを思い出す必要があります。
Javaではスーパークラスを継承するサブクラスの数に制限はありませんでした。
しかし、サブクラスから見てスーパークラスは一つだけと決まっていました。
これがJavaでは多重継承が禁じられているということの意味です。
では、継承関係にかかわらずポリモーフィズムを使いたいときにはどうしたらいいのでしょうか?
そうです。
インターフェースを使います。
そもそもinterfaceとは接点のことです。
人間とコンピュータとの接点をman – machine interface と言ったりしますね。
あるいは、パソコンと周辺装置のインターフェースという表現もあります。
例えば、USBというインターフェースを例にとりましょう。
USBの接続口がある製品は何でも、パソコンでもプリンタでもスキャナーでも相互に接続できます。
なぜなら、お互いに一定のルールを内包して共有しているからです。
Javaのインターフェースのイメージもこのルールを内包して共有するというところにあります。
同じインターフェースを組み込むことでクラスを共通ルールのもとに扱うことができるようになるのです。
1.Comparableインターフェース
例によりJava8のAPIに使用例の範を求めましょう。
前回も使ったIntegerクラスに再登場願います。
IDEでIntegerクラスの宣言を見てみましょう。
public final class Integer extends Number implements Comparable<Integer> { (以下省略) |
「implements Comparable」という文字列が見えます。
※後ろの<Integer>というのはGenerics:総称型というもので後述します。
この部分がインターフェースを使っている(実装している、といいます)部分です。
英語のimplemenには実装という意味があります。
Comparableの文字列の先頭文字が大文字になっています。
クラス同様インターフェースも先頭は大文字です。
compare:比較 にable:できる という意味の接尾語がついています。
比較できるためのインターフェースを実装しているわけですね。
比較できるというルールを内包して、共有しているわけです。
ちなみにクラスとインターフェースを名前だけで判別する際にこの接尾語に注目してableがついていたらインターフェースと考えると良いでしょう。
ただし、すべてのインターフェースにableがついているわけではないので注意しましょう。
では、次にComparableをAPIから検索してみます。
するとこのように書かれています。
このインタフェースを実装する各クラスのオブジェクトに全体順序付けを強制します。
「全体順序付けを強制します」とあります。
整数値にせよ、文字列にせよ、インスタンスを順序付けできないと並べ替え等ができませんからこれは大切なインターフェースですね。
しかし、equalsメソッドのところでも似たような議論がありましたが、何をもって順序を前、後ろと決めるのかという問題があります。
例えば、この新入社員研修のクラス一つとってみても、順序付けの基準は無限に考えられます。
名前のあいうえお順、身長順、年齢順、成績順、などなど。
そこで、Comparableインターフェースでは、比較するという抽象メソッドのみを決めて、実装は個々のクラスに任せるのです。
IDEを使ってComparableインターフェースの中身を見ていくと一番下に以下のような記述があります。
public int compareTo(T o);
()の後ろについている{}がありません。※私の環境では136行目でした。
中身のない抽象メソッドなのです。
このとき、抽象クラスの時に学んだabstractキーワードがついていないことに着目してください。
インターフェースでは、メソッドは基本、抽象メソッドになります。
※Java8からdefaultメソッドというものも使えるようになったのですが、本研修では触れません。
また、仮にpublicがついていなくてもpublicなメソッドになります。
インターフェースに定める抽象メソッドは暗黙的にpulblicで修飾されまるのです。
なぜなら、インターフェースは外部に公開することを目的としているものですので公開を制限するのはおかしいのです。
Comparableインターフェースを実装したクラスでは比較できるという性質を具体的に記述することが必要です。
では、このcompareToメソッドをオーバーライドしている個所をIntegerクラスから探してみましょう。
私の環境では、1,213行以降に次のような記述がありました。
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}(中略)
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
compareToメソッドの中でcompareメソッドを呼んでいます。
compareメソッドの中身の三項演算子はxとyを比較して
yが大きければ-1を返す
同じであれば01を返す
xが大きければ1を返す
ということをしているだけです。
では、そのことを確認するサンプルプログラムを書いてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Example40 { 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 |
確かにそうなっていますね。
ですので、(実用性はともかく)以下のように最大値を求めるプログラムが書けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Example40_1 { public static void main(String[] args) { Comparable[] c ={new Integer(10),new Integer(20), new Integer(1500), new Integer(202)}; Integer max = 0; for (int i = 0; i < c.length-1; i++) { if(c[i].compareTo(c[i+1])<0){ max = (Integer)c[i+1]; } } System.out.println(max); } } |
ここで着目していただきたいのは4行目です。
Comparable型の配列を宣言しています。
つまり、この配列にはComparableインターフェースを実装しているクラスのインスタンスであればなんでも入れることができます。
2.インターフェースのメソッドをオーバーライドする
例えば、Studentの年齢の最大値を求めるには以下のようなサンプルプログラムが考えられます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public class Example40_2 { public static void main(String[] args) { Comparable[] c = {new Student(21, 173), new Student(20, 169), new Student(19, 180), new Student(25, 175)}; Student max = null; for (int i = 0; i < c.length - 1; i++) { if (c[i].compareTo(c[i + 1]) < 0) { max = (Student) c[i + 1]; } } System.out.println(max); } } 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=" + age + ", height=" + height + '}'; } } |
<結果>
Student{age=25, height=175} |
そして、30行目ですが、実は戻り値は-1,0,1に限定されず、2つの値の大小がプラスマイナスで戻ればよいため、
return this.age – as.age; |
とシンプルに書いても同じことになります。
では、一番身長の高い人を見つけるにはどこをどのように変更すればよいでしょうか?
試してみてください。
ちなみに、equalsメソッドがObjectクラスのメソッドをオーバーライドして使うものだったのに対して、
CompareToメソッドはComparableインターフェースを実装して使うというところにこの2つの処理の適用範囲が表れていて興味深いですね。
つまり、等しいかどうかというのはすべてのクラスに共通の問題ですが、順序付けに関してはそれを必要としないクラスもあるということです。
3.インターフェースを使ったポリモーフィズム
インターフェースを使ってポリモーフィズムを実現することができます。
サブクラスは一種のスーパークラスだったので、スーパークラスの変数にサブクラスのインスタンスを参照させることができました。
同じように、インターフェース型の変数にインターフェースを実装したクラスのインスタンスを参照させることができるのです。
目的は継承の時と同様に、ポリモーフィズムや変数の仮引数で大は小を兼ねるときに使います。
例えば、人間クラスと猫クラスのインスタンスが混在している配列から年齢の一番高いインスタンスを見つけます。
ただし、直接人間と猫のようにクラスが違うものをcompareToメソッドで比較するとキャストが頻発して現実的ではありません。
ここでは、人間と猫の共通のクラスAnimalを用意してそこにcompareToメソッドを実装しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public class Example40_3 { public static void main(String[] args) { Comparable[] c = {new Cat(21, 63), new Human(20, 169), new Cat(29, 55), new Human(25, 175)}; Comparable max = null; for (int i = 0; i < c.length - 1; i++) { if (c[i].compareTo(c[i + 1]) < 0) { max = c[i + 1]; } } System.out.println(max); } } abstract class Animal implements Comparable{ int age; int height; public Animal(int age, int height) { this.age = age; this.height = height; } @Override public int compareTo(Object anotherAnimal) { Animal a = (Animal)anotherAnimal; return (this.age < a.age) ? -1 : ((this.age == a.age) ? 0 : 1); } @Override public String toString() { return this.getClass()+ "{ age=" + age + ", height=" + 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メソッドはクラス名を取得するメソッドです。
<結果>
class Cat{ age=29, height=55} |
4.複数インターフェースの実装
複数インターフェースを実装している例を見てみましょう。
JavaのAPIの中からStringクラスを見てみます。
すると
public final class String
extends Object
implements Serializable, Comparable<String>, CharSequence
となっていますね。
Comparableインターフェースはすでに見ましたが、Serializable, CharSequenceというインターフェースが加わっています。
※Serializable インタフェースはマーカーインタフェースといって、marker:目印としての意味だけの特殊なインターフェースです。マーカーインターフェースの役割はJava5以降アノテーションが担っているのでここでは説明を割愛します。Serializable はメソッドを一つも持ちませんが、ObjectOutputStream に利用できる、つまりディスクに保存したりネットワーク経由で送受信できるという特性を持っていることを示しています。この場合のserial(連続)とは直列化ともいい、 0,1の2進数の形にすることを意味します。
このようにimplementsの後に,(カンマ)で区切って複数のインターフェースを実装することができます。
今回はCharSequenceというインターフェースを追いかけてみます。
既知のすべての実装クラス:CharBuffer, Segment, String, StringBuffer, StringBuilder
すると文字と文字列のところで少し紹介したStringBuilderクラスがCharSequenceインターフェースを実装しているようです。
どのような意図があるのでしょうか?
StringクラスもStringBuilderクラスもCharSequenceインターフェースを実装しているということは、
どちらのクラスもCharSequenceインターフェース型の変数で扱えるということです。
ですから、メソッドの仮引数にCharSequence型を宣言しておけば実引数としてはStringのインスタンスもStringBuilderのインスタンスも受け取ることができるのです。
わざわざオーバーロードしなくても良いのです。
ここまで理解できたら練習問題をやってみましょう。
今回はインターフェースについて見てきました。
インターフェースを使うことで、Java8でも実質的には多重継承が可能となります。
より、柔軟なクラス体系を作り出すことができるということが理解できたのではないでしょうか?
また、抽象クラスかインターフェースかどちらを選択するかという点で言うと、インターフェースに軍配が上がることはこれまでお読みいただいた皆さんには理解いただけるものと思います。
次回のテーマは例外処理です。
例外処理を使って、途中で落ちてしまわない頑強なプログラムを作っていきましょう。
JavaSE8の解説に戻る