본문 바로가기
Study/다재다능 코틀린 프로그래밍

Day 8. 클래스 계층과 상속

반응형

본 포스트는 지인들과 스터디한 내용을 정리한 포스트입니다

 

http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931463422

 

다재다능 코틀린 프로그래밍 - 교보문고

다양한 프로그래밍 패러다임으로 살펴보는 코틀린 기본서 | 코틀린은 멀티패러다임 프로그래밍 언어다. 코틀린은 스크립트로 사용할 수도 있고, 객체지향 코드나 함수형 코드, 비동기 프로그램

www.kyobobook.co.kr


1. 인터페이스와 추상 클래스 생성

  • 인터페이스 만들기
    • 코드
      // remote.kts
      interface Remote {
          fun up()
          fun down()
          fun doubleUp() {
              up()
              up()
          }
      }
      
      class TV {
          var volume = 0
      }
      
      class TVRemote(val tv: TV): Remote {
          override fun up() {
              tv.volume++
          }
      
          override fun down() {
              tv.volume--
          }
      }
      
      
      val tv = TV()
      val remote: Remote = TVRemote(tv)
      println("Volume: ${tv.volume}")
      remote.up()
      println("After increasing: ${tv.volume}")
      remote.doubleUp()
      println("After doubleUp: ${tv.volume}")​
    • 코틀린의 인터페이스에서 구현된 메서드(코틀린 타입 디폴트 메서드, doubleUp() )은 java 1.6 이상에서 사용 가능
    • 인터페이스에서 디폴트 구현부가 없다면 실제 구현하는 클래스에서 전부 구현해줘야한다 ( 자바와 동일 )
    • TVRemote 클래스가 Remote 인터페이스를 구현한다는 것을 알리기 위해 ':' 콜론을 사용하고 Remote를 명시해줬다
    • 코틀린에서는 static 메서드를 인터페이스 안에 직접 만들 수 없다, 컴패니언 객체를 사용해야한다
      // remote.kts
      interface Remote {
          fun up()
          fun down()
          fun doubleUp() {
              up()
              up()
          }
      
          companion object {
              fun combine(first: Remote, second: Remote): Remote = object: Remote {
                  override fun up() {
                      first.up()
                      second.up()
                  }
      
                  override fun down() {
                      first.down()
                      second.down()
                  }
              }
          }
      }​
      • 위처럼 컴패니언 객체는 바로 interface에 작성하면 된다
  • 추상 클래스 생성하기
    • 코틀린 또한 Java처럼 abstract로 선언한 클래스를 추상 클래스라고 한다
    • 메서드도 abstract로 선언한 것을 추상 메서드라고 한다
    • 예제코드
      // abc.kts
      abstract class Musician(val name: String, val activeFrom: Int) {
          abstract fun instrumentType(): String
      }
      
      class Cellist(name: String, activeFrom: Int) : Musician(name, activeFrom) {
          override fun instrumentType() = "String"
      }
      
      val ma = Cellist("Yo-Yo Ma", 1961)​

      • 자식클래스에서 추상 클래스를 구현할때는 소속된 추상메서드를 override 키워드로 구현해야한다 ( JAVA와 동일 )
      • 자식 클래스의 생성자는 베이스 클래스에서는 필요하지 않은 파라미터를 추가적으로 받을 수 있으며, 자식 클래스에서 추가적으로 받은 파라미터를 val 혹은 var로 선언하면 프로퍼티가 된다
  • 인터페이스? 추상클래스?
    • 추상 클래스 vs 인터페이스
      • 인터페이스에 정의된 속성엔 백킹 필드가 없다. 인터페이스는 구현 클래스로부터 속성을 얻는 것을 추상 메소드에 의존한다. 반면에 추상 클래스는 백킹 필드를 가진다
      • 인터페이스는 한 번에 여러 개를 구현할 수 있지만, 클래스는 추상 클래스든지 일반 클래스든지 하나만 확장 가능하다
      • 구현 클래스에서 여러 개의 인터페이스를 한 번에 구현할 수 있다
      • 추상 클래스는 한 번에 하나의 클래스만 확장 가능
    • 선택 가이드
      • 여러 클래스 사이에서 상태를 다시 사용해야 한다면 추상 클래스
      • 각각의 클래스들이 각기 다른 구현 하는 것을 의도한다면 인터페이스

 

