객체와 클래스

·2021년 12월 12일
0
post-thumbnail

코틀린은 클래스 생성자를 함수처럼 사용할 수 있다 그리고 코틀린은 필드를 구현 해 주므로 굳이 시간과 노력을 들여 백킹필드를 정의할 필요가 없다. 개발자는 속성만 정의하면 코틀린이 필요한 부분에 백킹 필드(클래스의 속성 정보)를 만들어 준다.데이터 클래스를 이용하면 데이터를 처리하는 것보다 데이터를 보여주는데 집중할 수 있다.

📌 객체와 싱글톤


싱글톤이란 인스턴스를 하나만 만들어 참조하기 위한 패턴이다. 클래스의 인스턴스 수를 제어하는 건 간단한 일이 아니다. 스레드의 안전성을 보장해야 하는 동시에 인스턴스가 존재하는지를 확인하는 과정에서 오버헤드를 발생시켜서는 안된다.

코틀린은 싱글톤을 직접 지원함으로써 싱글톤을 구현할 때 발생할 수 있는 부담과 구현이 잘못될수 있는 리스크를 제거했다. 개발자가 필요하다면 클래스 정의 없이 객체를 생성할 수 있다. 간단한 상황이면 객체를 바로 사용할 수 있고, 추상클래스를 정의해야하는 복잡한 경우엔 클래스를 정의한 후 만들어서 사용할 수 있다.

객체 표현식으로 사용하는 익명객체

코틀린의 객체 표현식은 Java의 익명 클래스를 생성하는 데 유용하게 쓰잊만, JavaScript의 객체, C#의 익명 타입과 비슷하다. 대부분의 기본형식에서 객체 표현식은 object 키워드 이후에 { } 블록이 나온다.

원을 표현 할 데이터가 필요하다고 가정하고 구현하는 방법을 생각해보자

1. Circle 클래스 만들기 → 원하나를 표현하기 위해서 클래스를 만들기에는 과한 노력이 들어가는것 같다.
2. 여러개의 지역변수를 만들기 → 여러개의 지역변수가 서로 연관 있는지 코드상으로 파악하기 어렵다.


💡 객체 표현식은 이런 경우에 도움이 된다.

drawCircle

fun drawCircle() {
    val circle = object {
        val x = 10
        val y = 20
        val radius = 30
    }

    println("Circle x: ${circle.x}, Circle y : ${circle.y}, Circle radius : ${circle.radius}")
}

drawCircle()

💻 출력

Circle x: 10, Circle y : 20, Circle radius : 30

Circle 객체가 x, y, radius를 간단하게 그룹핑했다. 객체표현식은 여러 개의 지역변수를 그룹핑해 준다.
속성을 val로 정의하는 대신 var로 정의해 뮤터블하게 만들 수도 있다. 객체에 메소드를 추가하는 것도 가능하지만 그럴 경우에는 익명객체를 사용하기보다는 클래스를 만드는 편이 더 좋다.
익명객체에는 한계가 존재하기 때문이다 .

  • 익명객체의 내부 타입은 함수나 메소드의 리턴타입이 될 수 없다.
  • 익명객체의 내부 타입은 함수나 메소드의 파라미터가 될 수 없다.
  • 클래스안에 저장된 속성들이 있다면, 해당 속성들은 Any로 간주된다. 그러면 모든 속성이나 메소드에 직접 접근할 수 없게된다.

대부분의 기초적인 객체표현식은 지역변수를 그룹핑할 때만 사용하자

익명 객체를 인터페이스의 구현체로 만드는것도 가능하다. 마치 Java의 익명 내부 객체와 같다.
object 키워드와 { }블록 사이에 구현하길 원하는 인터페이스의 이름을 쉼표로 구분하여 적으면 된다.

Runnable


fun createRunnable(): Runnable {
    val runnable = object : Runnable {
        override fun run() {
            println("You called...")
        }
    }
    return runnable
}


val aRunnable = createRunnable()
aRunnable.run()

💻 출력

You called...

변수 runnable은 익명 내부 클래스의 인스턴스를 참조한다. 이 함수에서 생성된 인스턴스를 리턴할 수도 있고, 리턴 타입으로 사용할 수도 있다. 만약 인터페이스가 아니라 클래스를 확장한 경우라면 이 클래스는 타입으로 사용될 수도 있다.

