4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
- 자바 8 인터페이스와 비슷
- 추상메서드, 구현이 있는 메서드도 정의 가능
- 구현 있는 메서드 == 자바 8의 디폴트 메서드
- 자바 8 디폴트 메서드는 default 키워드를 반드시 사용해야하지만 코틀린 인터페이스는 아니다
- Q) 많약 2개 이상의 인터페이스를 상속받은 클래스에서, 각 인터페이스가 동일한 시그니쳐의 디폴트메소드를 정의한다면?
- 컴파일 오류가 발생 -> 명시적으로 겹치는 시그니처 메소드를 오버라이딩 해야함을 알려줌
- 다만, 상태(필드) 는 가질 수 없음
- 메소드 상속하는 쪽에서 override 를 필수로 붙여야함
- override 를 강제하여 얻는 이득 -> 실수로 상위클래스의 메소드를 오버라이드하는 것을 컴파일러가 잡아줌
4.1.2 open, final, abstract 변경자: 기본적으로 final
- 자바에서 모든 메소드는 기본적으로 상속이 가능함(즉, final 이 아님)
- 이로 인해 발생하는 문제 -> 취약한 기반 클래스
- 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨지는 문제
- 기반 클래스 입장에서 모든 하위 클래스를 분석하는 것은 불가능에 가깝기 때문에, 이런 문제를 방지하기 위해 선, 상속에 대한 엄격한 규칙을 제공할 필요가 있음
- 더군다나 하위클래스의 잘못된 오버라이딩으로 인해 기반클래스의 상태가 변경될 수도 있음
- 한마디로, 상속은 기반-하위 클래스 사이의 강결합을 유발하기 때문에 가급적 지양해야 함
- 기반 클래스의 캡슐화를 깨는 행위
- 이로 인해 발생하는 문제 -> 취약한 기반 클래스
- 코틀린의 메서드는 기본적으로 final이다.
- 기본적으로 상속을 지양한다는 사상으로부터 코틀린의 메서드는 기본 final로 만듦
- 특별히 하위 클래스에서 오버라이딩하게 의도된 메서드가 아니라면 모두 final로 만들자는 사상
- 상속을 적극적으로 사용하는 java 진영에 존재하는 프레임워크와 안 맞는 부분이 있을 수 있음
- 코틀린에선 오버라이딩이 의도된 메서드는 명시적으로 open 변경자를 달아줘야 함
- 오버라이딩한 메서드는 기본적으로 open이다
- 코틀린의 abstract와 자바의 abstract는 거의 같다
- 코틀린의 abstract 는 기본적으로 open이다
- 추상 클래스에 구현된 비추상 메서드는 기본적으로 final이다
- 인터페이스는 항상 open이다
- 인터페이스는 open, final, abstract 사용 불가
- 정리
4.1.3 가시성 변경자: 기본적으로 공개
- 기본적으로 public
- 정리
4.1.4 내부 클래스와 중첩된 클래스: 기본적으로 중첩된 클래스
- 우선 내부 <-> 중첩 차이가 뭘까?
- 자바의 하위 class => 내부 클래스
- 상위 클래스의 인스턴스를 통해서만 생성 가능
- 상위 클래스 인스턴스의 참조를 항상 가지고 있음
- 자바의 하위 static class => 중첩 클래스
- 그냥 독립된 클래스
- 자바의 하위 class => 내부 클래스
- 코틀린은 기본적으로 중첩된 클래스
- 정리
4.1.5 봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한
- 예제 (문제상황 == 봉인된 클래스 개념이 나온 배경)
- Expr의 구현체인 Num 과 Sum
- Expr 의 구현체는 이것 말고도 더 늘어날 수 있고 얼마나 많은 구현체가 있는지 이 코드만 보고 알 수 없음
- 이로 인해 항상 타입 비교 시 else 문을 넣어 부가적인 처리를 해야 함 -> 불필요한 부가 코드
- else 이외의 모든 조건에서 실제로 모든 case를 커버하는지 컴파일 타임에는 알 수 없음
- 위 문제에 대한 답으로 sealed 클래스가 도입됨
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화: 주 생성자와 초기화 블록
- 주 생성자 목적
- 생성자 파라미터 지정
- 생성자 파리미터에 의해 초기화되는 프로퍼티 정의
- 주 생성자의 변천사
- 본모습:
- 1단계 생략:
- 최종 생략본:
- 생성자도 메서드임 -> 디폴트 파라미터 지정 가능
- 주 생성자 생략본 + 디폴트 파라미터 지정으로 인해 흔히 자바에서 여러 생성자를 오버로딩 하는 형태를 대부분 커버가능
- 상속 상황에서의 생성자
-
- 하위 클래스는 기반 클래스의 생성자를 호출해야 함
- 위 이미지는 호출하는 방법을 보여줌(-> 생략된 형태)
-
4.2.2 부 생성자: 상위 클래스를 다른 방식으로 초기화
- 상속이 아닌 상황에선 주 생성자 + 디폴트 파라미터로 대부분의 경우가 커버되는 듯
- 상속하는 상황에서 생성자 문법이 제약이 있다.
- 기반 클래스를 다양한 방식으로 생성하는 방식을 주 생성자만으로 지원하지 못함
- 따라서, 기반 클래스를 다양한 방법으로 생성하고 싶을 땐, 아래와 같이 부 생성자를 작성한다
- 이것 말고도 불가피하게 파라미터 목록이 다른 경우에는 부 생성자를 여럿 둬야 함
4.2.3 인터페이스에 선언된 프로퍼티 구현
- 인터페이스에 프로퍼티는 선언 가능
- 상태를 저장할 수 없기 때문에, 뒷받침하는 필드를 가질 수는 없음
- 뒷받침하는 필드는 구현체에서 만들어야 함
- 인터페이스 예시
interface User {
val nickname: String
}
- 주 생성자에서 오버라이드한 프로퍼티
class PrivateUser(override val nickname: String) : User
- 내부적으로 getter 가 생성되고 이 getter의 뒷받침하는 필드가 만들어짐
- 객체 생성 시 뒷받침하는 필드가 초기화되고 이후에는 접근만 가능
커스텀 게터
class SubscribingUser(val email: String): User {
override val nickname: String
get() = email.substringBefore('@')
}
- 객체 생성 시 email 이 초기화됨
- nickname의 뒷받침하는 필드는 없고 커스텀게터를 통해 값을 얻음
- 커스텀 게터에서는 매번 email.substringBefore('@') 코드를 실행해서 값을 리턴
프로퍼티 초기화 식
class FaceBookUser(val accountId: Int): User {
override val nickname = getFaceBookId(accountId)
}
- 객체 생성 시 한 번만 프로퍼티 초기화 식이 실행되어 nickname에 값이 세팅됨
- 인터페이스에 게테와 세터가 있는 프로퍼티 선언 가능!
- 상태(필드)만 없으면 다 된다!
interface User {
val email: String
val nickname: String
get() = email.substringBefore('@')
}
- User의 구현은 반드시 email 은 override 해야 하지만 nickname 은 옵셔널
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근 -> field 키워드!
- 위 예시처럼 게터와 세터에서 뒷받침하는 필드에 접근하기 위해 field 키워드를 사용
- 게터에선 field 값을 읽을 수만 있고
- 세터에선 field 값을 읽거나 변경할 수 있음
4.2.5 접근자의 가시성 변경
- 클래스 내부에서는 변경 가능(var) 속성을 가지지만 밖에서는 읽기만 가능하도록 만드는 방법
class LengthCounter {
var counter: Int = 0
private set
fun addWord(word: String) {
counter += word.length
}
}
4.3 컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메서드: toString, equals, hashCode
- sample class
class Client(val name: String, val postalCode: Int)
- toString
- 기본 제공되는 객체의 문자열 표현은 Client@b684286 형태
- 이 기본 구현을 변경하려면 toString method를 ovveride 해야 한다
class Client(val name: String, val postalCode: Int) {
override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}
- equals
- 객체의 동일성은 == 를 통해 확인한다 <- 참조가 같은지 확인
- 객체의 동등성은 만드는 객체에 따라 정의하기 나름인데, 이 정의에 따라 equals 메서드를 override 한다
- 일반적으로 객체가 가진 모든 프로퍼티가 일치하면 동등하다는 규칙을 사용한다
- hashCode
- 자바에서는 equals를 override 하면 반드시 hashCode 도 override 해야 한다
- JVM 언어에서는 아래와 같은 규칙이 있다
- equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode를 가져야 한다
- java의 모든 hash container는 위 규칙을 기반으로 동작한다
- 즉, hash container에서 객체를 비교할 때 효율성을 위해 equals를 먼저 보지 않고 우선 hashCode를 비교해 보고
- hashCode 가 다르면 다른 객체라 간주
- hashCode 가 같으면 그때 equals를 호출해서 진짜 같은지 다시 확인
4.3.2 데이터 클래스: 모든 클래스가 정의해야 하는 메서드를 자동 생성
- 어떤 클래스가 데이터를 저장하는 역할만 수행한다면, toString, equals, hashCode는 반드시 override 해야 함
- data라는 변경자를 클래스 앞에 붙이면, 위 3개의 메서드를 포함한 여러 유용한 메소드를 자동 생성해 준다
4.3.3 클래스 위임: by
- 일반적으로 기존 클래스의 코드를 재사용하면서 확장하는 방법에는 상속과 합성이 있다
- 상속을 사용하면 부모-자식 강결합이 발생한다
- 부모-자식 사이의 결합도를 낮추면서 확장하는 방법으로 데코레이터페던을 사용한다
- 데코레이터페턴은 준비코드가 많이 필요하다
- 기존 부모 객체의 기능을 일부만 확장할 것인데,
- 확장하지 않는 다른 메서드들에 대해 모두 위임처리를 위한 메서드를 구현해줘야 함
- 이러한 준비 코드를 줄여주기 위해 코틀린은 by 키워드를 제공한다
- 예시
4.4 Object 키워드: 클래스 선언과 인스턴스 생성
- object 키워드는 다양한 상황에서 사용됨
- but, 클래스를 정의하면서 동시에 객체를 생성한다는 공통점이 있음
4.4.1 객체 선언: 싱글턴을 쉽게 만들기
object Payroll {
val allEmployees = arrayListOf<String>()
fun calculate() {
// todo....
}
}
- object라는 키워드 -> 클래스를 정의 + 그 클래스의 인스턴스를 만들어서 변수에 저장하는 작업을 내포
- 클래스와 마찬가지로 프로퍼티, 메서드, 초기화블록이 들어갈 수 있음
- 클래스와 달리 생성자를 가질 수는 없음
- 선언과 동시에 만들어지기 때문에 특정 생성자를 호출하는 개념이 아니라서
- 선언한 객체 이름에 점(.)을 찍는 형태로 내부 프로퍼티 & 메서드에 접근 가능
Payroll.caculate() // <- 이런식으로 사용가능
- object를 통해 만들어지는 싱글톤 객체도 상속을 지원한다
object CaseOneComparator : Comparator<File> {
override fun compare(o1: File, o2: File): Int {
return o1.path.compareTo(o2.path, ignoreCase = true)
}
}
fun someMethod() {
val files = listOf(File("/z"), File("/c"))
files.sortedWith(CaseOneComparator)
}
- 요런 식으로 특정 클래스를 상속받은 클래스를 싱글톤으로 만들 수 있다.
- 클래스를 만들고 -> 객체를 생성해서 사용할 경우 object를 사용해 코드를 줄일 수 있는 경우가 있을 것 같다.
- 어떠한 상태 저장도 필요 없는 경우 유용하게 사용될 듯
- 싱글톤 객체도 특정 클래스 내부에 정의할 수 있다.
- 이렇게 되면, 클래스의 인스턴스마다 object로 선언된 객체가 생성되는 것은 아니다
data class Person(val name: String) {
object NameComparator: Comparator<Person> {
override fun compare(o1: Person, o2: Person): Int {
return o1.name.compareTo(o2.name)
}
}
}
4.4.2 동반 객체: 팩토리 메서드와 정적 멤버가 들어갈 장소
- 코틀린에서 static을 지원 안 함 -> 정적 멤버 개념이 없음
- 그 대신 최상위 함수 & 객체 선언을 지원
- 최상위 함수의 경우 private으로 선언된 클래스 비공개 멤버에 접근이 불가함
- 따라서, 클래스 인스턴스와 관계없이 호출되어야 하지만, 클래스 내부 정보에 접근해야 할 필요가 있는 메서드가 필요할 때, 클래스에 중첩된 객체 선언의 멤버함수로 정의해야 한다.
- 그런 함수의 대표적인 예시가 팩토리 메서드임
- 특정 클래스에 종속되는 정적 멤버와 메서드가 있다면, 그건 동반 객체 내부에 선언하는 게 좋다.
- 특정 클래스에 독립되는 정적 메서드는 최상위함수로 만들면 될 듯
4.4.3 동반 객체를 일반 객체처럼 사용
- 동반 객체도 일반 객체와 동일하게 이름을 붙이거나 인터페이스 상속, 확장함수나 프로퍼티 정의가 가능하다.
동반객체에 이름 붙이기
class Person(val name: String) {
companion object Loader {
fun fromJson(jsonText: String): Person {
// ...
}
}
}
fun test() {
Person.Loader.fromJson(/*....*/)
}
동반객체에서 인터페이스 구현
interface JsonFactory<T> {
fun fromJson(jsonText: String): T
}
class Person(val name: String) {
companion object : JsonFactory<Person>{
override fun fromJson(jsonText: String): Person {
TODO("Not yet implemented")
}
}
}
동반객체 확장
// 비즈니스 로직 모듈
class Person(val name: String) {
companion object {
}
}
// 클라이언트 통신 모듈
fun Person.Companion.fromJson(json: String): Person {
// ...
}
val p = Person.fromJson(/*...*/)
4.4.4 객체 식: 무명 내부 클래스를 다른 방식으로 지정
- 자바에서 익명 클래스 만들던걸 object 키워드로 할 수 있다
- 이 경우에는 싱글톤을 만들 때와 달리 변수에 대입하게 되면 여러 개의 인스턴스를 만들 수도 있다
- 대부분 특정 클래스나 인터페이스를 상속받으면서 무명 클래스를 만든다.
val listener = object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
super.mouseClicked(e)
}
}
'kotlin' 카테고리의 다른 글
[Kotlin in Action] 6장: 코틀린 타입 시스템 > 6.1 널과 관련한 이야기 (0) | 2023.03.18 |
---|---|
[Kotlin in Action] 5장: 람다로 프로그래밍 (0) | 2023.03.05 |
[Kotlin in Action] 3장: 함수의 정의와 호출 (0) | 2023.02.08 |
[kotlin-tip] 코프링 환경에서 로깅하기 (0) | 2023.01.14 |
[Kotlin in Action] 2장: 코틀린 기초 (0) | 2023.01.14 |
댓글