Java:インターフェース
1学習の目的
- インターフェースで「何ができるか」を約束する設計ができるようになる。JV10で触れた「親を2つ持てない」制約を解決する。
- 抽象クラスとの違いを理解し、どちらを使うべきか判断できるようになる。実務のJavaコードを読む上で欠かせない知識になる。
2基礎解説
継承は「〜である」(BookはProductである)を表しますが、インターフェースは「〜できる」(配送できる・割引できる)を表します。1つのクラスは親を1つしか持てませんが、インターフェースは何個でも実装できます。
| 継承(extends) | インターフェース(implements) | |
|---|---|---|
| 個数 | 1つだけ | いくつでも |
| 意味 | 〜である | 〜できる |
| フィールド | 持てる | 定数のみ |
| 実装 | あり | 原則なし(defaultは可) |
- interface は「できることの一覧」。メソッドの名前と形だけを宣言し、中身は実装するクラスが書く。
- implements で実装する。複数を同時に実装できる——class SaleItem extends Product implements Shippable, Discountable のように書ける。
- インターフェースのメソッドは public が必須。実装側で public を書き忘れると「アクセス範囲を狭められない」というエラーになる。
- default を付ければ、インターフェースにも実装を書ける(Java 8以降)。共通の処理を持たせつつ、必要なら上書きもできる。
- 抽象クラス(abstract)は「未完成の親クラス」。フィールドを持てるが継承は1つだけ。共通のデータもあるなら抽象クラス、できることだけ揃えるならインターフェース。
現場使用例:支払い方法(カード・銀行振込・電子マネー)、通知手段(メール・SMS)、比較可能・並べ替え可能といった性質の表現。
// ① インターフェース=「できること」の宣言
interface Shippable {
int shippingFee(); // 中身は書かない
// default なら実装も書ける(Java 8以降)
default String shippingLabel() {
return "送料" + shippingFee() + "円";
}
}
interface Discountable {
double discountRate();
}
// ② implements で実装する
static class Product implements Shippable {
protected String name;
protected int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
public int shippingFee() { // ⚠ public が必須
return 600;
}
}
// ③ ⭐ 継承と複数のインターフェースを同時に
static class SaleItem extends Product implements Discountable {
SaleItem(String name, int price) {
super(name, price);
}
public double discountRate() {
return 0.2;
}
@Override
public int shippingFee() {
return 0;
}
}
// ④ インターフェース型でまとめて扱える
Shippable[] items = { new Product(…), new SaleItem(…) };
for (Shippable i : items) {
System.out.println(i.shippingFee());
}
// ⑤ 性質を持つか判定
if (item instanceof Discountable d) {
System.out.println(d.discountRate());
}
// ⑥ 抽象クラス(フィールドを持てる未完成の親)
static abstract class Shape {
abstract double area(); // 中身は子が書く
String describe() { // 共通処理は書ける
return "面積は" + area();
}
}
3基本ドリル(10問)
出力 == 600
interface Shippable { int shippingFee(); } と class Product implements Shippable を書く。
public class Main {
interface Shippable {
int shippingFee();
}
static class Product implements Shippable {
public int shippingFee() {
return 600;
}
}
public static void main(String[] args) {
Product p = new Product();
System.out.println(p.shippingFee());
}
}インターフェースは「約束」。implements したクラスは、宣言されたメソッドを必ず実装しなければならない。
A. 1つだけ B. 2つまで C. いくつでも D. 実装できない 選択★☆☆無料
解答 == C
継承(extends)との違い。
C
継承は1つだけだが、インターフェースは制限なし。「配送できる」「割引できる」「比較できる」を同時に持たせられる。
出力2行。600 / 0.2
implements A, B のようにカンマで並べる。
public class Main {
interface Shippable {
int shippingFee();
}
interface Discountable {
double discountRate();
}
static class Item implements Shippable, Discountable {
public int shippingFee() {
return 600;
}
public double discountRate() {
return 0.2;
}
}
public static void main(String[] args) {
Item i = new Item();
System.out.println(i.shippingFee());
System.out.println(i.discountRate());
}
}これが継承にはできないこと。JV10で触れた「親を2つ持てない」制約を、インターフェースなら回避できる。
public class Main {
interface Greetable {
String greet();
}
static class Person ____ Greetable {
public String greet() { return "こんにちは"; }
}
public static void main(String[] args) {
System.out.println(new Person().greet());
}
}出力 == こんにちは
「実装する」を意味する10文字のキーワード。
implements
継承は extends、インターフェースは implements。両方使う場合は extends 親 implements インターフェース の順で書く。
A. private B. public C. protected D. 不要 選択★☆☆無料
解答 == B
インターフェースのメソッドは暗黙的に public。
B
public を書き忘れるとコンパイルエラー。「継承したメソッドのアクセス範囲を狭めようとしている」と怒られる。
出力 == 送料600円
default String shippingLabel() { return ...; } と書く。
public class Main {
interface Shippable {
int shippingFee();
default String shippingLabel() {
return "送料" + shippingFee() + "円";
}
}
static class Product implements Shippable {
public int shippingFee() {
return 600;
}
}
public static void main(String[] args) {
System.out.println(new Product().shippingLabel());
}
}default なら実装クラスが書かなくてよい。共通の処理をインターフェース側に持たせられるので、実装の手間が減る。
public class Main {
interface Greetable {
String name();
____ String greet() {
return name() + "さん、こんにちは";
}
}
static class Person implements Greetable {
public String name() { return "山田"; }
}
public static void main(String[] args) {
System.out.println(new Person().greet());
}
}出力 == 山田さん、こんにちは
「既定の」を意味する7文字。Java 8以降で使える。
default
default メソッドはインターフェースに実装を持たせる仕組み。既存のインターフェースにメソッドを追加しても、実装クラスを壊さずに済む。
A. 同じ B. 抽象クラスはフィールドを持てるが継承は1つだけ C. インターフェースのほうが古い D. 抽象クラスは実装できない 選択★★☆広告解放
解答 == B
データを持てるかどうかが大きな違い。
B
共通のデータもあるなら抽象クラス、できることだけ揃えるならインターフェース——この基準で選ぶ。
出力 == 面積は28.3
abstract class Shape { abstract double area(); ... } と書く。
public class Main {
static abstract class Shape {
abstract double area();
String describe() {
return "面積は" + String.format("%.1f", area());
}
}
static class Circle extends Shape {
private double r;
Circle(double r) {
this.r = r;
}
double area() {
return Math.PI * r * r;
}
}
public static void main(String[] args) {
System.out.println(new Circle(3).describe());
}
}abstract なメソッドは中身を書かない。子クラスが必ず実装するので、親は「areaは必ずある」と信じて describe を書ける。
出力 == 600
Shippable[] items = { ... }; の形で、異なるクラスをまとめて扱える。
public class Main {
interface Shippable {
int shippingFee();
}
static class Normal implements Shippable {
public int shippingFee() { return 600; }
}
static class Book implements Shippable {
public int shippingFee() { return 0; }
}
public static void main(String[] args) {
Shippable[] items = {new Normal(), new Book()};
int total = 0;
for (Shippable i : items) {
total += i.shippingFee();
}
System.out.println(total);
}
}継承関係がなくても、同じインターフェースなら一緒に扱える。「共通の親クラス」を無理に作る必要がない。
4実践シナリオ(5問)
出力2行
それぞれ違うメッセージを返す。
public class Main {
interface Payment {
String pay(int amount);
}
static class CreditCard implements Payment {
public String pay(int amount) {
return "カードで" + String.format("%,d", amount) + "円を決済";
}
}
static class BankTransfer implements Payment {
public String pay(int amount) {
return "銀行振込で" + String.format("%,d", amount) + "円を請求";
}
}
public static void main(String[] args) {
Payment[] methods = {new CreditCard(), new BankTransfer()};
for (Payment m : methods) {
System.out.println(m.pay(10000));
}
}
}支払い方法が増えてもクラスを足すだけ。呼び出す側のコードは m.pay(金額) のまま変わらない。
出力2行。0 / 0.2
extends Product implements Discountable の順で書く。
public class Main {
interface Discountable {
double discountRate();
}
static class Product {
int shippingFee() { return 600; }
}
static class SaleItem extends Product implements Discountable {
@Override
int shippingFee() { return 0; }
public double discountRate() { return 0.2; }
}
public static void main(String[] args) {
SaleItem s = new SaleItem();
System.out.println(s.shippingFee());
System.out.println(s.discountRate());
}
}「商品である」かつ「割引できる」を両立。継承だけでは表現できなかった組み合わせが可能になる。
出力 == 0.2
instanceof Discountable d で判定する。
public class Main {
interface Shippable {
int shippingFee();
}
interface Discountable {
double discountRate();
}
static class Normal implements Shippable {
public int shippingFee() { return 600; }
}
static class Sale implements Shippable, Discountable {
public int shippingFee() { return 0; }
public double discountRate() { return 0.2; }
}
public static void main(String[] args) {
Shippable[] items = {new Normal(), new Sale()};
for (Shippable i : items) {
if (i instanceof Discountable d) {
System.out.println(d.discountRate());
}
}
}
}「その性質を持つか」で分岐できる。クラスの種類ではなく、能力で判定するほうが柔軟な設計になる。
出力 == マウス:送料600円
default メソッドの中から、宣言だけのメソッドを呼べる。
public class Main {
interface Shippable {
String name();
int shippingFee();
default String summary() {
return name() + ":送料" + shippingFee() + "円";
}
}
static class Product implements Shippable {
public String name() { return "マウス"; }
public int shippingFee() { return 600; }
}
public static void main(String[] args) {
System.out.println(new Product().summary());
}
}default メソッドは他のメソッドを呼べる。共通の組み立て処理をインターフェースに置き、部品だけ実装クラスに任せられる。
出力 == 山田:400,000円
抽象クラスはフィールドとコンストラクタを持てる。
public class Main {
static abstract class Employee {
protected String name;
Employee(String name) {
this.name = name;
}
abstract int salary();
@Override
public String toString() {
return name + ":" + String.format("%,d", salary()) + "円";
}
}
static class Engineer extends Employee {
Engineer(String name) {
super(name);
}
int salary() {
return 400000;
}
}
public static void main(String[] args) {
System.out.println(new Engineer("山田"));
}
}共通のフィールド(name)を持てるのが抽象クラスの利点。インターフェースでは name を持たせられず、各実装クラスで宣言する必要がある。
5仕上げ課題
インターフェース Shippable
・int shippingFee()
・String name()
・default String shippingLabel() … 送料0なら「送料無料」、それ以外は「送料600円」形式
インターフェース Discountable
・double discountRate()
クラス構成
・Product implements Shippable … name・price、送料600円
・Book extends Product … 送料0円
・SaleItem extends Product implements Discountable … 割引率0.2、送料600円
main の処理
① Shippable 配列に3件入れ、「マウス:3,200円(送料600円)」の形式で出力
② 送料の合計を出力
③ Discountable を持つものだけ、割引後価格を「セール品:割引後8,000円」の形式で出力
データ:マウス/3200、Java入門/2800、セール品/10000
期待される出力(5行)
マウス:3,200円(送料600円)
Java入門:2,800円(送料無料)
セール品:10,000円(送料600円)
送料合計:1,200円
セール品:割引後8,000円 総合★★★広告解放
出力5行が完全一致
shippingLabel は default メソッドで、送料が0かどうかで表示を切り替える。price を取得するため getPrice() も用意する。割引後は price × (1 - rate) を四捨五入。
public class Main {
interface Shippable {
int shippingFee();
String name();
default String shippingLabel() {
if (shippingFee() == 0) {
return "送料無料";
}
return "送料" + String.format("%,d", shippingFee()) + "円";
}
}
interface Discountable {
double discountRate();
}
static class Product implements Shippable {
protected String name;
protected int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
public String name() {
return name;
}
public int getPrice() {
return price;
}
public int shippingFee() {
return 600;
}
@Override
public String toString() {
return name + ":" + String.format("%,d", price)
+ "円(" + shippingLabel() + ")";
}
}
static class Book extends Product {
Book(String name, int price) {
super(name, price);
}
@Override
public int shippingFee() {
return 0;
}
}
static class SaleItem extends Product implements Discountable {
SaleItem(String name, int price) {
super(name, price);
}
public double discountRate() {
return 0.2;
}
}
public static void main(String[] args) {
Shippable[] items = {
new Product("マウス", 3200),
new Book("Java入門", 2800),
new SaleItem("セール品", 10000),
};
int shippingTotal = 0;
for (Shippable i : items) {
System.out.println(i);
shippingTotal += i.shippingFee();
}
System.out.println("送料合計:" + String.format("%,d", shippingTotal) + "円");
for (Shippable i : items) {
if (i instanceof Discountable d && i instanceof Product p) {
long discounted = Math.round(p.getPrice() * (1 - d.discountRate()));
System.out.println(p.name() + ":割引後"
+ String.format("%,d", discounted) + "円");
}
}
}
}この設計では、3つのクラスがそれぞれ違う組み合わせの性質を持っている。Product は配送できるだけ、Book は配送できて送料無料、SaleItem は配送できてさらに割引もできる——継承だけでは、この「割引もできる」を後から足せなかった。
shippingLabel() を default にした判断も重要だ。「送料0なら無料と表示する」というルールは全商品に共通なので、各クラスで書くのは無駄になる。かといって Product クラスに書くと、Product を継承しないクラスでは使えない。インターフェースの default なら、実装するすべてのクラスが恩恵を受けられる。
i instanceof Discountable d && i instanceof Product p という書き方にも意味がある。「割引できて、かつ価格を持つ」という2つの条件を同時に確認している。片方だけでは、割引率は分かっても元の価格が取れない。
継承とインターフェースの使い分けはこう考える。「〜である」という本質的な分類は継承(BookはProductである)、「〜できる」という能力はインターフェース(割引できる)。この基準で選べば、たいていの場面で迷わずに済む。
ここまででオブジェクト指向の主要な仕組みは揃った。次章では List と Map を学び、配列では扱いきれなかった可変長のデータを操作できるようになる。