익명 내부 클래스에 의해 구현된 인터페이스가 싱글 추상 메소드 인터페이스라면 함수의 블록과 return 키워드를 제거하여 간단한 싱글 표현식 함수로 구현할 수 있다.


fun createRunnable():Runnable= Runnable { println("you called..") }

익명 내부 클래스가 둘 이상의 인터페이스를 구현해야 한다면 리턴이 필요한 경우에는 반드시 리턴타입을 명시해줘야 한다.

fun createRunnable():Runnable=object :Runnable,AutoCloseable{
    override fun run() {
        println("You called...")
    }

    override fun close() {
        println("You closed...")
    }

}

Java에서 익명 내부 클래스를 사용한 부분이라면 어디든지 코틀린의 객체 표현식을 사용할 수 있다.

객체 선언을 이용한 싱글톤

object 키워드와 { } 블록 사이에 이름을 넣는다면, 코틀린은 이를 표현식이 아니라 명령문 또는 선언으로 인식한다.

익명 이너클래스의 인스턴스를 만들 땐 객체 표현식을 사용하도록 하고 싱글톤(하나의 인스턴스만을 만드는 클래스)을 만들 땐 객체 선언을 사용해라.
코틀린에서 싱글톤의 대표적인 예가 Util이다. object 키워드를 사용하면 싱글톤을 직접 생성할 수 있다.

Util


object Util {
    fun numberOfProcessors() = Runtime.getRuntime().availableProcessors()
}

객체 선언을 이용해서 만들 Util 객체가 싱글톤이다. Util로는 객체를 생성할 수 없다.
코틀린 컴파일러는 Util을 클래스로 취급하지 않는다. Util은 이미 객체인 상태이다.

Java의 private 생성자와 static 메소드만 가지고 있는 클래스라고 생각하면 된다. 싱글톤의 메소드를 Java에서 클래스의 static메소드를 호출하듯이 사용할 수 있다.

println(Util.numberOfProcessors())

싱글톤 메소드인 numberOfProcessors()를 이런식으로 호출 가능하다.

싱글톤은 val과 var로 선언된 속성 모두 가질 수 있다. 객체 선언은 객체 표현식처럼 인터페이스를 구현할 수도 있고, 이미 존재하는 클래스를 확장할 수도 있다.
싱글톤이 부모 클래스나 인터페이스를 가지고 있다면 싱글톤은 참조로 할당되거나 부모 타입의 파라미터로 전달될 수 있다.

Singleton

object Sun : Runnable {
    val radiusInKm = 696000
    var coreTemperactureInc = 15000000
    override fun run() {
        println("spin,,,")
    }
}

fun moveIt(runnable: Runnable) {
    runnable.run()
}

println(Sun.radiusInKm)
moveIt(Sun)

💻 출력

696000
spin,,,

moveIt() 함수가 Runnable의 인스턴스를 파라미터로 받고 있어서, 싱글톤 Sun을 moveIt()함수의 파라미터로 전달할 수 있다. 그리고 클래스가 인스턴스에 접근하는 것처럼 Sun을 moveIt() 함수의 파라미터로 전달할 수 있다.

탑레벨 함수 vs 싱글톤

모듈화는 애플리케이션의 안전성을 유지하는 매우 중요한 요소이다. 코틀린을 사용하면 애플리케이션의 요구에 따라 적절한 밸런스를 유지할 수 있다.

탑레벨 함수(패키지에 직접 작성된 함수들)과 싱글톤 중 무엇을 선택해야 할까❓

탑레벨 함수와 싱글톤이 모두 포함 된 예제

package chapter7.util
//탑 레벨 함수
fun unitsSupported() = listOf("Metruc", "Imperial")
fun precision(): Int = throw RuntimeException("Not implemented yet")

//싱글톤
object Temperature {
    fun c2f(c: Double) = c * 9.0 / 5 + 32
    fun f2c(f: Double) = (f - 32) * 5.0 / 9
}

object Distance {
    fun milesToKm(miles: Double) = miles * 1.609344
    fun kmToMiles(km: Double) = km / 1.609344
}

package chapter7.use

import chapter7.util.*
import chapter7.util.Temperature.c2f

