Kotlin の interface は、「このメソッドとプロパティを持っている」という約束を型として表す仕組みで、1つのクラスにいくつでも実装できます。 メソッドの本体(デフォルト実装)は書けますが、値を保存する場所(バッキングフィールド)とコンストラクタは持てません。この2点が abstract class との一番大きな違いです。

この記事では、interface の書き方をコピーして動かせるコードで示したうえで、abstract class との違いを比較表と選び方の手順にまとめます。同名のデフォルト実装がぶつかったときのエラー文と解決方法、プロパティに初期値を書いたときのエラー文、fun interface の SAM 変換と typealias の違い、by によるクラス委譲まで、すべて実行して確かめた結果を載せています。

確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc-jvm)/ OpenJDK 21.0.10。掲載した出力とエラー文は、すべてこの環境でコンパイル・実行したものです。

interface と abstract class の違い 早見表

比べる点 interface abstract class
状態(値を保存するプロパティ) 持てない(バッキングフィールドなし) 持てる
プロパティの初期値 val x = 10 書けない(ゲッターなら書ける) 書ける
コンストラクタ・init ブロック 持てない 持てる
1つのクラスに付けられる数 いくつでも 1つだけ
本体つきメソッド 書ける(デフォルト実装) 書ける
本体なしメンバーの書き方 何も付けない(自動で abstract) abstract を付ける
本体つきメンバーの上書き 何も付けなくても上書きできる open を付けたものだけ
上書きを止める final 付けられない 付けられる(既定が final)
可視性 public と private(private は本体つきのみ) public / protected / internal / private
インスタンス生成 できない できない
ラムダで実装(SAM 変換) fun interface ならできる できない
by による委譲 できる できない

interface でよく使う書き方の一覧です。

やりたいこと 書き方
定義する interface Printable { fun body(): String }
実装する(かっこは付けない) class Memo : Printable { override fun body() = "..." }
複数実装する class Article : Printable, Countable
デフォルト実装を書く fun print() { println(body()) }
プロパティを約束する val title: String
ゲッターつきプロパティ val header: String get() = "== $title =="
衝突した実装を選んで呼ぶ super<Logger>.start()
ラムダで実装できるようにする fun interface Validator { fun validate(s: String): Boolean }
実装を別のオブジェクトに任せる class A(inner: Repository) : Repository by inner

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin interfaceの使い方 abstract classとの違い」

interface の基本:定義・実装・デフォルト実装

interface のメンバーは4種類あります。本体のないメソッド、本体のあるメソッド(デフォルト実装)、値を約束するだけのプロパティ、ゲッターつきのプロパティです。

interface Printable {
    val title: String                      // 抽象プロパティ(実装側が用意する)
    val header: String get() = "== $title ==" // ゲッターつきプロパティ
    fun body(): String                     // 抽象メソッド
    fun print() {                          // デフォルト実装
        println(header)
        println(body())
    }
}

interface Countable {
    fun count(): Int
}

class Article(override val title: String, private val text: String) : Printable, Countable {
    override fun body() = text
    override fun count() = text.length
}

class Memo(private val text: String) : Printable {
    override val title = "メモ"
    override val header: String get() = "[$title]"
    override fun body() = text
}

fun main() {
    val items: List<Printable> = listOf(Article("お知らせ", "棚卸しは金曜です"), Memo("牛乳を買う"))
    items.forEach { it.print() }
    val a = Article("お知らせ", "棚卸しは金曜です")
    val c: Countable = a
    println(c.count())
}

実行コマンドと出力です。日本語を正しく表示するため、java に -Dstdout.encoding=UTF-8 を付けています。

kotlinc basic.kt -include-runtime -d basic.jar
java -Dstdout.encoding=UTF-8 -jar basic.jar
== お知らせ ==
棚卸しは金曜です
[メモ]
牛乳を買う
8

読み取れるポイントです。

  • 実装はコロンの後ろに名前を並べるだけ。クラスの継承と違い、interface 名の後ろにかっこは付けません。
  • 本体のないメンバーは、実装側で必ず override する。abstract と書かなくても抽象メンバーになります。
  • 本体のあるメンバーは、そのまま使っても上書きしてもよい。Memo は header を上書きし、Article は既定のまま使っています。
  • 抽象プロパティはコンストラクタで実装できる。class Article(override val title: String, ...) の形が最も短い書き方です。
  • 1つのクラスに複数の interface を付けられる。Article は Printable としても Countable としても扱えます。

