KotlinでAndroid-No.08(クラス)

対象バージョン:Android Studio v4.1, Kotlin v1.4

クラス

今回はKotlinのクラスについて確認していきます。

前置き

毎度の事ですが、ここで紹介している内容は、僕自身がKotlinでプログラミングする上で必要と思う事を記載しています。(ほぼ備忘録です)

間違いや、不足部分、正しくない使い方も多々あると思いますので、記載内容に疑問がある場合はKotlinの正式なサイトや、他の方の記事も参考にしてみてください。

基本的なクラス

最初は、普通の基本的なクラスを見て行きます。

基本的な(?)宣言

基本的というか、他の言語のクラスに近い宣言の記載方法は下記になります。

ただ、おそらく他のサイトなどで、クラスをこの様に宣言している所は少ないかも知れません^^;

// クラスの宣言
class MyClass
{
    // メンバ
    var id : Int
    var name : String

   // プロパティ
    val initialChar : Char
        get() {
            // 名前の先頭文字を取得
            return name[0].toChar()
        }
  
    // コンストラクタ(引数0)
    constructor() {
        this.id = -1
        this.name = "(none)"
    }

    // コンストラクタ(引数1)
    constructor(id: Int) {
        this.id = id
        this.name = "(none)"
    }

    // コンストラクタ(引数2)
    constructor(id : Int, name : String) {
        this.id = id
        this.name = name
    }
}

fun TestFunc()
{
    val mc = MyClass()
    dispMsg("[MyClass]id = " + mc.id + ", name = " + mc.name)
}

コンストラクタが他の言語の様に「クラス名()」ではなく、「constructor()」になっているのが特徴でしょうか?

各コンストラクタでは、未初期化の変数「id」、「name」をそれぞれ初期化しています。初期化をしないとエラーになります。

ここで、上記の様に「constructor」を使って宣言されるコンストラクターは「Secondary Constructor](セカンダリコンストラクタ)と呼ばれています。

上記の宣言方法は「Primary Constructor」(プライマリコンストラクタ)無しの宣言方法という事になります。

Primary Constructorを指定した宣言

引数0個の場合

引数が0個の場合のPrimary Constructorを指定した宣言は下記の様になります。

// 引数0個のPrimary Constructor付のクラス
class MyClass constructor() // もしくはclass MyClass()
{
    // メンバー
    var id : Int = -1
    var name : String = "(none)"

    /*
    // コンストラクタ(引数なし)
    constructor() {
        this.id = -1
        this.name = "(none)"
    }
    */
    
    // コンストラクタ(引数1)
    constructor(id: Int) {
        this.id = id
        this.name = ""
    }

    // コンストラクタ(引数2)
    constructor(id : Int, name : String) {
        this.id = id
        this.name = name
    }
}

