kotlin의 class

짱구·2023년 3월 12일
0

Class

코틀린은 자바와 마찬가지로 class 키워드를 사용하여 클래스를 선언합니다.

class Coffee {
}

또한 코틀린의 클래스는 본문을 생략할 수 있습니다.

class EmptyClass

생성자는 기본 생성자와 하나 이상의 보조 생성자가 존재 할 수 있습니다.

class Coffee constructor(val name: String)

또한 기본 생성자는 constructor 를 생략할 수 있습니다.

class Coffee(val name: String)

코틀린에서는 클래스에 프로퍼티를 선언할때 후행 쉼표 trailing comma를 사용할 수 있습니다.
후행 쉼표를 사용한다면 이전의 마지막 줄을 수정하지 않고 프로퍼티를 쉽게 추가할 수 있고 git에서 diff 등으로 코드를 비교했을때 변경사항을 명확히 알 수 있습니다.

class Coffee(
val name: String,
val price: Int, // trailing comma
)

Property

먼저 코틀린의 프로퍼티는 val , var 키워드를 모두 사용할 수 있습니다.

class Coffee(
var name: String = "", // 기본 값 추가
var price: Int = 0,
)

프로퍼티를 수정하거나 사용하려면 참조를 사용하면 해결이 가능합니다.

fun main() {
val coffee = Coffee()
coffee.name = "아이스 아메리카노"
coffee.price = 2000
println("${coffee.name} 가격은 ${coffee.price}")
}

getter, setter

코틀린은 var로 선언된 프로퍼티에 getter, setter를 자동으로 생성하며 아래의 코드는 실제론 필드의 setter를 사용해 값을 할당됩니다.
또한 해당 필드를 사용할시에는 getter가 사용됩니다.

coffee.name = "아이스 아메리카노" // setter가 적용된 상황

println("${coffee.name} 가격은 ${coffee.price}") // getter가 적용된 상황

val로 선언된 property는 getter만 존재하고 또한 코틀린은 커스텀 getter를 만들 수 있습니다.

class Coffee(
  var name: String = "",
  var price: Int = 0, // trailing comma
) {
  val brand: String
  get() = "스타벅스" // 커스텀 getter
}
fun main() {
  val coffee = Coffee()
  coffee.name = "아이스 아메리카노"
  coffee.price = 2000
  // brand를 포함해 출력
  println("${coffee.brand} ${coffee.name} 가격은 ${coffee.price}")
}

var로 선언된 property는 커스텀 setter를 만들 수 있습니다. (val은 setter를 만들 수 없음!!)
코틀린은 getter, setter에서 field라는 식별자를 사용해 필드의 참조에 접근하는 데 이를 Backing Field에 접근한다고 합니다.

class Coffee( var name: String = "", var price: Int = 0, ) {
  val brand: String
  get() {
  return "스타벅스"
  }
  var quantity: Int = 0
  set(value) { // 커스텀 setter
    if (value > 0) {
    field = value // 매개변수가 0보다 클 경우에만 field(quantity)에 값을 할당
    }
  }
}

fun main() {
  val coffee = Coffee()
  coffee.name = "아이스 아메리카노"
  coffee.price = 2000
  coffee.quantity = 1 // 주문 수량 추가
  
  println("${coffee.brand} ${coffee.name} 가격은 ${coffee.price} 수량은 ${coffee.quantity}")
}

그럼 Backing Field가 필요한 이유가 무엇일까? 코틀린에서 프로퍼티에 값을 할당할때 실제론 setter를 사용하는데 이때 무한 재귀 StackOverflow가 발생할 수 있습니다.

자바는 상태를 메서드로 나타냅니다.

public class Java_Coffee {
	private boolean isIced;
    
    public boolean Iced() {
    	return isIced;
    }
    
    public void setIced(boolean iced) {
   		isIced = iced;
    }
}

