-
4장. 클래스, 객체, 인터페이스 - (1) 클래스 계층 정의Kotlin 2022. 2. 24. 19:45
코틀린 선언
final이며 public
중첩 클래스
기본적으로 내부 클래스가 아니다. 즉, 코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없다.
생성자, 프로퍼티
필요하면 접근자를 직접 정의할 수 있다.
코틀린은 클래스를 data로 선언
컴파일러가 일부 표준 메소드를 생성해준다.
코틀린 언어가 제공하는 위임(delegation)
위임을 처리하기 위한 준비 메소드를 직접 작성할 필요 없다.
Object 키워드
- 클래스와 인스턴스를 동시에 선언하면서 만들 때 쓰는 키워드
- 싱글턴 클래스, 동반 객체, 객체식(자바의 무명 클래스에 해당)
클래스 계층 정의
코틀린 인터페이스
- interface
- 추상 메소드 有
- 구현이 있는 메소드 有
- 상태(필드) 無
코틀린 클래스
class
//간단한 인터페이스 선언하기 interface Clickable { fun click() } //단순한 인터페이스 구현하기 class Button : Clickable{ override fun click() = println("I was clicked") } fun main() { Button().click(); // I was clicked }
클래스 확장, 인터페이스 구현
클래스 이름 뒤에 콜론(:)을 붙이고 인터페이스와 클래스 이름을 적는다.
클래스는 인터페이스를 원하는 만큼 개수 제한 없이 마음대로 구현할 수 있지만, 클래스는 오직 하나만 확장할 수 있다.
override 변경자
위 클래스나 상위 인터페이스에 있는 프로퍼티나 메소드를 오버라이드 한다는 표시
자바와는 달리 코틀린에서는 override 변경자를 꼭 써야 한다.
실수로 상위 클래스의 메소드를 오버라이드 하는 경우를 방지해준다.
인터페이스 메소드
디폴트 구현을 제공
메소드 본문을 메소드 시그니처 뒤에 추가하면 된다.
//인터페이스 안에 본문이 있는 메소드 정의하기 interface Clickable { fun click()//일반 메소드 선언 fun showOff() = println("I'm clickable!") //디폴트 구현이 있는 메소드 } //동일한 메소드를 구현하는 다른 인터페이스 정의 interface Focusable { fun setFocus(b:Boolean) = println("I ${if (b) "got" else "lost"} focus.") fun showOff() = println("I'm focusable!") }
어느 쪽 showOff() 메소드가 선택될까?
어느 쪽도 선택되지 않는다. 클래스가 구현하는 두 상위 인터페이스에 정의된 showOff 구현을 대체할 오버라이딩 메소드를 직접 제공하지 않으면 다음과 같은 컴파일러 오류가 발생한다.
Class 'Button' must override public open fun showOff(): Unit defined in chapter04. Clickable because it inherits multiple interface methods of it
코틀린 컴파일러는 두 메소드를 아우르는 구현을 하위 클래스에 직접 구현하게 강제한다.
//상속한 인터페이스의 메소드 구현 호출하기 class Button : Clickable, Focusable{ override fun click() = println("I was clicked") override fun showOff() { // -- 1 super<Clickable>.showOff() // -- 2 super<Focusable>.showOff() // -- 2 } }
1 : 이름과 시그니처가 같은 멤버 메소드에 대해 둘 이상의 디폴트 구현이 있는 경우 인터페이스를 구현하는 하위 클래스에서 명시적으로 새로운 구현을 제공해야 한다.
2 : 상위 타입의 이름을 꺾쇠 괄호(<>) 사이에 넣어서 "super"를 지정하면 어떤 상위 타입의 멤버 메소드를 호출할지 지정할 수 있다.
Button 클래스는 이제 두 인터페이스를 구현한다.
자바
super 앞에 기반 타입을 적는다.
코틀린
꺽쇠 괄호 안에 기반 타입 이름을 지정한다.
Button 클래스는 Focusable 인터페이스 안에 선언된 setFocus의 구현을 자동으로 상속한다.
val button = Button() button.showOff() /* I'm clickable! I'm focusable! */ button.setFocus(true) //I got focus. button.click() //I was clicked
open, final, abstract 변경자 : 기본적으로 final
자바에서는 final로 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있다.
기본적으로 상속이 가능하면 편리하지만 문제점도 있다.
기본적으로 상속이 가능할 경우의 문제점
취약한 기반 클래스(fragile base class)
하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생긴다.
기반 클래스를 변경하는 경우 하위 클래스의 동작이 예기치 않게 바뀔 수도 있다는 면에서 기반 클래스는 '취약'하다
취약 기반 클래스의 해결법
"상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라"
즉, 하위클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들라는 뜻이다.
코틀린도 마찬가지 철학을 따른다.
자바의 클래스와 메소드는 기본적으로 상속에 대해 열려있지만 코틀린의 클래스와 메소드는 기본적으로 final이다.
Open 변경자
상속 허용하려면 클래스 앞에 붙여야 한다.
오버라이드를 허용하고 싶은 메소드나 프로퍼티의 앞에도 open 변경자를 붙여야 한다.
열린 메소드를 포함하는 열린 클래스 정의하기
open class RichButton : Clickable { // 이 클래스는 열려있다. 다른 클래스가 이 클래스를 상속할 수 있다. fun disable(){}//이 함수는 final이다. 하위 클래스가 이 메소드를 오버라이드할 수 없다. open fun animate(){} // 이 함수는 열려있다. 하위 클래스에서 이 메소드를 오버라이드할 수 있다. override fun click() {}//이 함수는 (상위 클래스에서 선언된) 열려있는 메소드르 오버라이드 한다. 오버라이드한 메소드는 기본적으로 열려있다. }
오버라이드 금지하기
//오버라이드 금지하기 open class RichButton2 : Clickable { final override fun click() {}//여기 있는 'final'은 쓸데 없이 붙은 중복이 아니다. 'final'이 없는 'override' 메소드나 프로퍼티는 기본적으로 열려있다. }
열린 클래스와 스마트 캐스트
클래스의 기본적인 상속 가능 상태를 final로 함으로써 얻을 수 있는 큰 이익은 다양한 경우에 스마트 캐스트가 가능하다는 점이다.
스마트 캐스트는 타입 검사 뒤에 변경될 수 없는 변수에만 적용 가능하다.
클래스 퍼스트의 경우 이는 val이면서 커스텀 접근자가 없는 경우에만 스마트 캐스트를 쓸 수 있다는 의미다. 이 요구 사항은 또한 프로퍼티가 final이어야만 한다는 뜻이다.
abstract
추상 클래스는 인스턴스화 할 수 없다.
추상 클래스 에는 구현이 없는 추상 멤버가 있기 때문에 하위 클래스에서 그 추상 멤버를 오버라이드 해야만 하는 게 보통이다.
추상 멤버는 항상 열려 있다. 따라서 추상 멤버 앞에 open 변경자를 명시할 필요가 없다.
추상 클래스 정의하기
abstract class Animated { // 이 클래스는 추상 클래스다. 이 클래스의 인스턴스를 만들 수 없다. abstract fun animate() // 이 함수는 추상 함수다. 이 함수에는 구현이 없다. 하위 클래스에서는 이 함수를 반드시 오버라이드 해야 한다. open fun stopAnimating(){} // 추상 클래스에 속했더라도 비추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드를 허용할 수 있다. fun animateTwice(){} }
코틀린의 상속 제어 변경자
변경자 이 변경자가 붙은 멤버는... 설명 final 오버라이드 할 수 없음 클래스 멤버의 기본 변경자다. open 오버라이드 할 수 있음 반드시 open을 명시해야 오버라이드 할 수 있다. abstract 반드시 오버라이드해야 함 추상 클래스의 멤버에만 이 변경자를 붙일 수 있다. 추상 멤버에는 구현이 있으면 안 된다. override 상위 클래스나 상위 인스턴스의 멤버를 오버라이드하는 중 오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이드르 금지하려면 final을 명시해야 한다. 가시성 변경자 : 기본적으로 공개
클래스 외부 접근을 제어
변경자 클래스 멤버 최상위 선언 public(기본 가시성임) 모든 곳에서 볼 수 있다 모든 곳에서 볼 수 있다 internal 같은 모듈 안에서만 볼 수 있다 같은 모듈 안에서만 볼 수 있다 protected 하위 클래스 안에서만 볼 수 있다 (최상위 선언에 적용할 수 없음) private 같은 클래스 안에서만 볼 수 있다 같은 파일 안에서만 볼 수 있다 자바의 기본 가시성인 패키지 전용은 코틀린에는 없다.
코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용한다. 그래서 패키지를 가시성 제어에 사용하지 않는다.
internal
"모듈 내부에서만 볼 수 있음"
모듈은 한 번에 한꺼번에 컴파일되는 코틀린 파일들을 의미한다.
ex) 프로젝트, 한 번 실행될 때 함께 컴파일되는 파일의 집합
모듈 내부 가시성은 여러분의 모듈의 구현에 대해 진정한 캡슐화를 제공한다는 장점
(캡슐화 : 속성과 행위를 하나로 묶고 구현 내용을 외부에 감춘다는 것)
최상위 선언 - private 가시성 허용
최상위 선언에는 클래스, 함수, 프로퍼티 등이 포함
비공개 가시성인 최상위 선언은 그 선언이 들어있는 파일 내부에서만 사용할 수 있다. 이 또한 하위 시스템의 자세한 구현 사항을 외부에 감추고 싶을 때 유용한 방법이다.
규칙
타입의 가시성은 클래스 자신의 가시성과 같거나 더 높아야 하고, 메소드의 시그니처에 사용된 모든 타입의 가시성은 그 메소드의 가시성과 같거나 더 높아야 한다는 더 일반적인 규칙에 해당한다.
이런 규칙은 어떤 함수를 호출하거나 어떤 클래스를 확장할 때 필요한 모든 타입에 접근할 수 있게 보장해준다.
코틀린은 public 함수인 giveSpeech 안에서 그보다 가시성이 더 낮은 타입인 TalkativeButton을 참조하지 못하게 한다.
internal open class TalkativeButton : Focusable { private fun yell() = println("Hey!") protected fun whisper() = println("Let's talk!") } fun TalkativeButton.giveSpeech(){//오류 : public 멤버가 자신의 internal 수신 타입인 TalkativeButton을 노출함 yell()// private 멤버여서 접근 불가 whisper() //protected 멤버여서 접근 불가 }
protected
멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다. 클래스를 확장한 함수는 그 클래스의 private이나 protected 멤버에 접근할 수 없다.
코틀린의 가시성 변경자와 자바
자바에서는 클래스를 private으로 만들 수 없으므로 내부적으로 코틀린은 private 클래스를 패키지-전용 클래스로 컴파일한다.
internal 변경자는 바이트코드상에서는 public이 된다.
코틀린 컴파일러가 internal 멤버의 이름을 보기 나쁘게 바꾼다(mangle)
이유
1. 한 모듈에 속한 어떤 클래스를 모듈 밖에서 상속한 경우 그 하위 클래스 내부의 메소드 이름이 우연히 상위 클래스의 internal 메소드와 같아져서 내부 메소드를 오버라이드 하는 경우를 방지하기 위함이다.
2. 실수로 internal 클래스를 모듈 외부에서 사용하는 일을 막기 위함이다.
내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
자바와 코틀린의 중첩 클래스와 내부 클래스의 관계
클래스 B 안에 정의된 클래스 A 자바에서는 코틀린에서는 중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) static class A class A 내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) class A inner class A 직렬화할 수 있는 상태가 있는 뷰 선언
interface State: Serializable interface View { fun getCurrentState() : State fun restoreState(state : State) {} }
자바에서 내부 클래스를 사용해 View 구현하기
public class Button2 implements View{ @Override public State getCurrentState() { return new ButtonState(); } @Override public void restoreState(State state) { } public class ButtonState implements State{} }
java.io.NoeSerializableException : Button 오류 발생하는 이유?
자바에서는 다른 클래스 안에 정의한 클래스는 자동으로 내부 클래스가 된다는 사실
ButtonState 클래스는 바깥쪽 Button2 클래스에 대한 참조를 묵시적으로 포함한다. 그러므로 직렬화할 수 없다.
해결법
ButtonState 클래스를 static 클래스로 선언
자바에서는 중첩 클래스를 static으로 선언하면 그 클래스를 둘러싼 바깥쪽 클래스에 대한 묵시적인 참조가 사라진다.
중첩 클래스를 사용해 코틀린에서 View 구현하기
코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같다.
내부 클래스로 변경해서 바깥쪽 클래스에 대한 참조를 포함하게 만들고 싶다면 inner 변경자를 붙여야 한다.
class Button3 : View{ override fun getCurrentState(): State = ButtonState() override fun restoreState(state: State) {} class ButtonState : State{} //이 클래스는 자바의 정적 중첩 클래스와 대응한다. }
this@Outer
내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하기 위해서 사용한다.
class Outer{ inner class Inner{ fun getOuterReference() : Outer = this@Outer } }
봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
인터페이스 구현을 통해 식 표현하기
코틀린 컴파일러는 when을 사용해 Expr 타입의 값을 검사할 때 꼭 디폴트 분기인 else 분기를 덧붙이게 강제한다.
interface Expr class Num(val value: Int) : Expr class Sum(val left: Expr, val right: Expr) : Expr fun eval(e:Expr):Int = when(e){ is Num -> e.value is Sum -> eval(e.right) + eval(e.left) else -> //else 분기가 꼭 있어야 한다. throw IllegalArgumentException("Unknown expression") }
버그 발생 시 해결법
sealed 클래스가 답이다.
상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다. sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 한다.
sealed 클래스로 식 표현하기
when 식에서 sealed 클래스의 모든 하위 클래스를 처리한다면 디폴트 분기(else 분기)가 필요 없다.
sealed로 표시된 클래스는 자동으로 open임을 기억할 것.
따라서 별도로 open 변경자를 붙일 필요가 없다.
sealed class Expr{ // 기반 클래스를 sealed로 봉인한다. class Num(val value: Int) : Expr()// 기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열한다. class Sum(val left: Expr, val right: Expr) : Expr() } fun eval(e:Expr):Int = when(e){//when 식이 모든 하위 클래스를 검사하므로 별도의 else 분기가 없어도 된다. is Expr.Num -> e.value is Expr.Sum -> eval(e.right) + eval(e.left) }
내부적으로 Expr 클래스는 private 생성자를 가진다. 그 생성자는 클래스 내부에서만 호출할 수 있다.
sealed 인터페이스를 정의할 수 없다.
봉인된 인터페이스를 만들 수 있다면 그 인터페이스를 자바 쪽에서 구현하지 못하게 막을 수 있는 수단이 코틀린 컴파일러에게는 없기 때문이다.
코틀린에서는 클래스를 확장할 때나 인터페이스를 구현할 때 모두 콜론(:)을 사용한다.
'Kotlin' 카테고리의 다른 글
4장. 클래스, 객체, 인터페이스 - (3) 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임 (0) 2022.03.07 4장. 클래스, 객체, 인터페이스 - (2)뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언 (0) 2022.02.25 3장. 함수 정의와 호출 - 2 (0) 2022.02.23 3장. 함수 정의와 호출 - 1 (0) 2022.02.22 2장. 코틀린 기초 - 클래스와 프로퍼티 (0) 2022.02.21