interface 同士の継承・private メソッド・companion object

interface は別の interface を継承でき、親のメンバーにデフォルト実装を与えることもできます。本体つきの private メソッドと companion object も置けます。

interface Named {
    val name: String
}

interface Person : Named {
    val firstName: String
    val lastName: String
    override val name: String get() = "$lastName $firstName"
}

data class Member(override val firstName: String, override val lastName: String) : Person

interface Config {
    fun get(key: String): String
    private fun prefix() = "app."
    fun full(key: String) = get(prefix() + key)

    companion object {
        const val DEFAULT = "none"
        fun empty(): Config = object : Config {
            override fun get(key: String) = DEFAULT
        }
    }
}

fun main() {
    println(Member("太郎", "山田").name)
    println(Config.empty().full("mode"))
}
山田 太郎
none

Member は firstName と lastName を用意するだけで、name は Person のゲッターから得られます。Config.empty() では object : Config { ... } という無名オブジェクトで、その場かぎりの実装を作っています。

interface のプロパティはバッキングフィールドを持てない

interface に書けるプロパティは「約束だけ」か「ゲッターで計算して返す」のどちらかです。初期値を書いたり、field を使ったりすると、次のエラーになります。

interface Shelf {
    val capacity: Int = 10
    var used: Int
        get() = field
        set(value) { field = value }
}
e2.kt:2:25: error: property initializers in interfaces are prohibited.
    val capacity: Int = 10
                        ^^
e2.kt:3:5: error: property in interface cannot have a backing field.
    var used: Int
    ^^^^^^^^^^^^^
書き方 interface で書けるか 理由
val capacity: Int 書ける 約束だけ。実装クラスが値を持つ
val capacity: Int get() = 10 書ける 呼ばれるたびに計算して返す。保存場所は不要
val capacity: Int = 10 エラー 値を保存する場所が必要になる
get() = field エラー field は保存場所そのもの

直し方は、初期値をゲッターに変えるか、値を持つ役目を実装クラスに任せるかです。

interface Shelf {
    val capacity: Int get() = 10   // 固定値はゲッターで
    var used: Int                  // 保存は実装クラスに任せる
}

class WoodShelf : Shelf {
    override var used: Int = 0     // クラスはフィールドを持てる
}

Kotlin 公式ドキュメントの Interfaces のページにも、interface で宣言したプロパティはバッキングフィールドを持てず、アクセサからも参照できないと書かれています。

コンストラクタ・init・protected・final も書けない

interface に書けないものを、エラー文と一緒にまとめます。

interface Shelf(val capacity: Int) {
    init { println("init") }
    protected fun secret() {}
    private fun noBody()
    final fun fixed() {}
}
書いたコード エラー文(kotlinc 2.4.20)
interface Shelf(val capacity: Int) interfaces cannot have constructors.
init { ... } anonymous initializers in interfaces are prohibited.
protected fun secret() {} modifier 'protected' is not applicable inside 'interface'.
internal fun f() = 1 modifier 'internal' is not applicable inside 'interface'.
private fun noBody() function 'noBody' without a body cannot be private.
final fun fixed() {} modifier 'final' is not applicable inside 'interface'.

実装し忘れや、interface を直接生成しようとしたときのエラー文は次のとおりです。

interface Printable {
    fun body(): String
}
class Memo : Printable

fun main() {
    val p = Printable()
}
e5.kt:8:1: error: class 'Memo' is not abstract and does not implement abstract member:
fun body(): String
class Memo : Printable
^^^^^^^^^^
e5.kt:11:13: error: interface 'interface Printable : Any' does not have constructors.
    val p = Printable()
            ^^^^^^^^^

同名のデフォルト実装がぶつかったとき

2つの interface が同じ名前のデフォルト実装を持っていると、両方を実装したクラスでは、どちらを使うかをコンパイラが決められません。

interface Logger {
    fun start() = println("Logger.start")
}

interface Timer {
    fun start() = println("Timer.start")
}

class Job : Logger, Timer
e1.kt:9:1: error: class 'Job' must override 'start' because it inherits multiple interface methods for it.
class Job : Logger, Timer
^^^^^^^^^

解決方法は、クラス側で start() を override し、super<型名>.メソッド名() で呼びたい実装を指定することです。 片方だけ呼んでも、両方呼んでも、どちらも呼ばずに書き直してもかまいません。

interface Logger {
    fun start() = println("Logger.start")
    fun name(): String
}