class Barista {
  public static void main(String[] args) {
    Java_Coffee coffee = new Java_Coffee();
    
    coffee.setIced(true);
    
    if(coffee.getIced()) { // 상태를 메서드로 표현
    	System.out.println("아이스 커피");
    }
  }
}

그에 비해 코틀린의 프로퍼티는 객체지향적입니다.
객체지향에서 객체의 상태는 프로퍼티로 표현하고 행위는 메서드로 표현합니다.
그렇게에 코틀린은 프로퍼티를 사용해 상태를 나타낼 수 있기 때문에 자바보다 더 객체지향적으로 코드를 작성할 수 있습니다.

class Coffee(
  var name: String = "",
  var price: Int = 0,
  var iced: Boolean = false,
) {
  val brand: String
      get() {
         return "스타벅스"
      }
  
  var quantity: Int = 0
	  set(value) {
          if (value > 0) {
          quantity = value
          }
	  }
}

fun main() {
  val coffee = Coffee()
  
  coffee.name = "아이스 아메리카노"
  coffee.price = 2000
  coffee.quantity = 1
  coffee.iced = true
  
  if (coffee.iced) { // 프로퍼티
  	println("아이스 커피")
  }
  
  println("${coffee.brand} ${coffee.name} 가격은 ${coffee.price} 수량은 ${coffee.quantity}")
}

Inheritance

자바의 상속

객체지향 핵심 원칙 중 하나인 상속 은 상속을 통해 기존 코드를 재사용하거나 확장할 수 있습니다.
자바는 기본적으로 모든 클래스가 상속이 가능하나 상속에 따른 부작용이 발생할 경우를 대비해 final 키워드로 막을 수 있습니다.
이펙티브 자바의 아이템 중 상속을 위한 설계와 문서를 작성하고 그렇지 않으면 상속을 금지하라 라는 주제가 있는데 여기에는 여러가지 상속에 대한 문제점에 대해 나와있으며 결과적으로 상속을 목적으로 만든 클래스가 아니라면 모두 final로 작성하는 것이 좋습니다.

대표적으로 System 클래스가 Final 키워드로 상속을 막고 있습니다.

System.out.println("Hello World");

public final class System {
	/* ... */
}

코틀린의 상속

자바의 모든 클래스의 조상은 Object이고 코틀린에서 모든 클래스의 조상은 Any입니다.
Any에는 equals, hashCode, toString이 존재하고 모든 클래스로 자동으로 상속됩니다.
코틀린의 클래스는 기본적으로 final class로 선언이 되어 상속을 막고 꼭 필요한 경우 open이라는 키워드로 상속을 허용할 수 있습니다.

open class Dog

코틀린에서 상속은 아래와 같은 문법으로 클래스 뒤에 :을 추가하고 상위 클래스를 작성합니다.

class Bulldog : Dog()

함수나 프로퍼티를 재정의할때도 마찬가지로 open 키워드로 오버라이드에 대해 허용해야합니다.

open class Dog {
  open var age: Int = 0
  open fun bark() {
  	println("멍멍")
  }
}

class Bulldog : Dog() {
  override var age: Int = 0
  override fun bark() {
	  println("컹컹")
  }
}

fun main() {
    val dog = Bulldog()
    dog.age = 2
    println(dog.age) // 2
    dog.bark() // 컹컹
}

프로퍼티는 기본 생성자를 사용해 오버라이드할 수 있습니다.

open class Dog(open var age: Int = 0) {
    open fun bark() {
        println("멍멍")
    }
}
class Bulldog(override var age: Int = 0) : Dog() {
    override fun bark() {
        println("컹컹")
    }
}
fun main() {
    val dog = Bulldog(age = 2)
    println(dog.age)
    dog.bark()
}

override된 함수나 프로퍼티는 기본적으로 open되어 있으므로 하위 클래스에서 오버라이드를 막기 위해선 final을 앞에 붙여주면 됩니다.
하위 클래스에서 상위 클래스의 함수나 프로퍼티를 접근할땐 자바와 마찬가지로 super 키워드를 사용합니다.

