조각들

 

추상 클래스

하나 이상의 추상 프로퍼티나 추상 메서드를 가진 클래스
 

  • 클래스 앞에 abstract 키워드를 사용한다.
  • 프로퍼티나 메서드도 abstract로 선언될 수 있으며 이때는 추상 프로퍼티나 추상 메서드라고 부른다.
  • abstract 키워드가 붙은 것은 하위 클래스에서 반드시 구현해야 한다.
  • 추상 클래스는 abstract 키워드 자체가 상속과 오버라이딩을 허용하고 있기 때문에 상속 시 open 키워드가 필요없다. 하지만 일반 프로퍼티에 대해선 open 키워드를 붙여주어야 한다.
  • 구현 클래스에서 override한 속성이나 메서드가 하위 클래스에서 상속을 금지하려면 final 지시자가 필요하다.
  • 추상 클래스에 아무런 속성이 없어도 구현 클래스에서 상속할 때는 위임호출이 필요하다.
  • 추상 클래스 내부에도 init 블록을 정의할 수 있다.
  • 구현 클래스에서 상속된 슈퍼클래스의 생성자를 위임 호출할 수 있다. 그래서 추상 클래스도 주생성자 정의가 가능하다.
  • 추상 클래스도 상속관계를 만들 수 있다. 그래서 추상 클래스 간의 계층구조를 구성한다. 이러면 여러 추상 클래스를 상속한 것처럼 사용할 수 있다.
  • 추상 클래스에 외부의 확장함수를 추상 메서드로 정의할 수 있다. 이렇게 하면 이 추상 클래스를 상속하는 클래스는 이 확장함수를 메서드처럼 사용할 수 있다.
  • 추상 클래스는 인터페이스와 다르게 (abstract 키워드가 붙지 않은) 프로퍼티에 상태 정보를 저장할 수 있다.
abstract class Vehicle(val name: String, val color: String, val weight: Double) {
	var year = "2018" //일반 프로퍼티 (초깃값인 상태를 저장할 수 있음)
    abstract var axSpeed: Double //추상 프로퍼티 (반드시 하위 클래스에서 재정의해 초기화해야 함)
}

 
하위 클래스를 생성하지 않고 단일 인스턴스로 객체를 생성하려면 object를 사용해서 지정할 수 있다.

abstract class Printer { //추상 클래스
	abstract fun print() //추상 메서드
}

val myPrinter = object: Printer() { //객체 인스턴스
	override fun print() { //추상 메서드의 구현
    	println("출력합니다.")
    }
}

fun main() {
	myPrinter.print()
}

 
 
 

인터페이스

추상 클래스가 있는데 인터페이스를 왜 쓸까?

 
클래스는 단일 상속만 되지만 인터페이스는 여러개를 구현할 수 있기 때문이다.
 
인터페이스를 사용하는 가장 큰 이유는 특정 구현에 의존적이지 않은 코드를 만들 수 있다는 점이다. 그래서 기능의 정의와 구현을 분리할 수 있고 구현 내용을 확장하거나 교체하기 쉽다. 따라서 확장이 쉬운 구조를 만들고 싶다면 인터페이스를 이용해야 한다.
 

  • 인터페이스 본문에서 메서드는 추상(abstract) 혹은 일반 메서드 모두 선언이 가능하지만 프로퍼티는 오직 추상 프로퍼티로만 선언해야 한다.
  • 메서드에 구현부를 포함하면 일반 메서드로 취급된다.
  • abstract 키워드가 없어도 프로퍼티는 모두 추상 프로퍼티로 취급된다.
  • 클래스가 인터페이스를 상속해도 인터페이스가 객체를 생성하지 못하므로 위임호출은 정의하지 않는다.
  • 자바에서 상속은 extends, 구현은 implements 키워드로 구별하고 있지만 코틀린에서는 둘 다 콜론(:)을 통해서 정의한다.
interface Pet {
    var category: String //abstract 키워드가 없어도 기본은 추상 프로퍼티
    fun feeding() //추상 메서드
    fun patting() { //일반 메서드: 구현부를 포함하면 일반적인 메서드로 취급됨
    	println("Keep patting!") //구현부
    }   
}

 

  • 상태를 저장할 수 없기에 프로퍼티는 기본값을 가질 수 없습니다. 단, val로 선언된 프로퍼티는 게터를 통해 필요한 내용을 구현할 수 있다.
  • 세터는 사용할 수 없다.
interface Pet {
    var category: String
    val msgTags: String //val 선언 시 게터의 구현이 가능
    	get() = "I'm your lovely pet!"        
}

println("Pet Message Tags: ${obj.msgTags}")

 
 

인터페이스를 활용한 의존성 제거

//Master 클래스가 Dog, Cat 클래스에 의존성을 가지는 case

