📚 클래스(2)

📔 클래스의 특징

  • 모든 클래스는 기본적으로 final로 지정되어있다. 이는 기본적으로 상속이 불가능함을 의미한다.
  • 상속이 필요한 경우 클래스 앞에 open 키워드를 사용하여 상속이 가능하도록 한다.
  • 클래스에만 open을 지정하는 것이 아니라 속성이나 메서드도 기본이 final이기 때문에 서브 클래스에서 재정의하기 위해서는 open 키워드를 붙여줘야 한다.


📔 상속


📖 상속 관계

  • 상속 관계(is-a): 상속을 하면 실제 슈퍼클래스와 서브클래스가 하나로 묶여 사용됨. 클래스 상속 관계에 상속하는 클래스를 표시
  • 연관 관계(has-a): 클래스를 사용하는 관계. 클래스 내부에 다른 클래스를 속성으로 처리
  • 구현 관계(implements): 인터페이스의 추상 메서드나 추상 속성은 실제 구현 클래스에서 모두 구현해야 함. 해당 인터페이스도 자료형이기 때문에 클래스의 상속 관계와 동일하게 처리됨
open class Super { // 슈퍼 클래스
    override fun toString() = "Super(id=${this.hashCode()})" // 문자열 출력 메서드 재정의
    open fun info() = "슈퍼 클래스 정보 확인"
    fun getSuper() = "슈퍼 클래스의 메서드" // 재정의 불가 메서드
}

class Sub : Super() { // 서브 클래스
    override fun toString() = "Sub(id=${this.hashCode()})" // 문자열 출력 메서드 재정의
    override fun info() = "서브 클래스 정보 확인"
}

val sup = Super() // 슈퍼 클래스 객체 생성
println(sup)
println(sup.info())

val sub = Sub() // 서브 클래스 객체 생성
println(sub)
println(sub.info())
println(sub.getSuper())
Super(id=948408147)
슈퍼 클래스 정보 확인
Sub(id=1987594887)
서브 클래스 정보 확인
슈퍼 클래스의 메서드


📖 상속 관계 구조화

open class Person(val name: String) { // 1레벨 슈퍼클래스
    open fun info() = "이름: $name"
}

open class Man(name: String, val age: Int) : Person(name) { // 2레벨 슈퍼클래스
    override fun info() = "이름 = $name 나이 = $age"
}

class Student(val school: String, name: String, age: Int) : Man(name, age) { // 3레벨 슈퍼클래스
    override fun toString() = "Student(school=$school, name=$name, age=$age)"
}

val st = Student("초등학교", "달님", 11)
println(st)
println(st.info()) // 상위의 메서드를 찾고 실행
Student(school=초등학교, name=달님, age=11)
이름 = 달님 나이 = 11


📖 상속 관계에 따른 제약

상위 클래스 내의 속성과 메서드가 모두 open되면 하위 클래스는 모든 것을 재정의해야 한다. 그래서 하위 클래스에 필요한 것만 재정의할 수 있도록 키워드로 제한을 둔다.

  • open, override 키워드는 하위 클래스에서 재정의할 수 있음
  • 상속을 못하게 제약하려면 override 앞에 final을 추가해야 함
open class Person(val name: String) { // 슈퍼클래스
    fun sayHello() = "안녕하세요" // 재정의 불가
    open fun sayBye() = "안녕히계세요" // 하위클래스에서 재정의 가능
    override fun toString() = "Person(name=$name)" // 상위 클래스 메서드 정의
}

open class Man(name: String, val age: Int) : Person(name) {
    final override fun sayBye() = "안녕히계세요 + $name" // 하위 클래스에 재정이 금지
    override fun toString() = "Man(name=$name, age=$age)" // 상위 클래스 메서드 재정의
}

class Student(val school: String, name: String, age:Int) : Man(name, age) { 
    override fun toString() = "Student(name=$name, age=$age, school=$school)" // 상위 클래스 메서드 재정의
}

val pn = Person("가님") // 최상위 슈퍼클래스 객체 생성
println(pn)
println(pn.sayBye())

val mn = Man("나님", 33) // 상위 슈퍼클래스 객체 생성
println(mn)
println(mn.sayBye())

val st = Student("초등학교", "다님", 11) // 서브 클래스 객체 생성
println(st)
println(st.sayBye())
Person(name=가님)
안녕히계세요
Man(name=나님, age=33)
안녕히계세요 + 나님
Student(name=다님, age=11, school=초등학교)
안녕히계세요 + 다님


📔 상속에 따른 생성자 호출