2. 중첩 클래스와 내부 클래스

  • 내부 클래스를 사용하면 효율성 저하를 피하면서 분리된 클래스를 사용하는 이점을 얻을 수 있다
  • 코틀린 클래스는 다른 클래스에 중첩될 수 있다
  • Java와 다르게 코틀린의 중첩 클래스는 외부 클래스의 private 멤버에 접근 할 수 없다, 하지만 inner 키워드를 사용하면 내부 클래스로 변하면서 제약이 사라진다
  • 예제코드
    // nestedremote.kts
    interface Remote {
        fun up()
        fun down()
        fun doubleUp() {
            up()
            up()
        }
    
        companion object {
            fun combine(first: Remote, second: Remote): Remote = object : Remote {
                override fun up() {
                    first.up()
                    second.up()
                }
    
                override fun down() {
                    first.down()
                    second.down()
                }
            }
        }
    }
    
    
    class TV {
        private var volumn = 0
        val remote: Remote
            get() = TVRemote()
    
        override fun toString() = "Volumn: ${volumn}"
    
        inner class TVRemote : Remote {
            override fun up() {
                volumn++
            }
            override fun down() {
                volumn--
            }
            override fun toString() = "Remote: ${this@TV}"
        }
    }
    
    val tv = TV()
    val remote = tv.remote
    println("$tv") // Volumn: 0
    remote.up()
    println("After increasing: $tv") // After increasing: Volumn: 1
    remote.doubleUp()
    println("After doubleUp: $tv") // After doubleUp: Volumn: 3​

    • 스트링 템플릿안에 this@TV는 외부 클래스인 TV의 인스턴스를 나타낸다
    • 부모 클래스(베이스 클래스)로 접근하려면 super를 사용하면 된다
    • object: {class} 를 통해서 익명 내부 클래스로 생성할 수 있다
      • 중첩 클래스나 내부 클래스에 특벼한 상태가 필요하다면 외부 클래스에서 하듯이 속성을 생성하면 된다

 

3. 상속

  • 코틀린은 클래스가 어쩌다 베이스 클래스로서 동작하는 것을 원하지 않는다
  • 개발자는 클래스가 베이스 클래스로서 사용되게 하려면 명시적인 권한을 제공해야한다
  • 인터페이스와 다르게 코틀린 클래스는 디폴트가 final이다
  • open이라고 명시되어있는 클래스만 상속이 가능하다
  • 즉 자식클래스는 open이라고 명시되어있는 부모클래스만 상속이 가능하며 그 중 열려있는 메소드만 오버라이드 가능하다
  • 상속하는 클래스에서 재정의를 방지하려면 final override를 명시해주면 된다
  • 예제코드
    // inheritance.kts
    import java.lang.RuntimeException
    
    open class Vehicle(val year: Int, open var color: String) {
        open val km = 0
        final override fun toString() = "year: $year, Color: $color, KM: $km"
        fun repaint(newColor: String) {
            color = newColor
        }
    }
    
    open class Car(year: Int, color: String) : Vehicle(year, color) {
        override var km: Int = 0
        set(value) {
            if (value < 1) {
                throw RuntimeException("can't set negative value")
            }
            field = value
        }
        fun drive(distance: Int) {
            km += distance
        }
    }
    
    val car = Car(2019, "Orange")
    println(car.year)
    println(car.color)
    car.drive(10)
    println(car)
    try {
        car.drive(30)
    } catch (ex: RuntimeException) {
        println(ex.message)
    }​

    • Java와 다르게 코틀린은 implements와 extends를 구분하지 않는다 ( 그냥 상속이라고 표현 )
    • setter를 재정의하기 위해 open된 km 변수를 다시 override했다
  • 코틀린은 접근 권한에 관한 제약사항을 좀 더 관대하고 느슨하게 만들 수 있다
    • private이나 protected 멤버를 자식 클래스에서는 public으로 만들 수 있다
    • 단 베이스 클래스의 public 멤버를 자식 클래스에서 protected로 만들 수는 없다

 