interface Timer {
    fun start() = println("Timer.start")
    fun name(): String = "timer"
}

class Job : Logger, Timer {
    override fun start() {
        super<Logger>.start()
        super<Timer>.start()
        println("Job.start")
    }
    override fun name(): String = "job/" + super<Timer>.name()
}

fun main() {
    val job = Job()
    job.start()
    println(job.name())
}
Logger.start
Timer.start
Job.start
job/timer

name() は Logger 側が本体なし、Timer 側が本体ありです。この組み合わせでもクラス側での override が必要で、本体を持つ Timer の実装だけを super<Timer>.name() で呼べます。

状況 override が必要か super の書き方
1つの interface だけがデフォルト実装を持つ 不要 super.start() で足りる
2つの interface が同名のデフォルト実装を持つ 必要 super<Logger>.start() のように型名を添える
片方が本体なし、片方が本体あり 必要 本体のある側を型名つきで呼ぶ

abstract class との違いをコードで確かめる

abstract class は、interface が持てない「状態」と「初期化の処理」を持てます。次の例では、カウンターの現在値という状態を abstract class に、ID とラベルという約束を interface に分けています。

interface Identified {
    val id: Int
    fun label(): String = "#$id"
}

abstract class Counter(start: Int) {
    var count: Int = start          // 状態(バッキングフィールド)を持てる
        private set
    protected abstract val step: Int

    init { println("Counter を $start から開始") }

    fun next(): Int {
        count += step
        return count
    }
}

class StockCounter(override val id: Int) : Counter(10), Identified {
    override val step = 5
}

fun main() {
    val c = StockCounter(7)
    println(c.label())
    println(c.next())
    println(c.next())
}
Counter を 10 から開始
#7
15
20

abstract class だからできていることは次の4つです。

  • コンストラクタ引数を受け取る:Counter(start: Int)
  • 値を保存する:var count: Int = start。セッターを private にして、外からは読むだけにしています
  • init で初期化処理を走らせる
  • protected なメンバーを持つ:step は子クラスにだけ見せています

クラスは1つしか継承できないので、: Counter(10), Identified のようにクラスは1つ、interface はいくつでも、という並べ方になります。クラスを2つ並べると次のエラーです。

abstract class A
abstract class B
class C : A(), B()
e5.kt:3:16: error: only one class can appear in a supertype list.
class C : A(), B()
               ^

どちらを選ぶかの判断手順

上から順に確かめ、最初に当てはまったものを選びます。

手順 確かめること 当てはまったら
1 共通で保存したい値(状態)や、生成時に必ず走らせたい処理があるか abstract class
2 コンストラクタで引数を受け取らせたいか、子クラスだけに見せたい protected メンバーがあるか abstract class
3 すでに別のクラスを継承しているクラスにも付けたいか interface
4 1つのクラスに複数の役割を持たせたいか interface
5 メソッドが1つだけで、ラムダで渡したいか fun interface
6 どれにも当てはまらない interface(後から abstract class を足せる)

迷ったときは interface から始めると、後の変更がしやすくなります。interface を実装したクラスは別のクラスも継承できますが、abstract class を継承したクラスはもう他のクラスを継承できないためです。両方を組み合わせて、約束は interface、共通の状態と処理は abstract class に置く形もよく使われます(上の StockCounter がその形です)。

fun interface と SAM 変換

抽象メソッドが1つだけの interface に fun を付けると、ラムダをそのまま実装として渡せるようになります。これを SAM 変換(Single Abstract Method の頭文字)と呼びます。

fun interface Validator {
    fun validate(input: String): Boolean

    fun and(other: Validator): Validator = Validator { validate(it) && other.validate(it) }
}

typealias Check = (String) -> Boolean

fun runValidator(v: Validator, s: String) = println("Validator: '$s' -> ${v.validate(s)}")
fun runCheck(c: Check, s: String) = println("Check    : '$s' -> ${c(s)}")

fun Validator.describe() = "Validator の拡張関数"

