- Kotlin
Kotlin interfaceの使い方 abstract classとの違い
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期連続の黒字決算。