1. 시작하기 전에
기본 요건
- Kotlin 플레이그라운드를 사용하여 Kotlin 프로그램을 수정하는 방법을 잘 알고 있어야 합니다.
- 이 과정 단원 1에서 살펴본 Kotlin 프로그래밍의 기본 개념을 알고 있어야 합니다. 특히
main()
프로그램과 값, 변수, 데이터 유형 및 작업을 반환하는 인수가 포함된 함수,if/else
문을 알아야 합니다. - Kotlin의 클래스를 정의하고 그 클래스에서 객체 인스턴스를 만들어 속성과 메서드에 액세스할 수 있어야 합니다.
학습할 내용
- 상속을 사용하여 클래스의 계층 구조를 구현하는 Kotlin 프로그램을 만듭니다.
- 클래스를 확장하고 기존 기능을 재정의하며 새 기능을 추가합니다.
- 변수의 올바른 공개 상태 수정자를 선택합니다.
빌드할 항목
- 클래스 계층 구조로 구현되는 다양한 유형의 주택이 포함된 Kotlin 프로그램
필요한 항목
- Kotlin 플레이그라운드에 액세스할 수 있도록 인터넷이 연결된 컴퓨터
2. 클래스 계층 구조란 무엇인가요?
사람들은 자연스럽게 속성과 동작이 비슷한 항목을 그룹으로 분류하고 그룹 내에서도 일정 유형의 계층 구조를 만듭니다. 예를 들어 채소와 같이 광범위한 카테고리를 사용할 수 있고 그 카테고리 안에 콩류라는 좀 더 구체적인 유형의 카테고리를 사용할 수 있습니다. 콩류 내에서도 더 구체적으로 완두콩, 강낭콩, 렌즈콩, 병아리콩, 대두 등이 있을 수 있습니다.
이는 계층 구조로 표현될 수 있는데 콩류는 채소의 속성(예: 식물, 식용 가능함)을 모두 포함하거나 상속받기 때문입니다. 마찬가지로 완두콩, 강낭콩, 렌즈콩도 모두 콩류의 속성과 고유한 자체 속성이 있습니다.
이러한 관계를 프로그래밍 측면에서 어떻게 표현할지 살펴보겠습니다. Vegetable
을 Kotlin의 클래스로 만들면 Legume
을 Vegetable
클래스의 하위 클래스 또는 서브클래스로 만들 수 있습니다. 즉, Vegetable
클래스의 모든 속성과 메서드가 Legume
클래스에 상속(즉, 사용 가능함)됩니다.
아래와 같이 클래스 계층 구조 다이어그램으로 이를 표현할 수 있습니다. Vegetable
을 Legume
클래스의 상위 클래스 또는 슈퍼클래스로 지칭할 수 있습니다.
Legume
의 서브클래스(예: Lentil
, Chickpea
)를 만들어 클래스 계층 구조를 유지하고 확장할 수 있습니다. 그러면 Legume
은 Vegetable
의 하위 클래스 또는 서브클래스가 될 뿐 아니라 Lentil
과 Chickpea
의 상위 클래스 또는 슈퍼클래스가 됩니다. Vegetable
은 이 계층 구조의 루트 또는 최상위(또는 기본) 클래스입니다.
Android 클래스의 상속
이전 Codelab에서 한 것처럼 클래스를 사용하지 않고도 Kotlin 코드를 작성할 수 있지만 Android의 여러 부분이 활동, 뷰, 뷰 그룹 등 클래스 형태로 제공됩니다. 따라서 클래스 계층 구조 이해는 Android 앱 개발에 중요하며 이를 통해 Android 프레임워크에서 제공하는 기능을 활용할 수 있습니다.
예를 들어 Android에는 화면의 직사각형 영역을 나타내고 그리기와 이벤트 처리를 담당하는 View
클래스가 있습니다. TextView
클래스는 View
클래스의 서브클래스입니다. 즉, TextView
는 View
클래스의 모든 속성과 기능을 상속받고 사용자에게 텍스트를 표시하는 특정 로직을 추가합니다.
한 단계 더 나아가 EditText
및 Button
클래스는 TextView
클래스의 하위 클래스입니다. TextView
및 View
클래스의 모든 속성과 메서드를 상속받고 고유한 특정 로직을 추가합니다. 예를 들어 EditText
는 화면에서 텍스트를 수정할 수 있는 자체 기능을 추가합니다.
View
및 TextView
클래스의 모든 로직을 복사하여 EditText
클래스에 붙여넣지 않고도 EditText
를 View
클래스의 서브클래스인 TextView
클래스의 서브클래스로 분류하면 됩니다. 그러면 EditText
클래스의 코드는 이 UI 구성요소를 다른 뷰와 다르게 하는 요소에 특히 중점을 둘 수 있습니다.
developer.android.com 웹사이트의 Android 클래스 문서 페이지 상단에서 클래스 계층 구조 다이어그램을 확인할 수 있습니다. 계층 구조 상단에 kotlin.Any
가 표시된다면 그 이유는 Kotlin의 모든 클래스에 공통 슈퍼클래스 Any가 있기 때문입니다. 자세한 내용은 여기를 참고하세요.
이와 같이 클래스 간 상속을 활용하는 방법을 학습하면 코드를 더 쉽게 작성하고 재사용하며 읽고 테스트할 수 있습니다.
3. 기본 클래스 만들기
주택의 클래스 계층 구조
이 Codelab에서는 바닥 면적, 층, 거주자가 있는 주택(사람이 사는 집)을 예로 사용하여 클래스 계층 구조의 작동 원리를 보여주는 Kotlin 프로그램을 빌드합니다.
다음은 빌드할 클래스 계층 구조의 다이어그램입니다. 루트에는 청사진과 유사하게 모든 주택에 적용되는 속성과 기능을 지정하는 Dwelling
이 있습니다. 그런 다음 정사각형 통나무집(SquareCabin
) 클래스, 둥근 오두막(RoundHut
) 클래스, 여러 층이 있는 RoundHut
인 둥근 타워(RoundTower
) 클래스가 있습니다.
구현할 클래스는 다음과 같습니다.
Dwelling
: 모든 주택에 공통으로 적용되는 정보를 담고 있는 구체적이지 않은 집을 나타내는 기본 클래스입니다.SquareCabin
: 바닥 면적이 정사각형인 나무로 만든 정사각형 통나무집입니다.RoundHut
: 바닥 면적이 원형인 짚으로 만든 둥근 오두막이고RoundTower
의 상위 요소입니다.RoundTower
: 바닥 면적이 원형이고 층이 여러 개인 돌로 만든 둥근 타워입니다.
추상 주택 클래스 만들기
어떤 클래스라도 클래스 계층 구조의 기본 클래스나 다른 클래스의 상위 클래스가 될 수 있습니다.
'추상' 클래스는 완전히 구현되지 않아서 인스턴스화할 수 없는 클래스입니다. 스케치라고 생각하면 됩니다. 스케치를 통해 무언가에 관한 아이디어와 계획을 통합하지만 그 무언가를 빌드하기에는 일반적으로 정보가 충분하지 않습니다. 스케치(추상 클래스)를 사용하여 청사진(클래스)을 만들고 청사진을 통해 실제 객체 인스턴스를 빌드합니다.
일반적으로 슈퍼클래스를 만들어 좋은 점은 모든 서브클래스에 공통적인 속성과 함수를 포함한다는 것입니다. 속성값과 함수 구현을 알 수 없으면 클래스를 추상으로 만듭니다. 예를 들어 Vegetables
에는 모든 채소에 공통으로 적용되는 여러 속성이 있지만 구체적이지 않은 채소의 인스턴스를 만들 수는 없습니다. 모양이나 색상 등을 모르기 때문입니다. 따라서 Vegetable
은 각 채소에 관한 구체적인 세부정보의 결정을 서브클래스에 맡기는 추상 클래스입니다.
추상 클래스 선언은 abstract
키워드로 시작합니다.
Dwelling
은 Vegetable
과 같은 추상 클래스가 됩니다. 여러 유형의 주택에 공통으로 적용되는 속성과 함수를 포함하지만 속성의 정확한 값과 함수 구현의 세부정보는 알 수 없습니다.
- Kotlin 플레이그라운드(https://developer.android.com/training/kotlinplayground)로 이동합니다.
- 편집기에서
main()
함수 내의println("Hello, world!")
를 삭제합니다. - 그런 다음 이 코드를
main()
함수 아래에 추가하여Dwelling
이라는abstract
클래스를 만듭니다.
abstract class Dwelling(){
}
건축 자재 속성 추가
이 Dwelling
클래스에서 주택마다 다를 수 있더라도 모든 주택에 적용되는 항목을 정의합니다. 모든 주택은 건축 자재로 만들어집니다.
Dwelling
내에서 건축 자재를 나타내는String
유형의buildingMaterial
변수를 만듭니다. 건축 자재는 변경되지 않으므로val
을 사용하여 변경 불가능한 변수로 만듭니다.
val buildingMaterial: String
- 프로그램을 실행하면 다음 오류가 발생합니다.
Property must be initialized or be abstract
buildingMaterial
속성에 값이 없습니다. 사실 개발자가 값을 제공할 수 없습니다. 건물이 구체적이지 않아서 구체적인 자재로 만들 수 없기 때문입니다. 따라서 오류 메시지가 나타내듯이 abstract
키워드를 buildingMaterial
선언의 접두사로 지정하여 여기서 정의되지 않음을 나타낼 수 있습니다.
- 변수 정의에
abstract
키워드를 추가합니다.
abstract val buildingMaterial: String
- 코드를 실행하면 어떠한 작업도 실행되지 않지만 이제 오류 없이 컴파일됩니다.
main()
함수에서Dwelling
인스턴스를 만들고 코드를 실행합니다.
val dwelling = Dwelling()
- 추상
Dwelling
클래스의 인스턴스를 만들 수 없으므로 오류가 발생합니다.
Cannot create an instance of an abstract class
- 잘못된 코드를 삭제합니다.
지금까지 완료된 코드는 다음과 같습니다.
abstract class Dwelling(){
abstract val buildingMaterial: String
}
수용 인원 속성 추가
주택의 또 다른 속성은 수용 인원입니다. 즉, 주택에서 살 수 있는 사람 수입니다.
모든 주택에는 변경되지 않는 수용 인원이 있습니다. 그러나 수용 인원은 Dwelling
슈퍼클래스 내에서 설정할 수 없습니다. 특정 유형의 주택에 관한 서브클래스에서 정의해야 합니다.
Dwelling
에서capacity
라는abstract
정수val
을 추가합니다.
abstract val capacity: Int
거주자 수에 관한 비공개 속성 추가
모든 주택에는 주택에 거주하는 여러 residents
(capacity
이하일 수 있음)가 있으므로 모든 서브클래스가 상속받아 사용하도록 Dwelling
슈퍼클래스에서 residents
속성을 정의합니다.
residents
를Dwelling
클래스의 생성자에 전달되는 매개변수로 만들 수 있습니다.residents
속성은var
입니다. 인스턴스가 만들어진 후 거주자 수가 변경될 수 있기 때문입니다.
abstract class Dwelling(private var residents: Int) {
residents
속성은 private
키워드로 표시됩니다. 비공개는 Kotlin의 공개 상태 수정자로, residents
속성이 이 클래스에만 표시되고 이 클래스 내부에서 사용할 수 있다는 의미입니다. 프로그램의 다른 위치에서는 액세스할 수 없습니다. 속성이나 메서드를 비공개 키워드로 표시할 수 있습니다. 공개 상태 수정자가 지정되지 않은 다른 경우에는 속성과 메서드가 기본적으로 public
이고 프로그램의 다른 위치에서 액세스할 수 있습니다. 건축 자재나 건물의 수용 인원에 관한 정보와 비교할 때 주택에 사는 사람 수는 일반적으로 비공개 정보이므로 이러한 판단은 합리적입니다.
주택의 capacity
와 현재 residents
수가 모두 정의된 상태에서 주택의 또 다른 거주자를 위한 공간이 있는지 확인하는 hasRoom()
함수를 만들 수 있습니다. Dwelling
클래스에서 hasRoom()
함수를 정의하고 구현하면 됩니다. 공간이 있는지 계산하는 공식이 모든 주택에 동일하기 때문입니다. residents
수가 capacity
보다 적으면 Dwelling
에 공간이 있고 함수는 이 비교에 기반하여 true
나 false
를 반환해야 합니다.
hasRoom()
함수를Dwelling
클래스에 추가합니다.
fun hasRoom(): Boolean {
return residents < capacity
}
- 이 코드를 실행하면 오류가 발생하지 않습니다. 아직 눈에 띄는 작업이 실행되지 않습니다.
완성된 코드는 다음과 같이 표시됩니다.
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
4. 서브클래스 만들기
SquareCabin 서브클래스 만들기
Dwelling
클래스 아래에서SquareCabin
이라는 클래스를 만듭니다.
class SquareCabin
- 다음으로
SquareCabin
이Dwelling
과 관련이 있음을 나타내야 합니다. 코드에서SquareCabin
이Dwelling
에서 확장된다고(또는Dwelling)
의 서브클래스라고) 나타내려 합니다.SquareCabin
이Dwelling
의 추상 부분에 관한 구현을 제공하기 때문입니다.
SquareCabin
클래스 이름 다음에 콜론(:
)을 추가하고 상위 Dwelling
클래스를 초기화하는 호출을 추가하여 이 상속 관계를 나타냅니다. Dwelling
클래스 이름 뒤에 괄호를 추가해야 합니다.
class SquareCabin : Dwelling()
- 슈퍼클래스에서 확장할 때는 슈퍼클래스에서 예상하는 필수 매개변수를 전달해야 합니다.
Dwelling
에는 입력으로residents
수가 필요합니다.3
과 같이 고정된 거주자 수를 전달할 수 있습니다.
class SquareCabin : Dwelling(3)
그러나 개발자는 프로그램의 유연성을 높여 가변적인 SquareCabins
거주자 수를 허용하기를 바랍니다. 따라서 residents
를 SquareCabin
클래스 정의의 매개변수로 만듭니다. residents
를 val,
로 선언하지 마세요. 이미 상위 클래스 Dwelling
에서 선언된 속성을 재사용하고 있기 때문입니다.
class SquareCabin(residents: Int) : Dwelling(residents)
- 코드를 실행합니다.
- 다음과 같이 오류가 발생합니다.
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling
추상 함수 및 변수를 선언하는 것은 나중에 값과 구현을 제공하겠다는 약속과 다름없습니다. 변수의 경우 추상 클래스의 모든 서브클래스가 값을 제공해야 한다는 의미입니다. 함수의 경우 모든 서브클래스가 함수 본문을 구현해야 한다는 의미입니다.
Dwelling
클래스에서 abstract
변수 buildingMaterial
을 정의했습니다. SquareCabin
은 Dwelling
의 서브클래스이므로 buildingMaterial
값을 제공해야 합니다. override
키워드를 사용하여 이 속성이 상위 클래스에서 정의되었고 이 클래스에서 재정의될 거라고 나타냅니다.
SquareCabin
클래스 내에서buildingMaterial
속성을override
하고"Wood"
값을 할당합니다.capacity
에도 같은 작업을 실행하여 거주민 6명이SquareCabin
에 살 수 있다고 나타냅니다.
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
완성된 코드는 다음과 같이 표시됩니다.
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
코드를 테스트하려면 프로그램에서 SquareCabin
인스턴스를 만듭니다.
SquareCabin 사용
Dwelling
및SquareCabin
클래스 정의 앞에 빈main()
함수를 삽입합니다.
fun main() {
}
abstract class Dwelling(private var residents: Int) {
...
}
class SquareCabin(residents: Int) : Dwelling(residents) {
...
}
main()
함수 내에서 거주민이 6명인squareCabin
이라는SquareCabin
인스턴스를 만듭니다. 건축 자재, 수용 인원,hasRoom()
함수에 관한 print 문을 추가합니다.
fun main() {
val squareCabin = SquareCabin(6)
println("\nSquare Cabin\n============")
println("Capacity: ${squareCabin.capacity}")
println("Material: ${squareCabin.buildingMaterial}")
println("Has room? ${squareCabin.hasRoom()}")
}
hasRoom()
함수는 SquareCabin
클래스에서 정의되지 않았고 Dwelling
클래스에서 정의되었습니다. SquareCabin
이 Dwelling
클래스의 서브클래스이므로 hasRoom()
함수는 무료로 상속되었습니다. 이제 hasRoom()
함수를 코드 스니펫에서 squareCabin.hasRoom()
으로 표시된 대로 모든 SquareCabin
인스턴스에서 호출할 수 있습니다.
- 코드를 실행하면 다음과 같이 출력됩니다.
Square Cabin ============ Capacity: 6 Material: Wood Has room? false
거주민이 6
으로, capacity
와 같은 squareCabin
을 만들었으므로 hasRoom()
은 false
를 반환합니다. 더 적은 수의 residents
로 SquareCabin
초기화를 시도할 수 있습니다. 그런 다음 프로그램을 다시 실행하면 hasRoom()
이 true
를 반환합니다.
with를 사용하여 코드 단순화
println()
문에서 squareCabin
의 속성이나 함수를 참조할 때마다 squareCabin.
을 얼마나 반복해야 하는지 확인합니다. 이 작업은 반복적이며 print 문을 복사하여 붙여넣을 때 오류의 원인이 될 수 있습니다.
클래스의 특정 인스턴스로 작업하고 이 인스턴스의 여러 속성과 함수에 액세스해야 한다면 with
문을 사용하여 '이 인스턴스 객체로 다음 작업을 모두 실행'하라고 나타낼 수 있습니다. with
키워드로 시작하고 괄호로 묶인 인스턴스 이름, 실행하려는 작업이 포함된 중괄호가 차례로 이어집니다.
with (instanceName) {
// all operations to do with instanceName
}
main()
함수에서with
를 사용하도록 print 문을 변경합니다.- print 문에서
squareCabin.
을 삭제합니다.
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
- 다시 코드를 실행하여 오류 없이 실행되고 출력이 동일하게 표시되는지 확인합니다.
Square Cabin ============ Capacity: 6 Material: Wood Has room? false
완성된 코드는 다음과 같습니다.
fun main() {
val squareCabin = SquareCabin(6)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
RoundHut 서브클래스 만들기
SquareCabin
과 같은 방식으로 또 다른 서브클래스인RoundHut
을Dwelling
에 추가합니다.buildingMaterial
을 재정의하고"Straw"
값을 제공합니다.capacity
를 재정의하고 4로 설정합니다.
class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
main()
에서 거주민이 3인RoundHut
인스턴스를 만듭니다.
val roundHut = RoundHut(3)
- 아래 코드를 추가하여
roundHut
에 관한 정보를 출력합니다.
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
- 코드를 실행하면 전체 프로그램이 다음과 같이 출력됩니다.
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Round Hut ========= Material: Straw Capacity: 4 Has room? true
이제 다음과 같은 클래스 계층 구조가 있습니다. Dwelling
이 루트 클래스, SquareCabin
과 RoundHut
이 Dwelling
의 서브클래스입니다.
RoundTower 서브클래스 만들기
이 클래스 계층 구조의 마지막 클래스는 둥근 타워입니다. 둥근 타워를 층이 여러 개인 돌로 만든 둥근 오두막으로 생각하면 됩니다. 따라서 RoundTower
를 RoundHut
의 서브클래스로 만들 수 있습니다.
RoundHut
의 서브클래스인RoundTower
클래스를 만듭니다.residents
매개변수를RoundTower
생성자에 추가하고 이 매개변수를RoundHut
슈퍼클래스 생성자에 전달합니다.buildingMaterial
을"Stone"
으로 재정의합니다.capacity
를4
로 설정합니다.
class RoundTower(residents: Int) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4
}
- 이 코드를 실행하면 오류가 발생합니다.
This type is final, so it cannot be inherited from
이 오류는 RoundHut
클래스를 서브클래스로 분류하거나 상속할 수 없음을 의미합니다. 기본적으로 Kotlin에서 클래스는 최종 클래스이며 서브클래스로 분류할 수 없습니다. abstract
클래스나 open
키워드로 표시된 클래스에서만 상속할 수 있습니다. 따라서 상속될 수 있도록 RoundHut
클래스를 open
키워드로 표시해야 합니다.
RoundHut
선언 시작 부분에open
키워드를 추가합니다.
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
main()
에서roundTower
인스턴스를 만들고 관련 정보를 출력합니다.
val roundTower = RoundTower(4)
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
전체 코드는 다음과 같습니다.
fun main() {
val squareCabin = SquareCabin(6)
val roundHut = RoundHut(3)
val roundTower = RoundTower(4)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
class RoundTower(residents: Int) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4
}
- 코드를 실행합니다. 이제 오류 없이 작동하고 다음과 같이 출력됩니다.
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Round Hut ========= Material: Straw Capacity: 4 Has room? true Round Tower ========== Material: Stone Capacity: 4 Has room? false
RoundTower에 여러 층 추가
RoundHut
은 암묵적으로 단층 건물입니다. 타워에는 보통 여러 층이 있습니다.
수용 인원을 고려하면 타워에 층이 많을수록 수용 인원도 많아집니다.
여러 층이 있도록 RoundTower
를 수정하고 층수에 따라 수용 인원을 조정합니다.
- 층수에 추가 정수 매개변수
val floors
를 사용하도록RoundTower
생성자를 업데이트합니다.residents
뒤에 삽입합니다. 이 매개변수를 상위RoundHut
생성자에 전달하지 않아도 됩니다.floors
가 여기RoundTower
에서 정의되고RoundHut
에는floors
가 없기 때문입니다.
class RoundTower(
residents: Int,
val floors: Int) : RoundHut(residents) {
...
}
- 코드를 실행합니다.
main()
메서드에서roundTower
를 만들 때 오류가 발생합니다.floors
인수에 숫자를 제공하지 않기 때문입니다. 누락된 인수를 추가할 수 있습니다.
또는 RoundTower
의 클래스 정의에서 아래와 같이 floors
의 기본값을 추가할 수 있습니다. 그런 다음 생성자에 전달되는 floors
값이 없으면 기본값을 사용하여 객체 인스턴스를 만들 수 있습니다.
- 코드에서
floors
선언 다음에= 2
를 추가하여 기본값 2를 할당합니다.
class RoundTower(
residents: Int,
val floors: Int = 2) : RoundHut(residents) {
...
}
- 코드를 실행합니다. 이제
RoundTower(4)
가 기본값이 2층인RoundTower
객체 인스턴스를 만들므로 컴파일됩니다. RoundTower
클래스에서 층수를 곱하도록capacity
를 업데이트합니다.
override val capacity = 4 * floors
- 코드를 실행하면 이제 2층
RoundTower
의 수용 인원이 8입니다.
완성된 코드는 다음과 같습니다.
fun main() {
val squareCabin = SquareCabin(6)
val roundHut = RoundHut(3)
val roundTower = RoundTower(4)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
class RoundTower(
residents: Int,
val floors: Int = 2) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4 * floors
}
5. 계층 구조의 클래스 수정
바닥 면적 계산
이 연습에서는 추상 클래스에서 추상 함수를 선언한 다음 서브클래스에서 그 기능을 구현하는 방법을 알아봅니다.
모든 주택에는 바닥 면적이 있지만 주택의 형태에 따라 다르게 계산됩니다.
Dwelling 클래스에서 floorArea() 정의
- 먼저
abstract
floorArea()
함수를Dwelling
클래스에 추가합니다.Double
을 반환합니다. Double은String
,Int
와 같은 데이터 유형입니다. 소수점 뒤에 소수 부분이 오는 숫자(예: 5.8793)인 부동 소수점 숫자에 사용됩니다.
abstract fun floorArea(): Double
추상 클래스에서 정의된 모든 추상 메서드는 추상 클래스의 서브클래스에서 구현되어야 합니다. 코드를 실행하려면 먼저 서브클래스에서 floorArea()
를 구현해야 합니다.
SquareCabin의 floorArea() 구현
buildingMaterial
, capacity
와 마찬가지로 상위 클래스에서 정의된 abstract
함수를 구현하므로 override
키워드를 사용해야 합니다.
SquareCabin
클래스에서 아래와 같이 키워드override
로 시작하여floorArea()
함수의 실제 구현 순으로 작성합니다.
override fun floorArea(): Double {
}
- 계산된 바닥 면적을 반환합니다. 직사각형이나 정사각형 면적은 가로 길이에 세로 길이를 곱한 값입니다. 함수의 본문은
return length * length
입니다.
override fun floorArea(): Double {
return length * length
}
길이는 클래스의 변수가 아니며 인스턴스마다 다르므로 SquareCabin
클래스의 생성자 매개변수로 추가할 수 있습니다.
SquareCabin
의 클래스 정의를 변경하여Double
유형의length
매개변수를 추가합니다. 속성을val
로 선언합니다. 건물의 길이는 변경되지 않기 때문입니다.
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
따라서 Dwelling
과 모든 서브클래스에는 생성자 인수로 residents
가 있습니다. Dwelling
생성자의 첫 번째 인수이므로 모든 서브클래스 생성자의 첫 번째 인수로 만들고 모든 클래스 정의에서 동일한 순서로 인수를 배치하는 것이 좋습니다. 따라서 새 length
매개변수를 residents
매개변수 뒤에 삽입합니다.
main()
에서squareCabin
인스턴스 만들기를 업데이트합니다.50.0
을SquareCabin
생성자에length
로 전달합니다.
val squareCabin = SquareCabin(6, 50.0)
squareCabin
의with
문 내에서 바닥 면적의 print 문을 추가합니다.
println("Floor area: ${floorArea()}")
코드가 실행되지 않습니다. RoundHut
의 floorArea()
도 구현해야 하기 때문입니다.
RoundHut의 floorArea() 구현
같은 방법으로 RoundHut
의 바닥 면적을 구현합니다. RoundHut
도 Dwelling
의 직접적인 서브클래스이므로 override
키워드를 사용해야 합니다.
원형 주택의 바닥 면적은 PI * 반지름 * 반지름입니다.
PI
는 수학 값으로, 수학 라이브러리에 정의되어 있습니다. 라이브러리는 프로그램이 사용할 수 있는 프로그램 외부에 정의된 함수 및 값의 사전 정의된 모음입니다. 라이브러리 함수 또는 값을 사용하려면 컴파일러에 함수나 값을 사용할 거라고 알려야 합니다. 이렇게 하려면 프로그램으로 함수나 값을 가져오면 됩니다. 프로그램에서 PI
를 사용하려면 kotlin.math.PI
를 가져와야 합니다.
- Kotlin 수학 라이브러리에서
PI
를 가져옵니다. 파일 상단의main()
앞에 배치합니다.
import kotlin.math.PI
RoundHut
의floorArea()
함수를 구현합니다.
override fun floorArea(): Double {
return PI * radius * radius
}
경고: kotlin.math.PI를 가져오지 않으면 오류가 발생하므로 이 라이브러리를 가져온 후 사용하세요. 또는 kotlin.math.PI * 반지름 * 반지름과 같이 PI의 정규화된 버전을 작성하면 import 문이 필요하지 않습니다.
RoundHut
생성자를 업데이트하여radius
를 전달합니다.
open class RoundHut(
residents: Int,
val radius: Double) : Dwelling(residents) {
main()
에서radius
10.0
을RoundHut
생성자에 전달하여roundHut
의 초기화를 업데이트합니다.
val roundHut = RoundHut(3, 10.0)
roundHut
의with
문 내에 print 문을 추가합니다.
println("Floor area: ${floorArea()}")
RoundTower의 floorArea() 구현
코드가 아직 실행되지 않고 다음 오류와 함께 실패합니다.
Error: No value passed for parameter 'radius'
RoundTower
에서 프로그램이 컴파일되려면 RoundHut
에서 상속되므로 floorArea()
를 구현하지 않아도 되지만 상위 RoundHut
과 radius
인수도 같도록 RoundTower
클래스 정의를 업데이트해야 합니다.
radius
도 사용하도록 RoundTower 생성자를 변경합니다.radius
를residents
뒤,floors
앞에 배치합니다. 기본값이 있는 변수는 끝에 나열하는 것이 좋습니다.radius
를 상위 클래스 생성자에 전달해야 합니다.
class RoundTower(
residents: Int,
radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
main()
에서roundTower
의 초기화를 업데이트합니다.
val roundTower = RoundTower(4, 15.5)
floorArea()
를 호출하는 print 문을 추가합니다.
println("Floor area: ${floorArea()}")
- 이제 코드를 실행할 수 있습니다.
RoundTower
의 계산이 정확하지 않습니다.RoundHut
에서 상속되고floors
수를 고려하지 않기 때문입니다.RoundTower
에서override floorArea()
하여 면적에 층수를 곱하는 다른 구현을 제공할 수 있습니다. 추상 클래스(Dwelling
)에서 함수를 정의하여 서브클래스(RoundHut
)에서 구현한 다음 서브클래스의 서브클래스(RoundTower
)에서 다시 재정의할 수 있습니다. 원하는 기능은 상속하고 원하지 않는 기능은 재정의할 수 있어서 매우 유용합니다.
override fun floorArea(): Double {
return PI * radius * radius * floors
}
이 코드는 작동하지만 이미 RoundHut
상위 클래스에 있는 반복되는 코드를 방지하는 방법이 있습니다. 상위 RoundHut
클래스에서 PI * radius * radius
를 반환하는 floorArea()
함수를 호출할 수 있습니다. 그런 다음 결과에 floors
의 수를 곱합니다.
RoundTower
에서floorArea()
의 슈퍼클래스 구현을 사용하도록floorArea()
를 업데이트합니다.super
키워드를 사용하여 상위 클래스에 정의된 함수를 호출합니다.
override fun floorArea(): Double {
return super.floorArea() * floors
}
- 다시 코드를 실행하면
RoundTower
에서 여러 층의 바닥 면적을 올바르게 출력합니다.
완성된 코드는 다음과 같습니다.
import kotlin.math.PI
fun main() {
val squareCabin = SquareCabin(6, 50.0)
val roundHut = RoundHut(3, 10.0)
val roundTower = RoundTower(4, 15.5)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
abstract fun floorArea(): Double
}
class SquareCabin(residents: Int,
val length: Double) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
override fun floorArea(): Double {
return length * length
}
}
open class RoundHut(residents: Int,
val radius: Double) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
override fun floorArea(): Double {
return PI * radius * radius
}
}
class RoundTower(residents: Int, radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
override val buildingMaterial = "Stone"
override val capacity = 4 * floors
override fun floorArea(): Double {
return super.floorArea() * floors
}
}
출력은 다음과 같이 표시됩니다.
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Floor area: 2500.0 Round Hut ========= Material: Straw Capacity: 4 Has room? true Floor area: 314.1592653589793 Round Tower ========== Material: Stone Capacity: 8 Has room? true Floor area: 1509.5352700498956
새 거주자가 방을 갖도록 허용
거주자 수를 1씩 늘리는 getRoom()
함수를 사용하여 새 거주자가 방을 갖도록 하는 기능을 추가합니다. 이 로직은 모든 주택에 동일하므로 Dwelling
에서 함수를 구현할 수 있습니다. 이를 통해 모든 서브클래스와 그 하위 요소에서 함수를 사용할 수 있습니다. 간단합니다.
참고
- 수용 인원이 남은 경우에만 거주자를 추가하는
if
문을 사용합니다. - 결과 메시지를 출력합니다.
residents++
를residents = residents + 1
의 약어로 사용하여residents
변수에 1을 추가할 수 있습니다.
Dwelling
클래스에서getRoom()
함수를 구현합니다.
fun getRoom() {
if (capacity > residents) {
residents++
println("You got a room!")
} else {
println("Sorry, at capacity and no rooms left.")
}
}
roundHut
의with
문 블록에 print 문을 추가하여getRoom()
과hasRoom()
이 함께 사용되면 어떤 일이 발생하는지 관찰합니다.
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()
이러한 print 문은 다음과 같이 출력됩니다.
Has room? true You got a room! Has room? false Sorry, at capacity and no rooms left.
자세한 내용은 솔루션 코드를 참고하세요.
둥근 주택에 카펫 맞추기
RoundHut
또는 RoundTower
에 사용할 카펫의 한 면의 길이를 알아야 한다고 가정해 보겠습니다. 함수를 RoundHut
에 넣어 모든 둥근 주택에서 사용할 수 있도록 합니다.
- 먼저
kotlin.math
라이브러리에서sqrt()
함수를 가져옵니다.
import kotlin.math.sqrt
RoundHut
클래스에서calculateMaxCarpetLength()
함수를 구현합니다. 원에 들어갈 수 있는 정사각형 카펫의 길이를 계산하는 수식은sqrt(2) * radius
입니다. 이는 위 다이어그램에 설명되어 있습니다.
fun calculateMaxCarpetLength(): Double {
return sqrt(2.0) * radius
}
수학 함수 sqrt(2.0)
에 Double
값 2.0
을 전달합니다. 함수의 반환 유형이 Integer
가 아닌 Double
이기 때문입니다.
- 이제
RoundHut
및RoundTower
인스턴스에서calculateMaxCarpetLength()
메서드를 호출할 수 있습니다. print 문을main()
함수의roundHut
과roundTower
에 추가합니다.
println("Carpet Length: ${calculateMaxCarpetLength()}")
자세한 내용은 솔루션 코드를 참고하세요.
축하합니다. 속성과 함수가 포함된 완전한 클래스 계층 구조를 만들어 더 유용한 클래스를 만드는 데 필요한 모든 것을 알아봤습니다.
6. 솔루션 코드
다음은 주석이 포함된 이 Codelab의 전체 솔루션 코드입니다.
/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/
import kotlin.math.PI
import kotlin.math.sqrt
fun main() {
val squareCabin = SquareCabin(6, 50.0)
val roundHut = RoundHut(3, 10.0)
val roundTower = RoundTower(4, 15.5)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Floor area: ${floorArea()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Floor area: ${floorArea()}")
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()
println("Carpet size: ${calculateMaxCarpetLength()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Floor area: ${floorArea()}")
println("Carpet Length: ${calculateMaxCarpetLength()}")
}
}
/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
/**
* Calculates the floor area of the dwelling.
* Implemented by subclasses where shape is determined.
*
* @return floor area
*/
abstract fun floorArea(): Double
/**
* Checks whether there is room for another resident.
*
* @return true if room available, false otherwise
*/
fun hasRoom(): Boolean {
return residents < capacity
}
/**
* Compares the capacity to the number of residents and
* if capacity is larger than number of residents,
* add resident by increasing the number of residents.
* Print the result.
*/
fun getRoom() {
if (capacity > residents) {
residents++
println("You got a room!")
} else {
println("Sorry, at capacity and no rooms left.")
}
}
}
/**
* A square cabin dwelling.
*
* @param residents Current number of residents
* @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
/**
* Calculates floor area for a square dwelling.
*
* @return floor area
*/
override fun floorArea(): Double {
return length * length
}
}
/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
residents: Int, val radius: Double) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
/**
* Calculates floor area for a round dwelling.
*
* @return floor area
*/
override fun floorArea(): Double {
return PI * radius * radius
}
/**
* Calculates the max length for a square carpet
* that fits the circular floor.
*
* @return length of square carpet
*/
fun calculateMaxCarpetLength(): Double {
return sqrt(2.0) * radius
}
}
/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
residents: Int,
radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
override val buildingMaterial = "Stone"
// Capacity depends on the number of floors.
override val capacity = floors * 4
/**
* Calculates the total floor area for a tower dwelling
* with multiple stories.
*
* @return floor area
*/
override fun floorArea(): Double {
return super.floorArea() * floors
}
}
7. 요약
이 Codelab을 통해 학습한 내용은 다음과 같습니다.
- 하위 클래스가 상위 클래스에서 기능을 상속받는 클래스 트리인 클래스 계층 구조를 만드는 방법. 속성과 함수가 서브클래스에 상속됩니다.
- 일부 기능을 서브클래스에서 구현하도록 남기는
abstract
클래스를 만드는 방법. 따라서abstract
클래스는 인스턴스화할 수 없습니다. abstract
클래스의 서브클래스를 만드는 방법override
키워드를 사용하여 서브클래스의 속성과 함수를 재정의하는 방법super
키워드를 사용하여 상위 클래스의 함수와 속성을 참조하는 방법- 서브클래스로 분류할 수 있도록 클래스를
open
으로 만드는 방법 - 속성을
private
으로 만들어 클래스 내에서만 사용할 수 있도록 하는 방법 with
구문을 사용하여 동일한 객체 인스턴스에서 여러 호출을 실행하는 방법kotlin.math
라이브러리에서 기능을 가져오는 방법
8. 자세히 알아보기
- 상속
- 클래스 및 상속
- 생성자
- 상속(추상, 개방, 재정의, 비공개)
with
(공식 정의)