fun main() {
   println(unitsSupported())
   println(Temperature.f2c(75.353))
   println(c2f(24.305))
}

main() 메소드 안에서 우리는 탑레벨 함수인 unitsSupported() 와 Temperature 싱글톤의 2cf() 메소드를 호출하고 있다.

  • 사용할 함수들이 하이레벨이거나 일반적이거나 넓게 사용될 예정이라면 패키지 안에 직접 넣어 탑레벨 함수로 사용 하는게 좋다.
  • 반대로 함수들이 연관되어 있다면 싱글톤을 사용하는게 좋다.

✔ 요약하자면 탑레벨 함수와 싱글톤을 이용한 함수의 그룹핑과 모듈화는 애플리케이션 니즈에 기반해야 한다.

우리가 프로그램의 행동, 계산, 작동에 집중할 때 함수와 싱글톤은 적절해진다. 하지만 상태를 다뤄야 한다면 클래스를 사용하는게 더 좋은 선택이다.

📌 클래스 생성


가장 작은 클래스

아주 짧게 클래스를 만드는 예시다. 클래스 이름 앞에 class 키워드를 적으면 된다.

class Car

읽기 전용 클래스

단 한줄의 코드로 yearOfMake라는 이름으로 Int타입의 읽기전용 속성을 만들었다. 코틀린 컴파일러는 생성자를 작성했고, 필드를 정의하고, 해당 필드에 접근해주는 getter을 추가했다

class Car(val yearOfMake: Int)

클래스의 정의에 대해 살펴보자. 위의 코드는 아래의 내용을 축약시킨 것이다

public class Car public constructor(public val YearOfMake:Int)

기본적으로 클래스의 멤버가 public이면 생성자 역시 public이다. 앞의 코드가 정의한 클래스는 실제로 코틀린에서 주 생성자로 정의하고 있다. constuctor 키워드는 접근제어나 주 생성자용 표기를 사용할 게 아니라면 필요없다.

인스턴스 생성하기

코틀린에서 객체를 만들 때는 함수를 사용하듯 그냥 클래스 이름을 이용한다.

val car = Car(2019)
println(car.yearOfMake) //2019

이뮤터블 변수인 car은 Car 클래스의 인스턴스를 참조하고 있다. yearofMake 속성은 인스턴스 car을 통해 직접적으로 접근할 수 있다.

읽기-쓰기 속성

class Car(val yearOfMake: Int, var color: String)

val car = Car(2019, "Red")
car.color = "Green"
println(car.color) //Green

새로 추가된 속성인 color는 String 타입이고 생성자에 의해서 초기화 된다. 하지만 yearOfMake와는 다르게 color는 언제든 값을 변경할 수 있다.

읽기전용 속성을 만들 때는 val, 변경 가능한 속성을 만들 때는 var을 사용하자.

들여다보기 - 필드와 속성

필드와 속성의 차이점에 대해서 생각해보자

✔ 필드는 클래스에서 비공개로 유지되고 get 및 set 속성을 통해 액세스된다.
✔ 속성은 클래스를 사용하는 것들에 의해 액세스되는 외부 방식에 영향을 미치지 않으면서 필드를 변경할 수있는 추상화 수준을 제공한다.

yearOfMake와 color는 Java의 관점에서 봤을 때 속성이라기보다는 필드에 가깝다. 하지만 이것들은 모두 필드가 아니라 속성이다.

🔥 코틀린에서는 클래스에 필드가 없다.

car.yearOfMake를 호출하면 실제로는 car.getYearOfMake()를 호출한 것이다.

class Car(val yearOfMake: Int, var color: String)

위 코드의 바이트 코드를 살펴보자

Compile from "Car.kt"
public final class Car {
private final int yearOfMake;
private java.lang.String color;
public final int getYearOfMake(); 
public final java.lang.String getColor();  
public final void setColor(java.lang.String);  
public Car(int, java.lang.String); 
}

Car 클래스를 만든 한 줄의 간결한 코틀린 코드는 속성을 위한 두 개의 백킹필드, 두 개의 getter, 하나의 setter, 생성자로 바뀌었다.

코틀린에선 getter, setter 대신 속성의 이름을 이용해서 속성에 접근할 수 있다.

속성 제어 변경

