1. 클래스와 생성자
클래스 선언
코틀린에서 클래스는 class 키워드로 선언합니다. 아래 코드에서 class User 부분이 클래스의 선언부이며 중괄호 { } 영역이 본문입니다. 만약 클래스의 본문에 입력하는 내용이 없다면 { }를 생략할 수 있습니다.
class User { }
클래스의 멤버는 생성자, 변수, 함수, 클래스로 구성됩니다. 이 중에서 코틀린의 생성자는 constructor 라는 키워드로 선언하는 함수입니다. 그리고 클래스 안에 다른 클래스를 선언할 수도 있습니다.
class User {
var name = "kkang"
constructor(name: String) {
this.name = name
}
fun someFun() {
println("name : $name")
}
class SomeClass { }
}
클래스는 객체를 생성해 사용하며 객체로 클래스의 멤버에 접근합니다. 그런데 코틀린에서는 객체를 생성할 때 new 키워드를 사용하지 않습니다.
val user = User("kim")
user.someFun()
주 생성자
코틀린 클래스는 생성자를 주 생성자와 보조 생성자로 구분합니다. 한 클래스 안에 주 생성자만 선언할 수도 있고 보조 생성자만 선언할 수도 있습니다. 물론 둘 다 선언할 수도 있습니다.
주 생성자는 constructor 키워드로 클래스 선언부에 선언합니다. 주 생성자 선언은 필수는 아니며 한 클래스에 하나만 가능합니다.
class User constructor() {
}
주 생성자를 선언할 때 constructor 키워드는 생략할 수 있습니다.
class User() {
}
만약 개발자가 클래스의 주 생성자를 선언하지 않으면 컴파일러가 매개변수가 없는 주 생성자를 자동으로 추가합니다.
class User {
}
주 생성자를 선언할 떄 필요에 따라 매개변수를 선언할 수도 있습니다.
class User(name: String, count: Int) {
}
위의 User 클래스를 선언하면서 주 생성자에 매개변수를 2개 선언했습니다. 그러면 객체를 생성할 때 매개변수의 타입과 개수에 맞는 인자를 전달해야 합니다.
val user = User("kkang", 10)
주 생성자를 이용해 객체를 생성할 때 특정한 로직을 수행할 수 있습니다. 주 생성자의 본문은 init 키워드를 이용해 작성합니다. 코틀린의 틀래스 안에서 init 키워드로 지정한 영역은 객체를 생성할 때 자동으로 실행됩니다. 클래스에서 init 영역은 꼭 선언할 필요는 없으므로 주 생성자의 본문을 구현하고 싶을 때만 사용합니다.
init 영역은 주 생성자뿐만 아니라 보조 생성자로 객체를 생성할 때도 실행됩니다. 하지만 보조 생성자는 클래스 안에 선언하므로 { }을 이용해 본문을 지정할 수 있습니다. 따라서 init 영역은 일반적으로 주 생성자의 본문을 구현하는 용도로 사용합니다.
class User(name: String, count: Int) {
init {
println("i am init....")
}
}
fun main() {
val user = User("kkang", 10)
}
참고로 생성자의 매개변수는 기본적으로 생성자에서만 사용할 수 있는 지역변수 입니다.
class User(name: String, count: Int) {
init {
println("name: $name, count: $count") // 성공!
}
fun someFun() {
println("name: $name, count: $count") // 오류!
}
}
만약 생성자의 매개변수를 클래스의 멤버 변수처럼 사용해야한다면 방법이 있습니다. 주 생성자의 매개변수는 생성자 안에서만 사용할 수 있는 지역 변수지만 매개변수를 var나 val 키워드로 선언하면 클래스의 멤버 변수가 됩니다.
class User(val name: String, val count: Int) {
fun someFun() {
println("name: $name, count: $count") // 성공
}
}
fun main() {
val user = User("kkang", 10)
user.someFun()
}
원래 함수는 매개변수를 선언할 때 var나 val 키워드를 추가할 수 없습니다. 그런데 주 생성자에서만 유일하게 var나 val 키워드로 매개변수로 선언할 수 있으며 이렇게 하면 클래스의 멤버 변수가 됩니다. 따라서 위 코드에서 생성자의 매개변수 name과 count를 someFun()이라는 멤버 함수에서 사용할 수 있습니다.
보조 생성자
보조 생성자는 클래스의 본문에 constructor 키워드로 선언하는 함수입니다. 클래스 본문에 선언하므로 여러 개를 추가할 수 있습니다.
class User {
constructor(name: String) {
println("constructor(name: String) call...")
}
constructor(name: String, count: Int) {
println("constructor(name: String, count: Int) call....")
}
}
fun main() {
val user1 = User("kkang")
val user2 = User("kkang", 10)
}
이처럼 코틀린의 생성자는 주 생성자와 보조 생성자로 나뉩니다. 클래스를 선언할 때 둘 중 하나만 선언하면 문제가 없지만, 만약 주 생성자와 보조 생성자를 모두 선언한다면 반드시 생성자끼리 연결해 주어야 합니다.
class User(name: String) {
constructor(name: String, count: Int) { // 오류!
....
}
}
위 코드는 오류가 납니다. 주 생성자가 없다면 보조 생성자를 선언하는 데 문제가 없지만 주 생성자가 있으므로 보조 생성자에서 주 생성자를 호출해 주어야 합니다. 보조 생성자는 객체를 생성할 때 호출되며, 이 때 클래스 내에 주 생성자가 있다면 this() 구문을 이용해 주 생성자를 호출해야 합니다.
class User(name: String) {
constructor(name: String, count: Int): this(name) { // 성공!
...
}
}
fun main() {
val user = User("kkang", 10)
}
보조 생성자 선언분에 this(name)만 추가한 코드입니다. 이렇게 하면 보조 생성자로 객체를 생성할 때 주 생성자가 함께 호출 됩니다.
만약 주 생성자가 있는 상태에서 보조 생성자를 여러 개 선언한다면 보조 생성자에서 this()로 다른 보조 생성자를 호출할 수도 있습니다. 그런데 이때에도 보조 생성자로 객체를 생성한다면 어떤 식으로든 주 생성자가 호출되게 해야합니다.
class User(name: String) {
constructor(name: String, count: Int): this(name) {
...
}
constructor(name: String, count: Int, email: String): this(name, count) {
...
}
}
fun main() {
val User = User("kkang", 10, "a@a.com")
}
2. 클래스를 재사용하는 상속
상속과 생성자
클래스를 선언할 때 다른 클래스를 참조해서 선언하는 것을 상속이라고 합니다. 코틀린에서는 클래스 상속을 받으려면 선언부에 콜론(:)과 함께 상속받을 클래스 이름을 입력합니다. 이렇게 하면 기존 클래스를 재사용할 수 있습니다.
open class Super { // 상속할 수 있게 open 키워드 이용
}
class Sub: Super() { // Super를 상속받아 Sub 클래스 선언
}
코틀린의 클래스는 기본적으로 다른 클래스가 상속할 수 없습니다. 다시 말해 class Super { } 처럼 클래스를 선언하면 다른 클래스에서 Super 클래스를 상속할 수 없습니다. 만약 다른 클래스에서 상속할 수 있게 선언하려면 open 키워드를 사용합니다. 즉, open class Super { } 라고 선언하면 Super 클래스의 상속을 허용합니다.
상위 클래스를 상속받은 하위 클래스의 생성자에서는 상위 클래스의 생성자를 호출해야 합니다. class Sub: Super() { } 코드에서 Super()는 Super 클래스를 상속받으면서 이 클래스의 매개변수가 없는 생성자를 호출합니다. 만약 매개변수가 있는 상위 클래스의 생성자를 호출할 때는 다음처럼 매개변수 구성에 맞게 인자를 전달해야 합니다.
open class Super(name: String) {
}
class Sub(name: String): Super(name) {
}
상위 클래스의 생성자 호출문을 꼭 클래스 선언부에 작성할 필요는 없습니다. 만약 하위 클래스에 보조 생성자만 있다면 상위 클래스의 생성자를 다음처럼 호출할 수 있습니다.
open class Super(name: String) {
}
class Sub: Super {
constructor(name: String): super(name) {
}
}
오버라이딩 - 재정의
open class Super {
var superData = 10
fun superFun() {
println("i am superFun : $superData")
}
}
class Sub: Super()
fun main() {
val obj = Sub()
obj.superData = 20
obj.superFun()
}
위 코드에서 Sub 클래스는 Super 클래스를 상속받고 있습니다. main 함수에서 Sub 클래스 객체의 사용을 통해 상속의 이점에 대해 알 수 있습니다. 그런데 때로는 상위 클래스에 정의된 멤버를 하위 클래스에서 재정의해야 할 수도 있습니다. 즉, 상위 클래스에 선언된 변수나 함수를 같은 이름으로 하위 클래스에서 다시 선언하는 것입니다. 이를 오버라이딩(overriding)이라고 합니다.
open class Super {
open var someData = 10
open fun someFun() {
println("i am super class function : $someData")
}
}
class Sub: Super() {
override var someData = 20
override fun someFun() {
println("i am sub class function : $someData")
}
}
fun main() {
val obj = Sub()
obj.someFun()
}
코틀린에서 오버라이딩 규칙은 먼저 상위 클래스에서 오버라이딩을 허용할 변수나 함수 선언 앞에 open 키워드를 추가하는 것입니다. open 키워드로 선언하지 않으면 하위 클래스에서 재정의할 수 없습니다. 그리고 open 키워드로 선언한 변수나 함수를 하위 클래스에서 재정의할 때는 반드시 선언문 앞에 override 라는 키워드를 추가해야 합니다.
접근 제한자
접근 제한자란 클래스의 멤버를 외부의 어느 범위까지 이용하게 할 것인지를 결정하는 키워드입니다. 물론 코틀린에서는 변수나 함수를 클래스로 묶지 않고 소스 파일의 최상위에 선언할 수 있으며, 이렇게 선언한 변수나 함수도 접근 제한자로 이용 범위를 지정할 수 있습니다.
코틀린에서 제공하는 접근 제한자에는 public, internal, protected, private이 있습니다.
접근 제한자 | 최상위에서 이용 | 클래스 멤버에서 이용 |
public | 모든 파일에서 가능 | 모든 클래스에서 가능 |
internal | 같은 모듈 내에서 가능 | 같은 모듈 내에서 가능 |
protected | 사용 불가 | 상속 관계의 하위 클래스에서만 가능 |
private | 파일 내부에서만 이용 | 클래스 내부에서만 이용 |
접근 제한자를 생략하면 public 이 기본입니다.
open class Super {
var publicData = 10
protected var protectedData = 20
private var privateData = 20
}
class Sub: Super() {
fun subFun() {
publicData++ // 성공!
protectedData++ // 성공!
privateData++ // 오류!
}
}
fun main() {
val obj = Super()
obj.publicData++ // 성공!
obj.protextedData++ // 오류!
obj.privateData++ // 오류!
}
3. 코틀린의 클래스 종류
데이터 클래스
데이터 클래스는 data 키워드로 선언하며 자주 사용하는 데이터를 객체로 묶어 줍니다. 데이터 클래스는 VO 클래스를 편리하게 이용할 수 있게 해줍니다.
class NonDataClass(val name: String, val email: String, val age: Int)
data class DataClass(val name: String, val email: String, val age: Int)
위의 두 클래스의 주 생성자는 매개변수 구성이 같습니다. 두 클래스의 차이를 알고자 매개변수에 똑같은 인자를 전달해서 객체를 2개씩 생성해봅시다.
fun main() {
val non1 = NonDataClass("kkang", "a@a.com", 10)
val non2 = NonDataClass("kkang", "a@a.com", 10)
val data1 = DataClass("kkang", "a@a.com", 10)
val data2 = DataClass("kkang", "a@a.com", 10)
}
VO 클래스는 데이터를 주요하게 다루므로 객체의 데이터가 서로 같은지 비교할 때가 많습니다. 객체가 같은지가 아니라 객체의 데이터가 같은지를 비교하는 경우입니다. 이때는 equals() 함수를 사용합니다.
println("non data class equals : ${non1.equals(non2)}")
println("data class equals : ${data1.equals(data2)}")
<실행결과>
non data class equals : false
data class equals : true
데이터 클래스는 데이터를 다루는데 편리한 기능을 제공하는 것이 주목적이므로 위의 코드처럼 주 생성자에 val, var 키워드로 매개변수를 선언해 클래스의 멤버 변수로 활용하는 것이 일반적입니다.
물론 데이터 클래스 본문에 변수나 함수를 추가할 수도 있지만 객체의 데이터를 비교할 때 이용하는 equals() 함수는 주 생성자에 선언한 멤버 변수의 데이터만 비교 대상으로 삼습니다.
data class DataClass(val name: String, val email: String, val age: Int) {
lateinit var address: String
constructor(name: String, email: String, age: Int, address: String): this(name, email, age) {
this.address.address
}
}
fun main() {
val obj1 = DataClass("kkang", "a@a.com", 10, "seoul")
val obj2 = DataClass("kkang", "a@a.com", 10, "busan")
println("obj1.equals(obj2) : ${obj1.equals(obj2)}")
}
<실행결과>
obj1.equals(obj2) : true
위 코드에서 데이터 클래스로 선언한 DataClass 는 주 생성자와 클래스 본문에 모두 멤버 변수가 있습니다. 그리고 두 객체를 생성할 때 주 생성자의 멤버 변수에는 똑같은 값을 대입하고, 본문에 선언한 멤버 변수에는 다른 값("seoul"과 "busan")을 대입해 보았습니다.
이 상태에서 두 객체를 equals() 함수로 비교하면 true 가 나옵니다. 두 객체의 일부 멤버 변숫값은 다르지만, 주 생성자에 선언한 멤버 변숫값이 같으면 true 입니다. 즉, 데이터 클래스의 equals() 함수는 주 생성자의 멤버 변수가 같은지만 판단합니다.
오브젝트 클래스
코틀린에서 오브젝트 클래스는 익명 클래스를 만들 목적으로 사용합니다. 익명 클래스는 말 그대로 이름이 없습니다. 클래스 이름이 없으므로 클래스를 선언하면서 동시에 객체를 생성해야 합니다. 그렇지 않으면 이후에 객체를 생성할 방법이 없습니다.
오브젝트 클래스는 선언과 동시에 객체를 생성한다는 의미에서 object라는 키워드를 사용합니다.
val obj = object {
var data = 10
fun some() {
println("data : $data")
}
}
fun main() {
obj.data = 20 // 오류!
obj.some() // 오류!
}
위의 코드를 보면 obj 객체로 클래스에 선언한 멤버에 접근하려고 시도하면 오류가 발생합니다. 클래스에는 data 변수와 some() 함수를 선언했는데도 접근할 수가 없습니다. 그 이유는 클래스 타입 때문입니다. object 키워드로 클래스를 선언했지만 타입을 명시하지 않았기 때문에 이 객체는 코틀린의 최상위 타입인 Any로 취급합니다. 그런데 Any 타입 객체에는 data, some() 이라는 멤버가 없어서 오류가 발생합니다.
그래서 object { } 형태로 익명 클래스를 선언할 수는 있지만 보통은 타입까지 함께 입력해서 선언합니다. 오브젝트 클래스의 타입은 object 뒤에 콜론(:)을 입력하고 그 뒤에 클래스의 상위 또는 인터페이스를 입력합니다.
예를 들어 object: A { } 형태로 선언하면 클래스를 A 타입으로 선언한 것입니다. 만약 A가 클래스면 A 클래스를 상속받은 하위 클래스를 익명으로 선언한 것이며, A가 인터페이스면 A 인터페이스를 구현한 익명 클래스를 선언한 것입니다. 따라서 object 객체를 A타입으로 이용할 수 있게 됩니다.
open class Super {
open var data = 10
open fun some() {
println("i am super some() : $data")
}
}
val obj = object: Super() {
override var data = 20
override fun some() {
println("i am object some() : $data")
}
}
fun main() {
obj.data = 30 // 성공!
obj.some() // 성공!
}
<실행 결과>
i am object some() : 30
컴패니언 클래스
컴패니언 클래스는 멤버 변수나 함수를 클래스 이름으로 접근하고자 할 때 사용합니다. 일반적으로 클래스의 멤버는 객체를 생성해서 접근해야 합니다. 그런데 컴패니언 클래스는 객체를 생성하지 않고서도 클래스 이름으로 특정 멤버를 이용할 수 있습니다.
class MyClass {
companion object {
var data = 10
fun some() {
println(data)
}
}
}
fun main() {
MyClass.data = 20 // 성공!
MyClass.some() // 성공!
}
클래스 내부에 companion object { } 형태로 선언하면 이 클래스를 감싸는 클래스 이름으로 멤버에 접근할 수 있습니다. 참고로 코틀린은 static 키워드를 지원하지 않습니다. 그래서 컴패니언 클래스가 결과적으로는 자바의 static을 대체한다고 할 수 있어요.
'Android' 카테고리의 다른 글
[깡쌤의 안드로이드 프로그래밍 with 코틀린][3. 앱의 기본 기능 구현하기] 6. 뷰를 이용한 화면 구성 (0) | 2024.12.25 |
---|---|
[깡쌤의 안드로이드 프로그래밍 with 코틀린][2. 코틀린 이해하기] 5. 코틀린의 유용한 기법 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][2. 코틀린 이해하기] 3. 코틀린 시작하기 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][1. 안드로이드 앱 개발 준비하기] 2. 안드로이드 앱의 기본 구조 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][1. 안드로이드 앱 개발 준비하기] 1. 개발 환경 준비하기 (0) | 2024.12.25 |