상속 관계일 때 슈퍼클래스의 멤버를 서브클래스에서 사용하므로 슈퍼클래스의 속성을 먼저 처리해야 한다. 따라서 생성자 호출 순서도 슈퍼클래스의 생성자가 먼저 호출되고 그다음 서브클래스의 생성자가 호출되어야 한다. 이러한 연결을 생성자 정의 시 명확히 해야 한다.


📖 생성자 간의 연결

open class Animal(val species: String) // 슈퍼클래스 주 생성자

class Pet(species: String, val subSpecies: String) : Animal(species) { // 서브클래스 주 생성자 및 슈퍼클래스 상속
    constructor(species: String, subSpecies: String, age: Int) : this(species, subSpecies) // 서브클래스 보조 생성자
}

val pet = Pet("개", "푸들", 4) // 객체 생성

println(" 종 : ${pet.species} 세부종 : ${pet.subSpecies}") // 속성 출력
 종 : 개 세부종 : 푸들

📖 생성자 간의 보조 생성자로 연결

open class Person {
    val name: String
    var gender: String = "여성"
    
    constructor(name: String) { // 첫 번째 보조 생성자
        this.name = name
        println("슈퍼클래스 보조 생성자 1 실행")
    }
    constructor(name: String, gender: String) : this(name) { // 기본 보조 생성자 위임 호출
        this.gender = gender
        println("슈퍼클래스 보조 생성자 2 실행")
    }
}

class Student : Person {
    var age: Int = 0
    constructor (name: String, age: Int) : super(name) { // 슈퍼클래스 생성자를 위임 호출
        this.age = age
        println("서브클래스 보조 생성자 1 실행")
    }
    constructor (name: String, gender: String, age: Int) : super(name, gender) { // 슈퍼클래스 생성자를 위임 호출
        this.age = age
        println("서브클래스 보조 생성자 2 실행")
    }
}

println("생성자 호출 순서 1: ")
val s1 = Student("최부식", 21)

println("생성자 호출 순서 2: ")
val s2 = Student("김규식", "남성", 25)
생성자 호출 순서 1: 
슈퍼클래스 보조 생성자 1 실행
서브클래스 보조 생성자 1 실행
생성자 호출 순서 2: 
슈퍼클래스 보조 생성자 1 실행
슈퍼클래스 보조 생성자 2 실행
서브클래스 보조 생성자 2 실행


📔 내부 클래스, 이너 클래스, 지역 클래스

  • 내부 클래스(Nested Class): 외부 클래스와 다르게 클래스 내부에서 별도의 기능을 처리하는 클래스
  • 이너 클래스(Inner Class): 외부 클래스와 연관된 처리를 하는 클래스
  • 지역 클래스(Local Class): 함수 내부에 선언하여 사용하는 클래스


📖 내부 클래스와 이너 클래스의 차이점

  • 내부 클래스는 외부 클래스의 이름으로 접근해서 객체를 생성할 수 있지만, 이너 클래스는 멤버처럼 객체로 접근해서 객체를 생성함
  • 내부 클래스는 외부 클래스와 별개의 클래스라 외부 클래스의 속성을 참조할 수 없지만, 이너 클래스는 외부 클래스의 속성을 참조할 수 있음

📖 내부 클래스(Nested Class)

class Outer {
    private val bar: Int = 1 // 외부 클래스의 비공개 속성
    
    class Nested { // 내부 클래스
        private val nestVar = 100
        fun foo() = 999
        
        // fun foo() = this@Outer.bar // 외부 클래스의 멤버 참조 시 예외 발생
    }
}

val demo = Outer.Nested() // 내부 클래스 객체 생성은 외부 클래스로 접근해서 생성

println(demo.foo()) // 내부 클래스의 메서드 실행

// Outer.Nested().nestVar // 내부 클래스의 비공개 속성 접근 시 예외 발생
999

📖 이너 클래스(Inner Class)

  • 이너 클래스 정의 시, inner 키워드를 클래스 앞에 붙임
  • 이너 클래스의 객체는 this이고 외부 클래스의 객체는 this@외부 클래스 이름으로 사용. 그래서 이너 클래스 내부에서 외부 클래스의 속성에 접근 가능
  • 외부 클래스에서 이너 클래스를 사용하려면 객체를 생성해서 사용
class Outer {
    private val bar: Int = 1 // 외부 클래스의 비공개 속성
    inner class Inner { // 이너 클래스
        private val bar = 100 // 동일한 이름의 속성 보유
        fun foo() = this@Outer.bar // 내부 클래스의 메서드로 외부 클래스 비공개 속성 접근
        fun fbar() = bar // 비공개 속성에 접근할 수 있는 메서드 제공
    }
    