Car 클래스에서 yearOfMake는 이뮤터블이고 color는 뮤터블이다. color를 null로 설정하거나 변경하는걸 코틀린은 막아주지만 빈 문자열로 설정되는걸 방지해주진 않는다.

color를 빈 문자열로 설정하는것을 방지해주는 코드를 만들어보자

class Car(val yearOfMake: Int, theColor: String) {
  var fuelLevel = 100
  var color = theColor
      set(value) {
          if (value.isBlank()) {
              throw RuntimeException("no empty, please")
          }
          field = value
      }
}

위 예제의 생성자는 하나의 속성을 정의하고, theColor라는 하나의 파라미터를 사용했다. 이 파라미터에는 val이나 var를 사용하지 않았다.

코틀린은 fuelLevel 속성에 사용되는 getter 와 setter를 만들어 낸다. color 속성은 getter만 만들고 setter는 제공되는 코드를 사용하게 된다. setter은 color에 빈 값이 들어가게 되면 예외를 발생시킨다.

코틀린은 필드를 내부적으로 만들었기 때문에 코드에서 필드에 접근할 수 있는 방법이 없다.
개발자는 getter나 setter에 있는 field 키워드를 통해서반 필드를 사용할 수 있다.

setter

val car = Car(2019, "Red")
car.color = "Green"
//car.fuelLevel--
println(car.fuelLevel)
try {
  car.color = ""
} catch (ex: Exception) {
  println(ex.message)
}
println(car.color)

color를 빈문자열로 변경하면 실행 시간 예외와 함께 변경에 실패한다.

💻 출력

100
no empty, please
Green

접근 제어자

코틀린에서 속성과 메소드는 public이 기본이다. 코틀린에는 public, private, protected, internal 네 개의 접근 제어자가 있다. public과 private는 Java와 동일하다.

✔ protected : 자식클래스들의 메소드가 속성에 접근할 수 있는 권한을 준다.
✔ internal : 같은 모듈에 있는 모든 코드에서 속성이나 메소드에 접근이 가능하다. 모듈은 함께 컴파일된 모든 소스 코드를 뜻한다.

internal은 네이밍 컨벤션을 이용해서 코틀린 컴파일러에 의해 다뤄지고, 실행 시간 오버헤드가 전혀 없다.

getter의 접근 권한은 속성의 접근 권한과 동일하지만 setter의 경우 개발자가 원하는 대로 접근 권한을 설정할 수 있다.

private_setter

class Car(val yearOfMake: Int, theColor: String) {
  var fuelLevel = 100
      private set
  var color = theColor
      set(value) {
          if (value.isBlank()) {
              throw RuntimeException("no empty, please")
          }
          field = value
      }
}

fuelLevel의 접근 제어자를 변경해서 클래스 내부에서만 변경 가능해지게 했다.

초기화 코드

객체를 초기화하는 코드가 값들을 설정하는 것보다 복잡하다면 생성자용 바디를 만들 필요가 있다. 코틀린은 생성자용 바디를 만들기 위해 특별한 공간을 제공해 준다.
클래스는 0개 이상의 init 블록을 가질 수 있다. 이 블록들은 주 생성자의 실행의 한 부분으로써 실행된다. 클래스 내부에서 선언된 속성을 사용하기 위해서는 init 블록을 속성 선언 아래에 위치시켜야 한다.

클래스 안에서 첫 번째로 속성을 가장 위에 정의하도록 하고, 그 후에 init 블록을 작성해라.
그리고 보조 생성자를 구현하도록 해라, 마자막으로 필요한 메소드를 작성하면 된다.

init

class Car(val yearOfMake: Int, theColor: String) {
  var fuelLevel = 100
      private set
  var color = theColor
      set(value) {
          if (value.isBlank()) {
              throw RuntimeException("no empty, please")
          }
          field = value
      }

  init {
      if (yearOfMake  < 2020) {
          fuelLevel = 90
      }
  }
}

init 블록에서 fuelLevel은 yearOfMake 값에 따라서 변경된다. fuelLevel에 접근해야만 하기 때문에 fuelLevel을 정의하는 시점보다 init 블록이 앞에 있으면 안된다.

class Car(val yearOfMake: Int, theColor: String) {

 var fuelLevel = if (yearOfMake < 2020) 90 else 100
      private set
      
