[Day 16] Data Class

Data Class

Kotlin 的 data class 其實就如同在撰寫 spring 時常常使用到的 dto (data transfer object),也就是這類 class 通常都是用來單純傳遞資料的物件。

data class 也可以說是所謂的貧血的領域模型(Anemic Domain Model),是一個只有 Setter 和 Getter不含業務邏輯的數據模型。

建立的限制

要建立一個 data class 有以下幾個限制:

  • primary constructor 至少要有一個參數。

  • primary constructor 的參數都要有 val 或 var 關鍵字(也就是不能有屬性)。

    像是這樣就是不行的,有兩個參數是屬性,val 或 var 宣告在 {} 內

    class Wallet(val id: Long, _balance: BigDecimal, _createTime: LocalDateTime) {

    var balance = _balance
        ...

    var createTime = _createTime
				...
  • data class 前面不能再有 abstract, open, sealed, inner 關鍵字。

舉例如下,這樣短短一行,就幫我們宣告了一個 data class!

data class Employee(var name: String, var id: Long)

如果在 Java 的話,就還要加上 lombok dependency 然後像下面這樣使用才有類似效果。

@Data
public class Employee {
    private String name;
    private Long id;
}

當然 lombok 還有一些好用的東西,像是 @Builder, @AllArgsConstructor, @NoArgsConstructor, @Delegate 等有用的 annotation

但是 Kotlin 的 data class 還帶給我們其他沒有的東西,像是解構和 copy() 的功能。

data class Employee(var name: String, var id: Long)

這短短的一行 data class 會幫我們自動建立

  • equals() - 幫我們產生物件比較的方法
  • hashCode() - 幫我們產生物件的 hash code
  • toString() - 幫我們產生要印出物件屬性的方法
  • componentN() - 用來提供解構用的
  • copy() - 提供複製物件用

透過 show kotlin bytecode 就可以看到為我們產生了這麼多東西

public final class Employee {
   @NotNull
   private String name;
   private long id;

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

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name = var1;
   }

   public final long getId() {
      return this.id;
   }

   public final void setId(long var1) {
      this.id = var1;
   }

   public Employee(@NotNull String name, long id) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.id = id;
   }

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

   public final long component2() {
      return this.id;
   }

   @NotNull
   public final Employee copy(@NotNull String name, long id) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      return new Employee(name, id);
   }

   // $FF: synthetic method
   public static Employee copy$default(Employee var0, String var1, long var2, int var4, Object var5) {
      if ((var4 & 1) != 0) {
         var1 = var0.name;
      }

      if ((var4 & 2) != 0) {
         var2 = var0.id;
      }

      return var0.copy(var1, var2);
   }

   @NotNull
   public String toString() {
      return "Employee(name=" + this.name + ", id=" + this.id + ")";
   }

   public int hashCode() {
      String var10000 = this.name;
      int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
      long var10001 = this.id;
      return var1 + (int)(var10001 ^ var10001 >>> 32);
   }

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

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

equals(), hashCode(), toString()

跟前面要點一樣,不多說。

componetN()

其中 componetN() 會長這樣

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

   public final long component2() {
      return this.id;
   }

因為如果今天要幫一般 class 做解構的程式的話,要定義 operator fun component1....到 N,看 constructor 內有幾個參數

class test(var name: String, var id: Long) {
    operator fun component1(): String {
        return name
    }
    operator fun component2(): Long {
        return id
    }
}

fun main() {
    val (name, id) = test("tim", 123L)
}

而 data class 會自動幫你產生解構! 很方便吧!

copy()

copy() 很方便可以立刻幫我們複製一模一樣的物件出來,但要注意的是 copy() 做得其實是所謂的淺拷貝 ,所以如果像是集合類的資料的話並不會另外複製一份新的集合類的記憶體來使用!

舉例來說,現在有以下兩個 data class EmployeeCompany

其中 Company 最後有個參數是 Employee 的集合資料

data class Employee(var name: String, var id: Long)

data class Company(var name: String, var employees: MutableList<Employee>)

下面首先宣告了兩個員工的物件

// copy()
val tim = Employee("tim", 555L)
val jean = Employee("jean", 666L)

val company = Company("Google", mutableListOf(tim, jean))
val subCompany = company.copy(name = "Google youtube")
company.employees.add(Employee("ann", 777L))

println(company)
println(subCompany)

然後加入了

val company = Company("Google", mutableListOf(tim, jean))

隨後我複製了原本的公司,並且給了不一樣的名稱

val subCompany = company.copy(name = "Google youtube")

接著希望母公司和子公司的員工清單會不一樣,所以又加了一個 ann 進去清單

company.employees.add(Employee("ann", 777L))

最後印出兩家公司的結果

令人吃驚的結果是兩家公司的員工都變成三位了,也就代表原公司的員工跟著一起變了,因為在 data class 的 copy() MutableList 都是用同一份!這點要特別注意!

Company(name=Google, 
employees=[Employee(name=tim, id=555), Employee(name=jean, id=666), Employee(name=ann, id=777)])


Company(name=Google youtube, 
employees=[Employee(name=tim, id=555), Employee(name=jean, id=666), Employee(name=ann, id=777)])

另外我也做了測試,如果參數是物件的話,結果會如何,這部分就沒有問題。

data class ObTest(var test: Test, var money: BigDecimal)

val o = ObTest(Test("tim",123), BigDecimal("1000"))
val o2 = o.copy()
o2.test = Test("ann",555)
o2.money = BigDecimal(500)
println(o2)
println(o)

結果印出來 o 並沒有被 o2 更改到,沒有問題!

ObTest(test=name: ann, id: 555, money=500)
ObTest(test=name: tim, id: 123, money=1000)

以上就是今天的介紹!我們明天見!

今日練習的程式在這: 請點我