10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

これがKotlinのオブジェクト指向プログラミングか!と思ったこと

Last updated at Posted at 2020-06-04

#はじめに
Kotlinを始めて1ヶ月ちょっと経過し、オブジェクト指向でプログラミングする機会も増えてきたので、これが Kotlin の文法か! と感じた場面の続編として、割と上部だけですがKotlinのクラス回りの書き方をまとめてみました。
実際はもっと奥深いです!

#コンストラクタの書き方
Kotlinのコンストラクタではプライマリコンストラクタとセカンダリコンストラクタに大別できます。
##プライマリコンストラクタ

class Person constructor(name: String, age: Int){
    var name: String
    var age: Int
    init {
        this.name = name
        this.age  = age 
    }
    fun selfIntroduction(): String {
        return "私は${this.name}です。${this.age}歳です。"
    }
}
println(Person("田中", 20).selfIntroduction())
//私は田中です。20歳です。

constructor()に渡った値による初期化する部分はinitブロック内で行います。
constructorキーワードはアクセス修飾子やアノテーションを伴わない場合は以下のように省略できます。

class Person(name: String, age: Int){
    var name: String
    var age: Int
    init {
        this.name = name
        this.age  = age 
    }
    fun selfIntroduction(): String {
        return "私は${this.name}です。 ${this.age}歳です。"
    }
}

さらにこれを省略した形が以下のようになります。

class Person(var name: String, var age: Int){
    fun selfIntroduction(): String {
        return "私は${this.name}です。 ${this.age}歳です。"
    }
}
println(Person("田中", 20).selfIntroduction())
//私は田中です。20歳です。

引数にvarやvalを付加することにより、そのままこれがプロパティ宣言になります。さらにコンストラクタに入ってきた値もinitブロック無しで自動的に初期化されます。この形で書くのがすっきりしていますし一般的ではないでしょうか?
##セカンダリコンストラクタ
一つのクラスに2個以上のコンストラクタを定義することができ、2つ目以降をセカンダリコンストラクタと呼びます。セカンダリコンストラクタは以下のconstructorブロックで定義します。

class Person(var name: String, var age: Int){//ここですでにプライマリコンストラクタが宣言されている。
    constructor(name: String): this(name, 16)//age=16としてプライマリコンストラクタを呼んでいる。
    constructor(): this("山田")//name="山田"としてこの1行上のセカンダリコンストラクタを呼んでいる。
    fun selfIntroduction(): String {
        return "私は${this.name}です。 ${this.age}歳です。"
    }
}
println(Person("田中", 20).selfIntroduction())
//私は田中です。20歳です。
println(Person("田中").selfIntroduction())
//私は田中です。16歳です。
println(Person().selfIntroduction())
//私は山田です。16歳です。

: this(...)は他のコンストラクタを呼び出す記述です。thisの引数をみてどのコンストラクタが呼ばれているかは自動的に判別されます。セカンダリコンストラクタは必ずプライマリコンストラクタ、または他のセカンダリコンストラクタを呼び出さなければいけません。

セカンダリコンストラクタに別のオブジェクトを渡して、そのオブジェクトから任意の値を取り出し、プライマリコンストラクタに渡すこともできます。

class Person (var name: String, var age: Int){
    constructor(map : Map<String, Any>): this(
        name = map["name"] as String,
        age = map["age"] as Int
    )
    fun selfIntroduction(): String {
        return "私は${this.name}です。 ${this.age}歳です。"
    }
}
var map  = mapOf("name" to "田中", "age" to 20, "Prefecture" to "神奈川")
println(Person(map).selfIntroduction())
//私は田中です。20歳です。

この例ではセカンダリコンストラクタ内のプロパティ名は省略していませんが、順番通りに書けば1つ前の例のように省略できます。

class Person (var name: String, var age: Int){
    constructor(map : Map<String, Any>): this(
        map["name"] as String,
        map["age"] as Int
    )
    fun selfIntroduction(): String {
        return "私は${this.name}です。 ${this.age}歳です。"
    }
}

#継承とオーバーライド

