섹션2. 코틀린에서 변수와 타입, 연산자를 다루는 방법
1강 - 코틀린에서 변수를 다루는 방법
1) 변수 선언 키워드 - var과 val의 차이점
- var: 불변 타입 변수 선언
- val: 가변 타입 변수 선언
- 모든 변수에 수정 가능 여부(var/val)를 반드시 명시해줘야한다.
- 타입을 컴파일러가 자동으로 추론해주기 때문에 명시적으로 작성해줄 필요없다.(
ex. var number1 = 10L) - 변수를 만들고 초기화해주지 않는 경우 타입을 무조건 명시해줘야한다.(
ex. var number1: Long) - 초기화되지 않은 변수를 사용시 컴파일 에러 발생한다.
- val 컬렉션이 있더라도 요소를 add로 추가할 수 있다.
- TIP) 모든 변수는 우선 val로 만들고 꼭 필요한 경우에만 var로 변경할 것
2) kotlin 에서의 Primitive Type
- kotlin에서는 자바처럼 Primitive Type과 Reference Type이 나뉘어져있지 않고, Reference Type만 사용된다.(Long, Double, …)
- Reference Type(ex. Long) 타입 하나로 합쳐져있지만, 연산을 할 경우 코틀린이 알아서 내부적으로 Primitive Type으로 변경해서 알아서 처리를 해준다.
- 즉, 프로그래머가 boxing / unboxing 을 고려하지 않아도 되도록 코틀린이 알아서 처리해준다.
3) Kotlin에서의 nullable 변수
- 코틀린은 기본적으로 모든 변수를 널이 들어갈 수 없게끔 설계해놓았다.
- 그러다보니 null 이 들어갈 수 있는 변수는 물음표(?)로 명시적으로 표현해줘야한다.
1
2
3
4
5
| var number3 = 1_000L
number3 = null // compile error
var number: Long? = 1_000L
number3 = null // ok
|
4) Kotlin에서의 객체 인스턴스화
- 자바에서처럼 new 키워드를 붙이지 않아야한다.
1
| var person = Person("jeonyoungho")
|
2강 - 코틀린에서 null을 다루는 방법
1) 코틀린에서의 null 체크
- 함수 인자와 반환값에 ? 를 붙여서 Nullable 여부를 제어할 수 있다.
- 코틀린에서는 널이 가능한 탕비을 완전히 다르게 취급한다.
- null이 가능한 타입만을 위한 기능(키워드)는 없을까? 있다! -> Safe Call
2) Safe Call과 Elvis 연산자
- Safe Call(안전한 호출)은 null이 아닐때만 실행시키는 기능이다.
1
2
3
| val str: String? = "ABC"
str.length // 불가능, 컴파일 에러
str?.length // 가능, str이 널이면 전체가 널이됨
|
- Elvis 연산자(?:)는 앞의 연산 결과가 null이면 뒤의 값을 사용하는 기능이다.
- ’?:’를 90도 회전하면 엘비스와 닮아서 엘비스 연산자라 불리게 됨
1
2
| val str: String? = "ABC"
str?.length ?: 0 // str이 널이면 전체 결과가 널이되고, 엘비스 연산자로 인해 0이 된다
|
- Safe Call과 Elvis 연산자 활용 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
| fun startsWithA1(str: String?): Boolean {
return str?.startsWith("A)
?: throw IllegalArgumentException("null이 들어왔습니다")
}
fun startsWithA2(str: String?): Boolean? {
return str?.startsWith("A")
}
fun startsWithA3(str: String?): Boolean {
return str?.startsWith("A") ?: false
}
|
- Elvis 연산은 early return에도 사용할 수 있다!
1
2
3
4
5
6
7
8
9
10
11
12
| public long calculate(Long number) {
if (number == null) {
return 0;
}
// ...
}
fun calculate(number: Long?): Long {
number ?: return ;
// ...
}
|
3) 널 아님 단언!!
- 아무리 생각해도 null이 될 수 없는 경우에 사용(!!)
1
2
3
| fun startsWithA2(str: String?): Boolean {
return str!!.startsWith("A") // 절대 널이 아님을 단언, 만약 널이 들어오게될 경우엔 런타임에서 NPE 발생(컴파일 타임엔 못잡음)
}
|
4) 플랫폼 타입
- 코틀린이란게 100프로 자바와 호환가능하다보니, 병행해서 쓰는 경우도 있다.
- 코틀린에서 자바코드를 가져다 사용할때 Null에 대한 어노테이션 정보를 코틀린이 이해한다.
- org.jetbvrains.annotations 패키지, javax.annotation 패키지의 NotNull, Nullable
- ex. 자바에서 Nullable 어노테이션이 getter 함수에 붙어잇으면 Nullable한 값으로 알아서 판단함
- 플랫폼 타입이란 코틀린이 null 관련 정보를 알 수 없는 타입으로 런타임시 예외가 발생할 수 있다.
3강 - 코틀린에서 Type을 다루는 방법
1) 기본 타입
- Byte, Shoort, Int, Long, Float, Double, 부호 없는 정수들
- 코틀린에서는 선언된 기본값을 보고 타입을 추론한다.
1
2
3
4
| val numbwer1 = 3 // Int
val numbwer1 = 3L // Long
val numbwer1 = 3.0f // Float
val numbwer1 = 3.0 // Double
|
- 자바에서는 기본 타입간의 변환이 암시적으로 이뤄질 수 있지만, 코틀린은 명시적으로 이뤄져야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| == java ==
int nubmer1 = 4;
long nubme2 = number1;
System.out.println(number1 + number); // 자바에서는 더 큰 타입으로 암시적 변경(int 타입의 값이 long 타입으로)
== kotlin ==
val number1 = 4
val number2: Long = number1 // Type mismatch, Koitlin 에서는 암시적 타입 변경이 불가능
val number2: Long = number1.toLong() // to변환타입()을 사용해야함
println(number1 + number2)
== 널 처리 추가시 적절한 처리 필요 ==
val number1: Int? = 3
val number2: Long = number1?.toLong() ?: 0L
|
2) 타입 캐스팅
- 자바는
instanceof로 변수가 주어진 타입인지 확인, 코틀린은 is 키워드 사용 - 자바는 괄호로 타입캐스팅, 코틀린은 as 키워드 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| fun printAgeIfPerson(obj: Any) {
if (obj is Person) {
val person = obj as Person // 생략 가능, 생략해도 Person 타입으로 간주 => 스마트 캐스트!!
println(person.age)
}
if (obj !is Person) { // Person 타입이 아니라면
}
}
== obj에 null이 들어올 수 있다면? ==
fun printAgeIfPerson(obj: Any?) {
val person = obj as Person
println(person.age) // obj가 널이면 NPE 발생
}
fun printAgeIfPerson(obj: Any?) {
val person = obj as? Person // obj가 널이면 전체가 널이됨
println(person?.age) // 널 반환
}
|
3) Kotlin의 3가지 특이한 타입
- Any
- Java의 Object 역할(모든 객체의 최상위 타입)
- 모든 Primitive Type의 최상위 타입도 Any
- Any 자체로는 널을 포함할 수 없어 널이 포함하고 싶다면, Any?로 표현
- Any에 equals / hashCode / toString 존재
- Unit
- Java의 void 와 동일한 역할
- (살짝 어려운 내용) Void와 다르게 unit은 그 자체로 타입 인자로 사용 가능
- 함수형 프로그래밍에서 Unit은 단 하나의 인스턴스만 갖는 타입을 의미. 즉, 코틀린은 Unit은 실제 존재하는 타입이라는 것을 표현
- Nothing
- 함수가 정상적으로 끝나지 않았다는 사실을 표현
- 무조건 예외 반환하는 함수 / 무한 루프 함수 등
- 실제 많이 사용되지는 않음
1
2
3
| fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
|
4) String Interpolation, String indexing
1
2
3
4
5
| val person = Person("jeon", 100)
println("이름: ${person.name}")
val name = "jeon"
println("이름: $name") // 중괄호 제거 가능
|
- TIP) 변수 이름만 사용하더라도 ${변수} 를 사용하는 것이 가독성, 일괄 변환, 정규식 활용 측면에서 좋다.
- 여러줄에 걸친 문자열을 작성해야할때 “”” 큰따옴표 세개로 표현 가능
1
2
3
4
5
6
7
| val name = "jeon"
"""
ABC
DEF
${name}
""".trimIndent()
println(str)
|
- 코틀린에서 문자열의 특정 문자 가져올땐 대괄호([])를 사용한다.
1
2
3
4
5
6
7
| == java ==
str.chatAt(0);
== kotlin ==
var str = "ABC"
println(str[0])
println(str[2])
|
4강 - 코틀린에서 연산자를 다루는 방법
1) 단한 연산자(++, –) / 산술 연산자(+, -, *, /, %) / 산술대입 연산자(+=, -=, *=, /=, %=)
2) 비교 연산자와 동등성, 동일성
- 비교 연산자(>, <, >=, <=)는 자바와 코틀린 완전 동일
- 단, 코틀린은 자바와 다르게 객체 비교시 비교 연산자를 사용하면 자동으로 compareTo를 호출
1
2
3
4
5
| val money1 = JavaMoney(2_000L)
val money2 = JavaMoney(1_000L)
if (money1 > money2) { // compareTo 자동 호출, 직접 compareTo 를 호출하지 않아도되서 더 직관적
}
|
- 동등성(Equality): 두 객체의 값이 같은가
- 동일성(Identity): 두 객체의 주소 값이 같은가
- 자바에서는 equals 호출, 코틀린에서는 == 호출(코틀린에서 ==를 사용하면 간접적으로 equals를 호출)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| == java ==
JavaMoney money1 = new JavaMoney(1_000L);
JavaMoney money2 = money1;
JavaMoney money3 = new JavaMoney(2_000L);
System.out.println(money1 == money2); // true
System.out.println(money1 == money3); // false
System.out.println(money1.equals(money3)); // true
== kotlin ==
val money1 = JavaMoney(1_000L)
val money2 = money1
val money3 = JavaMoney(1_000L)
println(money1 === money2) // true
println(money1 === money3) // false
println(money1 == money3) // true, 내부적으로 equals 호출
|
3) 논리 연산자 / 코틀린에 있는 특이한 연산자
| 논리 연산자(&&, | | , !)는 자바와 코틀린 완전 동일, 자바처럼 Lazy 연산을 수행한다. |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| == Lazy 연산 ==
fun main() {
if (fun1() || fun2()) {
println("fun1만 실행됨")
}
if (fun2() && fun1()) {
println("fun2만 실행됨")
}
}
fun fun1(): Boolean {
println("fun 1")
return true
}
fun fun2(): Boolean {
println("fun 2")
return false
}
|
- 코틀린에 있는 특이한 연산자
- in / !in: 컬렉션이나 범위에 포함되어 있다, 포함되어 있지 않다.(ex.
println(1 in numbers)) - a..b: a부터 b까지의 범위 객체를 생성한다.
- a[i]: a에서 특정 인덱스 i로 값을 가져온다.
- a[i] = b: a의 특정 인덱스 i에 b를 할당한다.
4) 연산자 오버로딩
- 코틀린에서는 객체마다 직접 플러스 연산, 마이너스 연산, 단항 연산, 비교 연산 등을 직접 정의할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
| data class Money(
val amount: Long
) {
operator fun plus(ohter: Money): Money { // 연산자 직접 정의
return Money(this.amount + other.amount)
}
}
val money1 = Money(1_000L)
val money2 = Money(2_000L)
println(money1.plus(money2)) // Money(amount=3000L)
println(money1 + money2) // Money(amount=3000L)
|
섹션3. 코틀린에서 코드를 제어하는 방법
5강 - 코틀린에서 제어문을 다루는 방법
1) if문
2) Expression과 Statement
- 자바에서 if-else는 Statement이지만, 코틀린에서는 Expression이다.
- Statement: 프로그램의 문장, 하나의 값으로 도출되지 않음
- Expression: 하나의 값으로 도출되는 문장
- Statement가 Expression을 포괄
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| == Java ==
ìnt score = 30 + 40; // 30 + 40은 70이라는 하나의 결과가 나오기에 Expression이면서 Statement
String grade = if (score >= 50) { // if 문을 하나의 값으로 취급하지 않으니 에러 발생 => Statement
"P";
} else {
"F";
}
String grade = score >= 50 ? "P" : "F"; // 3항 연산자는 하나의 값으로 취급하므로 에러가 없다! Expression이면서 Statement
== Kotlin ==
fun getPassOrFail(score: Int): String { // if-else와 if-elseif-else를 바로 계산해서 리턴할 수 있음, 따라서 3항 연산자가 필요하지 않기때문에 삭제되었다
return if (score > 50) {
"P"
} else {
"F"
}
}
fun getPassOrFail(score: Int): String {
return if (score > 50) {
"P"
} else if (score > 30) {
"C"
}
else {
"F"
}
}
|
- 간단한 TIP: 어떠한 값이 특정 범위에 포함되어 있는지, 포함되어 있지 않은지
1
2
3
4
5
6
7
| == Java ==
if (0 <= score && score <= 100) {}
== Kotlin ==
if (0 <= score && score <= 100) {} // ok
if (score in 0..100) {} // ok
|
3) switch와 when
- java의 swith는 코틀린의 when으로 사용 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| == java ==
private String getGradeWithSwith(int score) {
switch(score / 10) {
case 9:
return "A";
case 8:
return "B";
default:
return "D";
}
}
== kotlin ==
fun getGradeWithSwith(score: Int): String {
return when(score /10) { // when 역시 하나의 Expression이기 때문에 바로 리턴 가능
9 -> "A"
8 -> "B"
else -> "D"
}
}
// 코틀린의 when 은 좀 더 다양한 활용 가능, 다양한 조건으로 분기 처리 가능
fun getGradeWithSwith(score: Int): String {
return when(score) {
in 90..99 -> "A"
in 80..89 -> "B"
else -> "D"
}
}
when(값) {
조건부 -> 어떠한 구문
조건부 -> 어떠한 구문
else -> 어떠한 구문
}
// 조건부에는 어떠한 expression 이라도 들어갈 수 있음(ex. is Type)
fun startsWithA(obj: Any): Boolean {
return when (obj) {
is String -> obj.startsWith("A")
else -> false
}
}
// 여러 조건을 동시에 검사 가능(, 로 구분)
fun judgeNumber(number: Int) {
when (number) {
1,0,-1 -> println("어디서 많이 본 숫자입니다)
else -> println("1, 0, -1이 아닙니다")
}
}
// when에 들어가는 값 자체가 없을수도 있음(얼리 리턴처럼 동작)
fun earlyReturn(number: Int) {
when {
number == 0 -> println("주어진 숫자는 0입니다")
number % 2 == 0 -> println("주어진 숫자는 짝수입니다")
else -> println("주어진 숫자는 홀수입니다")
}
}
|
- 코틀린 when은 Enum Class 혹은 Sealed Class와 함께 사용할 경우, 더욱더 진가를 발휘한다.
6강 - 코틀린에서 반복문을 다루는 방법
1) for-each 문
- 콜론(:)대신 in 사용
- 자바와 동일하게 Iterable이 구현된 타입이라면 모두 들어갈 수 있다.
1
2
3
4
5
| val numbers = listof(1L, 2L, 3L)
for (number in numbers) {
println(number)
}
|
2) 전통적인 for문
1
2
3
4
5
6
7
8
9
10
11
| for (i in 1..3) {
println(i)
}
for (i in 3 downTo 1) { // 내려가는 경우
println(i)
}
for (i in 1..5 step 2) { // 1부터 5까지 2씩 올라가는 경우
println(i)
}
|
3) Progression과 Range
- Range는 Progression(등차수열,진행의 의미)를 상속받는다.
- ..를 타고 들어가보면
public operator fun rangeTo(other: Int): IntRange- 다음과 같이 IntRange를 반환하고, IntRange 클래스를 들어가보면
public class IntRange(start: Int, endInclusive: Int) : IntProgression(start, endInclusive, 1), ClosedRange<Int> {- IntProgression을 상속받고 있고 기본값으로 step값인 1을 받고있다.
- 그래서 1..5 이라는 의미는 1에서 5까지 1씩 상승하는 등차수열을 만들어달라는 의미가 된다.
- 3 downTo 1은 시작값 3, 끝값 1, 공차가 -1인 등차수열이 만들어지고
- 1..5 step 2는 시작값 1, 끝값 5, 공차가 2인 등차수열이 만들어진다.
- downTo, step 둘 다 함수이다.(중위 함수)
- 중위 함수(Infix Function)는 ‘변수.함수이름(argument)’ 대신 ‘변수 함수이름 argument’ 로 호출할 수 있도록 연산자처럼 사용하는 함수를 의미한다.
- 즉, 피연산자 사이에 함수 이름을 위치시켜 호출할 수 있는 함수이다.
- 3 downTo 1 이라는 코드에서는 3이 downTo라는 함수를 호출했고 그 매개변수가 1인것이다.
1
| public infix fun Int.downTo(to: Int): IntProgression {
|
- 즉, 코틀린에서 for문은 등차수열을 이용한다!
4) while 문
- do-while문을 포함하여 자바와 코틀린 완전 동일
7강 - 코틀린에서 예외를 다루는 방법
1) try catch finally 구문
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| fun parseIntOrThrow(str: String): Int {
try {
return str.toInt()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("주어진 ${str}는 숫자가 아닙니다")
} finally {
println("finish")
}
}
fun parseIntOrThrow(str: String): Int {
return try { // Expression 으로 간주되어 리턴을 한 번만 쓸 수 있음
str.toInt()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("주어진 ${str}는 숫자가 아닙니다")
} finally {
println("finish")
}
}
|
2) Checked Exception과 Unchecked Exception
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| == 자바 ==
public void readFile() throws IOException {
File currentFile = new File(".");
File file = new File(currentFile.getAbsoluteFile() + "/a.txt");
BufferedReader reader = new BufferedReader(new FileReader(file));
System.out.println(reader.readLine());
reader.close();
}
== 코틀린 ==
fun readFile() { // throws 구문이 없다. 코틀린에서는 Checked Exception과 Unchecked Exception 을 구분하지 않는다. 모두 Unchecked Exception이다.
val currentFile = File(".")
val file = File(currentFile.absolutePath + "/a.txt")
val reader = BufferedReader(FileReader(file))
println(reader.readLine())
reader.close()
}
|
3) try with resources
1
2
3
4
5
6
7
8
9
10
11
12
13
| == 자바 ==
public void readFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
System.out.println(reader.readLine());
}
}
== 코틀린 ==
fun readfile(path: String) { // try-with-resources 구문 없음, use를 사용
BufferedReader(FileReader(path)).use { bufferedReader ->
println(bufferedReader.readLine())
}
}
|
8강 - 코틀린에서 함수를 다루는 방법
1) 함수 선언 문법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| fun max(a: Int, b: Int): Int { // 접근 제어자 public은 생략 가능
if (a > b) {
return a
}
return b
}
fun max(a: Int, b: Int): Int = // 함수가 하나의 결과값이면 block 대신 '=' 사용 가능
if (a > b) {
a
} else {
b
}
fun max(a: Int, b: Int) = if (a > b) a else b // 함수를 쓸 때 중괄호 대신 = 을 썼기 때문에 반환타입 생략 가능
|
- block { } 을 사용하는 경우에는 반환 타입이 Unit이 아니라면, 명시적으로 작성해주어야 한다!
- 함수는 클래스 안에 있을 수도, 파일 최상단에 있을 수도 있습니다. 또한, 한 파일 안에 여러 함수들이 있을 수도 있습니다.
2) default parameter
- 자바에서는 함수의 매개변수에 기본값을 넣고싶을때 오버로딩을 이용하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public void repeat(String str, int number, boolean useNewLine) {
for (int i = 1; i <= num; i++) {
if (useNewLine) {
System.out.println(str);
} else {
System.out.print(str);
}
}
}
public void repeat(String str, int num) {
repeat(str, num, true);
}
public void repeat(String str) {
repeat(str, 3, true);
}
|
- 코틀린에서는 아래와 같이 기본값(default paramter)을 지정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| fun main() {
repeat("Hello World")
}
fun repeat(
str: String,
num: Int = 3,
useNewLine: Boolean = true
) {
for (i in 1..num) {
if (useNewLine) {
println(str)
} else {
print(str)
}
}
}
|
- 물론 코틀린에도 Java와 동일하게 오버로드 기능은 있다.
3) named argument(parameter)
- 만약 위 예제에서 num은 3을 쓸거고 useNewLine을 false로 하고싶을때는 아래와 같이 매개변수 이름을 이용하여 지정할 수 있다.
- 호출자가 어떤 파라미터에 이 값을 넣을거야라고 지정하는 기능
1
2
3
| fun main() {
repeat("Hello World", useNewLine = false)
}
|
- Named Argument로 인해 builder를 직접 만들지 않고 builder의 장점을 가지게 된다
1
2
3
4
5
6
7
8
9
10
| fun main() {
printNameAndGender("FEMALE", "jeon")
printNameAndGender(name = "jeon", gender = "MALE")
}
fun printNameAndGender(name: String, gender: String) {
println(name)
println(gender)
}
|
- Kotlin에서 Java 함수를 가져다 사용할 때는 named argument를 사용할 수 없다.
- 자바코드가 byte 코드로 변환될때 파라미터 이름이 보존되지않아서 사용 불가능하다.
4) 같은 타입의 여러 파라미터 받기(가변인자)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| == Java ==
public static void printAll(String... strings) {
for (String str : strings) {
System.out.println(str);
}
}
== Kotlin ==
fun main() {
printAll("A", "B", "C")
val array = arrayOf("A", "B", "C")
printAll(*array) // 배열을 가변인자에 넣어줄때는 * 를 붙여준다.
// *는 스프레드 연산자로 배열을 풀어주는 기능
}
fun printAll(vararg strings: String) {
for (str in strings) {
println(str)
}
}
|
섹션4. 코틀린에서의 OOP
9강 - 코틀린에서 클래스를 다루는 방법
1) 클래스와 프로퍼티
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| == Java ==
public class JavaPerson {
private final String name;
private int age;
public JavaPerson(String name, int age) {
this(name, age);
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
== Kotlin ==
class Person constructor(name: String, age: Int){
val name: String = name
var age: Int = age
}
|
- 생성자로부터 주입되는 값을 이용하여 타입이 추론 되므로 타입 또한 생략 가능하다.
1
2
3
4
| class Person constructor(name: String, age: Int){
val name = name
var age = age
}
|
- Property(프로퍼티) => 필드 + Getter + Setter
- 코틀린은 필드만 작성하면 프로퍼티의 나머지인 Getter, Setter를 자동으로 생성해준다.
- constructor 라는 keyword(지시어)는 생략 가능하다.
1
2
3
4
| class Person (name: String, age: Int) {
val name = name
var age = age
}
|
- 코틀린에서는 생성자를 만들어줄때 프로퍼티로 선언과 초기화를 동시에 해줄 수 있다.
1
2
3
4
5
| class Person (val name: String, var age: Int) {
}
class Person (val name: String, var age: Int) // body 가 없으므로 생략 가능
|
- ‘.필드’를 통해 getter와 setter를 바로 호출한다.(Java 클래스에 대해서도 동일)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| fun main() {
val person = Person("jeon", 100)
println(person.name) // getter
person.age = 10 // setter
println(person.age) // getter
val person = JavaPerson("jeon", 100)
println(person.name) // getter
person.age = 10 // setter
println(person.age) // getter
}
class Person(
val name: String,
var age: Int
)
|
2) 생성자와 init
- 자바에서는 다음과 같이 생성자에서 검증을 하고 필드값을 초기화 해줄 수 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
| public class JavaPerson {
private final String name;
private int age;
public JavaPerson(String name, int age) {
if (age <= 0) {
throw new IllegalArgumentException(String.format("나이는 %s일 수 없습니다", age));
}
this.name = name;
this.age = age;
}
}
|
- 코틀린에서는 다음과 같이 init (초기화) 블록에 작성할 수 있다.
- init 블록은 생성자가 호출되는 시점에 호출된다.
- 값을 적절히 만들어주거나, validation 하는 로직이 들어감
1
2
3
4
5
6
7
8
9
10
| class Person(
val name: String,
var age: Int
) {
init { // 클래스가 초기화되는 시점에 한 번 호출되는 block (여기에 검증 로직 구현)
if (age > 0) {
throw IllegalArgumentException("나이는 ${age}이;ㄹ 수 없습니다")
}
}
}
|
주 생성자(Primary Constructor)
- Kotlin에서 주 생성자는 클래스 정의와 함께 선언되는 생성자이다.
- 반드시 존재해야 하며, 만약 파라미터가 없는 경우 생략 가능하다.
- 주 생성자는 클래스 이름 바로 뒤에 선언되며, 클래스 인스턴스를 생성할 때 반드시 호출되는 기본 생성자입니다.
주 생성자의 선언 방법
- 주 생성자는 class 키워드 뒤에 선언되며, 클래스 이름과 함께 파라미터를 정의한다.
- 이 파라미터들은 클래스의 프로퍼티로 바로 사용할 수 있습니다.
1
| class Person(val name: String, var age: Int)
|
주 생성자의 특징:
- 클래스 정의와 함께 선언된다.
- 자동으로 프로퍼티가 생성된다.(주 생성자의 파라미터가 자동으로 val 또는 var로 변환)
- init 블록을 통해 초기화 작업을 수행할 수 있다.
부 생성자(Secondary Constructor)
- Kotlin에서는 부 생성자도 정의할 수 있다.
- 있을수도 있고, 없을수도 있다.
- 부 생성자는 주 생성자 외에 추가로 선언되는 생성자로, 주 생성자에서 할 수 없는 다른 초기화 작업을 추가할 때 사용된다.
- 최종적으로 주생성자를 this로 호출해야 한다.
부 생성자 선언 방법
- 부 생성자는 constructor 키워드를 사용하여 선언하며, 주 생성자 없이 사용할 수 있다.(body를 가질 수 있기 때문에)
- 부 생성자는 여러 개를 정의할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class Person(
val name: String,
var age: Int
) {
init {
if (age > 0) {
throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
}
}
constructor(name: String) : this(name, 1) // this를 이용하여 주 생성자를 호출
// 두번째 부 생성자에서는 첫번째 부 생성자를 this(name,11)를 통해 부른다.
// 첫번째 부 생성자가 주 생성자를 호출하므로 두번째 부 생성자도 결론적으로는 주 생성자를 호출하게 된다.
// 본문은 역순으로 실행된다
constructor(name: String, gender: String) : this(name,11) {
println("부생성자 2")
}
}
|
- 그런데 사실.. 코틀린에서는 부생성자보다는 default parameter를 권장한다.
1
2
3
4
5
6
7
8
9
10
11
| class Person(
val name: String = "jeon",
var age: Int = 1
) {
init {
println("init!!!")
if (age < 0) {
throw IllegalArgumentException("나이는 ${age}일 수 없다.")
}
}
}
|
- Converting과 같은 경우 부생성자를 사용할 수 있지만, 그보다는 정적 팩토리 메소드를 추천한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| class Person private constructor(
val name: String,
var age: Int
) {
init {
println("초기화 블럭 실행")
if (age < 0) {
throw IllegalArgumentException("no")
}
}
companion object {
// 정적 팩토리 메소드
fun createWithDefaultValues(): Person {
return Person("gil", 10)
}
fun createWithName(name: String): Person {
return Person(name, 10)
}
}
}
fun main() {
val person1 = Person.createWithDefaultValues() // 기본값을 사용하는 생성
val person2 = Person.createWithName("Alice") // 이름만 지정하는 생성
}
|
- companion object는 Kotlin에서 클래스 내부에 정의할 수 있는 동반 객체를 의미한다.
- companion object는 클래스의 인스턴스 없이도 접근할 수 있는 정적 객체이다.
- 이를 사용하면 자바의 static 메소드처럼, 클래스 수준에서 동작하는 메소드나 속성들을 정의할 수 있다.
3) 커스텀 getter, setter
- 나이가 성인인지 확인하는 함수를 구현한다고 할때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| == 자바 ==
public boolean isAdult() {
return this.age >= 20;
}
== 코틀린 ==
class Person(
val name: String = "gil",
var age: Int = 10
) {
init {
println("초기화 블럭 실행")
if (age < 0) {
throw IllegalArgumentException("no")
}
}
fun isAdult(): Boolean {
return this.age >= 20
}
}
class Person(
val name: String = "gil",
var age: Int = 10
) {
init {
println("초기화 블럭 실행")
if (age < 0) {
throw IllegalArgumentException("no")
}
}
val isAdult: Boolean // 커스텀 getter: Person 클래스에 프로퍼티가 있는것처럼 보여주는 것
get() = this.age>=20
val isAdult: Boolean // 이것도 가능
get() {
return this.age>=20
}
}
|
- 두 표현 방식은 프로퍼티처럼 접근하냐, 함수처럼 접근하냐의 차이가 있음
- 객체의 속성이라면 커스텀 getter, 그렇지 않다면 함수
- 예를 들어, isAdult 는 이 사람이 성인인가라는 속성을 확인하는 것처럼 보이기에 커스텀 getter 사용
4) backing field
- 위에서 커스텀 getter를 사용하면 자기 자신을 변형해 줄 수도 있다!
1
2
3
4
5
6
7
8
9
10
11
12
| class Person(
name: String = "jeon",
// 여기에 val을 붙여주면 코틀린이 알아서 name을 프로퍼티로 만들어준다.
// 그러므로 val을 빼서 그냥 생성자의 매개변수로 취급되게 만들어준다.
var age: Int = 1
) {
val name = name // 생성자에서 받은 값을 필드에 할당
get() = field.uppercase() // name으로 적으면 getter -> name -> getter -> name 무한 루프 발생하기 때문에 field 를 사용
// field를 하면 진짜 name을 가르키는 것을 의미하게 됨
// field는 무한루프를 막기 위한 예약어로 자기 자신을 가리킴
// 자기 자신을 가리키는 보이지 않는 field다라고해서 backing field라고 부름
}
|
- 하지만 커스텀 getter에서 backing field를 사용하는 경우는 드물다. 위 케이스는 아래와 같이도 구현 가능하다.
1
2
3
4
5
6
7
8
9
10
11
| class Person(
var name: String = "jeon",
var age: Int = 1
) {
// case1
fun getUppercaseName(): String = this.name.uppercase()
// case2
val upperCaseName: String
get() = this.name.uppercase()
}
|
- name을 set할때 대문자로 바꿔주는 케이스를 구현해보자.(커스텀 Setter)
1
2
3
4
5
6
7
8
9
| class Person(
name: String = "gil",
var age: Int = 10
) {
var name = name // setter 써야하니 var
set(value) {
field = value.uppercase() // 들어온 value 를 대문자로 바꿔서 실제 필드에 집어넣어준다라는 의미가 됨
}
}
|
- 사실 Setter 자체를 지양하기 때문에 커스텀 Setter도 잘 안쓴다.
- => Setter 보다는 update같은 함수를 만들어서 그안에서 값을 업데이트 시켜주는게 훨씬 더 깔끔한 코드를 작성할 수 있다보니, Setter 자체를 잘 안쓰게되고 커스텀 Setter도 잘 안쓰게 된다.
Reference