クラス(抽象クラス/インターフェース)
抽象クラス
クラスのメンバーにキーワード「abstract」をつけて宣言することができます。 これを抽象メンバーと言います。
抽象メンバーは処理や値といった具体的な実装を持ちません(持つことができません)
抽象メンバを持つクラスを抽象クラスと呼び、このクラスにも「abstract」キーワードを付ける必要があります。
抽象クラスはインスタンス化することができません。
その理由は、抽象クラスは抽象メンバという具体的な実装を持たないメンバーが含んでいて、 それ単体では不完全な状態だからです。
fun main(args: Array) {
// Personは抽象クラスなのでインスタンス化できない
// 以下のコードはビルドエラーになる
// val someone = Person("someone")
}
abstract class Person(name: String) {
abstract var name: String
abstract fun describe()
}
抽象メンバーは自動的にオーバーライド可能な「open」になります。
抽象クラスを直接インスタンス化することはできませんが、 抽象クラスを検証した派生クラスで抽象メンバーをオーバーライドして具体的な実装を与えることで 利用することができます。
fun main(args: Array) {
val someone: Person = Student("suzuki")
someone.describe()
}
abstract class Person(name: String) {
abstract var name: String
abstract fun describe()
}
class Student(name: String) : Person(name) {
override var name = name
override fun describe() {
println("name: ${name}")
}
}
name: suzuki
インターフェース
KotlinにはJavaと同じようなインターフェースが存在します。
インターフェースと抽象クラスとの違いは、データを保持できるかどうかです。
fun main(args: Array) {
val someone: Printable = Student("suzuki")
someone.Print()
someone.PrintImpl()
}
interface Printable {
fun Print() // 何もしないと自動的に「open」で「abstract」
fun PrintImpl() {
// インターフェースの関数でも実装することは可能
println("print impl")
}
}
class Student(name: String) : Printable {
val name = name
override fun Print() {
println("name = ${name}")
}
}
name = suzuki
print impl
クラス(オーバーライド)
メソッド
Kotlinでは、オーバーライド可能なメンバーには明示的に「open」キーワードを付けなければいけません。
また、子クラスで実際にオーバーライドしているメンバーには「override」キーワードを付ける必要があります。
fun main(args: Array) {
val student = Student("suzuki", 82.1)
student.describe()
}
open class Person(name: String) {
val name = name
open fun describe() {
println("Name is ${name}.")
}
}
class Student(name: String, score: Double) : Person(name) {
val score = score
override fun describe() {
println("Name is ${name}. Score is ${score}.")
}
}
Name is suzuki. Score is 82.1.
オーバーライドしたメンバー自身は、自動的に「open」になります。
(上記の例で言えば、Studentクラスのdescribe関数は、Studentの子クラスでオーバーライド可能です)
もし、これ以上、子クラスでオーバーライドしてほしくなければ、「final」キーワードを付けます。 そうすることで、それ以上のオーバーライドを禁じることができます。
fun main(args: Array) {
val student = Student("suzuki", 82.1)
student.describe()
}
open class Person(name: String) {
val name = name
open fun describe() {
println("Name is ${name}.")
}
}
class Student(name: String, score: Double) : Person(name) {
val score = score
// finalをつけるとこれ以上、子クラスでオーバーライドできなくなる
final override fun describe() {
println("Name is ${name}. Score is ${score}.")
}
}
プロパティ
プロパティもメソッドと同様にオーバーライドすることができます。
プロパティのオーバーライドでは、親クラスで読取り専用の「val」だったものも、子クラスで「var」にオーバーライドスルことができます。
これは、getterしかなかったプロパティにオーバーライドによってsetterを追加することが可能だということです。
fun main(args: Array) {
val student = Student("suzuki", 82.1)
student.name = ""
student.describe()
}
open class Person(name: String) {
// openをつけることでオーバーライド可能にする
open val name = name
open fun describe() {
println("Name is ${name}.")
}
}
class Student(name: String, score: Double) : Person(name) {
override var name = name
set(value) {
if (value.isEmpty()) {
field = "empty"
} else {
field = value
}
}
val score = score
// finalをつけるとこれ以上、子クラスでオーバーライドできなくなる
final override fun describe() {
println("Name is ${name}. Score is ${score}.")
}
}
Name is empty. Score is 82.1.
継承時の初期化の順序
子クラスをインスタンス化した時の、初期化の順番は次のとおりになります。
- 親クラスのコンストラクタに渡す引数の評価
- 親クラスの記述されている初期化処理
- 子クラスに記述されている初期化処理
fun main(args: Array) {
val student = Student("suzuki", 82.1)
}
open class Person(name: String) {
init {
println("Base: init block")
}
val name = name.also { println("Base: property initializer. ${it}") }
}
class Student(name: String, score: Double) : Person(name.also { println("Derived: Argument for Base Constructor") }) {
init {
println("Derived: init block")
}
var score = score.also { println("Derived: property initializer. ${it}") }
}
Derived: Argument for Base Constructor
Base: init block
Base: property initializer. suzuki
Derived: init block
Derived: property initializer. 82.1
super
子クラスで親クラスのメンバーにアクセスして処理を行ったり、値を設定したりしたい場合があります。
そのような場合には、「super」を使って親クラスのメンバーにアクセスします。
fun main(args: Array) {
val student = Student("suzuki", 82.1)
student.name = ""
student.describe()
}
open class Person(name: String) {
open val name = name
open fun describe() {
println("Name is ${name}.")
}
}
class Student(name: String, score: Double) : Person(name) {
override var name = name
set(value) {
if (value.isEmpty()) {
field = "empty"
} else {
field = value
}
}
val score = score
final override fun describe() {
// superを使って親クラスのメンバーを呼び出す
super.describe()
println("Name is ${name}. Score is ${score}.")
}
}
Name is empty.
Name is empty. Score is 82.1.
複数の実装をもつメンバーのオーバーライドルール
クラスやインターフェースを複数継承した場合に、それぞれの親クラスやインターフェースに 同じ名前のメンバーが別々の実装を持っている場合があります。
このような場合には、子クラスでは必ずそのメンバーをオーバーライドして子クラス独自に処理を実装する必要があります。
(superを使っていずれかの親クラスやインターフェースの処理を採用しても、もちろんOKです)
複数の親クラスやインターフェースを継承した場合は、「super<base>」とすることでそれぞれの親クラスやインターフェースにアクセスします。
fun main(args: Array) {
val student = Student("suzuki", 82.1)
student.describe()
}
open class Person(name: String) {
val name = name
open fun describe() {
println("Name is ${name}.")
}
}
interface Describable {
fun describe() {
println("dummy")
}
}
class Student(name: String, score: Double) : Person(name), Describable {
val score = score
// describeの実装が複数存在してしまうため、
// オーバーライドして独自に実装しなおさないとビルドエラーになる
override fun describe() {
// それぞれのdescribeにアクセスできる
super.describe()
super.describe()
println("Name is ${name}. Score is ${score}.")
}
}
Name is suzuki.
dummy
Name is suzuki. Score is 82.1.
クラス(ヴィジビリティ)
ヴィジビリティ修飾子
クラス、オブジェクト、インターフェース、コンストラクタ、関数、プロパティとそのセッターに対して、ヴィジビリティを設定することができます。
(ゲッターのヴィジビリティはプロパティのヴィジビリティと同じものになります)
Kotlinには、4種類のヴィジビリティ修飾子があります。
- private
- protected
- internal
- public
明示的にヴィジビリティ修飾子を設定しない場合、デフォルトのヴィジビリティ修飾子は「public」になります。
パッケージ
トップレベルに宣言されたクラス、オブジェクト、インターフェース、関数、プロパティのヴィジビリティは次の表のようになります。
ヴィジビリティ | どこから見えるか |
---|---|
public(デフォルト) | どこからでも参照可能 |
private | 同じファイル内のみ参照可能 |
internal | 同じモジュール内のみ参照可能 |
protected | トップレベルで宣言したものには使用できない |
example.kt
package visibilitytest
// getterにはどこからでも参照できる
var num: Int = 123
// setterはこのファイル内でしか参照できない
private set(value) {
if (value < 0) {
println("${value} は無効です")
return
}
field = value
}
// internalなので、この関数は同じモジュールからであれば参照できる
internal fun assignNum(value: Int) {
num = value
}
main.kt
package visibilitytest
fun main(args: Array) {
println("num = ${num}")
assignNum(-321)
// numのsetterはprivateなので、以下のコードはビルドエラー
// num = -321
}
num = 123
-321 は無効です
クラス/インターフェースメンバー
クラスとインターフェースのメンバーのヴィジビリティは次の表のようになります.
ヴィジビリティ | どこから見えるか |
---|---|
private | 同じクラス/インターフェース(のメンバー)から参照可能 |
protected | privateと同じ範囲+サブクラス(のメンバー)から参照可能 |
internal | internalなメンバーを持つクラスが宣言されているモジュール内から参照可能 |
public(デフォルト) | どこからでも参照可能 |
example.kt
package visibilitytest
import java.nio.DoubleBuffer
open class Person(name: String) {
var name = name
protected set(value) {
if (value.isEmpty()) {
println("Empty name is invalid")
return
}
field = value
}
internal fun printName() = println("Name is ${name}.")
}
class Student(name: String, score: Double) : Person(name) {
var score = score
fun toUpperCaseName() {
// name のsetterはprotectedなので、サブクラスではアクセルできる
name = name.toUpperCase()
}
}
class PersonDescriber(someone: Person) {
val someone = someone
fun describe() {
// printName はinternalなので、同じモジュール内では参照できる
someone.printName()
}
}
main.kt
import visibilitytest.Student
import visibilitytest.PersonDescriber
fun main(args: Array) {
val student = Student("suzuki taro", 80.0)
println("student is ${student.name}")
student.toUpperCaseName()
val desc = PersonDescriber(student)
desc.describe()
}
student is suzuki taro
Name is SUZUKI TARO.
プライマリコンストラクタ
プライマリコンストラクタにヴィジビリティを設定したい場合、明示的にconstructorキーワードをつける必要があります。
example.kt
package visibilitytest
// プライマリコンストラクタにprotectedを設定
open class Person protected constructor(name: String) {
var name = name
protected set(value) {
if (value.isEmpty()) {
println("Empty name is invalid")
return
}
field = value
}
internal fun printName() = println("Name is ${name}.")
}
class Student(name: String, score: Double) : Person(name) {
var score = score
fun toUpperCaseName() {
// name のsetterはprotectedなので、サブクラスではアクセルできる
name = name.toUpperCase()
}
}
class PersonDescriber(someone: Person) {
val someone = someone
fun describe() {
// printName はinternalなので、同じモジュール内では参照できる
someone.printName()
}
}
main.kt
import visibilitytest.Person
import visibilitytest.Student
import visibilitytest.PersonDescriber
fun main(args: Array) {
val student = Student("suzuki taro", 80.0)
println("student is ${student.name}")
student.toUpperCaseName()
val desc = PersonDescriber(student)
desc.describe()
// Personのプライマリコンストラクタはprotectedであり、ここからはアクセスできない
// そのため、直接Personをインスタンス化することはできない
// 以下のコードはビルドエラー
// val person = Person("someone")
}
クラス(継承)
Any
Kotlinでは、すべてのクラスは「Any」クラスを継承しています。
特に、親クラスについて指定していないクラスも、暗黙的に「Any」クラスを継承します。
なお、この「Any」はJavaの「java.lang.Object」とは別のクラスです。 「Any」には「equals()」「hashCode()」「toString()」という関数しかありません。
fun main(args: Array) {
val student = Student()
if (student is Any) {
println("student is Any")
} else {
println("student is not Any")
}
}
class Student
student is Any
クラスの継承
javaと違い、Kotlinのクラスはデフォルトでは継承させることができません。 つまり、Kotlinのクラスはデフォルトでfinalです。
(javaではデフォルトでは継承でき、finalをつけると継承不可能になります)
クラスを継承可能にするには「open」をクラスにつけます。
「open」なクラスを継承するには、クラスヘッダーでコロンを付けて親クラスを指定します。 </p
fun main(args: Array) {
val student = Student("suzuki")
if (student is Person) {
println("student is Person")
} else if (student is Any) {
println("student is Any")
} else {
println("student is not Any")
}
}
open class Person(name: String)
class Student(name: String): Person(name)
student is Person
子クラスがプライマリコンストラクタを持たない場合、親クラスのセカンダリコンストラクタに初期化処理を委譲することもできます。 子クラスのconstructorの後ろにコロンをつけて「super」で親クラスのセカンダリコンストラクタを呼び出します。
fun main(args: Array) {
val student = Student("suzuki", 15, 80.0)
println("name = ${student.name}")
println("age = ${student.age}")
println("score = ${student.score}")
}
open class Person(name: String) {
val name = name
var age = 0
constructor(name: String, age: Int) : this(name) {
this.age = age
}
}
class Student : Person {
var score = 0.0
constructor(name: String, age: Int, score: Double) : super(name, age) {
this.score = score
}
}
name = suzuki
age = 15
score = 80.0
継承をする場合、親クラスのコンストラクタは必ず何らかの形で呼び出す必要があります。
fun main(args: Array) {
}
open class Person
// 親クラスのプライマリコンストラクタを読んでいるためOK
class Student: Person()
// 親クラスのセカンダリコンスtラクタを読んでいるためOK
class Teacher: Person {
constructor(): super()
}
// 親クラスのプライマリコンストラクタもセカンダリコンストラクタもよんでいないためNG
// class Parent: Person
クラス(プロパティ)
プロパティの宣言
クラスのボディに「val」キーワードで宣言すると読取り専用で、 「var」キーワードで宣言すると変数としてプロパティを宣言できます。
プロパティは必ず初期化しないといけません。初期化は「プロパティイニシャライザ」か「イニシャライザブロック」で行います。
fun main(args: Array) {
val suzuki = Student("suzuki", 80.0)
// nameは読取り専用なので変更できない
// 下記のコードはビルドエラー
// suzuki.name = "tanaka"
suzuki.score = 70.0
println("name = ${suzuki.name}")
println("score = ${suzuki.score}")
println("note = ${suzuki.note}")
}
class Student(name: String, score: Double) {
val name = name
var score = score
// プロパティは必ず初期化しないといけない
// 次のコードはビルドエラーになる
// val desc: String
var note: String
init {
// イニシャライザブロックで初期化してもOK
note = "None"
}
}
name = suzuki
score = 70.0
note = None
ゲッター/セッター
プロパティはゲッター/セッターを持ちます。
上記の例でもインスタンス「suzuki」からプロパティ「score」に値をセットしています。 直接値をセットしているように見えますが、実際にはデフォルトセッターを通じて値がセットされている状態です。 値を取得している部分も、デフォルトゲッターを通じで値を取得しています。
カスタムゲッター/セッターを作成するには「get」「set」を利用します。
カスタムゲッター/セッター内で、そのプロパティ自身にアクセスするには「field」を利用します。 これを「バッキングフィールド」といいます。 (カスタムゲッター/セッターで、自分自身にアクセスするのに自分のプロパティ名を利用すると、 無限にゲッター/セッターを呼び出すことになりスタックオーバーフローになります。 例えば、nameゲッター内でreturn nameとすると再びnameゲッターを呼ぶことになります。 するとnameゲッターでnameゲッターを呼び、またそのnameゲッターでnameゲッターを呼び... と無限に呼び出してしまします)
fun main(args: Array) {
val suzuki = Student("suzuki", 80.0)
suzuki.score = 70.0
suzuki.score = -10.0
println("name = ${suzuki.name}")
println("score = ${suzuki.score}")
println("note = ${suzuki.note}")
}
class Student(name: String, score: Double) {
val name = name
get() = field.toUpperCase()
// nameは読取り専用なので、setterを作成することはできない
var score = score
get() = field / 100.0
set(value) {
if (value < 0.0 || 100.0 < value) {
return
}
field = value
}
var note: String
init {
note = "None"
}
}
name = SUZUKI
score = 0.7
note = None
また、必要に応じてゲッター/セッターにもヴィジビリティ修飾子やアノテーションを指定することができます.
fun main(args: Array) {
val suzuki = Student("suzuki", 80.0)
// scoreセッターはprivateなので、以下はビルドエラーになる
// suzuki.score = 70.0
// suzuki.score = -10.0
println("name = ${suzuki.name}")
println("score = ${suzuki.score}")
println("note = ${suzuki.note}")
}
class Student(name: String, score: Double) {
val name = name
get() = field.toUpperCase()
var score = score
get() = field / 100.0
private set(value) {
if (value < 0.0 || 100.0 < value) {
return
}
field = value
}
var note: String
init {
note = "None"
}
}
name = SUZUKI
score = 0.8
note = None
クラス(コンストラクタ)
クラスの宣言とクラスメンバ
クラスは「class」キーワードで宣言します。
クラスは、メンバと呼ばれる以下の要素で構成されます。
メンバ | 内容 |
---|---|
コンストラクタ イニシャライザブロック |
インスタンスの初期化 |
関数 | Javaでいうメソッド |
プロパティ | Javaでいうメンバにゲッター/セッターなどの機能をもたせたもの |
ネスト/インナークラス | クラス内で宣言されたクラス |
オブジェクト宣言 | シングルトンなどを作成するときに利用 |
コンストラクタ
クラスにはプライマリコンストラクタとセカンダリコンストラクタという2種類があります。
いずれも必須ではありません。
プライマリコンストラクタ
プライマリコンストラクタは「class」キーワードの後ろの方、 つまりクラスヘッダーに「constructor」をつけて記述します。
fun main(args: Array) {
val student = Student("suzuki", 80.0)
}
/* この部分がプライマリコンストラクタ */
class Student constructor(name: String, score: Double) {
}
なお、クラスのボディ(プロパティなど「{ }」の中身の部分)が不要な場合は、「{ }」を省略できます。
fun main(args: Array) {
val student = Student("suzuki", 80.0)
}
class Student constructor(name: String, score: Double)
プライマリコンストラクタの「constructor」部分は省略可能です。 逆に言うと、「constructor」が必要なのは、ヴィジビリティ修飾子(publicなど)やアノテーション(@Injectなど)をつけたい場合です。
fun main(args: Array) {
val student = Student("suzuki", 80.0)
val school = School("Kotrin School")
}
class Student(name: String, score: Double)
class School public constructor(name: String)
さて、この「プライマリコンストラクタ」の使い方ですが、コンストラクタの宣言部にはパラメータを記述することができるだけで、 何かの処理を行うコードを記述することはできません。
では、何かしらの処理をしたい場合どうするのかというと、「init」キーワードで指定された「イニシャライザブロック」にコードを記述します。
fun main(args: Array) {
val student = Student("suzuki", 80.0)
}
class Student(name: String, score: Double) {
init {
println("initializer")
println("name = ${name}")
println("score = ${score}")
}
}
initializer
name = suzuki
score = 80.0
また、プロパティイニシャライザとしても、プライマリコンストラクタの変数を利用できます。
fun main(args: Array) {
val student = Student("suzuki", 80.0)
println("name length = ${student.nameLength}")
println("description = ${student.description}")
}
class Student(name: String, score: Double) {
val nameLength = name.length
val description = "name: ${name} score: ${score}"
init {
println("initializer")
println("name = ${name}")
println("score = ${score}")
}
}
initializer
name = suzuki
score = 80.0
name length = 6
description = name: suzuki score: 80.0
また、イニシャライザブロックは分割することもできます。 分割した場合は、単純に上にあるものから順番に実行されていきます。
イニシャライザブロックの中でもプロパティを参照することはできますが、そのイニシャライザブロックより前に宣言されていなければなりません。
fun main(args: Array) {
val student = Student("suzuki", 80.0)
}
class Student(name: String, score: Double) {
init {
println("First Block")
println("name = ${name}")
println("score = ${score}")
}
val nameLength = name.length
init {
println("Second Block")
println("nameLength = ${nameLength}")
// 以下のコードはビルドエラー
// println("descripton = ${description}")
}
val description = "name: ${name} score: ${score}"
init {
println("Third Bllock")
println("descripton = ${description}")
}
}
First Block
name = suzuki
score = 80.0
Second Block
nameLength = 6
Third Bllock
descripton = name: suzuki score: 80.0
セカンダリコンストラクタ
また、クラスボディ内でconstructorキーワードを指定することで、セカンダリコンストラクタを宣言することもできます。
プライマリコンストラクタと違い、セカンダリコンストラクタは複数個持つこともできます。
fun main(args: Array) {
val school = School()
val suzuki = Student(school)
val tanaka = Student()
println("student count = ${school.students.count()}")
}
class Student {
constructor(school: School) {
school.students.add(this)
}
constructor() {
}
}
class School {
public val students = arrayListOf()
}
student count = 1
プライマリコンストラクタとセカンダリコンストラクタの両方がある場合、 セカンダリコンストラクタはプライマリコンストラクタに処理を委譲する(プライマリコンストラクタを呼び出す)必要があります。
また、プライマリコンストラクタを直接委譲しなくても、プライマリコンストラクタに委譲している他のセカンダリコンストラクタに委譲しても大丈夫です。
他のコンストラクタへの委譲は「this」キーワードを利用して行います。
fun main(args: Array) {
val school = School()
val sato = Student()
println(sato.nameKey)
println()
val suzuki = Student(school)
println(suzuki.nameKey)
println()
val tanaka = Student("tanaka", school)
println(tanaka.nameKey)
println()
println("student count = ${school.students.count()}")
}
class Student(name: String) {
public val nameKey = name.toUpperCase()
init {
println("initializer block")
}
constructor(): this("None") {
// thisでプライマリコンストラクタに委譲している
println("secondary constructor: 1")
}
constructor(school: School): this() {
// thisでセカンダリコンストラクタ1に委譲している
println("secondary constructor: 2")
}
constructor(name: String, school: School): this(name) {
// thisでプライマリコンストラクタに委譲している
println("secondary constructor: 3")
school.students.add(this)
}
}
class School {
public val students = arrayListOf()
}
initializer block
secondary constructor: 1
NONE
initializer block
secondary constructor: 1
secondary constructor: 2
NONE
initializer block
secondary constructor: 3
TANAKA
student count = 1
例外
例外
Kotlinでは、例外クラスはすべて「Throwable」クラスを継承しています。
例外クラスは必ずメッセージとスタックトレースを持っています。 また、任意で例外がスローされた原因についての情報も持つことができます。
例外をスローするためには、「throw」式を利用します。
また、「try-catch(-finally)」でスローされた例外をキャッチします。
fun main(args: Array) {
try {
checkArgs(100)
} catch (e: IllegalArgumentException) {
println("catch block.")
println(e.message)
println(e.stackTrace)
} finally {
println("finally block.")
}
}
fun checkArgs(num: Int) {
if (num > 10) {
throw IllegalArgumentException("num is too large.")
}
println("num is ${num}.")
}
catch block.
num is too large.
[Ljava.lang.StackTraceElement;@49476842
finally block.
catchブロックは何個あっても大丈夫です。
また、catchブロックやfinallyブロックは無くても構いませんが、catchブロックかfinallyブロックのいずれかは必要です。
また、tryは式ですので、if式やwhen式のように値を返します。
import kotlin.math.abs
fun main(args: Array) {
val num1 = checkNum(-5)
println("num1 is ${num1}")
val num2 = checkNum(-12)
println("num2 is ${num2}")
}
fun checkNum(num: Int) = try {
if (num < -10 || 10 < num) {
throw IllegalArgumentException("num is too large.")
}
abs(num)
} catch (e: IllegalArgumentException) {
println(e.message)
null
}
num1 is 5
num is too large.
num2 is null
チェック例外
結論から言うと、KotlinにはJavaのチェック例外に相当する機能はありません。
Kotlinの公式サイトでは、以下のJDKのインターフェースを例にして理由が挙げられています。
Appendable append(CharSequence csq) throws IOException;
このインターフェースはStringBuilderのインターフェースですが、「append」には「throws IOException」というチェック例外が設定されています。
つまり、プログラマはStringBuilderや、 このインターフェースを継承している何らかのlogやconsoleクラスに文字列を追加するたびに「IOException」をキャッチしなければいけません。
そうすると、いたるところに次のようなコードが埋め込まれることになります。
try {
log.append(message)
}
catch (IOException e) {
// 絶対に例外は発生しない
}
このような、例外をハンドリングしないキャッチブロックを作ることは、「Effective Java」などでも指摘されているように、お行儀がいいことではありません。
また、チェック例外については生産性の向上に寄与しないという批判的な意見も多いため、Kotlinでは採用していないようです。