//openで継承可能クラス
open class Person(var name: String = "ナナシ"){
    //openでoverride可能メソッド
    open fun selfIntroduction(): String {
        return "私は${this.name}です。"
    }
}

//クラス変数の定義の後の:で継承クラスを指定
class BusinessPerson(name: String, var depart: String): Person(name) {
    //superクラスの関数を書き換え
    //これ以上派生クラスでoverrideさせたくなければfinalを付ける
    final override fun selfIntroduction(): String{
        return "${super.selfIntroduction()}部署は${this.depart}です。"
    }
}

val person = BusinessPerson("田中", "営業")
println(person.selfIntroduction())
//私は田中です。部署は営業です。

open修飾子をつけることでそのクラスは継承可能クラスとなります。これをつけずに継承はできません。またメソッドにopenをつけることで、子クラスでメソッドのoverrideを許可できます。継承する時は子クラス側の宣言箇所の:Person(name)の部分で継承しています。この時、PersonのコンストラクタにはBusinessPersonに渡されたnameが同じく渡されます。

親クラスのメンバやメソッドにアクセスしたい場合はsuper.xxxでアクセスできます。BusinessPersonクラスをさらに派生させたいけど、これ以降の子クラスでメソッドのoverrideを許可したくない場合はfinal修飾子を用います。
##抽象クラス

//抽象クラス
abstract class Figure(var width: Double, var height: Double) {
    //図形の面積を求める
    //抽象メソッド
    abstract fun getArea(): Double
    fun getWidthAndHeight() = mapOf("width" to this.width, "height" to this.height)
}
//抽象クラスを継承した場合は抽象メソッドのオーバーライドが必須となる
//共通の機能だけを基底クラスで用意しておいて、個別の機能は派生クラスの実装に委ねる
class Triangle(width: Double, height: Double): Figure(width, height) {
    override fun getArea(): Double = this.width * this.height / 2
}

class Rectangle(width: Double, height: Double): Figure(width, height) {
    override fun getArea(): Double = this.width * this.height
}

val triangle = Triangle(10.0, 30.0)
val rectangle = Rectangle(10.0, 20.0)
println(triangle.getWidthAndHeight())//{width=10.0, height=30.0}
println(triangle.getArea())//150.0
println(rectangle.getWidthAndHeight())//{width=10.0, height=20.0}
println(rectangle.getArea())//200.0

抽象メソッドを定義するにはabstract修飾子を用います。抽象メソッドを含むクラス(抽象クラス)にもabstract修飾子が必要となります。抽象クラスを継承した子クラスでは、抽象メソッド(getArea)のoverrideが必須となります。このように抽象クラスでは共通の機能(getWidthAndHeight)だけを基底クラスに用意しておき、個別の機能(getArea)は派生クラスで実装を委ねたいときに使用できます。

##インターフェイス
インターフェイスの大まかな認識は

  • 全てのメソッドは抽象メソッドである
  • 全てのプロパティは抽象プロパティ(またはカスタムアクセサを持つ)
  • コンストラクタは持てない
  • 複数のインターフェースを同時に継承できる
    という点でしょうか。
    例えば、
abstract class A(val property1: Int, val property2: Int){
    abstract val property3: Int 
    abstract fun method1(): Unit
    abstract fun method2(): Int
    abstract fun method3(): String
    fun method4(property1, property2): Int {...}
}

class B(property1: Int, property2: Int): A(property1, property2){
    override val property3: Int = 0 //使わない
    override fun method1(): Unit {...}
    override fun method2(): Int {...}
    override fun method3(): String {...}//使わない
}

class C(property1: Int, property2: Int): A(property1, property2){
    override val property3: Int = 0
    override fun method1(): Unit {...}//使わない
    override fun method2(): Int {...}
    override fun method3(): String {...}
}

このように基底クラスAの子クラスBとCがあったとして、BにもCにも一部使わないものがあるときなどにコードが冗長になります。
そもそも使わないメソッドやプロパティを持っていること自体よろしくありません。
これをインターフェースを用いて書き換えると


interface X {
    fun method2(): Int
}

interface Y {
    val property3: Int
    fun method3(): String
}

