【Java入門】第14回 interface(インターフェイス)|定義と実装クラスの利用
2024.06.28
Javaではclass(クラス)に似た、interface(インターフェイス)と呼ばれるオブジェクトを利用してプログラムを作成することが多くあります。当記事では、interfaceの仕組みと利用の基本について説明します。
目次
Javaのinterface
一般的な「インターフェイス」とは
一般的な単語としてのインターフェイスは、元々は「境界面」や「接点」を意味する英単語(interface)で、異なる2つのものを繋ぐ部分などを指す言葉です。
例えば電子機器では、パソコンでマウスやキーボードを接続するポート、スマートフォンの充電ポート(USBポート)などもインターフェイスの1つですし、人と電子機器の間ではパソコンの画面とマウスや、スマートフォンのタッチパネルなどもインターフェイスの1つです。
また、機器と機器をつなぐ端子(上記のポートなど)を「ハードウェアインターフェイス」、マウスやキーボードの機器や、Webページやスマートフォンのアプリ上のボタンなど、人が操作するためのパーツを「ユーザーインターフェイス(UI)」と呼びわける場合もあり、「インターフェイス」という言葉が表す範囲は非常に大きく、ピンと来ないかもしれません。
今回の記事ではJava言語における「interface」について説明していきますが、機器と機器、人と機器を問わず、これら上記の「接点」が「インターフェイス」であるということを、なんとなく把握しておいてください。
※Javaのinterfaceの呼び方は、様々な本やサイトで「インタフェース」「インターフェース」「インターフェイス」など若干ブレがありますが、これらは同じものを指しています。
※以降、当記事でのみ、一般的なインターフェイスはカタカナで、Javaにおけるinterfaceを指す場合はアルファベットで「interface」と記述します。(他のJava入門の記事では「クラス」と「インターフェイス」と記述します)
Javaのinterface
Java言語におけるinterfaceの役割は、Javaのプログラムとプログラム間(クラスとクラス間)を結ぶためのインターフェイスを準備することです。
基本的にinterfaceでは、呼び出し口の形(機器における規格のようなもの)を定めることが目的になります。ここでは、指定した音階の音を鳴らすプログラムのインターフェイスとして、Gakkiというinterfaceを定義してみます。
public interface Gakki {
// 引数の音階の音を鳴らす という処理の呼び出し口を定義する
void sound(int scale);
}
interfaceである Gakki は、音を鳴らす処理の呼び出し口である「sound」を定義していますが、どのような音が鳴るのかは、Gakki interfaceを備えたクラスで準備してもらう必要があります。
下図の例では、Gakki を実装した様々な楽器の音を鳴らすためのクラスを作成しています。楽器クラスは、そのクラスにあった音を鳴らす処理(sound)を、各々が準備する必要があります。
このように、interfaceの処理を備えて利用できるようにしたクラスのことを、interfaceの「実装クラス」と呼びます。
interfaceはあくまで「処理の呼び出し口」を定義するだけで、クラスのように実体を持たないため、インスタンスを生成することはできません。(変数の型としては宣言できます)
interfaceとして利用する場合も、かならず実装クラスのインスタンスを利用することになります。
Gakki d = new Drum();
Gakki g = new Guitar();
Gakki p = new Piano();
Gakki t = new Trumpet();
Gakki v = new Violin();
変数の型をinterfaceの型で宣言することで、実体のクラスが何であっても「Gakki」を備えているクラスであれば、soundメソッドを呼び出すことで音を鳴らす処理を実行することができます。
// 詳細はわからないが、何らかのGakkiを返す処理の結果を受け取る
Gakki anyGakki = getAnyGakki();
// Gakkiであれば、指定した高さの音を鳴らすことはできる(どんな音が鳴るかは実体のクラス次第)
anyGakki.sound(1);
このようにすることで、Gakki で曲を演奏するプログラムを作成しておけば、受け取る Gakki の実体が変わっても、同じ曲を演奏することができるようになります。
また曲が一緒でも、利用したクラスによって音が違うように、interfaceとクラスの実装を分離することで、動作が実際のクラスによって変わることを多態性(ポリモーフィズム)と呼びます。これも、interfaceで実現できることの1つです。
interfaceに定義できるもの
メソッド
interfaceには、以下のメソッドを定義することができます。
・抽象メソッド
実装を持たない(動作が定義されていない)基本のメソッド。
interfaceに定義された抽象メソッドは、その実装クラスで全てオーバーライドする必要があります。
・defaultメソッド(Java8以降)
デフォルトの動作を定義したメソッド。
実装クラスでオーバーライドしない場合、interfaceに定義された動作となります。
また、実装クラスでオーバーライドすることも可能です。
・privateメソッド(Java9以降)
メソッドを定義したinterface内でのみ呼び出しが可能なメソッド。
実装クラスでinterfaceのprivateメソッドをオーバーライドすることは可能ですが、interfaceのdefaultメソッドで呼び出す場合はオーバーライドの影響を受けません。
メンバ変数
interfaceのフィールドにはメンバ変数を定義することはできますが、クラスのメンバ変数のように値を書き換えることはできません。(修飾子を設定しなくても、自動的に public static final の修飾子が設定されている状態になります)
このため、interfaceに宣言したメンバ変数は、必ず定数(値が変更されないもの)として扱われることになります。
interfaceの利用
クラスでの実装(implement)
前述のとおり、interfaceを利用するためには interface を実装(implement)したクラスを作成する必要があります。
実装クラスではinterfaceに定義された抽象メソッドを全てオーバーライドして、クラスが実行する処理を実装しなくてはなりません。
interfaceに定義された抽象メソッドの実装が不足している場合はコンパイルエラーとなります。
interface:
public interface ExampleInterface {
String getClassName(); // 型と名前だけ定義されている抽象メソッド
String getInterfaceName(); // 型と名前だけ定義されている抽象メソッド
}
実装クラス:
抽象メソッドの処理を全て実装します。
public class ExampleClass implements ExampleInterface {
@Override
public String getClassName() {
return "ExampleClass"; // 処理を実装(文字列「ExampleClass」を返却)
}
@Override
public String getInterfaceName() {
return "ExampleInterface"; // 処理を実装(文字列「ExampleInterface」を返却)
}
}
※Overrideアノテーション(@Override)は記述しなくても実行時に動作に差はありませんが、当記事ではオーバーライドであることを明示的に示すため記述するようにします。
実装クラスである ExampleClass クラスでは、ExampleInterface に定義された抽象メソッド「getClassName」と「getInterfaceName」に、実際の処理を実装しています。
「interfaceを実装したクラス」を継承したクラスは、明示的にimplement句で指定しなくても、スーパークラスが実装しているinterfaceの実装クラスになります。
また、継承したサブクラスがさらにメソッドをオーバーライドしたり、さらに別のinterfaceを追加で実装することも可能です。
継承したクラス(サブクラス):
ExampleClass のサブクラス ExampleClassExt で、getClassName メソッドをオーバーライドしています。
public class ExampleClassExt extends ExampleClass {
// getClassName メソッドだけオーバーライド
@Override
public String getClassName() {
return "ExampleClassExt";
}
}
ExampleClassExt クラスのプログラムコード上にはinterface(ExampleInterface)についての記述は有りませんが、実装クラスであるExampleClass を継承しているので、ExampleClassExt クラスも ExampleInterface の実装クラスになります。
defaultメソッド
Java8以降では、defaultキーワードを設定することで、interfaceにデフォルトの動作(クラスでオーバーライドしない場合の動作)を記述することができるようになりました。
/**
* InterfaceXaインターフェイス
*/
interface InterfaceXa {
// testメソッド(デフォルト実装なし)
void test();
// printメソッド(デフォルト実装有り)
default void print() {
System.out.println("これはInterfaceXaのdefault実装です。");
}
}
実装クラスを準備し、抽象メソッド test を実装します。
print メソッドはinterfaceにdefaultメソッドが準備されているため、実装クラスではオーバーライドしなくても呼び出すことができています。
/**
* ClassXaクラス
* InterfaceXaを実装するクラス
*/
public class ClassXa implements InterfaceXa {
// testメソッドをオーバーライドして実装する
@Override
void test() {
System.out.println("testを開始します。");
print(); // InterfaceXaにdefault実装があるので、オーバーライドしなくても利用できる
System.out.println("testを終了します。");
}
}
作成した実装クラスを用いて、それぞれのメソッドを呼び出してみます。
実装クラスでオーバーライドした test メソッド、オーバーライドしなかった print メソッドのどちらも実行できることが分かります。
オーバーライドしなかった print メソッドは、interfaceに記述した処理が実行されています。
/**
* 実装クラス(ClassXa)の動作確認用Mainクラス
*/
public class ClassXaMain {
/**
* mainメソッド
* @param args
*/
public static void main(String[] args) {
// InterfaceA型の変数として宣言が可能
InterfaceXa xa = new ClassXa();
// testメソッドを実行
xa.test();
System.out.println(""); // 実行結果を分けるために空行を出力
// printメソッドも呼び出し可能
xa.print();
}
}
実行結果:
testを開始します。
これはInterfaceXaのdefault実装です。
testを終了します。
これはInterfaceXaのdefault実装です。
privateメソッド
Java9以降のバージョンでは、interface内にprivateメソッドを記述することが可能になりました。
このメソッドは、実装クラスや継承したinterfaceからは不可視(呼び出し不可)となるため、同一interfaceに定義されたdefaultメソッドまたはprivateメソッドからのみ、実行可能なメソッドとなります。
以下の例では、defaultメソッドの中から同じinterface内のprivateメソッドを呼び出しています。
/**
* InterfaceXbインターフェイス
*/
interface InterfaceXb {
// printメソッド(デフォルト実装有り)
default void print() {
String msg = getPrintMessage(); // Interface内のprivateメソッドを実行
System.out.println(msg);
}
// privateなメソッド(interface内のdefaultメソッドからのみ実行可能)
private String getPrintMessage() {
return "これはInterfaceXbのdefault実装です。";
}
}
実装クラスを準備します。(interfaceに抽象メソッドが定義されていないので、オーバーライドするメソッドを記述していません)
/**
* ClassXbクラス
* InterfaceXbを実装するクラス
*/
public class ClassXb implements InterfaceXb { }
作成したクラスを利用して、printメソッドを実行してみます。
interfaceに準備されたdefalutメソッドが呼び出され、その中でprivateメソッドが実行されています。
また、実装クラスのインスタンスからは、interfaceのprivateメソッド(getPrintMessage)は実行できません。
/**
* 実装クラス(ClassXb)の動作確認用Mainクラス
*/
public class ClassXbMain {
/**
* mainメソッド
* @param args なし
*/
public static void main(String[] args) {
InterfaceXb xb = new ClassXb();
xb.print();
//String msg = xb.getPrintMessage(); ← getPrintMessageは不可視(実行不可)
}
}
実行結果:
これはInterfaceXbのdefault実装です。
クラスでの実装(複数interfaceの同時実装)
クラスは1つのクラスしか継承できませんが、interfaceは同時に複数個のinterfaceを実装することが可能です。
複数のinterfaceを実装するクラスは、対象のinterfaceに定義されている抽象メソッドを全て実装する必要があります。このとき、複数のinterfaceに同一の抽象メソッド(型、名前、引数が全て一致するメソッド)が定義されていた場合は、1つだけ実装します。
以下の例では、「Mobilephone」と「TabletPC」の2つのinterfaceを「Smartphone」クラスが実装しています。
/**
* Mobilephone インターフェイス
*/
interface Mobilephone {
// 電源ボタン動作
void power(boolean longPress);
// 時刻表示
void dispTime();
// 電話をかける(架電)
void call(String phoneNo);
// 電話をうける(受話)
void receive();
}
/**
* TabletPC インターフェイス
*/
interface TabletPC {
// 電源ボタン動作
void power(boolean longPress);
// 時刻表示
void dispTime();
// アプリを起動
void launchApp(String appName);
}
これらの2つのinterfaceでは、「boolean1個を引数とするvoid 『power』」と「引数なしのvoid『dispTime』」という同一の抽象メソッドがどちらのクラスにも定義されていますが、実装クラスであるSmartphoneクラスでは、それぞれ1つずつ実装します。
/**
* Smartphoneクラス
* Mobilephone、TabletPCの両interfaceを実装する
*/
public class Smartphone implements Mobilephone, TabletPC {
// 電源ボタン動作(Mobilephone、TabletPC 共通のメソッド)
@Override
public void power(boolean longPress) {
// Smartphoneでの電源ボタン動作を実装する
if(longPress) {
// (例)端末の電源ON/OFF処理を実装
} else {
// (例)画面のバックライトON/OFF処理を実装
}
}
// 時刻表示(Mobilephone、TabletPC 共通のメソッド)
@Override
public void dispTime() {
// Smartphoneでの時刻表示動作を実装する
}
// 電話をかける(発信)動作(Mobilephone のメソッド)
@Override
public void call(String phoneNo) {
// Smartphoneでの発信動作を実装する
}
// 電話をうける(受話)動作(Mobilephone のメソッド)
@Override
public void receive() {
// Smartphoneでの受話動作を実装する
}
// アプリを起動する動作(TabletPC のメソッド)
@Override
public void launchApp(String appName) {
// Smartphoneでのアプリ起動動作を実装する
}
}
Smartphoneクラスでの各メソッドの実装のイメージは、下図のようになります。
Smartphoneクラスでは、「power」メソッドと「dispTime」メソッドは2つのinterfaceに対して共通の実装となるため、どちらのinterface型の変数から呼び出しても同じ処理が実行されます。
Mobilephone phone = new Smartphone();
phone.dispTime(); // MobilephoneとしてdispTImeを呼び出し
TabletPC pc = new Smartphone();
pc.dispTime(); // TabletPCとしてdispTImeを呼び出し
interfaceの継承
Javaのinterfaceは、クラスと同様に継承する(サブインターフェイスを作成する)ことが可能です。interfaceを継承する場合、クラスと異なり複数個同時に継承することができます。
interfaceの継承は、クラスの継承と同様にextends句を利用して、継承元のinterface名を記述します。
interface InterfaceEx extends InterfaceXa;
複数個のinterfaceを同時継承する場合は、継承するinterfaceをカンマ区切りで記述します。
interface InterfaceDual extends InterfaceXa, InterfaceXb;
ただしクラスでの実装と同じく、型が異なり同じ名前と引数のメソッドがあるinterface同士は、同時に継承することはできません。
また、同一メソッドでdefaultで実装されているメソッド(名前&引数)があるinterface同士の場合、どちらのdefaultメソッドを引き継ぐか判断できないため、継承したinterfaceでオーバーライドすることが必要になります。
interface InterfaceEn {
// greet のdefaultメソッド
default String greet() {
return "Hello";
}
}
interface InterfaceJp {
// greet のdefaultメソッド
default String greet() {
return "こんにちは";
}
}
上記2つのinterfaceは「greet」のdefaultメソッドを実装しているため、両方継承すると処理が競合してしまうため、2つのinterfaceを継承する側でオーバーライドして競合を解消させる必要があります。
A)継承元のinterfaceとは関係なく、新たなdefaultメソッドの実装としてオーバーライドする。
interface InterfaceDual extends InterfaceEn, InterfaceJp {
// defalutメソッドを新しい処理("Ciao"を返却)として実装
@Override
default String greet(){
return "Ciao";
}
}
B)defaultメソッドとしてオーバーライドし、その中で呼び出すことで、いずれかのinterfaceのdefaultメソッドを実質的に継承させる。
※superで呼びさせる範囲は親interfaceまで(親の親以上は不可視になります)
interface InterfaceDual extends InterfaceEn, InterfaceJp {
// defalutメソッドとして、InterfaceEnのdefaultメソッドを実行
@Override
default String greet(){
return InterfaceEn.super.greet(); // Hello
}
}
C)抽象メソッドとしてオーバーライドする。
継承元のinterfaceのdefaultメソッドの実装を破棄して、実装をクラスに委ねる形となります。
interface InterfaceDual extends InterfaceEn, InterfaceJp {
// greet を新たに抽象メソッドとしてオーバーライド(継承元のdefaultメソッドを隠蔽)
@Override
String greet();
}
代表的なinterface
List (java.util.List)
List は、指定した型のオブジェクトを複数個、挿入順(インデックス)とともに保持することができるinterfaceの定義です。
特にList interfaceの実装クラスである ArrayList は、配列と異なり個数が可変であり、オブジェクトの除去や置き換えなどの機能も準備されているため、実践でも非常に高い頻度で利用されています。
下記の例では、List の実装クラスである ArrayList と LinkedList において、List に定義されているメソッド(add、get、size など)がどちらの実装クラスでも呼び出せることがわかります。
List<String> arrList = new ArrayList<String>();
List<String> lnkList = new LinkedList<String>();
// add Listの末尾(一番後ろ)の位置に、オブジェクトを格納
arrList.add("Hello");
lnkList.add("Good morning");
// get 指定したindexの位置のオブジェクトを取得
System.out.println(arrList.get(0)); // Hello
System.out.println(lnkList.get(0)); // Good morning
// size 現在保持しているオブジェクトの数を返す
int arrSize = arrList.size(); // 1
int lnkSize = lnkList.size(); // 1
Map (java.util.Map)
Map は、指定した型のオブジェクトを、キーとなるオブジェクトとともに保持することができるinterfaceの定義です。
下記の例のとおり、こちらもMap の実装クラスである HashMap と TreeMap において、Map に定義されているメソッド(put、get など)がどちらの実装クラスでも呼び出せます。
Map<String, String> hsMap = new HashMap<String, String>();
Map<String, String> trMap = new TreeMap<String, String>();
// put キーを指定してオブジェクトを格納
hsMap.put("EN", "Hello"); // キー:"EN" オブジェクト:"Hello"
trMap.put("JP", "こんにちは"); // キー:"JP" オブジェクト:"こんにちは"
// get キーを指定して格納されているオブジェクトを取得
System.out.println(hsMap.get("EN")); // Hello
System.out.println(trMap.get("JP")); // こんにちは
※List、Mapなどの詳細については、次回記事「配列とコレクション」で説明します。