open class Bulldog(final override var age: Int = 0) : Dog() {
    final override fun bark() {
        super.bark() // 멍멍
    }
}

추상클래스

코틀린은 자바와 마찬가지로 abstract 키워드를 사용해 추상클래스도 제공하며 하위 클래스에서 구현해야하는 프로퍼티나 함수 또한 abstract 키워드를 사용합니다.

abstract class Developer {
    abstract var age: Int
    abstract fun code(language: String)
}

class BackendDeveloper(override var age: Int) : Developer() {
    override fun code(language: String) {
        println("hi ${language}")
    }
}

fun main() {
    val backendDeveloper = BackendDeveloper(age = 20)
    println(backendDeveloper.age)
    backendDeveloper.code("Kotlin")
}

Interface

코틀린은 자바와 마찬가지로 interface 키워드로 인터페이스를 정의합니다.

interface Cart

코틀린의 인터페이스 내부에는 추상 메서드와 자바8의 디폴트 메서드처럼 구현체를 가진 함수를 모두 정의할 수 있습니다.

class Product(val name: String, val price: Int)

  interface Cart {
    fun add(product: Product)
    fun rent() {
  }
}

클래스에서 인터페이스를 구현(java에선 implements)할때는 :을 붙이고 인터페이스 명을 적습니다.
상속과 다른 점은 () 인터페이스는 생성자 호출이 없습니다. 당연하겠지만 인터페이스는 생성자가 존재할 수 없습니다.
또 코틀린은 자바와 마찬가지로 인터페이스는 상위 인터페이스를 가질 수 있습니다.

class MyCart() : Cart {
  override fun add(product: Product) {
  	/* ... */
  }
}

인터페이스에 프로퍼티 선언

자바에 경우 Java8부터는 인터페이스에서도 디폴트 메서드와 정적 메서드를 지원하게 되었지만 아직까지는 인터페이스에서 직접적으로 프로퍼티를 선언하는 것은 불가능합니다.
하지만 코틀린의 인터페이스는 프로퍼티를 선언 할 수 있습니다.

class Product(val name: String, val price: Int)

interface Cart {
    var coin: Int

    val weight: String
        get() = "20KG"

    fun add(product: Product)

    fun rent() {
        if (coin > 0) {
            println("카트를 대여합니다")
        }
    }

}

class MyCart(override var coin: Int) : Cart {

    override fun add(product: Product) {
        if (coin <= 0) println("코인을 넣어주세요")
        else println("${product.name}이(가) 카트에 추가됐습니다")
    }

}

fun main() {

    val cart = MyCart(coin = 100)
    cart.rent()
    cart.add(Product(name = "장난감", price = 1000)) // 카트를 대여합니다 // 장난감이(가) 카트에 추가됐습니다

    val cart2 = MyCart(coin = 0)
    cart2.rent()
    cart2.add(Product(name = "장난감", price = 1000)) // 코인을 넣어주세요

}

재정의 충돌 해결

클래스는 하나 이상의 인터페이스를 구현할 수 있다 Order엔 add가 구현이 있는 디폴트 함수이고 Cart는 abstract 함수입니다.
이때 동일한 시그니처를 가진 함수가 있는 경우 super<인터페이스>를 사용해 호출할 수 있습니다.

interface Order {
    fun add(product: Product) {
        println("${product.name} 주문이 완료되었습니다.")
    }
}

class MyCart(override var coin: Int) : Cart, Order {
    override fun add(product: Product) {
        if (coin <= 0) println("코인을 넣어주세요")
        else println("${product.name}이(가) 카트에 추가됐습니다")  // 주문
        super<Order>.add(product) // 장난감 주문이 완료되었습니다.
    }
}

두 인터페이스에 구현을 가진 동일한 디폴트 함수를 사용할 경우 컴파일 오류가 발생합니다.

interface Cart {
    fun printId() = println("1234")
}

interface Order {
    fun printId() = println("5678")
}

