1. 생성자와 초기화 블록
생성자는 다음처럼 정의하며, constructor 키워드는 생략해도 상관 없다.
class 클래스 이름 constructor(생성자의 매개변수 선언) {
}
아래는 예제 코드이다.
val 프로퍼티를 선언하고 있으며, init 블록(초기화 블록) 안에서 프로퍼티를 초기화하고 있다.
class Person constructor(name: String, age: Int) {
// val 프로퍼티 선언(초기화하지않음)
val name: String
val age: Int
// 초기화 블록
// init 블록 안에서 프로퍼티를 초기화하면 프로퍼티를 선언과 동시에 초기화하지 않아도 됨
init {
this.name = name
this.age = age
}
}
fun main(args: Array<String>): Unit {
// 생성자를 호출하면 init 블록이 실행되며 프로퍼티에 자동으로 값이 채워짐
val person = Person("홍길동", 46)
println("이름: ${person.name}")
println("나이: ${person.age}")
}
1) init 블록 나누어 쓰기
init 블록은 나누어 쓸 수도 있다.
인스턴스가 생성되면, 위에서부터 순서대로 프로퍼티의 선언 및 초기화문과 init 블록이 실행된다.
생성자의 매개변수는 init 블록 뿐만 아니라 프로퍼티를 선언과 동시에 초기화하는 데에도 사용할 수 있다.
class Size(width: Int, height: Int) {
val width = width
val height: Int
init {
this.height = height
}
val area: Int
init {
this.area = width * height
}
}
fun main(args: Array<String>): Unit {
val size = Size(10, 50)
println(size.area)
}
2) 생성자와 프로퍼티 한번에 쓰기
위의 예제를 보다보면 프로퍼티 선언문과 생성자 정의문이 비슷한 코드로 반복된다는 것을 알 수 있다.
코틀린은 간결함을 중시하기 때문에 생성자와 프로퍼티를 한번에 쓸 수 있는 문법을 제공한다.
생성자 매개변수 앞에 val 키워드나 var 키워드를 붙이면 동일한 이름의 프로퍼티가 같이 선언된다.
생성자 매개변수에 들어온 인수가 프로퍼티의 초기값이 된다.
아래 예제에서는 speed 매개변수에는 디폴트 인수를 지정해주고 있다.
class Car(val name: String, val speed: Int = 0)
fun main(args: Array<String>): Unit {
val car = Car("Niro HEV")
println(car.name)
println(car.speed)
val car2 = Car("Sportage HEV", 130)
println(car2.name)
println(car2.speed)
}
2. 보조 생성자
생성자는 여러 개를 둘 수 있는데 기본적인 문법은 아래와 같다.
class 클래스 이름 constructor(매개변수) {
// 보조 생성자1
constructor(매개변수): this(인수) {
}
// 보조 생성자2
constructor(매개변수): this(인수) {
}
}
클래스 이름 옆에 오는 생성자는 주 생성자, 클래스 내부에 오는 생성자는 보조 생성자라고 한다.
class Time(val second: Int) { // 프로퍼티를 선언 및 초기화하는 주 생성자
init {
println("init 블록 실행 중")
}
// 보조 생성자1
constructor(minute: Int, second: Int): this(minute * 60 + second) {
println("보조 생성자1 실행 중")
}
// 보조 생성자2
constructor(hour: Int, minute: Int, second: Int): this(hour * 60 + minute, second) {
println("보조 생성자2 실행 중")
}
init {
println("또 다른 init 블록 실행 중")
}
}
fun main() {
// 보조 생성자1이 호출되고 연이어 주 생성자가 곧바로 호출되어 init 블록들이 가장 먼저 수행
// 주 생성자가 끝나면 본격적으로 보조 생성자1의 코드가 실행
println("${Time(15, 6).second} 초")
// 보조 생성자2가 호출되고 연이어 보조 생성자1 호출, 연이어 주 생성자 호출. init 블록들 수행
println("${Time(6, 3, 17).second} 초")
}
/*
결과
init 블록 실행 중
또 다른 init 블록 실행 중
보조 생성자1 실행 중
906 초
init 블록 실행 중
또 다른 init 블록 실행 중
보조 생성자1 실행 중
보조 생성자2 실행 중
21797 초
*/
3. 프로퍼티와 Getter/Setter
프로퍼티는 실제로 데이터가 저장되는 공간(Field), 저장된 값을 읽으려고 할 때 호출되는 함수(Getter), 값을 저장하려고 할 때 호출되는 함수(Setter)로 이루어져 있다.
class Person {
var age: Int = 0
get() {
return field
}
set(value) {
// field는 실제로 값이 저장되는 프로퍼티 속의 변수를 나타내는 특수 식별자
field = if(value >= 0) value else 0
}
}
fun main() {
val person = Person()
// 프로퍼티에 특정 값을 대입하면, 프로퍼티에 해당하는 setter가 호출
person.age = -30
// 프로퍼티에서 특정 값을 읽어오려 하면, 프로퍼티에 해당하는 getter가 호출
println(person.age)
}
val 프로퍼티는 초기 값이 주어지면 더 이상 값을 변경(Set)할 수 없기 때문에 val 프로퍼티는 Getter만 존재한다.
Getter/Setter을 별도로 정의하지 않으면 다음과 같이 자동으로 정의된다.
class Person {
var age: Int = 0
get() {
return field
}
set(value) {
field = value
}
}
프로퍼티에 디폴트 Getter/Setter가 포함되어 있기 때문에 자바처럼 만들 필요는 없다.
하지만, Getter/Setter의 동작을 커스터마이징 하고 싶다면 별도로 정의가 필요하다.
프로퍼티의 Getter/Setter는 다양한 형태로 정의가 가능하다.
> 디폴트 Getter/Setter 정의(해당 코드가 필요한 이유는 접근 지정자 예제에서 다룸)
var age = 0
get
set
> Getter 속 문장이 하나일 때 축약 가능
var name = ""
get() = "이름: $field"
> val 프로퍼티이고, Getter의 반환 값이 field가 아니라면 다음처럼 프로퍼티의 타입을 생략할 수 있다.
Getter의 반환 타입으로 프로퍼티의 타입을 추론할 수 있기 때문이다.
class Person {
var age = 0
val isYoung get() = age < 30
}
4. 연산자 오버로딩
class Point(var x = 0, var y = 0) 이라는 클래스가 있다고 가정하고
아래와 같이 두 객체끼리 덧셈을 하려고 한다면 가능하지 않다.
val pt1 = Point(3, 7)
val pt2 = Point(2, -6)
val pt3 = pt1 + pt2
위의 코드를 연산자 오버로딩을 이용하면 가능하게 할 수 있다.
연산자 오버로딩을 하기 위해서는 멤버함수 정의문 앞에 operator를 붙이면 된다.
아래 예제 코드를 살펴보자
멤버함수 정의문 앞에 operator를 붙이면 Point의 인스턴스 간에 연산자를 사용했을 때 이 멤버 함수를 대신 호출해달라는 뜻이다.
plus, minus, times, div는 정해진 이름이며, 다른 이름을 사용하면 연산자 오버로딩이 제대로 되지 않는다.
class Point(var x:Int = 0, var y: Int = 0) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
operator fun minus(other: Point): Point {
return Point(x - other.x, y - other.y)
}
operator fun times(number: Int): Point {
return Point(x * number, y * number)
}
operator fun div(number: Int): Point {
return Point(x / number, y / number)
}
// 좌표 값을 출력
fun print() {
println("x: $x, y: $y")
}
}
fun main() {
val pt1 = Point(3, 7)
val pt2 = Point(2, -6)
val pt3 = pt1 + pt2
val pt4 = pt3 * 6
val pt5 = pt4 / 3
pt3.print()
pt4.print()
pt5.print()
}
지금까지 배운 연산자 중에 연사자 오버로딩이 가능한 연산자는 다음 표와 같다.
5. 번호 붙은 접근 연산자 []
[] 연산자는 표현식[표현식] 형태로 적으며, 객체의 일부 값을 추출해낼 때 사용한다.
[] 연산자에는 여러 개의 피연산자를 지정할 수 있다.
person[1, 2, 3,] ==> person.get(1, 2, 3)이 호출
person[1, 2] = "J" ==> person.set(1, 2, "J) 호출
class Person(var name: String, var birthday: String) {
// [] 연산자를 오버로딩하는 멤버 함수 get 선언 - position에 해당하는 위치의 프로퍼티 값을 반환
// person[0]은 컴파일 시 person.get(0)으로 번역
operator fun get(position: Int): String {
return when(position) {
0 -> name
1 -> birthday
else -> "알 수 없음"
}
}
// [] 연산자를 오버로딩하는 멤버 함수 set 선언 - position에 해당하는 위치의 프로퍼티 값을 value로 변경
// person[0] = "Java"는 컴파일 시 person.set(0, "Java")로 번역
operator fun set(position: Int, value: String) {
when (position) {
0 -> name = value
1 -> birthday = value
}
}
}
fun main() {
val person = Person("Kotlin", "2016-02-15")
println(person[0])
println(person[1])
println(person[-1])
person[0] = "Java"
println(person.name)
}
6. 호출 연산자
invoke는 () 연산자를 오버로딩하는 멤버 함수이다.
class Product(val id: Int, val name: String) {
operator fun invoke(value: Int) {
println(value)
println("id: $id\nname: $name")
}
}
fun main() {
val product = Product(762443, "코틀린 200제")
product(108) // 컴파일 시 product.invoke(108)로 번역
}
7. in 연산자
in 연산자는 어떤 값이 객체에 포함되어 있는지 여부를 조사하는 역할
in 연산자는 operator fun contains(매개변수: 타입): Boolean 멤버 함수로 오버로딩 가능하다.
in 연산자는 when 문에서도 쓸 수 있다.
fun main() {
println('o' in "Kotlin") // true
println("in" !in "Kotlin") // false
}
8. 멤버 함수의 중위 표기법
중위 표기법이란? 함수를 연산자처럼 호출하는 방법이며,
피연산자 연산자 피연산자의 순서로 표현식을 구성하는 방식을 뜻한다.
멤버 함수의 매개변수가 하나뿐이면 함수 호출을 중위 표기법으로 할 수 있다.
중위 표기법을 지원하려면 멤버 함수 선언문 앞에 infix를 붙여야 한다.
class Point(var x: Int = 0, var y: Int = 0) {
// base를 원점으로 생각했을 때의 좌표를 반환한다
infix fun from(base: Point): Point {
return Point(x - base.x, y - base.y)
}
}
fun main() {
// 아래 중위 표기법 호출은 Point(3,6).from(Point(1,1))의 문법적 설탕
val pt = Point(3, 6) from Point(1, 1)
println(pt.x)
println(pt.y)
}
9. 상속
상속은 기존에 존재하는 클래스를 확장하여 새로운 클래스를 정의하는 기법이다.
코틀린에서 기본적으로 클래스는 상속이 막혀있다.(기본적으로 final 사용)
상속을 허용하려면 클래스 정의부 앞에 open 키워드를 붙여주어야 한다.
상속을 할 때는 반드시 슈퍼클래스의 생성자를 호출해야 하기 때문에 서브클래스의 생성자 매개변수에 슈퍼클래스의 매개변수를 추가하여 이를 슈퍼클래스 생성자에 전달하게 해야 한다.
상속 문법은 아래와 같다.
class 클래승 이름: 슈퍼클래스 생성자(인수) {
}
상속을 하면 슈퍼클래스의 프로퍼티와 멤버 함수가 서브클래스에 그대로 복사되며, 상속은 하나의 클래스만 할 수 있다
open class Person(val name: String, val age: Int)
// : Person(name, age) 이부분이 Person 클래스를 상속하는 코드
class Student(name: String, age: Int, val id: Int): Person(name, age)
fun main() {
val person = Person("홍길동", 35)
val student = Student("익순이", 23, 20171217)
println(person.name)
println(person.age)
println(student.name)
println(student.age)
println(student.id)
}
10. 업캐스팅
캐스팅(형변환)이란?
특정 타입을 다른 타입으로 변환하는 것을 뜻하며, 코틀린에서는 서브클래스의 인스턴스를 슈퍼클래스 타입으로 가리킬 수 있다.(객체지향언어에서는 모두 동일함)
서브클래스의 인스턴스를 슈퍼클래스 타입으로 가리키는 것을 업캐스팅이라고 한다.
(반대의 의미는 다운캐스팅)
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int, val id: Int): Person(name, age)
fun main() {
// 서브클래스의 인스턴스를 슈퍼클래스 타입으로 가리키는 것을 업캐스팅이라고 한다.
val person: Person = Student("John", 32, 20171218)
/* person 참조 변수는 Student 인스턴스를 가리키고 있기는 하지만,
타입이 Person이기 때문에 name과 age 프로퍼티 밖에 접근하지 못한다. */
// println(person.id) // Kotlin: Unresolved reference: id
}
/* Person 타입으로 가리킨 Student의 인스턴스를 다시 원래 타입으로 가리킬 수는 없다. */
val student: Student = person // Kotlin: Type mismatch: inferred type is Person but Student was expected
슈퍼클래스 타입은 항상 슈퍼클래스 자체나 서브클래사의 인스턴스만 가리킬 수 있다.
이처럼 한 객체가 여러가지 타입을 가질 수 있는 성질을 다형성이라고 한다.
11. 오버라이딩과 프로퍼티를 오버라이딩하기
오버라이딩이란?
슈퍼클래스의 멤버 함수와 시그니처가 동일한 멤버 함수를 서브클래스에서 선언하면,
슈퍼클래스 멤버 함수의 동작을 덮어쓰기 할 수 있는 것
멤버 함수도 오버라이딩을 허용하려면 open 키워드를 맨 앞에 붙여주어야 한다.
코틀린에서는 오버라이딩 할 때는 override 키워드를 반드시 붙여야 한다.
open class AAA {
open fun func() = println("AAA")
}
class BBB : AAA() {
override fun func() {
super.func()
println("BBB")
}
}
fun main() {
AAA().func()
BBB().func()
}
override 키워드는 그 자체로 open 키워드가 포함되어 있다.
즉, override된 멤버 함수는 서브클래스에서 몇 번이고 재오버라이딩 가능하며, 멤버 함수의 재 오버라이딩을 막으려면 final 키워드를 사용해야한다.
open class CCC {
open fun hello() = Unit
}
open class DDD : CCC() {
// final을 붙여 더 이상 hello를 오버라이딩할 수 없게 만든다.
final override fun hello() = super.hello()
}
open class EEE : DDD() {
// 에러 => 'hello' in 'DDD' is final and cannot be overridden
override fun hello(){}
}
프로퍼티에도 함수의 일종인 Getter/Setter가 존재하므로, 이들도 오버라이딩이 가능하다.
프로퍼티를 오버라이딩 하기 위해서는 open 키워드를 사용해야 하며, 오버라이딩 할 때도 override 키워드를 사용해야한다.
open class AAA {
// number 프로퍼티에 open 키워드 붙임. Getter와 Setter를 커스터마이징
open var number = 10
get() {
println("AAA number Getter 호출됨")
return field
}
set(value) {
println("AAA number Setter 호출됨")
field = value
}
}
class BBB : AAA() {
// AAA 클래스의 number 프로퍼티를 오버라이딩
override var number: Int
get() {
println("BBB number Getter 호출됨")
return super.number
}
set(value) {
println("BBB number Setter 호출됨")
super.number = value
}
}
// 슈퍼클래스에서 val로 선언한 프로퍼티를 var로 오버라이딩할 수 있다.
open class CCC(open val number: Int = 0)
class DDD: CCC() {
override var number: Int = 0
get() = super.number
set // 디폴트 Setter
}
fun main() {
val test = BBB()
test.number = 5
test.number
var test2 = DDD()
test2.number = 10
test2.number
}
12. 다형성의 활용
멤버 함수를 호출할 때, 참조 변수가 실제로 가리키고 있는 객체의 멤버 함수가 호출된다.
open class AAA {
open fun hello() = println("AAA 입니다.")
}
class BBB : AAA() {
override fun hello() = println("BBB 입니다.")
}
fun main() {
val one = AAA()
val two = BBB()
val three: AAA = two
one.hello()
two.hello()
three.hello() // 참조 변수가 실제로 가리키고 있는 객체의 멤버 함수 호출
}
13. 클래스를 상속하는 객체
클래스 없이 객체를 만들 때 쓰는 object 표현식으로도 상속을 할 수 있다.
클래스를 상속하는 객체 문법은 아래와 같다.
object: 슈퍼클래스 이름(생성자 인수)
open class Person(val name: String, val age: Int) {
open fun print() {
println("이름: $name")
println("나이: $age")
}
}
fun main() {
/* Person 클래스를 상속하는 object 표현식
객체를 만들면서 어떤 클래스를 상속하려면 클래스간에 상속하듯이 obejct: 슈퍼클래스 이름(생성자 인수)를 붙여주면된다.
클래스 없이 객체를 만들면서 상속을 했으므로 이때의 상속은 1회용이 된다. */
val custom: Person = object : Person("Ellie", 29) {
override fun print() {
println("It's a object")
}
}
custom.print()
}
14. Any 클래스
코틀린에서는 어떤 클래스가 아무 클래스도 상속하지 않으면 자동으로 Any 클래스를 상속한다.
다른 클래스를 상속한다고 해도 그 클래스 또한 Any 클래스를 자동으로 상속하므로 간접적으로 Any 클래스를 상속하게 된다. 즉, 모든 코틀린 클래스들은 Any 클래스를 상속한다는 것이 보장된다.
Any 클래스에는 세 가지 멤버 함수가 있다.
모든 클래스는 Any 클래스를 상속하므로, 코틀린의 모든 클래스는 아래 3가지 멤버 함수를 갖는다.
open class Any{
// == 연산자를 오버로딩하는 멤버 함수
open operator fun equals(other: Any?): Boolean
// 객체 고유의 해시코드를 반환하는 멤버 함수
open fun hashCode(): Int
// 객체의 내용을 String 타입으로 변환하는 멤버 함수
open fun toString(): String
}
Any 클래스이 toString 멤버 함수를 오버라이딩하는 예제이다.
Building 클래스에서는 생성자와 프로퍼티를 한번에 쓰도록 하였고, override 키워드를 붙여 Any 클래스의 toString을 오버라이딩 하도록 했다.
class Building(val name: String = "", // 건물명
val date: String = "", // 건축일자
val area: Int = 0 // 면적(m²)
) {
override fun toString(): String =
"이름: ${this.name}\n" +
"건축일자: ${this.date}\n" +
"면적: ${this.area} m²"
}
main에서 Building의 인스턴스를 생성하고 Any 타입을 매개변수로 받는 printObject 함수에 building 객체를 인수로 전달했다. 코드 상으로는 Any 타입의 toString 멤버 함수를 호출하고 있지만, 다형성 덕에 실제로는 building 객체의 toString() 호출된다.
fun main() {
val building = Building("코틀린", area = 100)
/*
코드 상으로는 Any 타입의 toString 멤버 함수를 호출하고 있지만,
다형성 덕에 실제로는 building 객체의 toString() 호출
*/
printObject(building)
}
fun printObject(any: Any) {
println(any.toString())
/* println 함수는 전달한 인수가 String 타입이 아니면
내부적으로 println(any.toString()) 호출하기 때문에 아래와 같이 사용해도 됨 */
println(any)
}
'TIL > Kotlin' 카테고리의 다른 글
[TIL/Kotlin] 코틀린 중급문법_Nothing타입, Nullable, ?. 연산자, !! 연산자, ?: 연산자 (0) | 2023.05.14 |
---|---|
[TIL/Kotlin] 코틀린 중급문법_예외 처리, 예외 던지기 (0) | 2023.05.14 |
[TIL/Kotlin] 코틀린 중급문법_객체(Object), 클래스(Class), ===와 !== 연산자, 힙(Heap) 영역의 존재 이유 (0) | 2023.05.05 |
[TIL/Kotlin] 코틀린 기초문법_함수, Unit 타입, 디폴트 인수와 가변 인수 (0) | 2023.05.05 |
[TIL/Kotlin] 코틀린 기초문법_흐름 제어(조건문과 반복문, Label) (0) | 2023.05.05 |