fun main() {
    val notBlank = Validator { it.isNotBlank() }      // SAM 変換
    val short = Validator { it.length <= 5 }
    val both = notBlank.and(short)

    runValidator(both, "abc")
    runValidator(both, "abcdefgh")
    runValidator({ it.startsWith("a") }, "abc")       // 引数の位置でも SAM 変換

    val check: Check = { it.isNotBlank() }
    runCheck(check, "abc")
    runCheck(String::isNotEmpty, "")

    val plain: (String) -> Boolean = { it.isNotBlank() }
    val c2: Check = plain                              // typealias は同じ型
    val v2 = Validator(plain)                          // 関数型からはコンストラクタ風に包む
    println(c2("x"))
    println(v2.validate("x"))
    println(notBlank.describe())

    val obj = object : Validator {                     // SAM 変換を使わない書き方
        override fun validate(input: String) = input.all { it.isDigit() }
    }
    runValidator(obj, "12345")
}
Validator: 'abc' -> true
Validator: 'abcdefgh' -> false
Validator: 'abc' -> true
Check    : 'abc' -> true
Check    : '' -> false
true
true
Validator の拡張関数
Validator: '12345' -> true

Validator { it.isNotBlank() } の1行は、最後の object : Validator { override fun validate(...) ... } と同じ意味です。fun interface にはデフォルト実装(上の and)も足せるので、ラムダで作った部品同士を組み合わせられます。

fun を付け忘れたとき・条件を満たさないとき

interface Validator {
    fun validate(input: String): Boolean
}

fun interface Two {
    fun a()
    fun b()
}

fun interface WithProp {
    val x: Int
    fun a()
}

