# 4장 클래스, 객체, 인터페이스
### 이번장에서는...
#### 배운점, 느낀점
- 클래스와 인터페이스
- 싱글턴 클래스
- 동반 객체
- 객체 식(자바의 무명 클래스에 해당)
- 생성자와 프로퍼티
- 데이터 클래스
- 클래스 위임
- object 키워드 사용
---
- 코틀린 선언은 기본적으로 `final`이며 `public`이다.
- 중첩 클래스는 기본적으로 내부 클래스가 아니다. 코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없다.
- 필요하면 접근자를 직접 정의할 수 있다.
- 코틀린 컴파일러는 번잡스러움을 피하기 위해 유용한 메서드를 자동으로 만들어준다.
- 클래스르 data로 컴파일러가 일부 표준 메서드를 생성해준다.
- 코틀린 언어가 제공하는 위임을 사용하면 위임을 처리하기 위한 준비 메서드를 직접 작성할 필요가 없다.
### 클래스 계층 정의
- 코틀린의 가시성과 접근 변경자
- 코틀린 가시성 및 접근 변경자는 자바와 비슷하지만 아무것도 지정하지 않은 경우 기본 가시성은 다르다.
- 코틀린에 도입한 `sealed` 변경자는 클래스 상속을 제한한다
#### 코틀린 인터페이스
- 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메서드도 정의할 수 있다(이는 자바8의 디폴트 메서드와 비슷하다) 다만, 인터페이스에는 아무런 상태도(필드)도 들어갈 수 없다.
```kotlin
package part4
interface Clickable {
fun click()
}
package part4;
import kotlin.Metadata;
@Metadata(
mv = {1, 7, 1},
k = 1,
d1 = {"\\u0000\\u0010\\n\\u0002\\u0018\\u0002\\n\\u0002\\u0010\\u0000\\n\\u0000\\n\\u0002\\u0010\\u0002\\n\\u0000\\bf\\u0018\\u00002\\u00020\\u0001J\\b\\u0010\\u0002\\u001a\\u00020\\u0003H&¨\\u0006\\u0004"},
d2 = {"Lpart4/Clickable;", "", "click", "", "org.example.playground.main"}
)
public interface Clickable {
void click();
}
package part4
class Button : Clickable {
override fun click() = println("I was clicked")
}
fun main() {
val buttonJava = Button()
buttonJava.click()
}
package part4
interface Clickable {
fun click() // 일반 메서드 선언
fun showOff() = println(" I'm clickable!") // 디폴트 구현이 있는 메서드
}
package part4
class Button : Clickable, Focusable {
override fun click() = println("I was clicked")
override fun showOff() { // 이름과 시그니처가 같은 멤버 메서드에 대해 둘 이상의 디폴트 구현이 있는 경우 인터페이스를 구현하는 하위 클래스에서 명시적으로 새로운 구현을 제공해야한다.
super<Clickable>.showOff()
super<Focusable>.showOff() // 상위 타입의 이름을 꺽쇠 괄호(<>)사이에 넣어서 "super"를 지정하면 어떤 상위 타입의 멤버 메서드를 호출할지 지정할 수 있다
}
}
fun main() {
val buttonJava = Button()
buttonJava.click()
buttonJava.showOff()
}
취약한 기반 클래스
라는 문제는 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생긴다. 어떤 클래스가 자신을 상속하는 방법에 대해 정확한 규칙(어떤 메서드를 어떻게
오버라이드해야 하는지 등)을 제공하지 않는다면 그 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메서드를 오버라이드할 위험이 있다.
모든 하위 클래스를 분석하는 것은 불가능하므로 기반 클래스를 변경하는 경우 하위 클래스의 동작이 예기치 않게 바뀔 수도 있다는 면에서 기반 클래스는 취약하다.
이 문제를 해결하기 위해 이펙티브 자바에서는 '상속을 위한 설계와 문서를 갖추거나 그럴 수 없다면 상속을 금지하라' 조언을 한다. 이는 특별히 하위 클래스에서 오버라이드하게 의도된 클래스와 메서드가 아니라면 모두 final로 만들라는 뜻이다.
코틀린도 마찬가지 철학을 따라서, 자바의 클래스 메서드는 기본적으로 상속에 열려있지만 코틀린의 클래스와 메서드는 기본적으로 final이다.
어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다. 그와 더불어 오버라이드를 허용하고 싶은 메서드나 프로퍼티 앞에도 open 변경자를 붙여야한다.
package part4
open class RichButton : Clickable { // 이 클래스는 열려있다. 다른 클래스가 이 클래스를 상속할 수 있다.
fun disable() {} // 이 함수는 파이널이다. 하위 클래스가 이 메서드를 오버라이드할 수 없다.
open fun animate() {} // 이 함수는 열려있다. 하위 클래스에서 이 메서드를 오버라이드해도 된다.
override fun click() {} // 이 함수는 (상위 클래스에서 선언된) 열려있는 메서드를 오버라이드 한다. 오버라이드한 메서드는 기본적으로 열려있다.
}
package part4;
import kotlin.Metadata;
import part4.Clickable.DefaultImpls;
@Metadata(
mv = {1, 7, 0},
k = 1,
xi = 2,
d1 = {"\\u0000\\u0014\\n\\u0002\\u0018\\u0002\\n\\u0002\\u0018\\u0002\\n\\u0002\\b\\u0002\\n\\u0002\\u0010\\u0002\\n\\u0002\\b\\u0003\\b\\u0016\\u0018\\u00002\\u00020\\u0001B\\u0005¢\\u0006\\u0002\\u0010\\u0002J\\b\\u0010\\u0003\\u001a\\u00020\\u0004H\\u0016J\\b\\u0010\\u0005\\u001a\\u00020\\u0004H\\u0016J\\u0006\\u0010\\u0006\\u001a\\u00020\\u0004¨\\u0006\\u0007"},
d2 = {"Lpart4/RichButton;", "Lpart4/Clickable;", "()V", "animate", "", "click", "disable", "playground"}
)
public class RichButton implements Clickable {
public final void disable() {
}
public void animate() {
}
public void click() {
}
public void showOff() {
DefaultImpls.showOff(this);
}
}
package part4
open class RichButton : Clickable { // 이 클래스는 열려있다. 다른 클래스가 이 클래스를 상속할 수 있다.
fun disable() {} // 이 함수는 파이널이다. 하위 클래스가 이 메서드를 오버라이드할 수 없다.
open fun animate() {} // 이 함수는 열려있다. 하위 클래스에서 이 메서드를 오버라이드해도 된다.
final override fun click() {} // 이 함수는 (상위 클래스에서 선언된) 열려있는 메서드를 오버라이드 한다. 오버라이드한 메서드는 기본적으로 열려있다.
// 금지하려면 final을 붙여준다
}
package part4
abstract class Animate { // 이 클래스는 추상 클래스다. 이 클래스의 인스턴스를 만들 수 없다.
abstract fun animate() // 이 함수는 추상 함수다. 이 함수에는 구현이 없다. 하위 클래스에서는 이 함수를 반드시 오버라이드 해야만 한다
open fun stopAnimating() { // 추상 클래스에 속했더라도 비 추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드 할 수 있다.
}
fun animateTwice() { // 추상 클래스에 속했더라도 비 추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드 할 수 있다.
}
}
변경자 | 이 변경자가 붙은 멤버는 | 설명 |
---|---|---|
final | 오버라이드할 수 없음 | 클래스의 멤버의 기본 변경자 |
open | 오버라이드할 수 있음 | 반드시 open을 명시해야 오버라이드할 수 있다. |
abstract | 반드시 오버라디으 해야 함 | 추상 클래스의 멤버에만 이 변경자를 붙일 수 있다. 추상 멤버에는 구현이 있으면 안 된다. |
override | 상위 클래스나 상위 인스턴스의 멤버를 오버라이드 하는 중 | 오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다. |
internal
이라는 새로운 가시성 변경자를 도입했다(우리말로는 모듈 내부라고 변역한다.)변경자 | 클래스 멤버 | 최상위 선언 |
---|---|---|
public(기본 가시성임) | 모든 곳에서 볼 수 있다 | 모든 곳에서 볼 수 있다 |
internal | 같은 모듈 안에서만 볼 수 있다 | 같은 모둘 안에서만 볼 수 있다. |
protected | 하위 클래스 안에서만 볼 수 있다 | (최상위 선언에 적용할 수 없음) |
private | 같은 클래스 안에서만 볼 수 있다 | 같은 파일 안에서만 볼 수 있다 |
protected
멤버에 접근할 수 있지만, 코틀린에서는 그렇지 않다는 점에서 자바와 다르다는 사실에 유의하라package part4
internal open class TalkativeButton : Focusable {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}
fun TalkativeButton.giveSpeech() { // 오류: "public" 멤버가 자신의 "internal" 수신 타입인 "TalkactiveButton"을 노출함
yell() // 오류 yell에 접근할 수 없음: yell은 TalkactiveButton의 private 멤버임
whisper() // 오유 whisper에 접근할 수 없음: whisper는 TalkactiveButton의 protected 멤버임
}
public class TalkativeButton implements Focusable {
private final void yell() {
String var1 = "Hey!";
System.out.println(var1);
}
protected final void whisper() {
String var1 = "Let's talk!";
System.out.println(var1);
}
public void setFocus(boolean b) {
DefaultImpls.setFocus(this, b);
}
public void showOff() {
DefaultImpls.showOff(this);
}
}
package part4
class Button2 : View {
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State) {
}
class ButtonState : State {}
}
클래스 B 안에 정의된 클래스 A | 자바에서는 | 코틀린에서는 |
---|---|---|
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) | static class A | class A |
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) | class A | inner class A |
package part4
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
package part4
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.left) + eval(e.right)
else ->
throw IllegalAccessException("Unknown expression")
}
package part4
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 식이 모든 하위 클래스를 검사하므로 별도의 else 분기가 없어도 된다.
when (e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.left) + eval(e.right)
}
class User constructor(_nickname: String) { // 파라미터가 하나만 있는 주 생성자
val nickname: String
init { // 초기화 블록
nickname = _nickname
}
}
public final class User {
@NotNull
private final String nickname;
@NotNull
public final String getNickname() {
return this.nickname;
}
public User(@NotNull String _nickname) {
Intrinsics.checkNotNullParameter(_nickname, "_nickname");
super();
this.nickname = _nickname;
}
}
package part4
class User(_nickname: String) { // 파라미터가 하나만 있는 주 생성자
val nickname = _nickname
}
class User(val nickname: String) { // val은 파라미터에 상응하는 프로퍼티가 생성된다는 뜻이다.
}
package part4
class User(
val nickname: String,
val isSubscribed: Boolean = true // 생성자 파라미터에 대한 디폴트 값을 제공한다.
) {
}
/*
* 클래스에 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출해야할 필요가 있다.
* 기반 클래스를 초기화 하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘긴다.
* */
open class User(
val nickname: String,
) {}
class TwitterUser(nickname: String) : User(nickname) {}
open class ButtonKt // 인자가 없는 디폴트 생성자가 만들어진다
class RadioButton : ButtonKt() // ButtonKt의 생성자는 아무인자도 받지 않지만, Button 클래스를 상속한 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야한다.
class Secretive private constructor() // 이 클래스의 유일한 주생성자는 비공개다.
open class View2 {
constructor(ctx: Context) { // 부생성자
}
constructor(ctx: Context, attr: Attribute) { // 부생성자
}
}
class Mybutton : View2 {
constructor(ctx: Context) : super(ctx) {} // 상위 클래스의 생성자를 호출한다.
constructor(ctx: Context, attr: Attribute) : super(ctx, attr) {}
}
class Mybutton : View2 {
constructor(ctx: Context) : this(ctx, MY_STYLE) {}
constructor(ctx: Context, attr: Attribute) : super(ctx, attr) {}
}
interface User {
val nickname: String //User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야한다는 뜻
}
interface User {
val nickname: String
}
class PrivateUser(override val nickname: String) : User // 주 생성자에 있는 프로퍼티
class SubscribingUser(val email: String) : User {
override val nickname: String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val accountId: Int) : User {
override val nickname = getFacebookName(accountId) // 프로퍼티 초기화식
fun getFacebookName(accountId: Int): String {
return accountId.toString()
}
}
interface User {
val email: String
val nickname: String
get() = email.substringBefore('@') //프로퍼티에 뒷받침하는 필드가 없다. 대신 매번 결과를 계산해 돌려준다
}
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println(
"""
Address was changed for $name:
"$field" -> "$value".""".trimIndent()
) // 뒷받침하는 필드 값 읽기
field = value // 뒷받침하는 필드 값 변경하기
}
}
fun main(args: Array<String>) {
val user = User("Alice")
user.address = "Elsenheimerstrasse 47, 80689 Muenchen"
}
class LengthCounter {
var counter: Int = 0
private set // 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다
fun addWord(word: String) {
counter += word.length
}
}
fun main(args: Array<String>) {
val lengthCounter = LengthCounter()
lengthCounter.addWord("Hi!")
println(lengthCounter.counter)
}
class Client(val name: String, val postalCode: Int) {
override fun toString(): String {
return "Client(name='$name',postalCode = $postalCode)"
}
override fun equals(other: Any?): Boolean { // Any는 java.lang.Object에 대응하는 클래스로. 코틀린의 모든 클래스의 최상위 클래스다. Any?는 널이 될 수 있는 타입이므로 other은 null 일 수 있다.
if (this === other) return true // other가 Client 인지 검사한다.
if (javaClass != other?.javaClass) return false
// if(other == null || other !is Client) // is 검사는 자바의 instanceof와 같다. in 연산자의 결과를 부정해주는 연산자가 !in연산자 이다
// return false
other as Client
if (name != other.name) return false // 두 객체의 프로퍼티 값이 서로 같은지 검사한다.
if (postalCode != other.postalCode) return false
// return name == other.name && postalCode == other.postalCode
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + postalCode
return result
}
}
fun main(args: Array<String>) {
val client1: Client = Client("mala", 2)
println(client1.toString())
/* 코틀린에서 == 연산자는 참조 동일성을 검사하지 않고 객체의 동등성을 검사한다. 따라서 == 연산은 equals 호출하는 식으로 컴파일된다.
* 코틀린에서는 == 연산자가 두 객체를 비교하는 기보적인 방법이다. == 는 내부적으로 equals 호출해서 객체를 비교한다.
* 따라서 클래스가 equals 오버라이드하면 == 를 통해 안전하게 그 클래스의 인스턴스를 비교할 수 있다.
* 참조 비교를 위해서는 === 연산자를 사용할 수 있다. === 연산자는 자바에서 객체의 잠조를 비교할 때 사용하는 === 연산자와 같다.*/
val client2: Client = Client("heaven", 3)
println(client1 == client2)
val client3: Client = Client("mala", 2)
println(client1 == client3)
val processed = hashSetOf(Client("mala", 2))
println(processed.contains(Client("mala", 2)))
}
data class Client2(val name: String, val postalCode: Int)
public final class Client2 {
@NotNull
private final String name;
private final int postalCode;
@NotNull
public final String getName() {
return this.name;
}
public final int getPostalCode() {
return this.postalCode;
}
public Client2(@NotNull String name, int postalCode) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.postalCode = postalCode;
}
@NotNull
public final String component1() {
return this.name;
}
public final int component2() {
return this.postalCode;
}
@NotNull
public final Client2 copy(@NotNull String name, int postalCode) {
Intrinsics.checkNotNullParameter(name, "name");
return new Client2(name, postalCode);
}
// $FF: synthetic method
public static Client2 copy$default(Client2 var0, String var1, int var2, int var3, Object var4) {
if ((var3 & 1) != 0) {
var1 = var0.name;
}
if ((var3 & 2) != 0) {
var2 = var0.postalCode;
}
return var0.copy(var1, var2);
}
@NotNull
public String toString() {
return "Client2(name=" + this.name + ", postalCode=" + this.postalCode + ")";
}
public int hashCode() {
String var10000 = this.name;
return (var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.postalCode);
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Client2) {
Client2 var2 = (Client2) var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.postalCode == var2.postalCode) {
return true;
}
}
return false;
} else {
return true;
}
}
}
package part4
class Client2(val name: String, val postalCode: Int) {
fun copy(
name: String = this.name,
postalCode: Int = this.postalCode
) = Client2(name, postalCode)
override fun toString(): String {
return "Client2(name='$name', postalCode=$postalCode)"
}
}
fun main(args: Array<String>) {
val yun = Client2("mala", 1)
println(yun.copy(postalCode = 2))
}
class DelegatingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>()
override val size: Int
get() = innerList.size
override fun contains(element: T): Boolean {
return innerList.contains(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return innerList.containsAll(elements)
}
override fun isEmpty(): Boolean {
return innerList.isEmpty()
}
override fun iterator(): Iterator<T> {
return innerList.iterator()
}
}
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
class CountingSet<T>(
val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet { // MutableCollection의 구현을 innerSet에게 위임한다.
var objectsAdded = 0
override fun add(element: T): Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
objectsAdded += elements.size
return innerSet.addAll(elements)
}
}
fun main(args: Array<String>) {
val cset = CountingSet<Int>()
cset.addAll(listOf(1, 1, 3))
println("${cset.objectsAdded} objects were added, ${cset.size} remain")
}
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for (person in allEmployees) {
....
}
}
}
fun main(args: Array<String>) {
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
}
import java.io.File
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(o1: File, o2: File): Int {
return o1.path.compareTo(o2.path, ignoreCase = true)
}
}
fun main(args: Array<String>) {
val files = listOf(File("/Z"), File("/a"))
println(files.sortedWith(CaseInsensitiveFileComparator))
}
data class Person(val name: String) {
object NameComparator : Comparator<Person> {
override fun compare(o1: Person, o2: Person): Int =
o1.name.compareTo(o2.name)
}
}
fun main(args: Array<String>) {
val persons = listOf(Person("Bob"), Person("Alice"))
println(persons.sortedWith(Person.NameComparator))
}
public static final class NameComparator implements Comparator {
@NotNull
public static final Person.NameComparator INSTANCE;
public int compare(@NotNull Person o1, @NotNull Person o2) {
Intrinsics.checkNotNullParameter(o1, "o1");
Intrinsics.checkNotNullParameter(o2, "o2");
return o1.getName().compareTo(o2.getName());
}
// $FF: synthetic method
// $FF: bridge method
public int compare(Object var1, Object var2) {
return this.compare((Person) var1, (Person) var2);
}
private NameComparator() {
}
static {
Person.NameComparator var0 = new Person.NameComparator();
INSTANCE = var0;
}
}
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
fun main(args: Array<String>) {
A.bar()
}
package part4
class UserCompanionEx {
val nickname: String
constructor(email: String) {
nickname = email.substringBefore('@')
}
constructor(facebookAccountId: Int) {
nickname = getFacebookName(facebookAccountId)
}
private fun getFacebookName(facebookAccountId: Int): String {
return facebookAccountId.toString()
}
}
class UserCompanionEx private constructor(val nickname: String) { // 주 생성자를 비공개로 만든다.
companion object { // 동반 객체로 만든다
fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId)) // 페이스북 사용자 ID로 사용자를 만드는 팩토리 메서드
private fun getFacebookName(accountId: Int): String {
return accountId.toString()
}
}
}
class PersonCompanion(val name: String) {
companion object Loader { // 동반 객체에 이름을 붙인다.
fun fromJSON(jsonText: String): PersonCompanion = PersonCompanion(jsonText)
}
}
fun main(args: Array<String>) {
val person = PersonCompanion.Loader.fromJSON("{ name: 'Dirty' }")
println(person.name)
val person2 = PersonCompanion.fromJSON("{ name: 'Brent' }")
println(person2.name)
}
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
class PersonJson(val name: String) {
companion object : JSONFactory<PersonJson> {
override fun fromJSON(jsonText: String): PersonJson = PersonJson(jsonText) // 동반 객체가 인터페이스를 구현한다.
}
}
class PersonExtend(val firstName: String, val lastName: String) {
companion object {
fun print() {
println("여기 들어오니???")
}
// 비어있는 동반 객체를 선언한다.
}
}
fun PersonExtend.Companion.fromJSON(json: String): PersonExtend {
// 확장 함수를 선언한다
PersonExtend.print()
return PersonExtend("first", "last")
}
fun main(args: Array<String>) {
val p = PersonExtend.fromJSON("json")
}
abstract class PersonB(val name: String, val age: Int) {
abstract fun info()
}
fun main(args: Array<String>) {
val mala: PersonB = object : PersonB("mala", 0) {
override fun info() {
TODO("Not yet implemented")
}
}
}
[출처] Kotlin IN ACTION