    fun getBar() = println(Inner().fbar())
}

val demo = Outer().Inner().foo() // 이너 클래스가 멤버 클래스여서 객체로 접근

println(demo)
Outer().getBar()
1
100

📖 지역 클래스(Local Class)

fun localClasses() {
    open class Amphibian { // 함수 내부에 지역 클래스 정의
        open fun foo() = "foo"
    }
    class Frog: Amphibian() { // 상속받아서 지역 클래스 정의
        override fun foo() = "bar"
    }
    val amphibian: Amphibian = Frog() // 객체 생성
    
    println(amphibian.foo()) // 메서드 호출
}

localClasses()
bar

📖 지역 클래스의 객체 반환

특정 인터페이스를 상속한 팩토리 함수(다른 객체를 생성하는 함수)를 만들 경우, 지역 클래스를 정의해서 사용할 수 있다.

interface Amphibian { // 인터페이스 정의
    fun foo() : Unit
}

fun createAmphibian(): Amphibian { // 인터페이스로 
    
    class Frog : Amphibian { // 지역클래스에 인터페이스 상속 
        override fun foo() { // 메서드 재정의
            println("foo ")
        }
    }
    
    return Frog() // 지역 클래스의 객체 반환
}

val amphibian = createAmphibian() //내부클래스의 객체 생성
amphibian.foo() // 내부 클래스의 메서드 실행
println(amphibian.javaClass.kotlin)
foo 
class Line_8_jupyter$createAmphibian$Frog


📔 메서드에서 전역 변수 참조


📖 메서드 내부에서 전역 변수 참조

val globalVar = 999 // 전역 변수 할당

class A { // 클래스 몸체 내에서는 전역 변수 참조 불가
    fun methods(): Int {
        return globalVar
    }
}

val a = A()
println(a.methods())
999

📖 메서드 내부에서 전역 변수 갱신

var ar: Int = 999 // 전역 변수 할당

class AB {
    fun methodsA(x: Int): Int { 
        ar = ar + x // 전역 변수 갱신
        return ar // 전역 변수 참조
    }
}

AB().methodsA(10)
println(ar) // 전역 변수 갱신 결과
1009

📖 이너 클래스의 메서드에서 전역 변수 참조

var a: Int = 999 // 전역 변수 할당

class AB {
    
    inner class ABC { // 내부 클래스
        fun plusInt(x: Int): Int { // 내부 클래스의 메서드
            a += x // 전역 변수 갱신
            return a // 전역 변수 참조
        }
    }
}

AB().ABC().plusInt(100)
println(a) // 전역 변수 갱신 결과
1099


📔 외부 클래스의 상속 관계를 이너 클래스에서 처리

상속 관계를 가지는 외부 클래스를 정의하면 이너 클래스에서 속성을 참조할 때 상속 관계도 지정해야 한다.


📖 이너 클래스에서 this와 super 표기법

  • this : 이너 클래스의 객체는 항상 this로 참조
  • this@외부 클래스 : 이너 클래스에서 외부 클래스의 속성은 외부 클래스를 @ 다음에 표기해야 참조 가능
  • super@외부 클래스 : 외부 클래스의 상속 관계는 this 대신 super를 사용

📖 상속 관계 처리 확인

open class Base {
    open val attr: Int = 1
    open fun methods() = println("슈퍼 클래스 f()")
}

class Derived : Base() {
    override val attr: Int = super.attr + 1 // 상속에 따른 재정의: 슈퍼클래스 속성을 super로 접근
    override fun methods() = println("외부 클래스 f()")
    
    inner class Inner {
        val attr = 999
        fun methods() = println("이너 클래스 f()")
        fun test() {
            this.methods() // 이너 클래스의 메서드 참조
            Derived().methods() // 외부 클래스의 메서드 참조 
            super@Derived.methods() // 슈퍼 클래스의 메서드 참조
            println("이너 클래스 this.attr : ${this.attr}") // 이너 클래스의 속성 참조
            println("외부 클래스 this@Derived.attr : ${this@Derived.attr}") // 외부 클래스의 속성 참조
            println("슈퍼 클래스 super@Derived.attr : ${super@Derived.attr}") // 슈퍼 클래스의 속성 참조
        }
    }
}

val c1 = Derived()
c1.Inner().test() // 이너 클래스의 메서드 실행
이너 클래스 f()
외부 클래스 f()
슈퍼 클래스 f()
이너 클래스 this.attr : 999
외부 클래스 this@Derived.attr : 2
슈퍼 클래스 super@Derived.attr : 1