abstract class A(val property1: Int, val property2: Int){
    abstract fun method1()
    fun method4(): Int {...}

class B(property1: Int, property2: Int): A(property1, property2), X{
    override fun method1(){...}
    override fun method2(): Int {...}
}

class C(property1: Int, property2: Int): A(property1, property2), Y{
    override val property3 = 0
    override fun method1(){...}
    override fun method3(): String {...}
}   

このように子クラスごとで必要となるメソッドやプロパティ郡の最小単位をinterfaceとして定義しておいて、それを「実装」しています。
クラスの「継承」とインターフェイスの「実装」は: A(property1, property2), Xこの部分で同時に行うことができます。
interfaceではデフォルト実装を持つこともできます。
デフォルト実装をしておいてクラスによってはoverrideの必要がない場合はoverrideしなくても大丈夫です。

interface X {
    fun method2(): Int {return 0}
}

abstract class A(val property1: Int, val property2: Int){
    abstract fun method1()
    fun method4(): Int {...}
}
class D(property1: Int, property2: Int): A(property1, property2), X{
    override fun method1(){...}
    //override fun method2(): Int {...}
}

val d = D(1,1)
println(d.method2())//->0

#データクラス
一般的なクラスはデータ(プロパティ)とそれに関連する機能(メソッド)の集合体ですが、
データクラスはデータの集合体だけを扱うクラスです。

data class Member(
        val id: Int,
        val name: String,
        val age: Int,
        val hobby: String
)
val member = Member(1, "太郎", "20", "映画鑑賞")

データクラスのプライマリコンストラクタの引数は必ずvar,valをつける必要があり、また1つ以上の引数が必要です。
データクラスの目的は、単なるデータの集合体を表すため、classブロック(コンストラクタ宣言のあとの{...}の部分)は省略するのが一般的?だと思いますが、必要に応じてclassブロックを定義することもできます。
データクラスでは以下の4つのメソッドが自動的に定義されています。

  • equals
  • toString
  • componentN
  • copy
    ###equals
    同値性を確認します。
    同じデータクラスで、プライマリコンストラクタで定義された全てプロパティが等しい場合にそのオブジェクト同士は「同値」です。
    以下は同値の例です。
data class Book(
    val title: String,
    val author: String
){
    val price : Int? = null
}
val book1 = Book("こころ", "夏目漱石")
book1.price = 650
val book2 = Book("こころ", "夏目漱石")

println(book1.equals(book2))//->true
println(book1 == book2)//こうとも書けます。

プライマリコンストラクタ外で定義された変数が等しいかどうかまでは関係ありません。
一方こちらは同値ではありません。

val book1 = Book("銀河鉄道の夜", "宮沢賢治")
val book2 = Book("こころ", "夏目漱石")

println(book1 == book2)//->false

###toString
こちらはprintln()などでデータの文字列表現が要求された場合に自動的に呼び出されるメソッドです。

data class Book(
    val title: String,
    val author: String
){
    var price : Int? = null
}
val book1 = Book("こころ", "夏目漱石")
book1.price = 650
println(book1)//->Book(title=こころ, author=夏目漱石)
println(book1.toString())//こう書いてもOK

あんまりtoString()を意識してコーディングすることはないとは思います。
###componentN
データクラスの各プロパティの内容を個々の変数に分解するときに使います。
プライマリコンストラクタで宣言された変数しか切り出せません。

data class Book(
    val title: String,
    val author: String
){
    var price : Int? = null
}
val book1 = Book("こころ", "夏目漱石")
val (title, author) = book1
/*
val title = book1.component1()
val author = book1.component2()
と書いても同じです。
*/
println(title) //->こころ

component1,2,3...Nのメソッドはプライマリコンストラクタの個数によって自動的に定義してくれます。
###copy
特定のプロパティだけを変更してオブジェクトの複製を行います。
対象は例にも漏れずプライマリコンストラクタで宣言されたプロパティのみです。

data class Book(
    val title: String,
    val author: String
){
    var price : Int? = null
}
val book1 = Book("こころ", "夏目漱石")
book1.price = 650
val bookCopy = book1.copy(title = "それから")
println(bookCopy)//->Book(title=それから, author=夏目漱石)
println(bookCopy.price)//->null

#オブジェクト宣言
アプリの設定項目をまとめたクラスなど、そのクラスはシングルトン(1つしかインスタンスを生成しない)として宣言したいときはclassの代わりにobjectを用います。
またこれはクラスの宣言ではなくオブジェクトの宣言なのでコンストラクタは持てません。

object Config {
     val url = "https://kotlinlang.org/docs/reference/object-declarations.html"
     val userName = "hogehoge"
     