class MyCart() : Cart, Order	// 컴파일 오류

위와 같은 경우엔 super<인터페이스>를 사용하여 하위 클래스에서 직접 구현해야합니다.

class MyCart() : Cart, Order {
    override fun printId() {
        super<Cart>.printId()
        super<Order>.printId()
    }
}

fun main() {
    val myCart = MyCart()
    myCart.printId()
    // 1234
    // 5678
}

Enum

코틀린은 자바와 동일하게 서로 연관된 상수들의 집합을 enum class를 사용해서 정의할 수 있습니다.

enum class PaymentStatus {
	UNPAID, PAID, FAILED, REFUNDED
}

enum 클래스도 클래스이므로 생성자와 프로퍼티를 정의할 수 있습니다.

enum class PaymentStatus(val label: String) {
  UNPAID("미지급"),
  PAID("지급완료"),
  FAILED("지급실패"),
  REFUNDED("환불")
}

enum에서 정의된 프로퍼티를 사용하는 방법

println(PaymentStatus.PAID.label) // 미지급

정의된 상수 목록 뒤에 함수를 정의할 경우 세미콜론 ;을 붙여야한다.

enum class PaymentStatus(val label: String) {
  UNPAID("미지급"),
  PAID("지급완료"),
  FAILED("지급실패"),
  REFUNDED("환불");
  
  fun isPayable(): Boolean = false
}

각각의 결제 상태에 따라 isPayable 상태를 다르게 구현하고 싶을 땐 abstract 메서드를 가지게 하면됩니다.
각각의 상수는 익명 클래스형태로 abstract 함수를 구현할 수 있습니다.

enum class PaymentStatus(val label: String) {
    UNPAID("미지급") {
        override fun isPayable() = true
    },
    PAID("지급완료") {
        override fun isPayable() = false
    },
    FAILED("지급실패") {
        override fun isPayable() = false
    },
    REFUNDED("환불") {
        override fun isPayable() = false
    };
    abstract fun isPayable(): Boolean
}

fun main() {
    if (PaymentStatus.UNPAID.isPayable()) {
        println("결제 가능 상태")
    }
}

abstract 메서드를 사용하면 다른 클래스에서 재사용을 할 수 없기에 인터페이스로 구현을 하는 것을 권장합니다.

interface Payable {
    fun isPayable(): Boolean
}

enum class PaymentStatus(val label: String) : Payable {
    UNPAID("미지급") {
        override fun isPayable() = true
    },
    PAID("지급완료") {
        override fun isPayable() = false
    },
    FAILED("지급실패") {
        override fun isPayable() = false
    },
    REFUNDED("환불") {
        override fun isPayable() = false
    };
}

또한 자바와 마찬가지로 valueOf 메서드를 사용해서 enum 클래스를 생성할 수 있습니다.

val paymentStatus = PaymentStatus.valueOf("PAID")
println(paymentStatus.label) // 지급완료

enum 클래스에서 동등성 비교는 == 를 사용합니다.

if (paymentStatus == PaymentStatus.PAID) {
	println("결제 완료 상태") // 결제 완료 상태
}

enum 클래스의 상수를 나열하려면 values() : Array 를 사용합니다.

for (status in PaymentStatus.values()) {
	println("[$status](${status.label})")
}
// [UNPAID](미지급)
// [PAID](지급완료)
// [FAILED](지급실패)
// [REFUNDED](환불)

enum 클래스에서는 name과 ordinal 프로퍼티로 상수명과 순서를 얻을 수 있습니다.

for (status in PaymentStatus.values()) {
println("[${status.name}](${status.label}) : ${status.ordinal}")
}
// [UNPAID](미지급) : 0
// [PAID](지급완료) : 1
// [FAILED](지급실패) : 2
// [REFUNDED](환불) : 3

출처 : fastcampus

profile
코드를 거의 아트의 경지로 끌어올려서 내가 코드고 코드가 나인 물아일체의 경지

0개의 댓글