      ...
      

init 블록이 아니라 fuelLevel을 정의하는 시점에서 처리할 수도 있다.

가급적이면 init 블록은 1개만 만들고, 가능하다면 1개도 만들지 않도록 해라, 생성자에 최대한 아무 작업도 안 하는 것이 프로그램 안전성과 퍼포먼스 측면 모두에서 더 장점이 크다.

보조 생성자

주 생성자를 작성하지 않았다면 코틀린은 아규먼트가 없는 기본 생성자를 생성한다. 만약에 주 생성자가 모든 파라미터를 위한 기본 아규먼트를 가지고 있다면 코틀린은 주 생성자와 함께 아규먼트가 없는 생성자를 생성한다. 어떤 상황이든 생성자를 많이 만들수 있고 이렇게 만드는 생성자를 보조 생성자라고 한다.

✔ 보조 생성자는 주 생성자를 호출하거나, 다른 보조 생성자를 호출해야만 한다.
✔ 보조 생성자의 파라미터는 val이나 var을 사용할 수 없다.
✔ 보조 생성자에서 속성을 선언할 수 없다.
✔ 주 생성자와 클래스 내부에서만 속성을 정의할 수 있다.

Person

class Person(val first: String, val last: String) {
  var fulltime = true
  var location = "-"

  constructor(first: String, last: String, fte: Boolean) : this(first, last) {
      fulltime = fte
  }

  constructor(
      first: String, last: String, loc: String
  ) : this(first, last, false) {
      location = loc
  }

  override fun toString() = "$first $last $fulltime $location"

}
  • Person의 주 생성자는 2개의 속성 first, last를 val로 선언했다. 주 생성자에서 constructor은 선택사항이다.
  • 첫 번째 보조 생성자는 this를 이용해 주 생성자를 호출한다.
  • 두 번째 보조 생성자는 첫 번째 보조 생성자를 호출한다.

모든 보조 생성자는 주 생성자나 보조 생성자를 호출할 수 있다. 단, 생성자들 끼리 선로 호출하는 순환은 일어나선 안된다.

Person

println(Person("Jane", "Doe"))
println(Person("John", "Doe", false))
println(Person("Baby", "Doe", "home"))

💻 출력

Jane Doe true -
John Doe false -
Baby Doe false home
Green

인스턴스 메소드 정의

클래스 안에 메소드를 정의할 때는 fun 키워드를 이용한다. fun 키워드 앞에 private, protected, internal 키워드를 이용해 권한을 설정할 수 있다.(디폴트는 public)

Person

class Person(val first: String, val last: String) {

...

override fun toString() = "$first $last $fulltime $location"

  internal fun fulllname() = "$last, $first"
  private fun yearOfServiece(): Int = throw RuntimeException("Not Implement yet")

}

Person 클래스에서 fullName() 메소드를 iternal로 선언했고 yearOfServiece() 메소드를 private로 선언했다.

접근 권한이 없는 메소드에 접근을 시도한다면 오류가 발생한다.

val jane=Person("Jane", "Doe")
println(jane.fulllname())
jane.yearOfServiece() // ERROR : Cannot access 'yearOfServiece': it is private in 'Person'

인라인 클래스

코드 작성할 때 클래스와 프리미티브 타입 중 어떤 것을 사용할지 논쟁을 하곤 한다. 예를 들어 인증서에 대한 양식은 SSN 클래스를 통해 나타낼 수도 있고, 그냥 String을 사용할 수 있다.
클래스를 만들면 명확성을 띌 수 있다는 장점이 있지만 오버헤드가 발생하여 퍼포먼스가 나빠질 가능성이 있다.
💡 inline 클래스는 균형을 잡게 해주는 좋은 기능이다. 컴파일 시간에 클래스의 장점을 취할 수 있고, 실행 시간에는 프리미티브 타입으로 취급된다.

inline

inline class SSN(val id: String)

fun recieveSSN(ssn: SSN) {
  println("Recieve : $ssn")
}

recieveSSN(SSN("111-11-1111"))

컴파일러가 SSN의 인스턴스가 recieveSSN()으로 전달될 수 있다는 검증을 마치면 검증이 끝난 String 인스턴스를 recieveSSN() 함수에 직접 전달하여 메모리와 객체 생성 측면에서의 잠재적인 오버헤드와 wrapper을 제거한다.

inline 클래스는 속성과 메소드를 가질 수 있고 인터페이스를 구현할 수도 있다. 내부를 살펴보면 메소드는 프리미티브 타입을 받는 static 메소드가 inline 클래스로 둘러싸여 있다. inline 클래스는 final이 되어야 하고, 다른 클래스에 의해서 확장될 수 없다.

📌 컴패니언 객체와 클래스 멤버


컴패니언 객체는 클래스 안에 정의한 싱글톤이다. 컴패니언 객체는 인터페이스를 구현할 수도 있고 다른 클래스를 확장할 수 도 있다.

클래스 레벨 멤버

companion

class MachineOperator(val name: String) {
  fun checkin() = checkedIn++
  fun checkout() = checkedIn--

