interface で契約を定義

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

interface で契約を定義 とは

Java の interface を使って「契約」だけを定義し、implements で複数のクラスに同じ振る舞いを約束させる方法を学ぼう。

interface とは何か ─ 「契約」を書く仕組み

ここまでのレッスンで、class を使って「データ + 振る舞い」を一緒に包む方法を学んできました。Bank クラスや Product クラスを作って、フィールドとメソッドを並べる、というやり方です。しかし、現場の Java を眺めていると class と並んでもう一つよく登場するキーワードがあります。それが interface です。

interface は、ひとことで言えば 「メソッドのシグネチャ (=名前と引数と戻り値) だけを並べた契約書」 です。中身の実装は書かず、「このクラスを名乗るなら、こういうメソッドを必ず持っていてね」という約束ごとだけを定義します。JavaAndroid アプリを作る場面でも、SpringWeb API を組む場面でも、Comparable Runnable List Map など、いたるところに interface が顔を出します。

Java

interface Runnable2 { String name(); }

このコードを読むと「name() というメソッドを持っているはず」とだけ書いてあり、return がありません。interface はそれだけで完結していて、{ の中身は 後から実装するクラスが書く ことになります。

interface の最大の価値は「中身を後回しにできる」ことです。先に「こういうものが欲しい」と契約だけ書いておけば、誰がどう実装しようと利用側のコードは変えなくて済みます。これは大規模開発でチーム分担をするときに、ものすごく効いてきます。

classinterface の違い

両者の違いを表にまとめると、見通しが良くなります。

観点classinterface
キーワードclassinterface
インスタンス化new できる直接 new できない
フィールド自由に持てるpublic static final のみ
メソッドの中身書く原則書かない (default は例外)
多重○○クラスは 1 つしか extends できない複数 implements できる

つまり class は「実体を作る型」、interface は「型の名札 / 契約」と覚えるのが分かりやすいです。Java には「クラスの多重継承は禁止」というルールがありますが、interface を複数 implements するのは自由なので、機能の組み合わせは interface で表現するのが王道になります。

implements で契約を実装する

interface を実際に使うには、それを 実装する クラスを書きます。書き方は class クラス名 implements インターフェース名 です。extends ではなく implements を使うのがポイントで、ここを間違えると即コンパイルエラーになります。

Java

interface Runnable2 { String name(); } class MyRunner implements Runnable2 { @Override public String name() { return "MyRunner"; } }

上のコードでは、MyRunner クラスが Runnable2 という契約を引き受け、name() の中身を "MyRunner" を返すように書いています。@Override アノテーションは「これは親の契約を満たすメソッドですよ」というマークで、書いておくとタイポしたときにコンパイラが教えてくれるので強く推奨されます。

実装メソッドは必ず public で書く約束です。interface 側で public と書かれていなくても、Java は自動的に public abstract として扱うため、実装側で privateprotected下げる ことはできません。

多重実装 ─ interface の真骨頂

classextends できる親クラスが 1 つだけですが、interface はコンマで区切ってカンマ区切りで いくつでも implements できます。これは現実世界の「役割」を表現するのにとても便利です。

Java

interface Runnable2 { String name(); } interface Printable { void printInfo(); } class MyTask implements Runnable2, Printable { @Override public String name() { return "MyTask"; } @Override public void printInfo() { System.out.println("Task: " + name()); } }

MyTask は「実行できる」役割と「情報を表示できる」役割の両方を兼ね備えています。実装側で約束されたメソッドを すべて 書かないとコンパイルエラーになるので、抜け漏れの心配もありません。

diagram (will load when visible)

UML 風に書くと、interface は破線で表現します。MyTask のように 2 本の破線が伸びていれば「両方の契約を引き受けている」ことが一目で分かります。

default メソッドと static メソッド ─ Java 8 以降の進化

古い interface は「中身を絶対に書かない純粋な契約書」でしたが、Java 8 以降では default キーワードを付けることで interface の中にメソッドの中身も書けるようになりました。これは既存の interface にメソッドを後から足したいとき、すでに実装している全クラスを壊さずに済むよう導入された仕組みです。

Java

interface Runnable2 { String name(); default String greet() { return "Hello from " + name(); } static Runnable2 of(String n) { return () -> n; } }

default メソッドは、実装クラス側で オーバーライドしてもしなくても OK という緩い扱いです。static メソッドは interface 名から直接呼び出すユーティリティで、Runnable2.of("X") のように使います。Comparator.comparing(...) などはこの仕組みを使った代表例です。

default メソッドを乱用すると「interface なのか abstract class なのか分からない設計」になりがちです。あくまで「契約 + 共通実装の補助」として節度を持って使うのが現代の作法です。

よくある間違い

初学者が interface で踏むつまずきを 3 つ並べます。先回りして知っておきましょう。

  • extendsimplements の取り違え — クラスがクラスを継承するときは extends、クラスが interface を実装するときは implements です。class MyRunner extends Runnable2 と書くと「Runnable2 クラスを継承しようとしている」と解釈され、Runnable2interface の場合は no interface expected here 系のエラーになります
  • interface のフィールドを変数のつもりで書くinterface の中に int count = 0; と書くと、Java は黙ってそれを public static final int count = 0; と解釈します。インスタンスごとの状態は持てず、後から代入もできません。状態を持たせたいなら interface ではなく abstract class を選びましょう
  • 実装メソッドを public 以外で書く — 契約上のメソッドは暗黙的に public abstract 扱いなので、実装側を privatepackage-private (修飾子なし) にすると「アクセス修飾子を弱められない」エラーになります。@Override public String name() の形を体に染み込ませてください
diagram (will load when visible)

少し意外なのが、最後の「interface 同士の継承は extends」というルールです。interface A extends B, C のように、interface は他の interface複数 extends できます。

やってみよう

それでは、今回の課題に挑戦してみましょう。やることは次のとおりです。

  1. Runnable2 という interface を定義し、String name(); メソッドを宣言する
  2. MyRunner クラスで implements Runnable2 し、コンストラクタで受け取った文字列を name() から返すように実装する
  3. Solution.runnerName(String name) の中で new MyRunner(name)Runnable2 型変数で受けてから name() の戻り値を return する

テストでは runnerName("MyRunner") だけでなく runnerName("Hello")runnerName("") のように別の引数でも呼ばれます。name() のシグネチャがズレていたり、戻り値をハードコードしていたりするとここで fail するので、必ず引数を内部に渡してから Runnable2 型経由で取り出してください。interfaceclass と同じファイルに書いて構いません (public を付けない interface / class は同居できます)。

慣れてきたら、Printable のような 2 つ目の interface を作って多重 implements を試したり、default メソッドを足してみたりすると、interface の表現力が一段と分かってきます。次のレッスンでは、interface を引数や戻り値の型として使う ポリモーフィズム に踏み込んでいきます。

よくある質問

Q. abstract クラスと interface はどう使い分けますか?

A. 型の契約だけ定義するなら interface、共通実装も含めて部分的に書きたいなら abstract クラスです。Java 8 以降は interface も default メソッドで実装を持てるため、abstract の出番は減っていますが、フィールド共有が必要なら abstract が必要です。

Q. interface に変数を書けますか?

A. 暗黙的に public static final な定数だけ書けます。ミュータブルなフィールドは持てないため、状態を持たせたい場合は abstract クラスにしてください。定数置き場として使う場合も、専用クラス + private コンストラクタの方がベターな設計です。

Q. 1 つのクラスに複数の interface を実装できますか?

A. できます。implements A, B, C のようにカンマ区切りで列挙します。複数の default メソッドが同名で衝突した場合はコンパイルエラーになるため、override で明示的に解決します。多重継承の難しさをこの形で限定的に解放しているのが Java の設計です。

次のレッスン

次は default メソッド で、Java の interface を使って「契約」だけを定義し、implements で複数のクラスに同じ振る舞いを約束させる方法を学ぼう を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. interface の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. interface とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. interface Runnable2 を定義し、String name(); を宣言すること
  2. MyRunner クラスを implements Runnable2 で実装し、コンストラクタで受け取った文字列を name() から返すこと
  3. runnerName(String name) の中で MyRunner を生成し、必ず Runnable2 型変数で受けてから name() を呼ぶこと

入出力例

test-cases.txt

runnerName("MyRunner")"MyRunner" runnerName("Hello")"Hello" runnerName("")""

ヒント

main.java
main.java
学習モード

メモ

interface で契約を定義

⌘S で保存