fun main() {
    val v = Validator { it.isNotBlank() }
}
e4.kt:5:1: error: functional interface must have exactly one abstract function.
fun interface Two {
^^^
e4.kt:11:5: error: functional interface cannot have abstract properties.
    val x: Int
    ^^^
e4.kt:16:13: error: interface 'interface Validator : Any' does not have constructors.
    val v = Validator { it.isNotBlank() }
            ^^^^^^^^^
e4.kt:16:25: error: unresolved reference 'it'.
    val v = Validator { it.isNotBlank() }
                        ^^
エラー文 原因 直し方
interface ... does not have constructors. fun の付いていない interface をラムダで作ろうとした fun interface にするか、object : Validator { ... } で書く
functional interface must have exactly one abstract function. 抽象メソッドが2つ以上(または0) 抽象メソッドを1つにし、残りはデフォルト実装にする
functional interface cannot have abstract properties. 抽象プロパティがある プロパティをゲッターつきにするか、通常の interface にする

typealias の関数型との違い

「String を受け取って Boolean を返すもの」は、typealias Check = (String) -> Boolean でも表せます。違いは、typealias は既存の型の別名、fun interface は新しい型という点です。

fun interface Validator {
    fun validate(input: String): Boolean
}
typealias Check = (String) -> Boolean

fun runValidator(v: Validator) = v.validate("a")
fun runCheck(c: Check) = c("a")

fun main() {
    val plain: (String) -> Boolean = { it.isNotBlank() }
    runValidator(plain)      // 通る:関数型の値は引数の位置で SAM 変換される
    val v = Validator { it.isNotBlank() }
    runCheck(v)              // エラー:Validator は関数型ではない
}
e6.kt:13:14: error: argument type mismatch: actual type is 'Validator', but '(String) -> Boolean' was expected.
    runCheck(v)
             ^
比べる点 fun interface typealias(関数型)
型としての扱い 新しい型 既存の関数型と同じ型
呼び出し方 v.validate("a")(名前つき) c("a")
デフォルト実装などの追加メンバー 持てる 持てない
他の interface の継承 できる できない
その型だけに効く拡張関数 書ける 同じ形の関数型すべてに効く
関数型の値を渡す 引数の位置なら渡せる(SAM 変換) そのまま渡せる
逆向きに渡す Validator を関数型の引数には渡せない —

公式ドキュメントの Functional (SAM) interfaces のページでは、特定の引数と戻り値を持つ関数を受け取るだけなら関数型や typealias を、関数だけでは表しきれない取り決めや操作を持たせたいなら fun interface を使う、という使い分けが示されています。fun interface は機能が多いぶん、書く量と実行時の変換の手間が増えることがある、とも書かれています。

by で実装を別のオブジェクトに任せる(クラス委譲)

class A(inner: Repository) : Repository by inner と書くと、Repository のメンバーをすべて inner へ回すコードをコンパイラが用意してくれます。変えたいメソッドだけ override すれば、残りは書かなくて済みます。 継承を使わずに機能を足す方法で、final なクラスにも使えます。

interface Repository {
    fun find(id: Int): String
    fun save(id: Int, value: String)
    fun describe(): String = "Repository(${find(1)})"
}

class MemoryRepository : Repository {
    private val data = mutableMapOf(1 to "りんご")
    override fun find(id: Int) = data[id] ?: "なし"
    override fun save(id: Int, value: String) { data[id] = value }
}

class LoggingRepository(private val inner: Repository) : Repository by inner {
    override fun save(id: Int, value: String) {
        println("save($id, $value)")
        inner.save(id, value)
    }
}

fun main() {
    val repo: Repository = LoggingRepository(MemoryRepository())
    repo.save(2, "みかん")
    println(repo.find(2))
    println(repo.find(9))
    println(repo.describe())
}
save(2, みかん)
みかん
なし
Repository(りんご)

LoggingRepository は save だけを書いていますが、find と describe も使えています。MemoryRepository は open ではないので継承はできませんが、委譲なら包んで機能を足せます。

by の右に置けるのは interface を実装したオブジェクトで、左に書く型も interface に限られます。abstract class を : AbsBase by b のように書くと「delegation is supported only for interfaces.」というエラーになります。

委譲先からは、包んだ側の override が見えない

委譲で1つ気をつけたい点があります。委譲先のオブジェクトが自分のメンバーを呼ぶとき、包んだ側で override した内容は使われません。

interface Greeter {
    val name: String
    fun greet() = println("こんにちは、$name です")
}

class Base : Greeter {
    override val name = "Base"
}

class Wrapper(g: Greeter) : Greeter by g {
    override val name = "Wrapper"
}

fun main() {
    val w = Wrapper(Base())
    println(w.name)
    w.greet()
}
Wrapper
こんにちは、Base です

w.name は Wrapper が返しますが、greet() は Base へ回され、Base の中で読まれる name は Base 自身のものです。継承であれば「こんにちは、Wrapper です」になる場面なので、委譲と継承の違いとして覚えておくと役立ちます。greet() の表示も変えたいときは、Wrapper で greet() も override します。

比べる点 継承(abstract class / open class) 委譲(interface と by)
相手のクラスに必要な条件 open または abstract interface を実装していればよい
組み合わせられる数 1つ interface ごとに別のオブジェクトへ任せられる
相手の内部から呼ばれるメンバー 上書きした側が呼ばれる 相手自身のものが呼ばれる
相手を実行時に差し替える できない コンストラクタで渡す相手を変えられる

よくある質問

Q. interface のメソッドに open や abstract は必要ですか? A. 不要です。本体のないメンバーは自動的に抽象メンバーになり、本体のあるメンバーも実装クラスで上書きできます。実装クラス側には override が必要です。

Q. interface に定数を置きたいときはどうしますか? A. companion object の中に const val で書きます。本文の Config の例で Config.DEFAULT として使っています。interface 直下に val x = 10 と書くと「property initializers in interfaces are prohibited.」になります。

Q. デフォルト実装は Java からも使えますか? A. 公式ドキュメントの Interfaces のページによると、JVM では interface に宣言した関数は Java の default メソッドとしてコンパイルされ、この動きは -jvm-default コンパイラオプションで切り替えられます。

Q. abstract class と interface を両方使ってもよいですか? A. はい。約束を interface で定義し、複数の実装に共通する状態と処理を abstract class にまとめる形は、両方の長所を生かせます。利用する側の引数の型は interface にしておくと、abstract class を継承していない実装も受け取れます。

まとめ

  • interface は「約束」を表す型。デフォルト実装は書けるが、バッキングフィールド・コンストラクタ・init は持てない
  • abstract class は状態と初期化処理を持てる。ただしクラスは1つしか継承できない
  • 選び方は「共通の状態や生成時の処理があるか」から確かめる。なければ interface、ラムダで渡したいなら fun interface
  • 同名のデフォルト実装がぶつかったら override し、super に型名を添えて呼ぶ(super<Logger>.start())
  • プロパティの初期値はエラー。ゲッターにするか、値の保存を実装クラスに任せる
  • fun interface は新しい型、typealias は別名。追加メンバーや専用の拡張関数が欲しいなら fun interface
  • by による委譲は、変えたいメソッドだけ書けばよい。委譲先の内部からは、包んだ側の override が見えない点に注意

参考・出典

  • Kotlin 公式ドキュメント「Interfaces」 https://kotlinlang.org/docs/interfaces.html
  • Kotlin 公式ドキュメント「Functional (SAM) interfaces」 https://kotlinlang.org/docs/fun-interfaces.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin interfaceの使い方 abstract classとの違い」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?