「インターフェースって結局何のためにあるの?」。Javaを触り始めた頃、私も同じ壁にぶつかりました。
結論から言うと、インターフェースとはクラスが実装すべきメソッドの「型」だけを定義する契約の仕組みです。
抽象クラスと何が違うのか、implementsって書けと言われるけど中身が空っぽなのに何の意味があるのか、正直しっくり来ないまま先に進んだ記憶があります。
この記事では、interfaceの基本構文からJava 8以降のdefault/staticメソッドまでを扱います。
さらにFunction・Consumer・Supplier・Predicateといった関数型インターフェースも、実際のコード例を交えて解説します。
読み終わる頃には、interfaceを「なんとなく書く」段階から「意図を持って使う」段階に進めるはずです。
Javaインターフェースとは何か
Javaのインターフェースとは、クラスが実装すべきメソッドの「型」だけを定義し、具体的な処理内容は持たない仕組みです。
中身のない設計図、と言い換えるとイメージしやすいはずです。
実装クラスはinterfaceが約束したメソッドをすべて実装する義務を負い、その代わりに異なるクラス同士でも同じ振る舞いを保証できます。
クラスとインターフェースの違いをわかりやすく解説
クラスはフィールドとメソッドの両方を持ち、インスタンス化して直接使えます。
一方インターフェースは基本的に処理の中身を持たず、単体でインスタンス化もできません。
クラスが「実体」なら、インターフェースは「契約書」に近い存在です。実装クラスは契約書に書かれたメソッドを、すべて自分の責任で実装しなければなりません。
インターフェースのメリットと使いどころ
継承関係のないクラス同士に共通の振る舞いを持たせたいとき、インターフェースが役に立ちます。
たとえば「Dog」クラスと「Robot」クラスは継承では繋がりません。
それでもどちらも「動く」という振る舞いを持つなら、Movableインターフェースを共通実装させれば、呼び出し側は具体的な型を意識せずに扱えます。
この仕組みをポリモーフィズムと呼びます。
実際、私も新人時代に数千行のif-elseが連なる「モンスターメソッド」を保守した経験があります。
処理を1つ追加するたびに全条件分岐を目視で確認する必要があり、チーム全員がうんざりしながら作業していました。
その処理をポリモーフィズムで設計し直したところ、追加・修正にかかるコストが体感で約5分の1にまで減りました。
型による分岐が増えてきたら、インターフェース化を検討するサインだと思っています。
- 疎結合:呼び出し側が実装の詳細を知らなくて済む
- 拡張性:新しい実装クラスを追加しても既存コードに影響しない
- テストのしやすさ:モックオブジェクトへの差し替えが容易になる
Javaインターフェースの宣言とimplements実装方法
ここからは実際の構文を見ていきます。
宣言はinterfaceキーワード、実装クラス側はimplementsキーワードを使うだけなので、覚えることは多くありません。
interface宣言の基本構文
インターフェースはclassの代わりにinterfaceキーワードで宣言します。
中に書けるメソッドは基本的に抽象メソッド(処理内容を持たない宣言だけのメソッド)です。
public interface Movable {
void move();
void stop();
}
抽象メソッドにはabstract修飾子を書きませんが、暗黙的にpublic abstractとして扱われます。
フィールドを持たせることもできますが、その場合は自動的にpublic static finalの定数になる点に注意が必要です。
私はオブジェクト指向を学び始めた頃、参考書によくある「車はクラス」という例えが全く腑に落ちませんでした。
「車じゃなくてシステムが作りたいんだよ」と心の中でツッコんでいたのを覚えています。
実務でRPGシステムを設計したときに初めて、クラスやインターフェースで役割を分割する意味が実感できました。
implementsによる実装クラスの書き方
実装クラスはimplementsキーワードでインターフェースを指定し、宣言されたメソッドをすべてオーバーライドします。
1つでも実装漏れがあるとコンパイルエラーになるため、契約を守らせる強制力が働きます。
public class Dog implements Movable {
@Override
public void move() {
System.out.println("ドッグが走る");
}
@Override
public void stop() {
System.out.println("ドッグが止まる");
}
}
複数のインターフェースを実装する多重継承
Javaのクラスはextendsで1つの親クラスしか継承できませんが、implementsはカンマ区切りで複数指定できます。
これがインターフェースならではの強みです。
public class Robot implements Movable, Chargeable {
// Movableとchargeableの両方のメソッドを実装する
}
抽象クラスは単一継承しか許されないので、複数の役割を1つのクラスに持たせたいときはインターフェースが唯一の選択肢になります。
default・staticメソッド(Java 8以降の拡張)
Java 7までのインターフェースは抽象メソッドしか持てませんでしたが、Java 8以降はdefaultメソッドとstaticメソッドが追加され、処理の中身を持たせられるようになりました。
defaultメソッドで共通処理を持たせる
defaultメソッドは、実装クラス側でオーバーライドしなくても使えるデフォルトの処理を持たせられるメソッドです。
既存のインターフェースにメソッドを追加しても、すべての実装クラスを一斉に修正しなくて済みます。
public interface Movable {
void move();
default void announce() {
System.out.println("移動を開始します");
}
}
staticメソッドの使いどころ
staticメソッドはインターフェース名から直接呼び出せるユーティリティメソッドです。
実装クラスに関係なく共通で使う処理をまとめたいときに向いています。
実際、Java標準ライブラリのComparator.comparing()もこの仕組みで提供されています。
関数型インターフェースの基本
抽象メソッドを1つだけ持つインターフェースを関数型インターフェースと呼びます。
この制約があるおかげで、ラムダ式を使って簡潔に実装を渡せるようになります。
@FunctionalInterfaceアノテーションの役割
@FunctionalInterfaceは、そのインターフェースが抽象メソッドを1つだけ持つことをコンパイラにチェックさせるアノテーションです。
付けなくても動作は変わりませんが、うっかり2つ目の抽象メソッドを追加してしまうミスをコンパイル時点で防げます。
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b);
}
ラムダ式との組み合わせ方
関数型インターフェースはラムダ式の受け皿になります。無名クラスで長々と書いていた処理が、1行のラムダ式で表現できるようになるのが最大のメリットです。
Calculator add = (a, b) -> a + b;
System.out.println(add.calculate(3, 5)); // 8
標準API 4大関数型インターフェース
java.util.function パッケージには、自作しなくても使える汎用的な関数型インターフェースが用意されています。
中でもFunction・Consumer・Supplier・Predicateの4つは登場頻度が高く、StreamAPIでも頻繁に使われます。
Function・Consumerの使い分け
Functionは引数を受け取り値を返す変換処理、Consumerは引数を受け取るだけで値を返さない処理に使います。
「変換するか、消費するだけか」で選ぶと迷いません。
| インターフェース | メソッド | 用途 |
|---|---|---|
| Function<T,R> | R apply(T t) | T型を受け取りR型を返す変換処理 |
| Consumer<T> | void accept(T t) | T型を受け取り値は返さない消費処理 |
Function<Integer, Integer> square = n -> n * n;
Consumer<String> printer = s -> System.out.println(s);
Supplier・Predicateの使い分け
Supplierは引数を受け取らず値を供給するだけの処理、Predicateは引数を受け取ってtrue/falseの判定結果を返す処理です。
SupplierはgetSomething()のような値の遅延生成、Predicateはfilter()での条件判定によく使われます。
Supplier<String> greeting = () -> "こんにちは";
Predicate<Integer> isEven = n -> n % 2 == 0;
この4つを覚えておくだけで、StreamAPIのmap()・forEach()・filter()に渡す処理の型が何を意味しているのか、迷わず読めるようになります。
抽象クラスとの違いは?
「じゃあ抽象クラスと何が違うの」という疑問は、interfaceを学ぶ人が必ず通る道です。ここでは要点だけ整理しておきます。
使い分けの判断基準
最大の違いは多重実装ができるかどうかです。抽象クラスは単一継承しか許されない一方、インターフェースは複数実装が可能です。
「is-a」の強い関係で共通の実装も持たせたいなら抽象クラス、「〜できる」という振る舞いの契約だけ課したいならインターフェースを選びます。
より詳しい比較や判断フローは、Javaの抽象クラス解説記事で整理しているので、こちらもあわせて読んでみてください。
よくある質問
インターフェースはフィールドを持てますか?
持てますが、自動的にpublic static finalの定数になります。インスタンスごとに異なる値を持つフィールドは定義できません。
コンストラクタは定義できますか?
定義できません。インターフェースは単体でインスタンス化できないため、コンストラクタという概念自体が存在しません。
privateメソッドは使えますか?
Java 9以降であれば使えます。default/staticメソッド同士で共通処理をまとめたいときに、インターフェース内部だけで使うprivateメソッドとして定義できます。
2つのインターフェースで同名のdefaultメソッドが競合したらどうなりますか?
コンパイルエラーになります。実装クラス側で該当メソッドを明示的にオーバーライドし、どちらの処理を使うか(あるいは独自の処理を書くか)を指定する必要があります。
インターフェースと抽象クラス、どちらを先に学ぶべきですか?
インターフェースから学ぶのがおすすめです。構文がシンプルで、implementsによる実装の強制力を先に理解しておくと、抽象クラスの共通処理を持たせる仕組みとの違いも掴みやすくなります。
インターフェースを使わずに設計するとどうなりますか?
クラスごとに型を意識した分岐処理(if文やswitch文)が増え、実装クラスを追加するたびに呼び出し側のコードも修正が必要になります。振る舞いの契約をインターフェースで切り出すことで、この修正範囲を最小限に抑えられます。
まとめ|インターフェースを使いこなす3つのポイント
最後に、この記事の要点を3つに絞ってまとめます。
- 契約としての役割:interfaceはメソッドの型を約束し、実装クラスに実装義務を課す
- 多重実装の強み:抽象クラスにはできない複数インターフェースの同時実装ができる
- 関数型インターフェース:Function/Consumer/Supplier/Predicateを押さえればStreamAPIの理解が一気に進む
私自身、最初はinterfaceの空っぽさに戸惑いましたが、実装を強制する「契約」だと捉え直してから、コードの見通しがぐっと良くなりました。
型で分岐を書くか迷ったときは、「振る舞いの契約として切り出せるか」を判断基準にするのがおすすめです。
まずは1つ、自分のコードでMovableのようなシンプルなインターフェースを作ってみることから始めてみてください。
