abstract class, 추상 클래스는 대략적인 설계 명세와 공통의 기능을 구현한 클래스로, 상속하여 하위 클래스에서 대략적인 설계 명세를 구체화하고 공통의 기능을 사용하기 위한 목적의 클래스이다. 기본적으로 클래스이기 때문에 일반 프로퍼티, 일반 메서드를 선언할 수 있고, 추가적으로 추상 프로퍼티, 추상 메서드를 abstract 키워드를 통해 선언 가능하다. 그리고 추상 클래스는 자체적으로 객체를 생성할 수 없다.
// 추상 클래스, 주 생성자에는 일반 프로퍼티 선언의 매개변수 3개가 있다.
abstract class Vehicle(val name: String, val color: String, val weight: Double) {
// 추상 프로퍼티 (반드시 하위 클래스에서 override로 재정의 해야 한다)
abstract var maxSpeed: Double
// 일반 프로퍼티 (초기 값인 상태를 저장할 수 있다. 하위 클래스에서 override 불가)
protected var year = "2018"
// 추상 메서드 (반드시 하위 클래스에서 override로 구현 해야 한다)
abstract fun start()
abstract fun stop()
// 일반 메서드 (하위 클래스에서 override 불가)
protected fun displaySpecs() {
println("Name: $name, Color: $color, Weight: $weight, Year: $year, Max Speed: $maxSpeed")
}
}
추상 프로퍼티와 추상 메서드는 반드시 하위 클래스에서 override로 구체화해야 한다. 일반 프로퍼티와 일반 메서드는 하위 클래스에서 override는 불가하다. 하지만 private 접근 제한자로 제한되지 않았다면 접근 가능하다. 상속한 하위 클래스에서만 접근 가능하도록 protected 접근 제한자를 붙이는 것을 권장한다.
인터페이스는 추상 클래스와 마찬가지로 대략적인 설계 명세와 공통의 기능을 구현 가능하지만 추상 클래스와 다르게 프로퍼티에 상태를 저장할 수 없다. 하지만 추상 클래스에서 허용하지 않는 다중 상속(정확히는 다중 구현)이 인터페이스에서는 가능하다. 추상 클래스와 마찬가지로 자체적으로 객체를 생성할 수 없다.
그리고 추상 클래스는 다른 클래스의 하위 클래스가 되는 것이 가능하지만, 인터페이스는 다른 클래스의 하위 클래스로도 불가능하고 다른 인터페이스를 구현하는 것도 불가능하다.
interface Animal {
// 추상 프로퍼티 (반드시 구현 클래스에서 override로 재정의 해야 한다)
val name: String
// 게터를 구현한 프로퍼티
val age: Int
get() = 10
// 추상 메서드 (반드시 구현 클래스에서 override로 구현해야 한다)
fun makeSound()
// 일반 메서드
fun eat() {
println("${name}이 먹이를 먹습니다")
}
}
기본적으로 abstract를 붙이지 않아도 추상 프로퍼티와 추상 메서드로 선언된다. 그래서 반드시 구현 클래스에서 override로 구체화해야 한다.
그리고 게터를 구현한 프로퍼티와 일반 메서드는 구현 클래스에서 override하여 재정의할 수 있지만 강제성은 없다. (게터를 구현한 프로퍼티와 일반 메서드는 사실 거의 사용되지 않는다.) 그리고 interface 내에서 protected, internal 접근 제한자는 적용 불가능하다.
interface Animal {
val age: Int = 10 // Property initializers are not allowed in interfaces 오류 발생
}
인터페이스 내에서 상태를 저장할 수 없다는 것은 프로퍼티의 초기화가 불가능하다는 것을 의미한다. 즉 위의 예시에서 age가 10이라는 상태를 저장할 수 없다는 것이다.
interface Animal {
val age: Int // Property in an interface cannot have a backing field 오류 발생
get() {
field
}
}
또한 상태를 저장할 수 없다는 것은 메모리에서 상태(데이터)를 저장하는 변수인 백킹 필드를 가지지 않는다는 것을 의미한다. 그래서 커스텀 게터, 커스텀 세터에서 field 식별자가 사용 불가능하다. (위의 예시 참고)
class AnimalClass {
val age = 10
}
interface AnimalInterface {
val age: Int
}
보다 깊은 이해를 위해 위의 코드와 같이 클래스와 인터페이스를 선언하고 decompile을 해보았다.
public final class AnimalClass {
private final int age = 10;
public final int getAge() {
return this.age;
}
}
public interface AnimalInterface {
int getAge();
}
클래스에서는 age 필드가 존재하고 age = 10;으로 초기화되어있지만, 인터페이스에서는 필드가 존재하지 않고 getAge() 메서드만 있을 뿐이다.
interface AnimalInterface {
val age: Int
get() = 10
}
그렇다면 위 코드의 Animal interface의 age처럼 커스텀 게터를 구현한 프로퍼티는 상태를 저장하는 게 아닌걸까? 정답은 "아니다"이다. 상태를 저장하는 게 아니라 단순히 접근할 때마다 동일한 값을 반환하고 있을 뿐이다. 이해를 위해 decompile을 해보았다.
public interface AnimalInterface {
int getAge();
public static final class DefaultImpls {
public static int getAge(@NotNull AnimalInterface $this) {
return 10;
}
}
}
decompile된 클래스에서는 아까 보았듯이 age = 10;으로 초기화해주는 코드가 있었지만 커스텀 게터를 구현한 프로퍼티를 decompile하면 초기화해주는 코드가 없다. 대신 static 메서드를 통해, 접근할 때마다 매번 10이라는 동일한 값을 반환해주는 것을 알 수 있다. 이로써 커스텀 게터를 구현한 프로퍼티도 상태를 저장하는 것이 아니라는 것을 알 수 있다.