  companion object {
      var checkedIn = 0
      fun minimimBreak() = "15 minutes every 2 hours"
  }
}

컴패니언 객체는 companion object 키워드를 이용해서 정의되었다.
checkedIn은 MachineOperator 클래스의 클래스 레벨 속성이 되었다. 이와 유사하게 minimimBreak 메소드 역시 어떤 인스턴스에도 속하지 않는 클래스 레벨 메소드다.


MachineOperator("Mater").checkin()
println(MachineOperator.minimimBreak()) //15 minutes every 2 hours
println(MachineOperator.checkedIn) //1

컴패니언 객체의 멤버는 클래스 이름을 참조하여 접근 할 수 있다.
checkin()메소드 같은 인스턴스 메소드는 타깃 인스턴스가 필요하다. 컴패니언 객체 minimimBreak() 같은 메소드는 인스턴스를 이용해서 접근할 수 없다. 클래스 이름을 통해 접근해야 한다.

컴패니언 객체 접근하기

컴패니언 객체의 속성이 아닌 컴패니언 객체 자체의 참조가 필요한 경우도 있다.

✔ 클래스.Companion

val ref=MachineOperator.Companion

✔ 컴패니언 객체에 명확한 이름을 만들어 접근

companion 이름

class MachineOperator(val name: String) {
  fun checkin() = checkedIn++
  fun checkout() = checkedIn--
  
  companion object MachineOperatorFactory{
      var checkedIn = 0
      fun minimimBreak() = "15 minutes every 2 hours"
  }
}

val ref=MachineOperator.MachineOperatorFactory

팩토리로 사용하는 컴패니언

컴패니언을 팩토리로 사용하기 위해서는 클래스에 private 생성자를 만들어야 한다. 그 후 컴패니언 객체에서 생성된 인스턴스를 리턴하기 전에 인스턴스를 처리하는 메소드를 하나 이상 생성한다.

factory

class MachineOperator private constructor(val name: String) {
  fun checkin() = checkedIn++
  fun checkout() = checkedIn--

  companion object {
      var checkedIn = 0
      fun minimimBreak() = "15 minutes every 2 hours"
      fun create(name: String): MachineOperator {
          val instance = MachineOperator(name)
          instance.checkin()
          return instance
      }
  }
}

MachineOperator 클래스의 생성자가 privater가 되었다. 클래스 외부에서 우리는 어떤 인스턴스도 직접 생성할 수 없다. create() 메소드가 인스턴스를 생성하는 유일한 방법이기 때문에 컴패니언 객체가 클래스의 팩토리로서 동작한다.

val operator=MachineOperator.create("Mater")
println(MachineOperator.checkedIn)

create() 메소드는 MachineOperator 클래스에서 직접 호출된 컴패니언 객체로 라우팅되어 있다.

Static과는 다르다

클래스 이름을 이용해 컴패니언 객체에 접근하는 방법을 보고 나면 컴패니언 객체의 멤버가 static 멤버가 되는 듯 보일지도 모른다. 하지만 static과는 다르다

📌 제네릭 클래스 생성


코틀린에는 Pair라고 불리는 두 가지 타입의 다른 객체를 소유하고 있는 클래스가 있다.
Pair를 이용해서 PriorityPair라는 클래스를 만들 예정이다. PriorityPair는 동일한 타입의 객체 2개를 소유하는 객체이다. 그리고 그 객체는 큰 객체가 첫 번째, 작은 객체가 두 번째에 오도록 정렬된다. 객체의 순서를 정하기 위해서 Comparable<T> 인터페이스의 compareTo() 메소드를 사용할 것이다.

PriorityPair

class PriorityPair<T : Comparable<T>>(member1: T, member2: T) {
  val first: T
  val second: T