4. 씰드 클래스

  • final 클래스란 open으로 표기되어 있지 않아서 자식클래스가 하나도 없는 클래스를 의미
  • open과 abstract 클래스는 그 반대로 여러 자식 클래스를 가질 수 있으며, 어떤 클래스가 상속 받았는지 전혀 알 수 없다
  • 이를 개선하고자 코틀린에서는 sealed 클래스라는 것을 지원한다
    • 작성자가 지정한 몇몇 클래스에서만 상속할 수 있도록 하는 중간 영역
    • 동일한 파일에 작성된 다른 클래스들에 확장이 허용되지만 그 외의 클래스들은 확장할 수 없는 클래스
  • 예제코드
    // Card.kt
    package chapter8
    
    import java.lang.RuntimeException
    
    sealed class Card(val suit: String) {
        class Ace(suit: String) : Card(suit)
        class King(suit: String) : Card(suit) {
            override fun toString() = "King of $suit"
        }
    
        class Queen(suit: String) : Card(suit) {
            override fun toString() = "Queen of $suit"
        }
    
        class Jack(suit: String) : Card(suit) {
            override fun toString() = "Jack of $suit"
        }
    
        class Pip(suit: String, val number: Int) : Card(suit) {
            init {
                if (number < 2 || number > 10) {
                    throw RuntimeException("Pip has to be betwwen 2 and 10")
                }
            }
        }
    }​
     
    • sealed 클래스의 생성자는 private으로 취급된다
    • 자식 클래스는 sealed 클래스의 싱글톤 객체가 될 수 있다
    • 해당 예제에서는 Card의 5개의 자식클래스가 정의되어있고 범위를 벗어나는 다른 클래스에서 Card를 상속하려한다면 컴파일 오류가 난다
  • 사용예제
    // UseCard.kt
    package chapter8
    
    import chapter8.Card.Ace
    import chapter8.Card.King
    import chapter8.Card.Queen
    import chapter8.Card.Jack
    import chapter8.Card.Pip
    
    fun process(card: Card) = when (card) {
        is Ace -> "${card.javaClass.name} of ${card.suit}"
        is King, is Queen, is Jack -> "$card" 
        is Pip -> "${card.number} of ${card.suit}"
    }
    
    fun main() {
        println(process(Ace("Diamond"))) // chapter8.Card$Ace of Diamond
        println(process(Queen("Clubs"))) // Queen of Clubs
        println(process(Pip("Spades", 2))) // 2 of Spades
        println(process(Pip("Hearts", 6))) // 6 of Hearts
    }​
  • sealed 클래스와 when 조합을 사용할땐 else를 사용하면 안된다 ( 절대 사용해서는 안된다는 경고가 뜬다 )

 

5. Enum의 생성과 사용

  • 예제코드
    // CardWithEnum.kt
    package chapter8
    
    enum class Suit { 
        CLUBS, DIAMONDS, HEARTS, SPADES
    }
    
    ...​
  • valueOf() 메서드를 통해 String이 주어지면 같은 naming을 갖는 enum 인스턴스를 얻을 수 있다 ( 매치되는 값이 없다면 실행 시간 예외가 발생 )
  • values() 메서드를 통해 enum이 가진 값들을 반복할 수 있다 ( name과 순서가 반환 )
  • 상태를 갖는 예제코드
    // initlizaeenum.kts
    enum class Suit(val symbol: Char) {
        CLUBS('\u2663'),
        DIAMONDS('\u2666'),
        HEARTS('\u2665') {
            override fun display() = "${super.display()} $symbol"
        },
        SPADES('\u2660');
    
        open fun display() = "$symbol $name"
    }​
  • 코틀린 컴파일러는 가능한 경우 enum 클래스의 인스턴스를 최소한으로 생성해 주고, 익명 내부 클래스가 필요한 경우엔 이 역시 생성 가능하다
728x90
반응형