class Master {
    fun playWithPet(dog: Dog){} //매개변수를 다르게 한 오버로딩
    fun playWithPet(cat: Cat){} //가짓수가 많아지면 불편함
}

fun main() {
    val master = Master()
    val dog = Dog("Toto", "Small")
    val cat = Cat("Coco", "BigFat")
    master.playWithPet(dog)
    master.playWithPet(cat)
}

 

//Dog, Cat 클래스가 Pet 인터페이스를 구현하게 하고, Pet 인터페이스 구현체인 Dog(), Cat()을
//인자로 받아 Master 클래스 <-> Dog, Cat 클래스 사이의 직접적인 의존성을 제거한 case

class Master {
    fun playWithPet(pet: Pet){} //인터페이스를 객체로 매개변수 지정
}

fun main() {
    val master = Master()
    val dog = Dog("Toto", "Small")
    val cat = Cat("Coco", "BigFat")
    master.playWithPet(dog)
    master.playWithPet(cat)
}

 

여러 인터페이스의 구현

만일 이름이 동일한 경우 super<인터페이스 이름>.메서드 이름() 형태로 구분할 수 있다.

override fun jump() {
    super<Horse>.jump()
}

 
 

인터페이스 계층 처리

인터페이스도 다른 인터페이스를 상속할 수 있다. 즉 인터페이스를 계층으로 만들어서 사용할 수 있다. 계층을 만드는 이유는 구현 클래스가 다양할 경우 구현 클래스의 상속을 계층에 맞춰서 만들 수 있기 때문이다. 
 
Q. (364pg) Cable을 구현하면 상위 인터페이스의 추상 메서드를 가져다 쓸 수 있는데 굳이 override를 해줌으로써 얻는 효용이 무엇인지 모르겠습니다.
 
 
추상 메서드가 중복되었을 때 처리 방안
 
여러 개의 인터페이스를 상속받을 때 이름이 동일한 추상 메서드가 있을 수 있다. 이를 구현할 때는 하나만 구현한다.

class D : Kclass(), Bable {
    override fun canUse() = println("can Use")
}

 
 

sealed 클래스

sealed 클래스도 추상 클래스이지만 별도로 지정해 사용하는 이유는 특정 추상 클래스를 상속해 구현하는 것을 제한하기 위함이다. 그래서 특정 파일이나 sealed 클래스 내부에 한정해서 작성해서 클래스의 상속관계를 한정하는 데 사용한다.
 
Q. 이해를 잘 못했는데 보통 class를 만들면 여러 곳에서 상속을 받을 수 있지만 어떤 class에 대한 상속이 같은 파일 내에서만 받을 수 있게 제한하는 목적으로 쓰는 것이라고 보면 될까요? 만약 그렇다면 클래스를 private으로 선언하면 되지 않을까요?
 

  • sealed 클래스는 항상 최상위 클래스가 되어야 하므로 가장 먼저 정의한다.
  • sealed 클래스를 상속하는 서브 클래스는 반드시 같은 파일 내에 선언한다. 단, sealed 클래스를 상속하지 않고 그 서브 클래스를 상속한 경우는 같은 파일에 작성하지 않아도 된다.
  • sealed 클래스는 기본적으로 추상 클래스이다.
  • 봉인 클래스로 처리할 때 내부 클래스가 있을 때는 private 생성자만 가진다.
  • sealed 클래스 내부에 서브 클래스를 정의할 수 있으며 class, data class, object 모두 가능하다.
  • sealed 클래스에 확장함수를 추가할 수 있다.
  • sealed 클래스는 명확히 하위 클래스를 알 수 있어서 when 표현식에서 만들어진 객체가 어떤 하위 클래스인지 점검할 수 있다. 이때 주의할 점은 현재 상태로 모든 하위 클래스를 명확히 알 수 있어서 별도의 else가 필요 없다.
sealed class SealedClass {
    class SubX(val intVal: Int) : SealedClass()
    class SubY(val stringVal : String) : SealedClass()
}

class SubZ(val longVal: Long) : SealedClass()

fun printTtype(type: SealedClass) : String =
    when(type) {
    	is SealedClass.SubX -> "매개변수 타입 : integer"
        is SealedClass.SubY -> "매개변수 타입 : string"
        is SubZ -> "매개변수 타입 : long"
    } //명확하게 서브 클래스 확정, else가 필요없음
    
println(printType(SubZ(100L)))
println(printType(SealedClass.SubX(100)))
println(printType(SealedClass.SubY("문자열")))

//매개변수 타입 : long
//매개변수 타입 : integer
//매개변수 타입 : string

'Kotlin > 스터디' 카테고리의 다른 글

코틀린 스터디 - 11장  (0) 2023.09.24
코틀린 스터디 - 10장  (0) 2023.09.17
코틀린 스터티 - 8장  (0) 2023.09.03
코틀린 스터디 - 6장  (0) 2023.08.20
코틀린 스터디 - 5장  (0) 2023.08.13