  init {
      if (member1 >= member2) {
          first = member1
          second = member2
      } else {
          first = member2
          second = member1
      }
  }

  override fun toString() = "${first}, ${second}"
}

두 멤버의 값이 Comparable 인터페이스의 compareTo() 메소드에 의해서 정해진다.
compareTo() 메소드를 직접 사용하는 대신에 >= 연산자를 사용했다. toString() 메소드는 first와 second 속성을 String으로 리턴해준다.

println(PriorityPair(2, 1)) //2, 1
println(PriorityPair("A", "B")) //B, A

PriorityPair<T>의 인스턴스는 Comparable<T>의 인터페이스를 구현하고있는 타입이라면 어떤 타입이든 이용해서 만들 수 있다.

📌 데이터 클래스


✔ 코틀린의 data class는 특정한 행동, 동작보다는 데이터를 옮기는데 특화된 클래스이다.
✔ 주 생성자에는 val이나 var를 사용한 속성 정의가 적어도 하나 이상 필요하다.
✔ 데이터 클래스에서는 val 이나 var가 아닌 파라미터는 사용할 수 없다.
✔ 여러가지 메소드가 제공된다 - equals(), hashCode(), toString(), copy(), component 메소드

data class

data class Task(val id: Int, val name: String,
  val complete: Boolean, val assigned: Boolean)

val task1 = Task(1, "Created Project", false, true)
println(task1)
println("Task name : ${task1.name}")

val task1Completed=task1.copy(complete = true, assigned = false)
println(task1Completed)

💻 출력

Task(id=1, name=Created Project, complete=false, assigned=true)
Task name : Created Project

toString() 메소드가 String을 리턴했다. toString() 메소드는 속성과 속성 값들을 생성자의 파라미터가 리스팅된 순서 그대로 정렬해서 가지고 있다.

copy()

val task1Completed=task1.copy(complete = true, assigned = false)
println(task1Completed)

💻 출력

Task(id=1, name=Created Project, complete=true, assigned=false)

데이터 클래스는 copy() 메소드를 생성한다. copy() 메소드는 대상 객체의 모든 속성을 족사하여 새 객체를 생성하는 메소드이다. 메소드에 대한 각 파라미터는 기본 아규먼트를 전달받고, 네임드 아규먼트를 이영해서 대체 값을 모든 속성에 전달할 수 있다.

component()


val (id,_,_,isAssigned)=task1
println("Id: $id Assigned: $isAssigned")

component 메소드의 주된 목적은 구조분해다. 추출 될 값이 들어갈 지역변수를 val이나 var로 정의할 수 있다. 필요하지 않은 속성값은 _ 로 대체했다.

데이터 클래스의 구조분해는 편해보이지만 중대한 한계가 있다. 주 생성자에 새로운 파라미터를 넣게 된다면 컴파일 오류는 발생하지 않지만 속성에 의도하지 않은 다른 속성값을 할당하게 된다.

데이터 클래스는 아래와 같은 상황에서만 사용하자

  • 행동, 동작 보다는 데이터 자체에 집중된 모델링을 할 경우
  • equals(), hashCode(), toString(), copy() 가 생성되길 원하거나 copy()만 생성되길 원할 경우
  • 주 생성자에 적어도 하나 이상의 속성이 포함되어야 할 경우
  • 주 생성자를 속성만으로 구성해야 할 경우
  • 구조분해 기능을 이용해서 데이터를 쉽게 추출하고 싶은 경우




🔑 정리


코틀린은 우리가 기존에 사용하던 Java를 뛰어넘는 객체지향 프로그래밍을 지원해준다.

코틀린은 필드를 생성하고, getter와 setter도 생성해 준다.

인스턴스 멤버는 클래스 안에 있고, 클래스 멤버는 컴패니언 객체에 들어있다.

싱글톤은 코틀린에서 최고의 객체이다.

데이터 클래스는 행동보다 데이터 자체에 집중해야 하는 경우에 유용하다.



출처 : 다재다능 코틀린 프로그래밍

profile
개발하고싶은사람

0개의 댓글