如何使用 data class

如何使用 data class

這邊來討論一下 data class,什麼是 data class,簡單來說就是會自動幫你生成一些實用或者標準的方法。

說明

舉個例子來說好了,假設現在有一個類別叫做 Person,如下面這樣宣告。

class Person(name: String, age: Int) {
    private lateinit var userName: String
    private var userAge: Int
    init {
        userName = name
        userAge = age
    }
}

接著對其反組譯看看 Java 會長怎樣?

public final class Person {
   private String userName;
   private int userAge;

   public Person(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.userName = name;
      this.userAge = age;
   }
}

結果程式長出來跟我們做的事情差不多,只是把傳入的參數放置到指定的類別成員上,但是如果你想要有 getter/setter 的話就要再額外寫方法來處理。

不過 Kotlin 提供了另外一個方法可以讓你很輕鬆的就產出 getter/setter 了,只要將傳入建構子參數前面加上 val 它就會自動產生了,程式碼如下。

class Person(val name: String, val age: Int) {}

我們將其反組譯來看,可以看到在 Java 程式會長這樣。

public final class Person {
   @NotNull
   private final String name;
   private final int age;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final int getAge() {
      return this.age;
   }

   public Person(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.age = age;
   }
}

這邊我們可以發現,從建構子內傳進來的參數會自動轉化成 getter / setter,透過這樣我們就可以直接存取傳進來的變數。

但是我們會想,如果想要內建一些 toString、hashCode 以及 equals 這些方法呢?Kotlin 也提供了相關的語法可以讓你輕鬆操作,如果我們對於這個類別加入了 data 的宣告,如下面所示。

data class Person(val name: String, val age: Int)

接著我們對其反組譯來看看長怎樣。

public final class Person {
   @NotNull
   private final String name;
   private final int age;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final int getAge() {
      return this.age;
   }

   public Person(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.age = age;
   }

   @NotNull
   public final String component1() {
      return this.name;
   }

   public final int component2() {
      return this.age;
   }

   @NotNull
   public final Person copy(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      return new Person(name, age);
   }

   // $FF: synthetic method
   // $FF: bridge method
   @NotNull
   public static Person copy$default(Person var0, String var1, int var2, int var3, Object var4) {
      if((var3 & 1) != 0) {
         var1 = var0.name;
      }

      if((var3 & 2) != 0) {
         var2 = var0.age;
      }

      return var0.copy(var1, var2);
   }

   public String toString() {
      return "Person(name=" + this.name + ", age=" + this.age + ")";
   }

   public int hashCode() {
      return (this.name != null?this.name.hashCode():0) * 31 + this.age;
   }

   public boolean equals(Object var1) {
      if(this != var1) {
         if(var1 instanceof Person) {
            Person var2 = (Person)var1;
            if(Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

你會發現除了剛剛講的三個方法以外,還有增加許多內建功能,比如說 copy、componentN等相關方法。

那我們來釐清一下到底發生什麼事情了?

首先,如果你透過建構子傳進來的參數,將會產生以下的幾種方法。

  • equals()/hashCode()

  • toString() 會變成 Person(name=givemepass, age=25)

  • 會產生 componentN() functions ,N 代表你有幾個成員變數是從建構子傳進來的,如果你是宣告在類別內部的話,就沒你的份。

  • copy()

有幾個重點可能需要注意一下。

  • 如果你要宣告成 data class,那麼就必須至少傳入一個建構子參數。

  • 傳入的參數必須標記成 val 或者 var

  • 這個 class 不能是 abstract、open、sealed 或者 inner。

  • 如果你要覆寫 equals()hashCode()toString()這些方法,那麼就必須在 data class 的內部事先宣告,那麼就不會出現內建的方法了。

data class Person(val name: String) {
    override fun toString(): String {
        return "abc"
    }
}

反組譯以後就會看到這個方法被你宣告的方法覆寫了。

@NotNull
public String toString() {
  return "abc";
}
  • copy 會直接回傳一個新的物件
@NotNull
public final Person copy(@NotNull String name, int age) {
  Intrinsics.checkParameterIsNotNull(name, "name");
  return new Person(name, age);
}

如果你想要複製一個物件可以這樣玩。

data class Person(val name: String, val age: Int)

fun main(args: Array<String>) {
    val p1 = Person("givemepass1", 20)
    val p2 = p1.copy("givemepass2", 30)
    println(p1)
    println(p2)
    println("p1 == p2 :${p1 == p2}")
}

結果如下。

Person(name=givemepass1, age=20)
Person(name=givemepass2, age=30)
p1 == p2 :false

這邊有一個很有趣的例子。

data class Person(val name: String){
    var age = 30
}

fun main(args: Array<String>) {
    val p1 = Person("givemepass")
    val p2 = Person("givemepass")
    p1.age = 20
    p2.age = 30
    println("p1 == p2 :${p1 == p2}")
}

猜看看結果會是怎樣?

公佈答案,結果是 true。

p1 == p2 :true

為什麼啊?因為 data class 對於 equals 只限定於有傳入的參數,因此,如果你的成員是宣告在類別內部的,那麼就會被視為同一個物件。

參考資料

https://kotlinlang.org/docs/reference/data-classes.html