はじめに
自己紹介
皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、約30年にわたって企業システムの開発に携わってきました。現在も現役のアーキテクトとして、重要システムの技術設計やソリューションの選定を担当しています。直近では、システム開発へのAI適用(AI駆動開発)という全社的な施策でリーダーを務めています。
セミナーでの登壇や雑誌への技術記事の寄稿を行ってきたほか、以下の書籍も執筆しています。
- 「マスタリング Java EE 5」(翔泳社、2007年)
- 「アプリケーションアーキテクチャ設計パターン」(技術評論社、2017年)
- 「Javaエンジニアのためのソフトウェアテスト実践入門」(技術評論社、2024年)
Udemy講座のご紹介
この記事の内容は、私が講師を務めるUdemy講座『新 Java Basic編 / AI対応版』の一部の範囲をカバーしたものです。『新 Java Basic編 / AI対応版』はこちらのリンクから購入できます。
この講座は、以下のような皆様にお薦めします。
- AIが書いたJavaのコードを読んで、正しさと設計の良し悪しを判断できるようになりたい方
- 新人のシステムエンジニアとして、Javaの基礎を仕組みから身に付けたい方
- AIを前提とした開発へ学び直し(リスキリング)をしたい方
- 「Oracle認定Javaプログラマ」の取得を目指し、Javaの基礎を固めたい方
AIとチャットできるオールインワンの学習環境をリリースしました
Udemy講座の演習を動かすための学習環境として、Windows向けのデスクトップアプリ「Codinable(コーディナブル)」をリリースしました。
エディタ、実行結果、テスト結果、SQL、Webブラウザ、そしてAIチャットを1つの画面にまとめた、オールインワンの学習環境です。
学習に必要なものは、すべてCodinableの中で完結します。
- 演習の実行環境(Java 25、Gradle、Bashなど)が一式同梱されており、JDKのインストールや環境変数の設定は不要です
- インストールして「新規コース開始・再開」からコースを選ぶだけで、講座のすべての演習がそのまま動きます
- 書き換えて動かなくなっても、「初期化」で配布時の状態に戻せます
- 開いている演習のファイルと実行結果を踏まえて、AIに質問したり(Askモード)、ファイルの書き換えを指示したり(Agentモード)できます
AIに書き換えを指示し、差分を確かめてから実行する、というAI駆動開発の基本の流れを、Codinableの中だけで安全に体験できます。
AIチャットは、受講者ご自身が生成AIサービス(Anthropic、OpenAI、Googleのいずれか)のAPIキーを登録して使うBYOK(Bring Your Own Key)方式です。利用料金は各社から直接請求されます。APIキーを登録しなくても、AIチャット以外の機能はすべて使えます。
Codinableのインストーラは、以下のGitHubのReleasesページからダウンロードできます(64ビット版のWindows 10/11、管理者権限は不要)。
この記事を含むシリーズ全体像
この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。
12.1 インタフェースと多態性
チャプターの概要
チャプター11.1では抽象クラスによる多態性を、チャプター11.2では委譲を取り上げました。本チャプターでは、実装と外部仕様を分離するための仕組みであるインタフェースを取り上げます。インタフェースは、クラスの階層とは無関係に「〜できる」という能力を付け加え、利用する側を実装クラスから切り離します。AIエージェントに設計を指示するときも、AIが書いたコードをレビューするときも、「どの型に依存しているか」は最も重要な観点の1つです。インタフェースの構造と、抽象クラスとの使い分けを図で整理します。
12.1.1 インタフェースによる外部仕様の分離
本レッスンでは、インタフェースの宣言と実装を読み、変数の型によって何が見えるのかを判断できるようになります。
クラスはモノ、インタフェースは外部仕様
インタフェースとは、外部から見たときの仕様(能力やスペック)を表す型です。クラスが「モノ」を表すのに対し、インタフェースは「ネット購入できる」「ダウンロードできる」といった能力を表すため、WebPurchasableやDownloadableのように「動詞+able」の名前がよく付けられます。インタフェースもコンパイルされるとクラスファイルになり、変数・引数・戻り値の型として使えます。
ECサイトの商品モデルで、クラスとインタフェースの関係を示します。
- 生活雑貨・紙の書籍・電子書籍は、モノを表すクラスである
- ネット購入可能とダウンロード可能は、外部仕様を表すインタフェースである
- 3つのクラスは、すべてネット購入可能を実装する
- 電子書籍だけはダウンロード可能も実装し、1つのクラスが2つの外部仕様を持つ
Webで購入する機能を作る人は「ネット購入可能」だけを、ダウンロード機能を作る人は「ダウンロード可能」だけを意識すれば済みます。これがチャプター11.1で取り上げた抽象化の考え方です。
宣言と実装の構造
インタフェースはinterfaceキーワードで宣言し、中心となるメンバーは抽象メソッドです。インタフェースの抽象メソッドは、修飾子を書かなくてもpublic abstractと見なされます。クラスはimplementsでインタフェースを実装し、そのすべての抽象メソッドをオーバーライドします。親クラスの継承と同時に書く場合は、class クラス名 extends 親クラス名 implements インタフェース名1, インタフェース名2のようにextendsを先に書きます。親クラスは1つだけですが、インタフェースは複数実装できます。
2つのインタフェースと2つの実装クラスの関係を示します。
- インタフェース
FooはsayYes()を、インタフェースBarはsayHello()とsayGoodbye()を宣言する -
HogeはFooだけをimplementsし、sayYes()を実装する -
PiyoはFooとBarの両方をimplementsする -
Piyoは、Foo由来のsayYes()、Bar由来のsayHello()とsayGoodbye()、独自のsayGoodnight()を持つ
クラス図では、インタフェースの実装を白抜きの三角と点線で表し、名前の上に<<interface>>と書きます。Piyoのコードを以下に示します。
- 12_interface: pro.kensait.java.basic.lsn_12_1_1.Piyo
public class Piyo implements Foo, Bar {
@Override
public void sayYes() { //【1】
System.out.println("Piyo says YES!");
}
@Override
public void sayHello() { //【2】
System.out.println("Piyo says Hello!");
}
@Override
public void sayGoodbye() {
System.out.println("Piyo says Goodbye!");
}
public void sayGoodnight() { //【3】
System.out.println("Piyo says Goodnight!");
}
}
-
Foo由来のsayYes()【1】と、Bar由来のsayHello()【2】・sayGoodbye()の3つの実装が必要になる - インタフェースのメソッドは
publicと見なされるため、実装するメソッドにもpublicが必要になる -
sayGoodnight()【3】のように、インタフェースにないメソッドも追加できる
インタフェースの型は「窓」になる
Foo foo = new Piyo();のように、インスタンスは実装しているインタフェースの型の変数に代入できます。変数から呼び出せるのは、その型が宣言しているメソッドだけです。インタフェースは、インスタンスを見るための「窓」のように働きます。
1つのPiyoインスタンスを、異なる型の変数から見た様子を示します。
- 中央のヒープに1つの
Piyoインスタンスがあり、sayYes()・sayHello()・sayGoodbye()・sayGoodnight()の4つのメソッドを持つ - 左上の
Foo型の変数fooからは、sayYes()だけが見える - 左下の
Bar型の変数barからは、sayHello()とsayGoodbye()だけが見える - 右側の
Piyo型の変数piyoからは、4つのメソッドすべてが見える - 3つの変数は、いずれも同じ1つのインスタンスを参照している
引数の型にインタフェースを使うと、メソッドは実装クラスを知らずに済みます。
void talk(Foo foo) { //【1】
foo.sayYes();
}
void greet(Bar bar) { //【2】
bar.sayHello();
bar.sayGoodbye();
}
-
Piyoのインスタンスは、talk()【1】とgreet()【2】のどちらにも渡せる -
greet()は、Barを実装したクラスのインスタンスであれば何でも受け取れ、その実装には関心がない -
List<String> list = new ArrayList<>();(チャプター18.1参照)のように、変数をインタフェースの型で宣言するのも同じ考え方である
レビューの観点
- 変数・引数・戻り値の型が、実装クラス(
ArrayListなど)ではなく、必要な外部仕様を表すインタフェース(Listなど)になっているか - 実装クラスのメソッドが、インタフェースの名前が表す能力(
Downloadableなら「ダウンロードできる」)どおりの振る舞いになっているか(形だけ実装して空のままになっていないか) - インタフェースの名前が、そのインタフェースの表す能力や役割を表しているか
12.1.2 クラスの階層と無関係な機能の追加
本レッスンでは、継承の階層では表せない「一部のクラスだけの共通機能」を、インタフェースでどう表すかを読み取れるようになります。
名寄せの機能を一部の顧客だけに持たせる
顧客モデルに、一般会員・ゴールド会員・プラチナ会員の3つの種別があり、すべて抽象クラスCustomerBaseを継承しているとします。ここに「同じ名前と住所の2人の顧客を1人に統合する(名寄せする)」機能を加えますが、対象は一般会員とゴールド会員だけです。CustomerBaseに実装するとプラチナ会員まで対象になってしまうため、名寄せに必要な外部仕様をNameAggregatorインタフェースとして定義し、対象のクラスだけに実装させます。
同様に、ゴールド会員とプラチナ会員だけが持つ家族会員の機能をFamilySpecインタフェースとして加えた、顧客モデル全体の構造を示します。
- 3つの会員クラスは、抽象クラス
CustomerBaseを継承する -
GeneralCustomerとGoldCustomerは、名寄せのためのNameAggregatorを実装する -
GoldCustomerとPlatinumCustomerは、家族会員のためのFamilySpecを実装する -
GoldCustomerは、1つの親クラスと2つのインタフェースを同時に持つ
NameAggregatorインタフェースのコードを以下に示します。
- 12_interface: pro.kensait.java.basic.lsn_12_1_2.NameAggregator
interface NameAggregator {
String getName(); // 名前を取得する
String getAddress(); // 住所を取得する
int getPoint(); // ポイントを取得する
void setPoint(int point); // ポイントを更新
void invalidate(); // 状態を無効にする
}
- 名寄せに必要な外部仕様だけを、5つの抽象メソッドとして宣言している
-
invalidate()以外はCustomerBaseから継承したメソッドが実装と見なされるため、GeneralCustomerとGoldCustomerに追加するのはinvalidate()だけで済む
インタフェースの型で受け取るユーティリティ
名寄せの処理は、ユーティリティクラスNameAggregatorsがNameAggregator型の引数で受け取ります。
- 12_interface: pro.kensait.java.basic.lsn_12_1_2.NameAggregators
public static NameAggregator aggregate(NameAggregator n1, NameAggregator n2) {
n1.setPoint(n1.getPoint() + n2.getPoint());
n2.invalidate();
return n1;
}
- 引数の型が
CustomerBaseではなくNameAggregatorのため、PlatinumCustomerを渡すとコンパイルエラーになる - 同じクラスの
isSame()は、名前と住所の両方が一致するかを判定する
Mainで、同じ名前と住所を持つ一般会員のAlice(3000ポイント)とAlice(1000ポイント)を名寄せしたときの流れを示します。
- 左側に
NameAggregator型の変数alice1(ID 1、3000ポイント)とalice2(ID 2、1000ポイント)があり、実体はどちらもGeneralCustomerである - 中央の
NameAggregators.isSame()が名前と住所の一致を確かめ、trueを返す - 続いて
NameAggregators.aggregate()が、alice1のポイントを4000に更新し、alice2のinvalidate()を呼び出す - 右側の結果は、
alice1が「point=4000、invalid=false」、alice2が「point=1000、invalid=true」である - 下部の
PlatinumCustomerからaggregate()への矢印には「コンパイルエラー(NameAggregatorではない)」という×印が付いている
このように、インタフェースを使うと、継承の階層とは無関係に一部のクラスだけに機能を持たせ、その機能を使う処理を共通化できます。
レビューの観点
- 一部のクラスにしか当てはまらない機能が、共通の親クラスに入れられていないか
- インタフェースの抽象メソッドが、その機能に必要な外部仕様だけに絞られているか(無関係なメソッドが混ざっていないか)
- 機能を使う処理の引数が、具体的なクラスや広すぎる親クラスではなく、そのインタフェースの型になっているか
12.1.3 インタフェースの様々なメンバーと抽象クラスとの使い分け
本レッスンでは、インタフェースが持てるメンバーと、抽象クラスとの違いを整理し、AIが選んだ構造の妥当性を判断できるようになります。
インタフェースが持てるメンバー
インタフェースは、抽象メソッドのほかに次のメンバーを持てます。
【表】インタフェースのメンバー
| メンバー | 書き方 | 意味・使いどころ |
|---|---|---|
| 抽象メソッド | void invalidate(); |
public abstractと見なされる。実装クラスに実装を強制する |
| 定数 | int MAX_NAME_LENGTH = 100; |
public static finalと見なされる。関連する定数をまとめる(列挙型はチャプター15.1参照) |
| デフォルトメソッド | default boolean isFamily(...) { ... } |
実装を持ち、実装クラスにそのまま引き継がれる |
| スタティックメソッド | static boolean isSame(...) { ... } |
NameAggregator.isSame(a, b)のように呼ぶ。そのインタフェース向けのユーティリティ |
privateメソッド |
private ... { ... } |
Java 9以降。インタフェースの中だけで使う処理 |
インタフェースはextendsで別のインタフェースを継承でき、クラスと違って複数を継承できます。
デフォルトメソッドの例として、FamilySpecのコードを以下に示します。
- 12_interface: pro.kensait.java.basic.lsn_12_1_2.FamilySpec
interface FamilySpec {
CustomerBase[] getFamily(); //【1】
void setFamily(CustomerBase[] family);
default boolean isFamily(CustomerBase target) { //【2】
for (CustomerBase customer : getFamily()) {
if (target.equals(customer)) return true;
}
return false;
}
}
- 家族会員の取得と設定【1】は、抽象メソッドとして実装クラスに任せている
-
isFamily()【2】は、getFamily()の呼び出しだけで完結するため、デフォルトメソッドとして実装している -
getFamily()がnullを返すと、isFamily()の拡張for文でNullPointerExceptionが発生する(チャプター14.1で取り上げる)
メソッドの衝突
複数のインタフェースを実装すると、メソッドが衝突することがあります。戻り値の型とシグネチャ(メソッド名と引数の型の並び)が完全に同じなら、1つのメソッドで両方を実装できます。一方、シグネチャが同じで戻り値の型だけが異なる場合は、解決する手段がありません。
戻り値の型だけが異なる抽象メソッドの衝突を示します。
- 上段左の
<<interface>> Fooはint doSomething();を、上段右の<<interface>> BarはString doSomething();を宣言している - 下段の
Hogeクラスは、FooとBarの両方をimplementsしている -
Hogeの中には、int doSomething()とString doSomething()の2つの実装が必要になる - 2つのメソッドはシグネチャが同じため、
Hogeに「コンパイルエラー:同じシグネチャのメソッドは複数宣言できない」という×印が付いている
なお、2つのインタフェースが同じシグネチャのデフォルトメソッドを持つ場合は、実装クラスでそのメソッドをオーバーライドしなければコンパイルエラーになります。オーバーライドしたメソッドの中では、Foo.super.doSomething()のように、どちらのデフォルトメソッドを使うかを指定して呼び出せます。
抽象クラスとインタフェースの使い分け
抽象クラスとインタフェースは、どちらも多態性を実現しますが、表すものが異なります。
【表】抽象クラスとインタフェースの比較
| 観点 | 抽象クラス | インタフェース |
|---|---|---|
| 表す関係 | is-a(〜である) | can-do(〜できる) |
| 1つのクラスが持てる数 | 1つ(継承) | 複数(実装) |
| インスタンスフィールド | 持てる | 持てない(定数だけ) |
| コンストラクタ | 持てる | 持てない |
| 主な用途 | 共通の状態と共通の実装 | 外部仕様、一部のクラスだけの能力 |
顧客モデルでの使い分けを示します。
- 中央に抽象クラス
CustomerBaseがあり、「共通の状態(id・name・point)と共通の実装」というラベルが付いている -
CustomerBaseの下に、GeneralCustomer・GoldCustomer・PlatinumCustomerが継承の線で縦につながる(is-a) - 横方向には
NameAggregatorとFamilySpecが、それぞれ対象のクラスだけに点線でつながる(can-do) - 右下の注記は「縦の関係は1本だけ、横の能力は必要な数だけ」である
レビューの観点
- 状態を持たない能力を表すだけなのに、抽象クラスにして継承の1枠を使っていないか
- デフォルトメソッドに、実装クラスの状態を前提とした処理や、
nullを考慮しない処理が書かれていないか - インタフェースの定数を使うためだけに、無関係なクラスにそのインタフェースを実装していないか
- 複数のインタフェースを実装したクラスで、同じシグネチャのメソッドが意図どおりに解決されているか
このチャプターで学んだこと
本チャプターでは、以下を取り上げました。
- インタフェースは実装と外部仕様を分離する型であり、クラスは
implementsで複数のインタフェースを実装できること - インタフェースの型の変数からは、その型が宣言したメソッドだけが見え、利用する側は実装クラスに依存しなくて済むこと
- インタフェースを使うと、継承の階層とは無関係に一部のクラスだけに機能を持たせ、その機能を使う処理を共通化できること
- インタフェースは抽象メソッドのほかに定数・デフォルトメソッド・スタティックメソッドを持てること、戻り値の型だけが異なるメソッドは衝突して実装できないこと
- 抽象クラスはis-aの関係と共通の状態・実装を、インタフェースはcan-doの能力を表すという使い分けの基準








