1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

12.1 インタフェースと多態性~新Java Basic編 / AI対応版

1
Last updated at Posted at 2023-02-04

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、約30年にわたって企業システムの開発に携わってきました。現在も現役のアーキテクトとして、重要システムの技術設計やソリューションの選定を担当しています。直近では、システム開発へのAI適用(AI駆動開発)という全社的な施策でリーダーを務めています。
セミナーでの登壇や雑誌への技術記事の寄稿を行ってきたほか、以下の書籍も執筆しています。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『新 Java Basic編 / AI対応版』の一部の範囲をカバーしたものです。『新 Java Basic編 / AI対応版』はこちらのリンクから購入できます。

この講座は、以下のような皆様にお薦めします。

  • AIが書いたJavaのコードを読んで、正しさと設計の良し悪しを判断できるようになりたい方
  • 新人のシステムエンジニアとして、Javaの基礎を仕組みから身に付けたい方
  • AIを前提とした開発へ学び直し(リスキリング)をしたい方
  • 「Oracle認定Javaプログラマ」の取得を目指し、Javaの基礎を固めたい方

AIとチャットできるオールインワンの学習環境をリリースしました

Udemy講座の演習を動かすための学習環境として、Windows向けのデスクトップアプリ「Codinable(コーディナブル)」をリリースしました。
エディタ、実行結果、テスト結果、SQL、Webブラウザ、そしてAIチャットを1つの画面にまとめた、オールインワンの学習環境です。

Codinableの画面構成

学習に必要なものは、すべてCodinableの中で完結します。

  • 演習の実行環境(Java 25、Gradle、Bashなど)が一式同梱されており、JDKのインストールや環境変数の設定は不要です
  • インストールして「新規コース開始・再開」からコースを選ぶだけで、講座のすべての演習がそのまま動きます
  • 書き換えて動かなくなっても、「初期化」で配布時の状態に戻せます
  • 開いている演習のファイルと実行結果を踏まえて、AIに質問したり(Askモード)、ファイルの書き換えを指示したり(Agentモード)できます

AIに書き換えを指示し、差分を確かめてから実行する、というAI駆動開発の基本の流れを、Codinableの中だけで安全に体験できます。

AIチャットに書き換えを指示し、差分を確かめて実行する

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・BarとクラスHoge・Piyoの関係

  • インタフェース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による名寄せの流れ

  • 左側に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を考慮しない処理が書かれていないか
  • インタフェースの定数を使うためだけに、無関係なクラスにそのインタフェースを実装していないか
  • 複数のインタフェースを実装したクラスで、同じシグネチャのメソッドが意図どおりに解決されているか

このチャプターで学んだこと

本チャプターでは、以下を取り上げました。

  1. インタフェースは実装と外部仕様を分離する型であり、クラスはimplementsで複数のインタフェースを実装できること
  2. インタフェースの型の変数からは、その型が宣言したメソッドだけが見え、利用する側は実装クラスに依存しなくて済むこと
  3. インタフェースを使うと、継承の階層とは無関係に一部のクラスだけに機能を持たせ、その機能を使う処理を共通化できること
  4. インタフェースは抽象メソッドのほかに定数・デフォルトメソッド・スタティックメソッドを持てること、戻り値の型だけが異なるメソッドは衝突して実装できないこと
  5. 抽象クラスはis-aの関係と共通の状態・実装を、インタフェースはcan-doの能力を表すという使い分けの基準
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?