     fun printUrl() {println(url)}
}

val config = Config
config.printUrl()//->https://kotlinlang.org/docs/reference/object-declarations.html

##オブジェクト式
あるクラスの再利用は想定しておらず、その場限りでしか使わないときはオブジェクト式として記述できます。

btn.setOnClickListenere(object: View.OnClickListener{
    override fun onClick(view: View) {
        Log.v("Sample Button", "Clicked!")
    }
}) 

以下の構文で書けます。

object: <基底クラスまたはインターフェース>{
    <クラス本体>
}

View.OnclickListenerインターフェースをimplementし、そのonClickメソッドをオーバーライドしたオブジェクトを渡しています。
##Single Abstract Method
View.OnclickListenerは一つだけしか抽象メソッドを持ちませんが、そのようなインターフェースをSAM(Single Abstruct Method)と呼びます。
このようなインターフェースはラムダ式に変換することもできます。

btn.setOnClickListener({
    view: View -> Log.v("Sample Button", "Clicked!")
})

直感的にはインターフェースの宣言とその実装、overrideまで全てこのラムダ式で表現しているということでしょうか。
さらにラムダ式では引数を利用していなければ省略できますし、関数の引数にラムダ式しか存在しない場合、()も省略できます。

btn.setOnClickListener{Log.v("Sample Button", "Clicked!")}

もはや原型がほとんどありませんがKotlinらしい記法なのではないでしょうか。

#コンパニオンオブジェクト
直感的にはクラス内部でのオブジェクト宣言?がコンパニオンオブジェクトです。
あるクラスのメンバ(value, function)などがそのクラス内のどのメソッドでも共通であるときなどに使えます。
Javaのstaticメンバと同じような感じです。
以下は例です。
コンストラクタをprivateにすることによってクラス外からインスタンス化できないようにしています。そのためこのクラス共通の関数としてインスタンスを返すものを定義する必要があります。
Japaneseのインスタンスを返す関数getInstanceを定義します。companion object内で宣言することによりこのクラスで一意な関数として定義できます。
またJapaneseには人種を表すプロパティRACEがあります。RACEJapaneseクラスの中では一意な定数"日本人"として定義できます。

class Japanese private constructor(var name : String){
    companion object {
        const val RACE = "日本人"
        fun getInstance(): Japanese {
            return Japanese("太郎")
        }
    }
    override fun toString(): String {
        return "${this.name}, $RACE"
    }
}
val person1 = Japanese.getInstance()
println(person1.toString())//->太郎, 日本人

複数のJapaneseインスタンスを扱う必要があるが、どのインスタンスでも共通の変数や関数はcompanion objectにしておくといった使い方ができます。

#Enumクラス
曜日や季節などの特定の値から構成されるときに重宝します。

enum class Season {
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER
}
val s = Season.SPRING
println(s)//->SPRING
//型チェック
println(s is Season) //->true

Enumクラスにプロパティを持たせて特定の値を特定の値に変換したりするときにも使用できます。

enum class Season(val seasonCode, val display: String){
    SPRING(1, "春"),
    SUMMER(2, "夏"),
    AUTUMN(3, "秋"),
    WINTER(4, "冬")
}
val display = when(seasonCode) {
    Season.SPRING.seasonCode -> Season.SPRING.display
    Season.SUMMER.seasonCode -> Season.SUMMER.display
    Season.AUTUMN.seasonCode -> Season.AUTUMN.display
    Season.WINTER.seasonCode -> Season.WINTER.display
    else -> null
}

マジックナンバーを避けたいときに使えますね!
#最後に
Kotlinのオブジェクト指向周りの記述はもっと奥があるので、奥を触る機会があればまた書こうかと思います。

10
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?