- JAVA
Java interfaceのdefaultメソッドと多重継承の衝突
defaultメソッドとは、インターフェースの中に「本体つき」で書けるメソッドで、実装クラスが何も書かなければその本体がそのまま使われる仕組みです。 Java 8 で default と static が、Java 9 で private と private static が加わり、インターフェースに書けるメソッドは抽象メソッドを含めて5種類になりました。
本体を持てるようになった結果、2つのインターフェースから同じ名前の default を受け継ぐ「多重継承の衝突」が起こり得ます。この記事では、4種類のメソッドの違いを表で整理したうえで、衝突したときに javac が出すエラー文と、解決に使う3つのルールを、すべて実際にコンパイル・実行した結果つきで説明します。コードはそのまま貼って動かせる完成形です。
確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(javac 21.0.10)。エラー文は javac の英語表示と日本語表示の両方を載せています。
interfaceに書けるメソッドの早見表
| 種類 | 書き方 | 導入 | 本体 | 実装クラスでのオーバーライド | 呼び出し方 |
|---|---|---|---|---|---|
| 抽象メソッド | String name(); |
最初から | なし | 必須(実装する) | obj.name() |
| default | default String greet() { ... } |
Java 8 | あり | できる(任意) | obj.greet()、実装クラス内からは Greeter.super.greet() |
| static | static Greeter of(String n) { ... } |
Java 8 | あり | できない(継承されない) | Greeter.of("田中")(インターフェース名のみ) |
| private | private String decorate(String s) { ... } |
Java 9 | あり | できない(外から見えない) | 同じインターフェース内の default・private から |
| private static | private static String bracket(String s) { ... } |
Java 9 | あり | できない(外から見えない) | 同じインターフェース内のどのメソッドからでも |
衝突したときの解決ルールは次の3つです。上から順に当てはめます。
| 順 | ルール | 例 | 結果 |
|---|---|---|---|
| 1 | クラスの実装が優先 | 親クラスに public String start() があり、インターフェースにも default の start() がある |
親クラスのものが使われる |
| 2 | より具体的なインターフェースが優先 | Printer extends Device で、両方に default の start() がある |
子にあたる Printer のものが使われる |
| 3 | 決まらなければ自分で書く | 無関係な Printer と Scanner の両方に default の start() がある |
コンパイルエラー。クラスで start() を書き、Printer.super.start() で選ぶ |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java interfaceのdefaultメソッドと多重継承の衝突」
4種類のメソッドをまとめて動かす
まずは4種類を1つのインターフェースに並べたコードです。Kinds.java という名前で保存してください。
interface Greeter {
String name(); // 抽象メソッド(実装クラスが書く)
default String greet() { // default(Java 8〜)
return decorate("こんにちは、" + name() + "さん");
}
static Greeter of(String name) { // static(Java 8〜)
return () -> name;
}
private String decorate(String s) { // private(Java 9〜)
return bracket(s) + " from " + name();
}
private static String bracket(String s) { // private static(Java 9〜)
return "[" + s + "]";
}
}
class Polite implements Greeter {
public String name() { return "佐藤"; }
@Override
public String greet() { // default は上書きできる
return Greeter.super.greet() + " いつもありがとうございます";
}
}
public class Kinds {
public static void main(String[] args) {
Greeter g = Greeter.of("田中"); // static はインターフェース名で呼ぶ
System.out.println(g.greet()); // default はインスタンスから呼ぶ
System.out.println(new Polite().greet());
}
}
javac -d out Kinds.java
java -cp out Kinds
実行結果:
[こんにちは、田中さん] from 田中
[こんにちは、佐藤さん] from 佐藤 いつもありがとうございます
このコードから読み取れるポイントは4つあります。
- default は「上書きしてもしなくてもよい」メソッドです。
Greeter.of("田中")が返すラムダ式はname()しか書いていませんが、greet()を呼べています。 - static はインターフェース名で呼ぶ道具です。ここではインスタンスを作る入口(ファクトリメソッド)として使っています。
- private は default の中身を小分けにするための部品です。
decorateは抽象メソッドのname()を呼べます。 - private static はインスタンスに触れない部品です。
bracketの中からname()は呼べません。
defaultメソッドの呼び出し方
default メソッドは、普通のインスタンスメソッドと同じように obj.greet() で呼びます。実装クラスで上書きしたうえで元の本体も使いたいときは、インターフェース名.super.メソッド名() と書きます。上の Polite の Greeter.super.greet() がその例です。
staticメソッドはインターフェース名でしか呼べない
クラスの static メソッドは子クラスの名前でも呼べますが、インターフェースの static メソッドは実装クラスに継承されません。次のコードをコンパイルして確かめます。
interface Greeter {
String name();
static Greeter of(String name) { return () -> name; }
}
class Polite implements Greeter {
public String name() { return "佐藤"; }
}
public class StaticCall {
public static void main(String[] args) {
Greeter g = Greeter.of("田中"); // OK
Greeter a = Polite.of("鈴木"); // 実装クラス名では呼べない
Greeter b = g.of("高橋"); // インスタンスからも呼べない
}
}
コンパイル結果:
StaticCall.java:11: error: cannot find symbol
Greeter a = Polite.of("鈴木"); // 実装クラス名では呼べない
^
symbol: method of(String)
location: class Polite
StaticCall.java:12: error: illegal static interface method call
Greeter b = g.of("高橋"); // インスタンスからも呼べない
^
the receiver expression should be replaced with the type qualifier 'Greeter'
2 errors
日本語表示では「シンボルを見つけられません」「staticインタフェース・メソッド・コールが不正です/受信式は型修飾子'Greeter'で置換される必要があります」と出ます。どちらも Greeter.of(...) に直せば通ります。
privateメソッドは実装クラスから見えない
private メソッドは、そのインターフェースの中だけで使える部品です。実装クラスから呼ぶと、存在しないメソッドとして扱われます。
interface Greeter {
private String decorate(String s) { return "[" + s + "]"; }
default String greet() { return decorate("hello"); }
}
class Polite implements Greeter {
public String greet() { return decorate("hi"); }
}
PrivCall.java:6: error: cannot find symbol
public String greet() { return decorate("hi"); }
^
symbol: method decorate(String)
location: class Polite
1 error
実装クラスにも使わせたい処理は default にし、インターフェース内の整理だけが目的なら private にする、と使い分けます。
privateがJava 9からであることをコンパイラで確かめる
javac --release 8 を付けると、Java 8 の文法としてコンパイルできます。private メソッドを含むインターフェースで試した結果です。
interface Greeter {
private String decorate(String s) { return "[" + s + "]"; }
default String greet() { return decorate("hi"); }
}
javac --release 8 -d out V8.java
V8.java:2: error: private interface methods are not supported in -source 8
private String decorate(String s) { return "[" + s + "]"; }
^
(use -source 9 or higher to enable private interface methods)
1 error
エラーになったのは private の行だけで、default の行はそのまま受け付けられています。default と static は Java 8 から、private と private static は Java 9 からという区切りが、コンパイラの表示からも読み取れます。
修飾子の組み合わせで出るエラー
書き間違えやすい組み合わせを1つのファイルにまとめてコンパイルしました。
interface Greeter {
String name();
private String a(); // private なのに本体がない
default String b(); // default なのに本体がない
static String c() { return name(); } // static から抽象メソッドを呼ぶ
private default String d() { return ""; } // private と default の併用
default final String e() { return ""; } // final は付けられない
default synchronized String f() { return ""; }
}
| 書いたもの | javac のエラー文 | 日本語表示 |
|---|---|---|
| 本体のない private / default | missing method body, or declare abstract |
メソッド本体がないか、abstractとして宣言されています |
static の中から name() を呼ぶ |
non-static method name() cannot be referenced from a static context |
staticでないメソッド name()をstaticコンテキストから参照することはできません |
private default |
illegal combination of modifiers: private and default |
修飾子privateとdefaultの組合せは不正です |
default final |
modifier final not allowed here |
修飾子finalをここで使用することはできません |
default synchronized |
modifier synchronized not allowed here |
修飾子synchronizedをここで使用することはできません |
Java言語仕様(JLS 9.4)でも、インターフェースのメソッドに付けられる修飾子は public・private・abstract・default・static・strictfp とされ、final・synchronized・native は付けられないこと、abstract・default・static は同時に2つ以上付けられないことが定められています。
もう1つ、実装クラス側で起こりやすいのが public の付け忘れです。インターフェースのメソッドは、private を付けない限り public です。上書きする側も public にします。
interface Greeter {
default String greet() { return "hi"; }
}
class Impl implements Greeter {
String greet() { return "hello"; } // public を付け忘れた
}
Misc2.java:5: error: greet() in Impl cannot implement greet() in Greeter
String greet() { return "hello"; } // public を付け忘れた
^
attempting to assign weaker access privileges; was public
1 error
同名のdefaultが衝突するとどうなるか
ここからが本題です。無関係な2つのインターフェースが、同じ名前・同じ引数の default メソッドを持っているとします。
interface Printer {
default String start() { return "Printer: 印刷を始めます"; }
}
interface Scanner {
default String start() { return "Scanner: 読み取りを始めます"; }
}
class MultiDevice implements Printer, Scanner {
}
javac -d out Conflict.java
コンパイル結果:
Conflict.java:7: error: types Printer and Scanner are incompatible;
class MultiDevice implements Printer, Scanner {
^
class MultiDevice inherits unrelated defaults for start() from types Printer and Scanner
1 error
日本語表示の javac では次のようになります。
Conflict.java:7: エラー: タイプPrinterとScannerは互換性がありません。
class MultiDevice implements Printer, Scanner {
^
クラス MultiDeviceは型PrinterとScannerからstart()の関連しないデフォルトを継承します
エラー1個
inherits unrelated defaults(関連しないデフォルトを継承します)が、default 衝突の目印です。 start() を一度も呼んでいなくても、クラスを宣言した時点でエラーになります。どちらを使うかをコンパイラが勝手に決めることはなく、書いた人に選んでもらう、という設計です。
A.super.method()で解決する
解決方法は、実装クラスで同じメソッドを自分で書くことです。その中で インターフェース名.super.メソッド名() を使えば、どちらの default を使うかを指定できます。
interface Printer {
default String start() { return "Printer: 印刷を始めます"; }
}
interface Scanner {
default String start() { return "Scanner: 読み取りを始めます"; }
}
class MultiDevice implements Printer, Scanner {
@Override
public String start() {
// どちらを使うかを自分で決める。両方呼んでもよい
return Printer.super.start() + " / " + Scanner.super.start();
}
}
class PrintFirst implements Printer, Scanner {
@Override
public String start() { return Printer.super.start(); }
}
class OwnStart implements Printer, Scanner {
@Override
public String start() { return "OwnStart: 独自の手順で始めます"; }
}
public class Resolve {
public static void main(String[] args) {
System.out.println(new MultiDevice().start());
System.out.println(new PrintFirst().start());
System.out.println(new OwnStart().start());
}
}
実行結果:
Printer: 印刷を始めます / Scanner: 読み取りを始めます
Printer: 印刷を始めます
OwnStart: 独自の手順で始めます
Printer.super.start() の super は「親クラス」ではなく「Printer というインターフェースの default 本体」を指します。super.start() とだけ書くと親クラス(ここでは Object)を探しにいくため、インターフェース名を前に付ける点がポイントです。
衝突を解決する3つのルールを実行結果で確かめる
衝突がいつもエラーになるわけではありません。次の流れで自動的に決まる場合があります。
flowchart TD
A[同じシグネチャのメソッドが複数の経路から届く] --> B{親クラスに同じメソッドがある?}
B -- はい --> C[ルール1: クラスのメソッドが使われる]
B -- いいえ --> D{一方のインターフェースが他方を継承して上書きしている?}
D -- はい --> E[ルール2: より具体的なインターフェースの default が使われる]
D -- いいえ --> F[ルール3: コンパイルエラー。クラスで書いて A.super.m で選ぶ]
ルール1:クラスの実装が優先される
親クラスから受け継いだメソッドと、インターフェースの default が同じシグネチャ(名前と引数の並び)のとき、クラスのメソッドが使われます。
interface Printer {
default String start() { return "Printer(default)"; }
}
class BaseDevice {
public String start() { return "BaseDevice(クラスの実装)"; }
}
class OfficeDevice extends BaseDevice implements Printer {
}
abstract class AbstractDevice {
public abstract String start(); // 抽象メソッドでもクラス側が優先される
}
class HomeDevice extends AbstractDevice implements Printer {
public String start() { return "HomeDevice(自分で実装)"; }
}
public class ClassWins {
public static void main(String[] args) {
System.out.println(new OfficeDevice().start());
Printer p = new OfficeDevice();
System.out.println(p.start()); // 変数の型が Printer でも結果は同じ
System.out.println(new HomeDevice().start());
}
}
実行結果:
BaseDevice(クラスの実装)
BaseDevice(クラスの実装)
HomeDevice(自分で実装)
OfficeDevice は何も書いていませんが、エラーにならず BaseDevice の start() が使われました。変数の型を Printer にしても結果は変わりません。default は「クラスが何も持っていないときの既定の動き」であり、クラスが持っていればそちらが勝つ、と覚えると整理しやすくなります。
このルールは、親クラスのメソッドが 抽象メソッドでも当てはまります。HomeDevice から start() の実装を外すと、default があるにもかかわらず次のエラーになります。
interface Printer {
default String start() { return "Printer(default)"; }
}
abstract class AbstractDevice {
public abstract String start();
}
class HomeDevice extends AbstractDevice implements Printer {
}
ClassAbs.java:7: error: HomeDevice is not abstract and does not override abstract method start() in AbstractDevice
class HomeDevice extends AbstractDevice implements Printer {
^
1 error
「default があるから実装は足りているはず」と考えたくなる場面ですが、クラス側の abstract が優先されるため、実装が必要になります。
ルール1の注意:親クラスのメソッドがpublicでないとき
クラスが優先されるということは、クラス側のメソッドがインターフェースの要求(public)を満たしている必要があるということでもあります。親クラスのメソッドに public が付いていない例です。
interface Printer {
default String start() { return "Printer(default)"; }
}
class BaseDevice {
String start() { return "BaseDevice"; } // public を付けていない
}
class OfficeDevice extends BaseDevice implements Printer {
}
Weaker.java:7: error: start() in BaseDevice cannot implement start() in Printer
class OfficeDevice extends BaseDevice implements Printer {
^
attempting to assign weaker access privileges; was public
1 error
日本語表示では「BaseDeviceのstart()はPrinterのstart()を実装できません/(public)より弱いアクセス権限を割り当てようとしました」です。default に切り替わるのではなく、エラーとして知らせてくれます。直し方は、BaseDevice.start() を public にするか、OfficeDevice で public の start() を書くかのどちらかです。
ルール2:より具体的なインターフェースが優先される
インターフェース同士に継承関係があり、子のインターフェースが default を上書きしている場合は、子(より具体的な方)の default が使われます。
interface Device {
default String start() { return "Device(親インターフェース)"; }
}
interface Printer extends Device {
@Override
default String start() { return "Printer(子インターフェース)"; }
}
class LaserPrinter implements Device, Printer {
}
// ひし形:Printer だけが上書きし、Scanner は Device のものをそのまま引き継ぐ
interface Scanner extends Device {
}
class MultiDevice implements Printer, Scanner {
}
public class Specific {
public static void main(String[] args) {
System.out.println(new LaserPrinter().start());
System.out.println(new MultiDevice().start());
}
}
実行結果:
Printer(子インターフェース)
Printer(子インターフェース)
LaserPrinter は Device と Printer の両方を implements していますが、Printer が Device の start() を上書き済みなので、Printer のものに決まります。
MultiDevice はいわゆるひし形継承の形です。Scanner 経由で届く start() は、もともと Device のものです。Printer がそれを上書きしているので、Printer の default が選ばれ、エラーになりません。Java言語仕様(JLS 9.4.1)にも、別の経路ですでに上書きされた default は継承されないという規定と、これと同じ形の例(Top・Left・Right・Bottom)が載っています。
ルール3:決まらないときはコンパイルエラーになる
ひし形でも、左右の両方が上書きしていると、どちらがより具体的かを決められません。
interface Device {
default String start() { return "Device"; }
}
interface Printer extends Device {
default String start() { return "Printer"; }
}
interface Scanner extends Device {
default String start() { return "Scanner"; }
}
class MultiDevice implements Printer, Scanner {
}
Diamond.java:10: error: types Printer and Scanner are incompatible;
class MultiDevice implements Printer, Scanner {
^
class MultiDevice inherits unrelated defaults for start() from types Printer and Scanner
1 error
最初の衝突と同じエラー文です。この場合は前の章と同じく、MultiDevice で start() を書いて Printer.super.start() か Scanner.super.start() を選びます。
3つのルールの結果一覧
| ケース | 親クラス | インターフェースの関係 | 結果 |
|---|---|---|---|
OfficeDevice extends BaseDevice implements Printer |
public の start() あり |
- | クラスの start() |
HomeDevice extends AbstractDevice implements Printer |
abstract の start() あり |
- | 実装が必要(書かないとエラー) |
OfficeDevice(親の start() が public でない) |
あり | - | weaker access privileges のエラー |
LaserPrinter implements Device, Printer |
なし | Printer が Device を上書き | Printer の default |
MultiDevice implements Printer, Scanner(Scanner は上書きなし) |
なし | ひし形・片側だけ上書き | Printer の default |
MultiDevice implements Printer, Scanner(両方上書き) |
なし | ひし形・両側が上書き | unrelated defaults のエラー |
MultiDevice implements Printer, Scanner(無関係) |
なし | 継承関係なし | unrelated defaults のエラー |
defaultと抽象メソッドが同名のとき
片方が default、もう片方が抽象メソッドという組み合わせもあります。
interface Printer {
default String start() { return "Printer(default)"; }
}
interface Startable {
String start(); // こちらは抽象メソッド
}
class MultiDevice implements Printer, Startable {
}
AbsDef.java:7: error: MultiDevice is not abstract and does not override abstract method start() in Startable
class MultiDevice implements Printer, Startable {
^
1 error
日本語表示は「MultiDeviceはabstractでなく、Startable内のabstractメソッドstart()をオーバーライドしません」です。default があっても、抽象メソッドの「実装してほしい」という要求は自動では満たされません。 Startable を作った人は実装クラスに中身を書いてほしいと考えているので、default で黙って埋めることはしない、という判断です。
直し方は衝突のときと同じで、クラスに start() を書きます。Printer の本体をそのまま使うなら、次の1行で済みます。
class MultiDevice implements Printer, Startable {
@Override
public String start() { return Printer.super.start(); }
}
A.super.method()で指定できるのは直接implementsしたものだけ
インターフェース名.super で指定できるのは、そのクラスが直接 implements(インターフェースなら extends)しているインターフェースです。2つ上のインターフェースを指定すると、次のエラーになります。
interface Device {
default String start() { return "Device"; }
}
interface Printer extends Device {
default String start() { return "Printer"; }
}
class LaserPrinter implements Printer {
public String start() { return Device.super.start(); } // 祖父にあたるインターフェース
}
Indirect.java:8: error: not an enclosing class: Device
public String start() { return Device.super.start(); } // 祖父にあたるインターフェース
^
1 error
日本語表示は「Deviceは内部クラスを囲みません」です。文面から原因が読み取りにくいエラーなので、X.super.m() の X が直接 implements したインターフェースかどうかを最初に確認すると早く解決できます。
では Device も直接 implements すれば呼べるかというと、別のエラーになります。
class LaserPrinter implements Device, Printer {
public String start() { return Device.super.start(); }
}
Indirect2.java:8: error: bad type qualifier Device in default super call
public String start() { return Device.super.start(); }
^
redundant interface Device is extended by Printer
1 error
日本語表示は「デフォルトのスーパー・コールの型修飾子Deviceが不正です/冗長なインタフェースDeviceはPrinterによって拡張されました」です。Printer がすでに Device の start() を上書きしているので、それを飛び越えて呼ぶことはできません。Device の本体が必要なら、Printer 側に Device.super.start() を呼ぶメソッドを用意するか、共通処理を static メソッドに切り出して両方から呼ぶ形にします。
equals・toString・hashCodeはdefaultにできない
インターフェースで toString() の既定の表示を用意したくなることがありますが、Object クラスの public メソッドと同じシグネチャの default は書けません。
interface Named {
String name();
default String toString() { return "Named(" + name() + ")"; }
default boolean equals(Object o) { return o instanceof Named n && n.name().equals(name()); }
default int hashCode() { return name().hashCode(); }
}
ObjM.java:3: error: default method toString in interface Named overrides a member of java.lang.Object
default String toString() { return "Named(" + name() + ")"; }
^
ObjM.java:4: error: default method equals in interface Named overrides a member of java.lang.Object
default boolean equals(Object o) { return o instanceof Named n && n.name().equals(name()); }
^
ObjM.java:5: error: default method hashCode in interface Named overrides a member of java.lang.Object
default int hashCode() { return name().hashCode(); }
^
3 errors
日本語表示は「インタフェース Namedのデフォルト・メソッドtoStringはjava.lang.Objectのメンバーをオーバーライドします」です。
理由は、先ほどのルール1(クラスが優先)にあります。すべてのクラスは Object を継承しているので、toString() などは必ずクラス側に実装があります。default を書いても使われることがないため、言語仕様(JLS 9.4.1.2)でコンパイルエラーと定められています。仕様書にも「インターフェースを実装するクラスは、そのメソッドの実装を自分で継承するため」という趣旨の説明があります。
代わりの書き方
同じ目的は、次のどれかで実現できます。
interface Named {
String name();
default String describe() { return "Named(" + name() + ")"; } // 別名の default にする
static String describe(Named n) { return "Named(" + n.name() + ")"; }
}
record Member(String name) implements Named {
@Override
public String toString() { return describe(); } // クラス側で toString を上書き
}
abstract class NamedBase implements Named { // 共通実装は抽象クラスに置く
@Override public String toString() { return describe(); }
@Override public boolean equals(Object o) { return o instanceof Named n && n.name().equals(name()); }
@Override public int hashCode() { return name().hashCode(); }
}
class Team extends NamedBase {
public String name() { return "開発チーム"; }
}
public class ObjAlt {
public static void main(String[] args) {
System.out.println(new Member("田中"));
System.out.println(new Team());
System.out.println(new Team().equals(new Team()));
Named lambda = () -> "ラムダ";
System.out.println(lambda.describe());
}
}
実行結果:
Named(田中)
Named(開発チーム)
true
Named(ラムダ)
| やりたいこと | 書き方 |
|---|---|
| 表示用の文字列を共通化したい | describe() のような別名の default を作る |
toString() の結果も揃えたい |
各クラスの toString() から describe() を呼ぶ |
equals・hashCode まで共通化したい |
抽象クラスに実装を置き、そこから継承する |
| 値をまとめるだけのクラス | record にすると equals・hashCode・toString が自動で用意される |
defaultメソッドが役立つ場面
公開済みのインターフェースにメソッドを足す
default が導入された大きな理由は、すでに実装クラスがあるインターフェースに、あとからメソッドを足せるようにするためです。抽象メソッドを足した場合と default を足した場合の違いを、3つのファイルで確かめます。
// Shape.java
public interface Shape {
double area();
}
// Square.java
public class Square implements Shape {
public double area() { return 4.0; }
}
// Main.java
public class Main {
public static void main(String[] args) {
Shape s = new Square();
System.out.println(s.describe());
}
}
まず Shape.java と Square.java をコンパイルしておきます(javac Shape.java Square.java)。そのあとで Shape に抽象メソッドを足し、Square は再コンパイルしないまま Main を動かします。
// Shape.java(抽象メソッドを足した版)
public interface Shape {
double area();
String describe();
}
javac Shape.java Main.java
java Main
Exception in thread "main" java.lang.AbstractMethodError: Receiver class Square does not define or inherit an implementation of the resolved method 'abstract java.lang.String describe()' of interface Shape.
at Main.main(Main.java:4)
コンパイル済みの Square は describe() を持っていないため、実行時に AbstractMethodError で止まります。Square を再コンパイルしようとすると、今度はコンパイルエラーになります。
Square.java:1: error: Square is not abstract and does not override abstract method describe() in Shape
public class Square implements Shape {
^
1 error
次に、同じメソッドを default として足します。
// Shape.java(default で足した版)
public interface Shape {
double area();
default String describe() { return "面積は " + area(); }
}
javac Shape.java Main.java
java Main
面積は 4.0
Square には手を入れていないのに、新しいメソッドが使えるようになりました。 ライブラリのインターフェースを育てていくときに、利用側のコードを変えずに済むのが default の強みです。標準ライブラリでも、Iterable の forEach や Comparator の reversed などがこの形で提供されています。
関数型インターフェースに便利メソッドを添える
抽象メソッドが1つだけのインターフェース(関数型インターフェース)は、ラムダ式で実装できます。default と static は抽象メソッドの数に数えられないので、いくつ足してもラムダ式で書ける性質は保たれます。
import java.util.*;
@FunctionalInterface
interface Validator {
boolean test(String s); // 抽象メソッドは1つだけ
default Validator and(Validator other) { return s -> test(s) && other.test(s); }
default Validator negate() { return s -> !test(s); }
static Validator notBlank() { return s -> s != null && !s.isBlank(); }
static Validator maxLength(int n) { return s -> s.length() <= n; }
}
public class Fn {
public static void main(String[] args) {
Validator v = Validator.notBlank().and(Validator.maxLength(5));
for (String s : List.of("abc", "", "abcdefg")) {
System.out.println("\"" + s + "\" -> " + v.test(s));
}
// 標準ライブラリの例
List<String> list = new ArrayList<>(List.of("b", "c", "a"));
list.sort(Comparator.<String>naturalOrder().reversed()); // static と default
list.forEach(System.out::println); // Iterable の default
}
}
実行結果:
"abc" -> true
"" -> false
"abcdefg" -> false
c
b
a
static メソッドで部品を作り、default メソッドでつなげる、という組み合わせは、Comparator.naturalOrder().reversed() と同じ形です。
defaultメソッドと抽象クラスの使い分け
本体を持てるという点では、default メソッドつきのインターフェースと抽象クラスは似ています。違いを表にまとめます。
| 観点 | インターフェース(default つき) | 抽象クラス |
|---|---|---|
| 複数を同時に受け継ぐ | できる(implements はいくつでも) | できない(extends は1つ) |
| インスタンスのフィールド | 持てない(定数のみ) | 持てる |
| コンストラクタ | 書けない | 書ける |
equals・toString・hashCode の共通実装 |
書けない | 書ける |
| メソッドのアクセス範囲 | public か private | public・protected・パッケージ・private |
| ラムダ式での実装 | 抽象メソッドが1つならできる | できない |
| あとからメソッドを足す | default で足せる | 本体つきメソッドで足せる |
選び方の目安は次のとおりです。
| 状況 | 選ぶもの |
|---|---|
| 「できること」を表す型で、状態は持たない | インターフェース |
| 抽象メソッドを組み合わせるだけの便利メソッドを添えたい | default メソッド |
| フィールドに値を持ち、それを使う共通処理がある | 抽象クラス |
equals・toString まで共通にしたい |
抽象クラス、または record |
| 両方ほしい | インターフェースで型を決め、抽象クラスで共通実装を提供する |
最後の「両方」は、標準ライブラリの List(インターフェース)と AbstractList(抽象クラス)と同じ構成です。
エラー文から原因を引く逆引き表
この記事で実際に出したエラー文を、原因と直し方つきでまとめます。
| エラー文(javac) | 原因 | 直し方 |
|---|---|---|
types A and B are incompatible; ... inherits unrelated defaults for m() from types A and B |
無関係な2つのインターフェースから同じ default を受け継いだ | クラスで m() を書き、A.super.m() などで選ぶ |
X is not abstract and does not override abstract method m() in B |
同名の抽象メソッドがある、または親クラスの abstract が優先された | クラスで m() を実装する |
m() in Base cannot implement m() in A ... attempting to assign weaker access privileges; was public |
クラス側の同名メソッドが public でない | クラス側を public にする |
not an enclosing class: A |
A.super.m() の A を直接 implements していない |
直接 implements しているインターフェース名にする |
bad type qualifier A in default super call ... redundant interface A is extended by B |
A を上書きしている B も implements している | B.super.m() を使う。A の本体が必要なら B 側に入口を用意する |
default method toString in interface X overrides a member of java.lang.Object |
Object のメソッドを default にしようとした | 別名の default にするか、抽象クラスに置く |
illegal static interface method call |
インスタンスからインターフェースの static を呼んだ | インターフェース名.メソッド() にする |
cannot find symbol(実装クラス名で static を呼んだ/private を呼んだ) |
static は継承されない。private は外から見えない | インターフェース名で呼ぶ。共有したい処理は default にする |
private interface methods are not supported in -source 8 |
Java 8 向けにコンパイルしている | Java 9 以上を対象にする |
missing method body, or declare abstract |
default・private・static に本体がない | 本体を書く |
illegal combination of modifiers: private and default |
併用できない修飾子 | どちらか一方にする |
よくある質問
Q. defaultメソッドを持つインターフェースを2つimplementsすると、必ずエラーになりますか?
A. いいえ。同じ名前・同じ引数の default が両方にあるときだけです。名前が違う、または引数の型や数が違えば、別のメソッドとして両方使えます。同じシグネチャでも、親クラスに実装がある場合(ルール1)や、片方がもう片方を継承して上書きしている場合(ルール2)は自動的に決まります。
Q. A.super.method() と super.method() は何が違いますか?
A. super.method() は親クラスのメソッドを呼びます。A.super.method() は、直接 implements しているインターフェース A の default 本体を呼びます。インターフェースの default を呼びたいときは、必ずインターフェース名を前に付けます。
Q. staticメソッドも衝突しますか?
A. しません。インターフェースの static メソッドは実装クラスに継承されず、常に インターフェース名.メソッド() で呼ぶためです。2つのインターフェースに同名の static があっても、A.of() と B.of() として別々に使えます。
Q. defaultメソッドにfinalを付けて、上書きできないようにできますか?
A. できません。modifier final not allowed here というコンパイルエラーになります。上書きされたくない処理は、インターフェースの static メソッドや private メソッドにするか、クラス側の final メソッドとして用意します。
まとめ
- インターフェースに書ける本体つきメソッドは4種類:default・static(Java 8〜)、private・private static(Java 9〜)。
- default はインスタンスから、static はインターフェース名から呼ぶ。static は実装クラスに継承されず、private はインターフェースの外から見えない。
- 無関係な2つのインターフェースの default が衝突すると
inherits unrelated defaultsのコンパイルエラーになる。クラスでメソッドを書き、A.super.method()で選ぶ。 - ルール1:クラスの実装が優先。親クラスのメソッドが abstract でも、public でなくても、クラス側が基準になる。
- ルール2:より具体的なインターフェースが優先。ひし形でも片側だけが上書きしていれば自動で決まる。
- ルール3:決まらなければ自分で書く。default と抽象メソッドが同名のときも、クラスでの実装が必要。
equals・toString・hashCodeは default にできない。別名の default、抽象クラス、record で代わりに書く。
参考・出典
- The Java Language Specification, Java SE 21 Edition「Chapter 9. Interfaces」(9.4 Method Declarations、9.4.1 Inheritance and Overriding、9.4.1.2、9.4.1.3)https://docs.oracle.com/javase/specs/jls/se21/html/jls-9.html
- The Java Language Specification, Java SE 21 Edition「Chapter 8. Classes」(8.1.5 Superinterfaces、8.4.8 Inheritance, Overriding, and Hiding)https://docs.oracle.com/javase/specs/jls/se21/html/jls-8.html
- The Java Language Specification, Java SE 21 Edition「Chapter 15. Expressions」(15.12.3:TypeName.super 形式の呼び出し)https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java interfaceのdefaultメソッドと多重継承の衝突」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。