오늘 배운 내용이 워낙 많기도 하고, 이미 글로 정리를 다 해놔서 다시 적는 것은 큰 의미가 없을 것 같지만 이해하기 쉽지 않았던 내용을 몇 개만 기록으로 남겨보면 다음과 같다.
간단하게 생각하면, 부모 클래스가 가진 특징을 자식 클래스 역시 가질 수 있게 한다는 것이다.
코틀린은 생략된 final 키워드로 기본적으로 상속을 막아두었지만, open 키워드로 상속 관계를 만들 수 있게 되어 있다.
상속을 이용하면 부모 클래스만 변경해도 자식 클래스에도 변경된 내용이 반영된다. 즉, 일일이 자식 클래스를 잡고 작업할 필요가 없으니까 효과적으로 공수를 줄일 수 있다.
fun main() {
var bird = Bird("새")
var chicken = Chicken("닭")
var sparrow = Sparrow("참새")
var pigeon = Pigeon("비둘기")
bird.fly()
chicken.fly()
sparrow.fly()
pigeon.fly()
}
//bird 같은 경우에는 그냥 name에 '새' 가 들어가고 저장이 되는데
open class Bird(name:String) {
var name: String = ""
init { // 주 생성자를 이용
// this는 현재 클래스의 상태변수를 의미합니다
// var name: String = ""
this.name = name //이 property의 name
}
fun fly() {
println("${name} 날아요~")
}
}
class Chicken(name: String) : Bird(name) {
//chicken은 '닭'을 주는데
// 그러면 Chicken(name: String) 에 '닭' 이 오는데
// 넘어온 '닭' 을 Bird Class에 넘긴다.
// Bird Class로 넘어온 '닭'이 this.name 에 들어간다.
}
class Sparrow(name: String) : Bird(name) {
}
class Pigeon(name: String) : Bird(name) {
}
부모 클래스의 정보(프로퍼티)나 행위(메소드)를 재설계하는 행위인데, 주로 부모 클래스의 메소드를 재설계한다.
공통적인 내용을 부모 클래스에서 관리하면 편리하지만, 자식 클래스에게 개성을 주고 싶을 때가 있다.
예를 들어, 부모 클래스가 '새'인데, '난다'라는 특징을 가지고 있다. 닭도 새고 독수리도 새인데, 닭이 나는 것과 독수리가 나는 것은 분명히 다르다. 닭은 잘 날지 못하고 독수리는 잘 난다는 것을 표현해 주고 싶을 때 오버라이딩을 써주면 된다.
그러면 기능을 독립적으로 따로 만들면 안되냐고 할 수 있겠지만, 객체 지향 프로그래밍 관점에서는 클래스 간의 관계가 만들어지는 것을 통해 일관성을 유지한다. 그리고 서로 간의 관계가 명확해야 재사용하기도 쉽기 때문에 오버라이딩을 사용한다.
fun main() {
var bird = Bird("새")
var chicken = Chicken("닭", 2)
var sparrow = Sparrow("참새", "갈색")
var pigeon = Pigeon("비둘기", "서울")
bird.fly() //"새는 날아요~"
chicken.fly() // "${age}살의 ${name}가 날아봅니다~ 꼬끼오!" -> 왜 이렇게 출력되냐? override 했기 때문임
sparrow.fly()
pigeon.fly()
}
open class Bird(name:String) { // 이 클래스는 상속이 가능하게 해줄게요
var name: String = ""
init {
// this는 현재 클래스의 상태변수를 의미합니다
// var name: String = ""
this.name = name
}
open fun fly() { // 이 메소드는 오버라이딩 할 수 있게 해줄게요
println("${name}은 날아요~")
}
}
class Chicken(name: String, age: Int) : Bird(name) {
var age:Int = 0
init {
this.age = age
}
override fun fly() {
// ctrl + O를 눌러 생성할 수 있는 super.fly
// super객체는 부모의 객체를 의미하며 자동으로 생성됨
// 즉 부모객체의 fly메소드를 부르는 행위임
// 필요없으니 주석처리완료
// super.fly()
println("${age}살의 ${name}가 날아봅니다~ 꼬끼오!")
}
}
class Sparrow(name: String, color: String) : Bird(name) {
var color:String = ""
init {
this.color = color
}
override fun fly() {
// super객체는 부모의 객체를 의미하며 자동으로 생성됨
// 즉 부모객체의 fly메소드를 부르는 행위임
// 필요없으니 주석처리완료
// super.fly()
println("${color}의 ${name}이 날아봅니다~ 짹짹!")
}
}
class Pigeon(name: String, address: String) : Bird(name) {
var address: String = ""
init {
this.address = address
}
override fun fly() {
// super객체는 부모의 객체를 의미하며 자동으로 생성됨
// 즉 부모객체의 fly메소드를 부르는 행위임
// 필요없으니 주석처리완료
// super.fly()
println("${address} 살고있는 ${name}가 날아봅니다~ 구구!")
}
}
인터페이스는 공통적으로 필요한 기능을 외부해서 추가해줄 수 있기 때문에 사용한다. 그런데 공통적으로 필요한 기능이면 상속을 쓰면 되지 않냐? 라고 할수 있는데, 굳이 인터페이스를 쓰는 이유가 있다.
코틀린에서 부모로 삼을 수 있는 클래스는 하나뿐이다. 그러다보면 상속으로 모든 것을 처리하기 어렵다. 그래서 근본적인 공통점을 상속받는 것이고, 추가적인 기능들을 인터페이스를 이용하는 것인데 예를 들어보자.
닭, 참새, 비둘기라는 새의 분류에 오리를 추가하고 싶다. 그런데 오리는 '헤엄치는' 새인데 대체로 새들은 '헤엄치기'를 하지 않는다. 오리는 새는 새지만, 조금 특수한 종류의 새인 것이다.
이런 특징들을 추가시켜주기 위해 인터페이스를 이용하는 것이다.
interface 인터페이스이름 {
fun 메소드이름() //추상 메소드
}
인터페이스는 기본적인 로직이 존재하지 않고, 이름만 이용하는 추상 메소드만 이용한다.
최근에는 추상 메소드가 아닌 메소드도 허용하지만 일반적인 원칙은 추상 메소드만 허용하는 것이기 때문에, 추상 메소드를 작성하는 습관을 들이는 것이 좋다.
사실 인터페이스를 이해하기 위해서는 오버라이딩에 대한 이해가 필수적인데, 오버라이딩을 이해해야 인터페이스에 추상 메소드를 쓰는 이유를 파악할 수 있기 때문이다.
먼저 오버라이딩을 이해하기 위해 노력해보아야 할 듯하다.