Primary Constructorを利用しない場合との変更点は以下になります。

  • クラス名の後に「constructor()」もしくは「()」が付く(「constructor」は省略可能
  • メンバを初期化している
  • 引数0個のSecondary Constructorをコメントアウトしている。

この例では、引数0個のコンストラクタを、Secondary Constructorから、Primary Constructorへ移しているという事になります。

以下の部分がPrimary Constructorとなります。

class MyClass constructor() // もしくはclass MyClass()
{
    // メンバ
    var id : Int = -1
    var name : String = "(none)"

Kotlinでは、変数を初期化なしでは利用できないので、初期化が必須になります。

上記では、メンバ宣言箇所で初期化していますが、initブロックで初期化する事も出来ます。

class MyClass()
{
    // メンバ
    var id : Int
    var name : String
  
    // initでの初期化
    init {
        id = -1 
        name = "(none)"
    }

initブロックではオブジェクト(インスタンス)生成時に記載されている処理が実行されます。メンバの初期化以外の処理も実行可能です。

また、Primary ConstructorとSecond Constructorともに引数の個数・型の順番が同じもの宣言する事が出来ないので、ここでは、引数0 個のSecondary Constructorを宣言するとエラーになるのでコメントアウトしています。
(どちらのコンストラクタを実行すればよいか分からなくなるので、当然と言えば当然ですね…)

また、Primary Constructorが宣言されている場合には、Secondary ConstructorからPrimary Constructorを実行する(呼び出す)必要がありますが、引数0個の場合は記載を省略できます。

ただし、前述のinitブロックを記載した際には、コンパイル時に下記の様なコンパイルエラー(「Primary constructor call expected」)が出るので、Primary Constructorを呼び出す必要があります。(エディタ上ではエラーにならないのが厄介ですね…)

Secondary ConstructorからPrimary Constructorを呼び出すには、下記の様に記載します。
(「constructor(…)」の後に「: this()」を付けます)

    // コンストラクタ(引数1)
    constructor(id: Int) : this() {
        this.id = id
        this.name = ""
    }

    // コンストラクタ(引数2)
    constructor(id : Int, name : String) : this() {
        this.id = id
        this.name = name
    }

引数が複数個の場合

宣言方法①

引数が複数個存在するPrimary Constructorを持つクラスの宣言は下記の様になります。

// 引数が複数個のPrimary Constructorを持つクラス
class MyClass2(var id :Int, var name : String)

fun TestFunc()
{
    val mc2 = MyClass2(1, "one")
    dispMsg("id = "+ mc2.id + ", name = " + mc2.name)
}

この例では、メンバの宣言が省略しています。また、Secondary Constructorも記載していないので、C言語のプロトタイプ宣言の様に見えますが、れっきとしたクラスの宣言です。

上記の様な宣言をした場合、Primary Constructorの引数名をそのまま、メンバ名として利用できます。

「var」もしくは「val」が付与されていれば、メンバーとして扱われます。

宣言方法②

Primary Constructorの引数に「var」や「val」を付けない場合は、それはメンバーとして扱われません。

class MyClass2(id :Int, name : String)
{
    var myId : Int = id
    var myName : String = name
}

fun TestFunc()
{
    val mc2 = MyClass2(1, "one")
    //dispMsg("id = "+ mc2.id + ", name = " + mc2.name) // これはエラーになる
    dispMsg("id = "+ mc2.myId + ", name = " + mc2.myName)
}
Secondary Constructorを宣言する場合

複数の引数を持つPrimary Constructorを宣言したクラスにSecndary Constructorを宣言した場合は、必ず、Primary Constructorの呼び出しが必要になります。

【間違い】
エディタ上ではエラーにはなりませんが、コンパイルエラー(「Primary constructor call expected」)が発生します。

class MyClass2(var id :Int, var name : String)
{
    constructor()
    {
        id = -1;
        name = "(none)"
    }
}

【正解】
Secondary Constructorの後ろに「: this(引数)」を指定します。

class MyClass2(var id :Int, var name : String)
{
    constructor() : this(-1, "(none)")
    {
        // 初期化処理
    }

また、Primary Constructorと同じ型の順序の引数を持つSecondary Constructorは宣言できません。(型の順序が異なっていれば、個数が同じでも宣言は可能です)

引数のデフォルト値の指定

Primary Constructor / Secondary Constructorの引数にデフォルト値を指定する事も可能です。

class MyClass2(var id :Int = -999, var name : String = "(none)")

fun TestFunc()
{
    val mc2_0 = MyClass2() // mc2_0.id = -999, mc2_0.name = "(none)"
    val mc2_1 = MyClass2(1) // mc2_1.id = 1, mc2_1.name = "(none)"
    val mc2_2 = MyClass2(1, "One") // mc2_2.id = 1, mc2_2.name = "One"
}

引数の異なるコンストラクタを呼び出している様に見えますね。

では、このMyClass2に引数1個のSecondary Constructorを宣言したら、どうなるのでしょうか?

class MyClass2(var id :Int = -999, var name : String = "(none)")
{
    constructor(id : Int) : this(id, "<NONE>")
    {
        // 初期化処理
    }
}

fun TestFunc()
{
    val mc2_1 = MyClass2(1) // mc2_1.id = 1, mc2_1.name = "<NONE>"
}

正解は、引数の個数が一致するコンストラクタが優先される様です。

オブジェクト生成時の処理順

下記の様なクラスで、Secondary Constructorを呼び出した場合の処理順をログで確認してみました。

class MyClass()
{
  // メンバの初期化
  var id : Int = 0
  var name : String = "(zero)"
  
  // initブロック
  init{
    Log.i("MyClass", "[START]init block id = " + id + ", name = " + name)
    id = 1
    name = "(one)"
    Log.i("MyClass", "[END]init block id = " + id + ", name = " + name)
  }
  
  // Secondary constructor
  constructor(id : Int, name : String) :this() {
    Log.i("MyClass", "[START]Secondary constructor id = " + this.id + ", name = " + this.name)
    this.id = id
    this.name = name
    Log.i("MyClass", "[END]Secondary constructor id = " + this.id + ", name = " + this.name)
  }
}

fun TestFunc()
{
    val mc = MyClass(2, "two")
    Log.i("MyClass", "mc : id = " + mc.id + ", name = " + mc.name)
}

上記を実行すると、Debugタブには下記の様に表示されました。

I/MyClass: [START]init block id = 0, name = (zero)
    [END]init block id = 1, name = (one)
I/MyClass: [START]Secondary constructor id = 1, name = (one)
I/MyClass: [END]Secondary constructor id = 2, name = two
I/MyClass: mc : id = 2, name = two

これから、下記の様に処理が行われる様です。

  1. メンバの初期化の実行(Primary Constructorの実行)
  2. initブロックの実行
  3. Secondary constructorの実行

初期化時に処理順が重要な場合は、これも少し念頭に置いておいた方がよさそうです。

メンバー・プロパティ・メソッド

アクセス修飾子

ここまで、クラスのメンバには、特にアクセス修飾子を指定しませんでしたが、どのような属性を持っているかを、Android Studioのエディタ上で確認してみます。

何もアクセス修飾子を指定しないと、「public final」として扱われる様です。

public, protected, privateの扱いは他の言語と同じ扱いです。

アクセス修飾子インスタンスからのアクセス継承先からのアクセス
public
protected×
private××

「final」が定義されている場合は、継承先のクラスでoverrideして利用する事が出来ない事を意味しています。

「public」や「final」など属性を明示的に記載できますが、Android Studioのエディタ上では、省略可能を示す薄いグレーで表示される様になります。

Primary Constructorの引数にも、privateやprotected属性を指定する事も出来ます。

プロパティやメソッドについても、アクセス修飾子を指定をしなければ「public final」として扱われます。

メソッド

メソッドを定義する場合、文法的に問題ないと思われてもエラーになる場合があるので、注意が必要です。

例えば、以下の様なメンバーのゲッター(get + パラメータ名)・セッター(set + パラメータ名)を宣言するとエラーになります。

「JVM上ですでに同じ名前の関数があるよ」的なエラーになります。

おそらく、上記例では「id」パラメータがpublic扱いなので、Kotlinが自動的にゲッター・セッターを生成しているという事なのだろうと思われます。

実際、「id」をprivateに変更したり、引数や戻り値のと、このエラーはなくなります。

ゲッターやセッターのつもりでなくても、「get」や「set」で始まるメソッド名には注意が必要と言う事になりますね…^^;

プロパティ

プロパティは、「メンバーの様に利用できるゲッター・セッターを隠蔽したメソッド」と個人的に解釈しています。

以下の様に、初期値の代わりにget(), set()を記載します。

    // 名前の最初の文字の取得
    val initialChar : Char
        get() {
            return name[0].toChar()
        }

    // 名前の長さの取得と設定
    var nameLen : Int
        get() { return name.length }
        set(value) {
            if(value <= 0)
                name = ""
            else if(value <= name.length)
                name = name.substring(0..value-1)
            else {
                val spaceNum = value - nameLen
                val formatStr = "%" + spaceNum + "s"
                name = name + formatStr.format(" ")
            }
        }

ゲッターだけのプロパティは「val」で宣言し、ゲッター+セッター(もしくはセッターのみ)の場合は「var」で宣言します。

クラスの継承

親クラスの宣言

クラスを継承させる場合には、クラスの宣言時に「open」属性を追加する必要があります。

// 継承可能なクラス
open class ParentClass()
{
    // メンバ
    var id : Int = -1
    var name : String = "(none)"
}

「open」属性を指定しないクラスは、「final」(継承不可)属性となっています。

子クラスの宣言

子クラスを宣言する場合には、子クラスの後に「: 親クラス」を指定します。

また、継承したクラスは親のコンストラクタを呼び出す必要があります。

【宣言方法①】

// 子クラス(Parentクラスを継承)
class ChildClass : ParentClass {
    // コンストラクタ(1)
    constructor() : super()
    // コンストラクタ(2)
    constructor(id : Int) : super()
    {
        this.id = id
    }
    // コンストラクタ(3)
    constructor(name : String) : this()
    {
        this.name = name
    }
}

上記の場合、コンストラクタ(1)とコンストラクタ(2)は親クラスのコンストラクタ「super()」を呼び出しています。
コンストラクタ(3)は子クラス自身のコンストラクタ「this()」を呼び出していますが、その先で「super()」を呼び出している事になります。

【宣言方法②】

// 子クラス(Parentクラスを継承)
class ChildClass() : ParentClass()
{
    // コンストラクタ(1)
    constructor(id : Int) : this()
    {
        this.id = id
    }

    // コンストラクタ(2)
    constructor(name : String) : this()
    {
        this.name = name
    }
}

Primary Constructorを利用している場合は、継承元の親クラス名のあとにコンストラクタを呼び出すために「(引数、引数、…)」を付けます。

また、この宣言方法では、小クラスのSecondary Constructorから、「super()」を使って親クラスのコンストラクタは呼び出せません。

生成時処理順

子クラスのオブジェクト(インスタンス)を生成した場合の処理順は下記の様になります。

  1. 親クラスのコンストラクタ
  2. 子クラスのメンバの初期化(子クラスのPrimary Constructor)
  3. 子クラスのinitブロック
  4. 子クラスのSecondary Constructor

data class(データクラス)

値を格納するのに特化したクラスとしてdata classというものが用意されています。

宣言

クラス宣言の前に、「data」が付く以外は特に普通のクラスとは変わりません。メンバやコンストラクタ、メソッドを持たす事も可能です。

// データクラス
data class Person(val name : String, val id : Int)

コンポーネントへのアクセス

data classの特徴として、「.component1()」の様なメソッドでメンバの値を取得する事が可能です。(どれくらい有用かは不明ですが…)

val person1 = Person("sato", 10001)
// メンバの値の取得
dispMsg("person1 : id = " + person1.name + ", id = " + person1.id)
// componentN()を使用したメンバの値の取得
dispMsg("person1 : id = " + person1.component1() + ", id = " + person1.component2())

上記の実行結果は下記の様に同じものになります。

person1 : id = sato, id = 10001
person1 : id = sato, id = 10001

copy()メソッド

data classには、自分自身のコピーオブジェクトを生成するための、copy()メソッドが用意されています。

// データクラスのコピー
val person2 = person1.copy()
dispMsg("person2 : id = " + person2.name + ", id = " + person2.id)

// nameを指定してコピー
val person3 = person1.copy(name = "suzuki")
dispMsg("person3 : id = " + person3.name + ", id = " + person3.id)

// id を指定してコピー
val person4 = person1.copy(id = 20001)
dispMsg("person4 : id = " + person4.name + ", id = " + person4.id)

上記の実行結果は下記の様になります。

person2 : id = sato, id = 10001
person3 : id = suzuki, id = 10001
person4 : id = sato, id = 20001

enum class(列挙型クラス)

列挙型クラスは下記の様に宣言します。

// 曜日
enum class DayOfWeek
{
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

fun TestFunc()
{
    val day1 = DayOfWeek.MONDAY
    val day2 = DayOfWeek.TUESDAY
    if(day1 == day2)
        dispMsg("Same")
    else
        dispMsg("Different")
}

enumクラスには、「.name」という隠れたメンバーが存在します。
day1.nameを出力すると「”MONDAY”」という文字列が取得できます。

また、enumクラスには、各要素に対応するパラメータを割り当てる事や、それを利用したメソッドを追加する事も出来ます。(値は重複していても構いません)

// 曜日
enum class DayOfWeek(val id : Int, val str : String)
{
    MONDAY(0, "mon"),
    TUESDAY(1, "tue"),
    WEDNESDAY(2, "wed"),
    THURSDAY(3, "thu"),
    FRIDAY(4, "fri"),
    SATURDAY(5, "sat"),
    SUNDAY(6, "sun");

    // 休日ならtrue, 平日ならfalse
    fun isHoliday() : Boolean =((this.id == 5) || (this.id == 6))
}

fun TestFunc()
{
    var day = DayOfWeek.SUNDAY
    dispMsg("id = " + day.id + ", str = " + day.str + ", name = " + day.name + " : " +
            if(day.isHoliday()) "休日" else "平日" )

    day = DayOfWeek.FRIDAY
    dispMsg("id = " + day.id + ", str = " + day.str + ", name = " + day.name + " : " +
            if(day.isHoliday()) "休日" else "平日" )
}

上記を実行すると、下記の様になります。

id = 6, str = sun, name = SUNDAY : 休日
id = 4, str = fri, name = FRIDAY : 平日

ちなみに、enumクラス宣言時のパラメータに「name」を指定すると、隠れメンバ名と競合するので、エラーになります…^^;

Sealed class (シールドクラス)

Sealed class(シールドクラス)は、継承できる場所が、同じファイル内でしか継承できなくなるクラスです。

上記で、継承可能なクラスの場合には、「open」キーワードを指定すると記載しましたが、openキーワードを付けた場合には、どの場所からもそのクラスを継承したクラスを作成できますが、特定の場所でしか継承させたくない場合に利用します。

// Sealed Class
sealed class SealedClass(var id : Int)

// Sealed Classを継承したクラス
class SubSealedClass(var subId : Int) : SealedClass(subId)

また、Sealed Classをそのままオブジェクトとしては利用できません。(継承した子クラスを利用する事になります。)

また、同じファイルででも下記の様にClassの外で宣言されたSealed Classを継承したり、Classの中で宣言されたSealed Classを継承するような使い方はできません。

object表記

objectキーワードを使うと、無名クラスの様な使い方が出来ます。

fun TestFunc() {
    // object キーワードを利用例
    val member = object {
        var id : Int = 0
        var name : String = "(none)"
        var age : Int = 0

        fun getStr() : String
        {
            return "id = " + id + ", name = " + name + ", age = " + age
        }
    }

    member.id = 100
    member.name = "Nakamura"
    member.age = 30

    dispMsg(member.getStr())
}

実行結果は、下記の様になります。

id = 100, name = Nakamura, age = 30

objectキーワードを利用した場合、他のクラスを継承する事は出来ますが、コンストラクタの設置は出来ません。

※個人的には、きちんとクラス設計してコーディングした方が好みではありますが…^^;

Extension Function(拡張関数)、Extension Properties(拡張プロパティ)

すでに定義されているクラスで、「こんなメソッドやプロパティがあればなぁ」と思った時に、クラスにメソッドやプロパティを追加せずに、クラス外に関数・プロパティを設置できます。

// テスト結果格納データクラス
data class TestScore(var kokugo : Int, var sansu : Int, var rika : Int, var shakai : Int)

// 拡張関数(合計点の取得)
fun TestScore.getTotalScore() = kokugo + sansu + rika + shakai
// 拡張プロパティ(平均点)
val TestScore.averageScore : Float
    get() { return getTotalScore().toFloat() / 4 }

fun TestFunc()
{
    // テスト点数の格納
    val myTestScore = TestScore(75, 86, 90, 68)
    // 合計点の表示
    dispMsg("Total Score   : " + myTestScore.getTotalScore())
    // 平均点の表示
    dispMsg("Average Score : " + myTestScore.averageScore)
}

実行結果

Total Score   : 319
Average Score : 79.75

※個人的には、こういったコードをみると「あれ?このクラスにこんなメソッドやプロパティってあったっけ?」と惑わされそうな仕様ではありますが…^^;

その他

クラスに関しては、Abstract Class(抽象クラス)やInterface Class(インターフェースクラス)などもありますが、今回は省略します。

また、その他にもKotlinの特徴的な機能もあると思いますが、それについても省略します。

自分の勉強が追い付かないという点もありますが、とりあえずは必要最小限という所で収めておきたいと思います^^;


今回のクラスに限らず、Kotlinは色々なコードの書き方(基本は省略)が許されていますね^^;

確かに、どの書き方も矛盾が無ければエラーにならないので、「構文解釈凄いな!」とも思いますが、他の人の書いたコードを読み解くには、自分自身にもその構文解析の能力を必要とされると思うと少しゲンナリする部分もあります^^;

次回予告

次回は、Kotlinの文字列操作について記載したいと思います。


前の記事次の記事
No.07(関数・メソッド)No.09(文字列)