https://www.inflearn.com/course/java-to-kotlin/dashboard 를 수강하고 학습한 내용을 간단하게 정리한다.
1강. 코틀린에서 변수를 다루는 방법
var과 val의 차이점
- var: 바꿀 수 있는 변수 (ex) var number1 = 10L
- val: 바꿀 수 없는 변수 (ex) val number2 = 10L
- 코틀린에서는 모든 변수에 수정 가능 여부(var/val)를 명시해주어야 한다.
- 코틀린에서는 타입을 의무적으로 작성하지 않아도 된다.
- 하지만 작성이 가능함 (ex) var number1: Long = 10L
- 초기값을 지정해주지 않는 경우
- var number1 -> 오류
- 초기값을 지정해주지 않는 경우 무조건 타입을 명시해줘야 한다.
- var number: Int
- val 변수에 초기값을 지정해주지 않는 경우 처음에 한하여 초기값을 넣어줄 수 있다.
- val number2: Int
number2 = 10L
- val number2: Int
- var number1 -> 오류
- val인 경우에도 컬렉션에 원소 추가가 가능하다. (Java 와 동일)
Kotlin에서의 Primitive Type
- Java에서는 long은 primitive type, Long은 reference type
- Kotlin에서는 구분하고 있지 않음. Long으로 표현해도 Kotlin에서 필요하다면 알아서 내부적으로 primitive type으로 변경해서 처리해줌
- 즉, 프로그래머가 boxing/unboxing을 고려하지 않아도 되도록 kotlin이 알아서 처리해준다.
Kotlin에서의 nullable 변수
- 기본적으로 모든 변수는 null이 들어갈 수 없음
- var number3: Long? = null -> 이와 같이 ?를 붙여주는게 nullable 이라는 의미
- Kotlin에서는 nullable 변수면 ?를 붙여줘야함
Kotlin에서의 객체 인스턴스화
- Kotlin에서는 객체 인스턴스화를 할 때 new를 붙이지 않는다.
- val person = Person("홍길동")
2. 코틀린에서 null을 다루는 방법
Kotlin에서의 null 체크
1. Java에서의 코드
public boolean startsWithA1(String str) {
if (str == null) {
throw new IllegalArgumentException("null이 들어왔습니다.");
}
return str.startsWith("A");
}
2. Kotlin에서의 코드
fun startsWithA1(str: String?): Boolean {
if (str == null) {
throw IllegalArgumentException("null이 들어왔습니다.")
}
return str.startsWith("A")
}
// 아래와 같이 null 일 수 있는 값에 대해 함수 호출하려고 하면 오류남
fun startsWithA2(str: String?): Boolean {
return str.startsWith("A") // 오류
}
// 이렇게 위에서 null 체크를 해주면 허용
fun startsWithA2(str: String?): Boolean {
if (str == null) {
return false
}
return str.startsWith("A") // 오류
}
Safe Call과 Elvis 연산자
null이 가능한 타입만을 위한 기능
1. Safe Call: null이지 않을 때만 호출. null인 경우 null로 리턴
val str: String? = "ABC"
str.length // 불가능
str?.length // 가능
2. Elvis 연산자: 앞의 값이 null이면 뒤의 값을 사용
val str: String? = "ABC"
str?.length ?: 0 // null이면 0
그래서 위의 코드를 kotlin스럽게 바꾸면?
fun startsWithA1(str: String?): Boolean {
return str?.startsWith("A")
?: throw IllegalArgumentException("null이 들어왔습니다")
}
이것도 kotlin으로 바꿀 수 있음
// Java
public long calculate(Long number) {
if (number == null) return 0;
}
// kotlin
fun calculate(number: Long?): Long {
number ?: return 0
}
널 아님 단언!!
nullable type이지만, 절대 null이 될 수 없는 경우
fun startsWithA1(str: String?): Boolean {
return str!!.startsWith("A")
}
-> 근데 만약 null이 들어오면? NPE가 남
플랫폼 타입
Kotlin에서 Java 코드를 가져다 사용할 때 어떻게 처리될까?
아래처럼 Java 상에서 Nullable이면 person.name 못넣음. NotNull 이면 넣을 수 있음
이처럼 Java의 Annotation을 사용하면 Kotlin에서는 이를 이해할 수 있다.
만약 annotation이 없다면? Kotlin에서는 알 수 없음. 이런 타입을 "플랫폼 타입"이라고 한다.
오류도 나지 않고 runtime에 NPE가 난다.
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
@Nullable
public String getName() {
return name;
}
}
fun main() {
val person = Person("AAA")
startsWithA(person.name)
}
fun startsWithA(str:String): Boolean {
return str.startsWith("A")
}
3. 코틀린에서 Type을 다루는 방법
기본 타입
Java와 동일 Byte, Short, Int, Long, Float, Double
1. 코틀린에서는 선언된 기본값을 보고 타입을 추론한다.
val num1 = 2 // int
val num2 = 2L // long
2. 코틀린에서는 기본 타입간의 변환은 명시적으로 이루어져야 한다. <-> Java에서는 암시적 변환
val num1 = 4
val num2: Long = num1 // Type mismatch compile error
val num2: Long = num1.toLong() // success
타입 캐스팅
기본 타입이 아닌 경우 타입 캐스팅
| value가 Type이면 | value가 Type이 아니면 | |
| value is Type | return true | return false |
| value !is Type | return false | return true |
| value as Type | Type으로 타입 캐스팅 | 예외 발생 |
| value as? Type | Type으로 타입 캐스팅 | null (+ value가 null이면 null) |
// java
public void printAgeIfPerson(Object obj) {
if (obj instanceof Person) {
Person person = (Person) obj;
System.out.println(person.getAge());
}
}
// kotlin
fun printAgeIfPerson(obj: Any) {
if (obj is Person) {
// as Person은 생략 가능
// 스마트 캐스트: 컨택스트를 분석해서 Person인지 체크를 해줌 -> 타입 인지
val person = obj as Person
println(person.age)
}
}
Kotlin의 3가지 특이한 타입
Any
- Java의 Object 역할 (모든 객체의 최상위 타입)
- 모든 Primitive Type의 최상위 타입도 Any이다.
- Any 자체로는 null을 포함할 수 없어 null을 포함하고 싶다면, Any?로 표현
- Any에 equals / hashCode / toString 존재
Unit
- Java의 void와 동일한 역할
- void와 다르게 Unit은 그 자체로 타입 인자로 사용 가능하다. (void는 Void 클래스 사용 필요)
- 함수형 프로그래밍에서 Unit은 단 하나의 인스턴스만 갖는 타입을 의미. 즉, 코틀린의 Unit은 실제 존재하는 타입이라는 것을 표현.
Nothing
- 함수가 정상적으로 끝나지 않았다는 사실을 표현하는 역할
- 무조건 예외를 반환하는 함수 / 무한 루프 함수 등
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
String Interpolation, String indexing
문자열과 변수 연결
val person = Person("홍길동", 100)
println("이름 : ${person.name}")
여러 줄에 걸친 문자열
val withoutIndent =
"""
ABC
123
234
""".trimIndent() // trimIndent는 앞에 공백 제거용
println(withoutIndent)
문자열의 특정 문자 가져오기
val str = "ABCDE"
val ch = str[1] // java에서는 charAt
4. 코틀린에서 연산자를 다루는 방법
단항 연산자 / 산술 연산자
Java와 동일
단, Java와 다르게 객체를 비교할 때 비교 연산자를 사용하면 자동으로 compareTo를 호출
비교 연산자와 동등성, 동일성
- 동일성 Identity: 완전히 동일한 객체인가. 즉, 주소가 같은가
- 동등성 Equality: 두 객체의 값이 같은가
| 동일성 | 동등성 | |
| Java | == | equals |
| Kotlin | === | == (간접적으로 equals를 호출) |
논리 연산자 / 코틀린에 있는 특이한 연산자
논리 연산자
- Java와 동일
- Java처럼 Lazy 연산을 수행 (if (fun1() || fun2()) 일 때 fun1이 true면 fun2는 실행X)
코틀린에 있는 특이한 연산자
- in / !in : 컬렉션이나 범위에 포함되어 있다. 포함되어 있지 않다
- a..b: a부터 b까지의 범위 객체를 생성
- a[i]: a에서 특정 index i로 값을 가져온다
- a[i] = b: a의 특정 index i에 b를 넣는다
연산자 오버로딩
코틀린은 객체마다 연산자를 직접 정의할 수 있다.
val money1 = Money(1_000L)
val money2 = Money(2_000L)
println(money1 + money2) // 3000 -> toString이 구현되어 있기 때문에 가능
5. 코틀린에서 제어문을 다루는 방법
if문
- Java에서 if-else는 Statement인데, Kotlin에서는 Expression이다.
- Statement: 프로그램의 문장, 하나의 값으로 도출되지 않는다
- Expression: 하나의 값으로 도출되는 문장 (ex. Java의 3항 연산자)
- Kotlin에서는 if-else를 expression으로 사용할 수 있기 때문에 3항 연산자가 없다.
fun validateScoreIsNotNegative(score: Int) { // Unit(=void) 리턴 생략
if (score < 0) {
throw IllegalArgumentException("${score}는 0보다 작을 수 없습니다") // new 없음
}
}
// kotlin에서 if-else는 expression이기 때문에 그대로 return이 가능하다
fun getPassOrFail(sroce: Int): String {
return if (score >= 50) {
"P"
} else {
"F"
}
}
Expression과 Statement
// java
if (0 <= score && score <= 100)
// kotlin
if (score in 0..100)
if (score !in 0..100)
switch와 when
Kotlin에서는 switch문이 없고 when문이 있다.
// kotlin에서는 swith가 없고 when을 사용한다.
fun getGradeWithSwitch(score: Int): String {
return when (score) { // when도 expression
in 90..99 -> "A"
in 80..89 -> "B"
in 70..79 -> "C"
else -> "D"
}
}
// 조건부에는 어떤 expression도 들어갈 수 있다.
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("오답입니다.")
}
}
// when에 들어오는 값은 없을 수도 있다. early return처럼 동작
fun judgeNumber2(number: Int) {
when {
number == 0 -> println("주어진 숫자는 0입니다")
number % 2 == 0 -> println("주어진 숫자는 짝수입니다")
else -> println("주어진 숫자는 홀수입니다")
}
}
6. 코틀린에서 반복문을 다루는 방법
for-each문
fun main() {
val numbers = listOf(1L, 2L, 3L)
for (number in numbers) { // java에서는 :, kotlin에서는 in
println(number)
}
}
전통적인 for문 & Progression과 Range
- downTo, step도 함수 (중위 호출 함수)
- Kotlin에서 전통적인 for문은 등차수열을 사용한다.
// 1, 2, 3
fun main() {
for (i in 1..3) {
println(i)
}
}
// 3, 2, 1
fun main() {
for (i in 3 downTo 1) {
println(i)
}
}
// 두 칸씩 올라가는 경우
fun main() {
for (i in 1..5 step 2) {
println(i)
}
}
while문
while, do-while문은 모두 java와 동일
var i = 1
while (i <= 3) {
println(i)
i++
}
7. Kotlin에서 예외를 다루는 방법
try catch finally 구문
java와 문법적으로 동일
fun parseIntOrThrow(str: String): Int {
try {
return str.toInt()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("주어진 ${str}는 숫자가 아닙니다")
}
}
// 실패 시 null 반환
fun parseIntOrThrowV2(str: String): Int {
return try { // try-catch 문도 expression으로 취급
str.toInt()
} catch (e: NumberFormatException) {
return null
}
}
Checked Exception과 Unchecked Exception
Java에서는 IOException이 던져질 수 있다는 것을 표시해줘야 함
Kotlin에서는 아래에서 빨간줄 없음 -> Kotlin에서는 checked exception과 unchecked exception을 구분하지 않는다. 모두 Unchecked exception이다.
class FilePrinter {
fun readFile() {
val currentFile = File(".")
val file = File(currentFile.absolutePath + "/a.txt")
val reader = BufferReader(FileReader(file))
println(reader.readLine())
reader.close()
}
try with resources
// Java
public void readFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) { // 자동으로 외부자원 닫아줌
System.out.println(reader.readLine())
}
}
// Kotlin
// try with resources 대신 use라는 inline 확장 함수 사용
fun readFile(path: String) {
BufferedReader(FileReader(path)).use { reader ->
println(reader.readLin())
}
}
8. 코틀린에서 함수를 다루는 방법
함수 선언 문법
// Step1 두 정수를 받아 더 큰 정수를 반환하는 예제
fun max(a: Int, b: Int): Int {
return if (a > b) {
a
} else {
b
}
}
// Step2 함수가 하나의 결과값이면 block 대신 = 사용 가능
fun max(a: Int, b: Int): Int =
if (a > b) {
a
} else {
b
}
// Step3 함수를 쓸 때 중괄호 대신 =을 썼기 때문에 타입 추론이 가능해 return 타입 생략 가능
// block {}을 사용하는 경우에는 반환 타입이 Unit이 아니면 명시적으로 작성해주어야 함
fun max(a: Int, b: Int) = if (a > b) a else b
함수는 클래스 안에 있을 수도
파일 최상단에 있을 수도 있고
한 파일 안에 여러 함수들이 있을 수도 있다.
default parameter
밖에서 파라미터를 넣어주지 않으면 기본값을 사용하도록 해준다. (Java의 오버로드 기능을 대체. Kotlin에서 오버로드 있음)
// 주어진 문자열을 N번 출력
fun repeat(
str: String,
num: Int = 3,
useNewLine: Boolean = true
) {
for (i in 1..num) {
if (useNewLine) {
println(str)
} else {
println(str)
}
}
}
named argument (parameter)
장점: builder를 직접 만들지 않고 builder의 장점을 가지게 된다. 어떤 param에 어떤 값을 넣어주는지 명확해짐
참고: Kotlin에서 Java 함수를 가져다 사용할 대는 named argument를 사용할 수 없다. (JVM상에서 Java가 바이트 코드로 변환됐을 때 parameter 이름을 보존하고 있지 않기 때문)
fun main() {
// 셋 모두 같은 결과
repeat("Hello World")
repeat("Hello World", 3, false)
repeat("Hello World", useNewLine = false) // named argument
}
같은 타입의 여러 파라미터 받기 (가변인자)
// 문자열을 N개 받아 출력
fun printAll(vararg strings: String) { // vararg
for (str in strings) {
println(str)
}
}
fun main() {
// string 여러개 넣어줄 때
printAll("A", "B")
// 배열로 넣어줄 때 * (spread 연산자: 배열 안에 있는 것들을 꺼내줌)
var array = arrayOf("A", "B")
printAll(*array)
}
9. 코틀린에서 클래스를 다루는 방법
클래스와 프로퍼티
// public이 default이기 때문에 생략 가능
// constructor에서 타입 선언되어 있기 때문에 필드에서 타입 생략 가능
// 코틀린에서는 필드만 만들면 getter, setter를 자동으로 만들어 줌 (프로퍼티 = getter + setter)
class Person constructor(name: String, age: Int) {
val name
var age
}
// constructor 생략 가능
class Person (name: String, age: Int) {
val name
var age
}
// 생성자에서 프로퍼티를 만들어 줄 수 있음
// body는 아무것도 없기 때문에 {} 생략 가능
class Person(
val name: String,
var age: Int
)
// 코틀린에선 .필드를 통해 getter와 setter를 바로 호출한다
// java 클래스에 대해서도 .필드로 getter, setter를 사용
fun main() {
val person = Person("홍길동", 100)
println(person.name)
person.age = 10
}
생성자와 init
class Person(
val name: String,
val age: Int
) {
// init 블록: 생성자가 호출되는 시점에 호출
init {
if (age <= 0) {
throw IllegalArgumentException("ㅇㅇㅇ")
}
println("초기화 블록")
}
// 주생성자(primary constructor)는 반드시 존재해야 한다. 단, 파라미터가 하나도 없다면 생략 가능
// 2번째 이후의 부생성자는 아래와 같이 만들어짐
// 부생성자는 최종적으로 주생성자를 this로 호출해야 한다.
constructor(name: String): this(name, 1) {
println("첫 번째 부생성자")
}
// 부생성자는 body를 가질 수 있다.
constructor(): this("홍길동", 1) {
println("두 번째 부생성자")
}
}
생성자의 호출은 역순으로 된다.
// 두 번째 부생성자로 객체를 생성한 경우 출력
초기화 블록
첫 번째 부생성자
두 번째 부생성자
다만, 추가 constructor를 만드는 것보다 default parameter를 kotlin에서는 권장한다.
class Person(
val name: String = "홍길동",
var age: Int = 1,
)
Converting과 같은 경우 부생성자를 사용할 수 있지만, 그보다는 정적 팩토리 메소드를 추천한다.
커스텀 getter, setter
커스텀 getter
class Person(
val name: String,
val age: Int
) {
init {
if (age <= 0) {
throw IllegalArgumentException("ㅇㅇㅇ")
}
println("초기화 블록")
}
fun isAdult(): Boolean {
return this.age >= 20
}
// custom getter: 위 함수를 아래와 같이 만들 수 있다.
val isAdult: Boolean
get() = this.age >= 20
// 이름을 대문자로 바꿔서 반환
val name: String = name // 주생성자에서 받은 name을 불변 프로퍼티 name에 바로 대입
// name으로 하면 무한루프 발생 -> backing field 자기 자신을 가리키는 예약어
get() = field.uppercase()
// backing field 사용해서 custom getter 만들일 거의 없음
val upperCaseName: String
get() = this.name.uppercase()
}
커스텀 setter
class Person(
val name: String,
val age: Int
) {
init {
if (age <= 0) {
throw IllegalArgumentException("ㅇㅇㅇ")
}
println("초기화 블록")
}
var name = name
set(value) {
field = value.uppercase()
}
}
10. 코틀린에서 상속을 다루는 방법
추상 클래스
Java, Kotlin 모두 추상 클래스는 인스턴스화 할 수 없다.
// 추상클래스
abstract class Animal(
protected val species: String,
// 프로퍼티를 override할 땐 추상 프로퍼티가 아니라면 open 붙여줘야 함
protected open val legCount: Int,
) {
abstract fun move()
}
// 상속받는다는 의미의 ':' (타입표시도 동일하지만 상속은 보통 앞에 띄어쓰기를 해줌)
class Cat(
species: String
) : Animal(species, 4) { // 상위 클래스의 생성자에 값을 넣어줌
// java는 annotation이었지만 Kotlin은 키워드
override fun move() {
println("고양이가 걸어간다")
}
}
class Penguin(
species: String
) : Animal(species, 2) {
private val wingCount: Int = 2
override fun move() {
println("펭귄이 움직인다")
}
// getter를 Override하기
override val legCount: Int
// super: 상위 클래스에 접근
get() = super.legCount + this.wingCount
}
인터페이스
- Java. Kotlin 모두 인터페이스를 인스턴스화 할 수 없다.
- Kotlin에서는 backing field가 없는 프로퍼티를 Interface에 만들 수 있다.
interface Flyable {
// default 키워드 없이 메소드 구현이 가능
fun act() {
println("파닥파닥")
}
}
interface Swimable {
// Kotlin에서는 backing field가 없는 프로퍼티를 Interface에 만들 수 있다.
val swimAbility: Int
fun act() {
println("어푸어푸")
}
}
class Penguin(
species: String
) : Animal(species, 2), Swimable, Flyable { // interface 구현도 ':'를 사용
private val wingCount: Int = 2
override fun move() {
println("펭귄이 움직인다")
}
override val legCount: Int
get() = super.legCount + this.wingCount
override fun act() {
// 중복되는 인터페이스를 특정할 때 "super<타입>.함수"를 사용
super<Swimable>.act()
super<Flyable>.act()
}
override val swimAbility: Int
get() = 3
}
클래스를 상속할 때 주의할 점
open class Base(
open val number: Int = 100
) {
init {
println("Base Class")
println(number)
}
}
class Derived(
override val number: Int
) : Base(number) {
init {
println("Derived Class")
}
}
Derived(300) 하면?
Base Class
0
Derived Class
- 왜 0일까?
- Intellij 경고: Acessing non-final property 'number' in constructor
- Derived를 인스턴스화하면 상위클래스인 Base의 생성자를 실행하는데, 이때 하위클래스인 Derived의 number를 호출하지만 초기화가 되어있지 않음
- 즉, 상위 클래스를 설계할 때 생성자 또는 초기화 블록에 사용되는 프로퍼티에는 open을 피해야 한다
상속 관련 지시어 정리
- final: override를 할 수 없게 한다. default로 보이지 않게 존재한다.
- open: override를 열어 준다.
- abstract: 반드시 override 해야 한다.
- override: 상위 타입을 오버라이드 하고 있다.
11. 코틀린에서 접근 제어를 다루는 방법
자바와 코틀린의 가시성 제어
- Java의 기본 접근 지시어는 default
Kotlin의 기본 접근 지시어는 public - Kotlin에서는 패키지라는 개념을 접근 제어에 사용하지 않기 때문에 모듈을 접근 제어하는 기능이 생겼다.
| Java | Kotlin | ||
| public | 모든 곳에서 접근 가능 | public | 모든 곳에서 접근 가능 |
| protected | 같은 패키지 또는 하위 클래스에서만 접근 가능 | protected | 선언된 클래스 또는 하위 클래스에서만 접근 가능 -> Kotlin에서는 Java와 다르게 패키지를 namespace를 관리하기 위한 용도로만 사용. 가시성 제어에는 사용되지 않기 때문. |
| default | 같은 패키지에서만 접근 가능 | internal | 같은 모듈에서만 접근 가능 -> 모듈: 한 번에 컴파일 되는 kotlin 코드 |
| private | 선언된 클래스 내에서만 접근 가능 | private | 선언된 클래스 내에서만 접근 가능 |
코틀린 파일의 접근 제어
코틀린은 .kt 파일에 변수, 함수, 클래스 여러개를 바로 만들 수 있다.
| 파일에서의 접근 제어 | |
| public | default값. 어디서든 접근할 수 있다. |
| protected | 파일(최상단)에는 사용 불가능 -> protected가 선언된 클래스 또는 하위 클래스에서 사용되는 지시어이기 때문 |
| internal | 같은 모듈에서만 접근 가능 |
| private | 같은 파일에서만 접근 가능 |
다양한 구성요소의 접근 제어
| 클래스에서의 접근 제어 | |
| public | default값. 어디서든 접근할 수 있다. |
| protected | 선언된 클래스 또는 하위 클래스에서만 접근 가능 |
| internal | 같은 모듈에서만 접근 가능 |
| private | 선언된 클래스 내에서만 접근 가능 |
| 생성자에서의 접근 제어 |
|
| public | default값. 어디서든 접근할 수 있다. |
| protected | 선언된 클래스 또는 하위 클래스에서만 접근 가능 |
| internal | 같은 모듈에서만 접근 가능 |
| private | 같은 파일에서만 접근 가능 |
- 생성자에 접근 지시어를 붙이려면 constructor를 써줘야 함
ex) class Cat internal constructor (...)
| 프로퍼티에서의 접근 제어 | |
| public | default값. 어디서든 접근할 수 있다. |
| protected | 파일(최상단)에는 사용 불가능 -> protected가 선언된 클래스 또는 하위 클래스에서 사용되는 지시어이기 때문 |
| internal | 같은 모듈에서만 접근 가능 |
| private | 같은 파일에서만 접근 가능 |
Java와 Kotlin을 함께 사용할 경우 주의할 점
- Internal은 바이트 코드 상 public이 된다. 때문에 Java 코드에서는 Kotlin 모듈의 internal 코드를 가져올 수 있다.
- Kotlin의 protected와 java의 protected는 다르다. java는 같은 패키지의 Kotlin protected 멤버에 접근할 수 있다.
12. 코틀린에서 object 키워드를 다루는 방법
static 함수와 변수
companion object
- static과 유사한 역할. 클래스와 동행하는 유일한 오브젝트
- companion object도 하나의 객체로 간주. 따라서 이름을 붙일 수도 있고, interface를 구현할 수도 있다. (이름을 안쓴다면 'Companion' 이라는 이름이 생략된 것)
- companion object에 유틸성 함수를 넣어도 되지만, 최상단 파일을 활용하는 것을 추천.
class Person private constructor(
var name: String,
var age: Int,
) {
// kotlin에는 static이 없음 -> companion object
companion object Factory {
// const가 없으면 런타임 시에 0이 할당되지만
// const를 붙이면 컴파일 시에 0이 할당됨
// 즉, 진짜 상수를 표현하고 기본타입과 string에만 붙여줄 수 있다.
private const val MIN_AGE = 0
fun newBaby(name: String): Person {
return Person(name, MIN_AGE)
}
// Java에서 호출할 때
// Person.Companion.secondBaby("abc");
// Person.secondBaby("abc"); // JvmStatic 붙이면 다음과 같이 Java에서 호출할 수 있다.
@JvmStatic
fun secondBaby(name: String): Person {
return Person(name, MIN_AGE + 1)
}
}
}
싱글톤
Kotlin에서는 싱글톤 클래스를 만들기 위해 object만 붙여주면 된다.
fun main() {
println(Singleton.a)
Singleton.a += 10
}
object Singleton {
var a: Int = 0
}
익명 클래스
특정 인터페이스나 클래스를 상속받은 구현체를 일회성으로 사용할 때 쓰는 클래스
// Movable이라는 interface가 있다고 할 때
fun main() {
moveSomething(object : Movable {
override fun move() {
println("move")
}
override fun fly() {
println("fly")
}
})
}
private fun moveSomething(movable: Movable) {
movable.move()
movable.fly()
}
13. 코틀린에서 중첩 클래스를 다루는 방법
중첩 클래스의 종류
Java에서?
- Static을 사용하는 중첩 클래스: 클래스 안에 static을 붙인 클래스
- Static을 사용하지 않는 중첩 클래스
- 내부 클래스: 클래스 안의 클래스
- 지역 클래스: 메소드 안의 클래스
- 익명 클래스: 일회성 클래스
코틀린의 중첩 클래스와 내부 클래스
Static을 사용하는 중첩 클래스: 바깥 클래스와 연결안됨. OuterClass.this.field1 이렇게 불러올 수 없음
Static을 사용하지 않는 중첩 클래스 - 내부 클래스: 바깥 클래스와 연결. OuterClass.this.field1 이렇게 불러올 수 있음
-> Static을 사용하는 내부 클래스를 사용하라
왜?
1. 내부 클래스는 숨겨진 외부 클래스 정보를 가지고 있어, 참조를 해지하지 못하는 경우 메모리 누수가 생길 수 있고, 이를 디버깅 하기 어려움
2. 내부 클래스의 직렬화 형태가 명확하게 정의되지 않아 직렬화에 있어 제한이 있다.
그래서 Kotlin에서는 이 규칙을 따르고 있다.
// 그냥 내부 클래스 쓰면 연결되지 않은 중첩 클래스가 만들어짐
class JavaHouse(
prviate val address: String,
private val livingRoom: LivingRoom
) {
class LivingRoom(
private var area: Double,
)
}
// inner를 붙이면 연결된 중첩 클래스가 만들어짐 (권장되지 않는 클래스 안의 클래스)
class House(
prviate val address: String,
private val livingRoom: LivingRoom
) {
inner class LivingRoom(
private var area: Double,
) {
val address: String
get() = this@House.address // 외부 클래스 접근
}
}
14. 코틀린에서 다양한 클래스를 다루는 방법
Data Class
계층간의 데이터를 전달하기 위한 DTO(Data Transfer Object)
// data를 붙이면 자동으로 equals, hashCode, toString 을 만들어줌
data class PersonDto(
val name: String,
val age: Int,
)
Enum Class
추가적인 클래스를 상속받을 수 없다. 인터페이스는 구현할 수 있으며, 각 코드가 싱글톤이다.
enum class Country(
private val code: String,
) {
KOREA("KO"),
AMERICA("US")
;
}
Enum + When 을 함께 썼을 경우
- 읽기 쉬운 코드
- 컴파일러가 country의 모든 타입을 알고 있어 다른 타입에 대한 로직(else)을 작성하지 않아도 된다.
- Enum에 변화가 있으면 IDE단에서 warning을 준다.
fun handleCountry(country: Country) {
when (country) {
Country.KOREA -> TODO()
Country.AMERICA -> TODO()
}
}
Sealed Class, Sealed Interface
상속이 가능하도록 추상클래스를 만들고 싶은데, 외부에서는 이 클래스를 상속받지 않았으면 좋겠을 경우
-> 하위 클래스를 봉인(seal)하자!
- 컴파일 타임 때 하위 클래스의 타입을 모두 기억한다. 즉, 런타임 때 클래스 타입이 추가될 수 없다.
- 하위 클래스는 sealed class와 같은 패키지에 있어야 한다.
- Enum과 다른점: 클래스를 상속받을 수 있고 하위 클래스는 멀티 인스턴스가 가능하다. (<-> 싱글톤)
sealed class Car(
val name: String,
val price: Long
)
class Avante : Car("아반떼", 1_000L)
class Sonata : Car("쏘나타", 1_000L)
// enum과 유사해서 when과 함께 주로 사용된다.
private fun handleCar(car: Car) {
when (car) {
is Avante -> TODO()
is Sonata -> TODO()
}
}
15. 코틀린에서 배열과 컬렉션을 다루는 방법
배열
fun main() {
val array = arrayOf(100, 200)
for (i in array.indices) {
println("${i} ${array[i]}")
}
for ((idx, value) in array.withIndex()) {
println("$idx $value")
}
}
코틀린에서의 Collection - List, Set, Map
- 가변(Mutable) 컬렉션: 컬렉션에 element를 추가, 삭제할 수 있다.
- MutableList, MutableSet, MutableMap
- 불변 컬렉션: 컬렉션에 element를 추가, 삭제할 수 없다.
- List, Set, Map
- 불변 컬렉션이라 하더라도 reference type인 element의 필드는 바꿀 수 있다.
// list
fun main() {
val numbers = listOf(100, 200)
val emptyList = emptyList<Int>() // 타입추론이 가능하면 <>를 생략 가능
println(numbers[0])
for (number in numbers) {
println(number)
}
for ((idx, number) in numbers.withIndex()) {
println("$index $number")
}
// 가변List
// 기본 구현체는 arrayList
val mutableNumbers = mutableListOf(100, 200)
mutableNumbers.add(300)
}
// Set
fun main() {
val numbers = setOf(100, 200)
for (number in numbers) {
println(number)
}
for ((index, number) in numbers.withIndex()) {
println("$index $number")
}
}
// Map
fun main() {
val map = mutableMapOf<Int, String>()
map[1] = "MONDAY"
map.put(2, "TUESDAY")
mapOf(1 to "MONDAY", 2 to "TUESDAY")
for (key in map.keys) {
println(key)
println(map[key])
}
for ((key, value) in map.entries) {
println(key)
println(value)
}
}
컬렉션의 null 가능성, Java와 함께 사용하기
- List<Int?>: 리스트에 null이 들어갈 수 있지만, 리스트는 절대 null이 아님
- List<Int>?: 리스트에는 null이 들어갈 수 없지만, 리스트는 null일 수 있음
- List<Int?>?: 리스트에 null이 들어갈 수도 있고, 리스트가 null일 수도 있음
- Kotlin쪽의 컬렉션이 Java에서 호출되면 컬렉션 내용이 변할 수 있음을 감안해야 한다.
- 혹은, 코틀린 쪽에서 Java에도 있는 기능인 Collections.unmodifiableXXX()를 활용하면 변경 자체를 막을 수는 있다.
16. 코틀린에서 다양한 함수를 다루는 방법
확장함수
Java로 만들어진 라이브러리를 유지보수, 확장할 때 Kotlin 코드를 덧붙이고 싶은 니즈
-> 어떤 클래스안에 있는 메소드처럼 호출할 수 있지만, 함수는 밖에 만들 수 있게 하자
// String 클래스를 확장
fun String.lastChar(): Char {
return this[this.length - 1]
}
// 사용할 때는 원래 String에 있는 멤버함수 처럼 사용할 수 있다.
val str: String = "ABC"
str.lastChar()
- 확장함수는 클래스에 있는 private 또는 protected 멤버를 가져올 수 없다.
- 멤버함수와 확장함수의 시그니처가 같다면 멤버 함수를 우선적으로 호출한다.
- 확장함수는 현재 타입을 기준으로 호출된다.
infix 함수
변수.함수이름 대신 '변수 함수이름 argument' 이렇게 호출해 주는 것
// 중위함수
infix fun Int.add2(other: Int): Int {
return this + other
}
// 중위함수 호출 시
3 add2 4
inline 함수
함수가 호출되는 대신, 함수를 호출한 지점에 함수 본문을 그대로 복사하는 것
inline fun Int.add(other: Int): Int {
return this + other
}
- 함수를 파라미터로 전달할 때에 오버헤드를 줄일 수 있다.
- 하지만 inline 함수의 사용은 성능 측정과 함께 신중하게 사용되어야 한다.
지역함수
함수 안에 함수를 선언하는 것
fun createPerson(..): Person {
fun validateName(name: String, fieldName: String) {
if (...)
}
validateName("name", "fieldname")
return Person()
}
- 함수로 추출하면 좋을 것 같은데, 이 함수를 지금 함수 내에서만 사용하고 싶을 때
- depth가 깊어지기도 하고, 코드가 그렇게 깔끔하지는 않다.
17. 코틀린에서 람다를 다루는 방법
코틀린에서의 람다
코틀린에서는 Java와 다르게 함수가 그 자체로 값이 될 수 있다. 변수에 할당할수도, 파라미터로 넘길 수도 있다
// 함수 이름이 없음 - 람다 (타입은 생략 가능)
val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
return fruit.name == "사과"
}
val isApple2 = { fruit: Fruit -> fruit.name == "사과" }
// 호출하는 방법
isApple(fruits[0])
isApple.invoke(fruit[0])
// parameter로 넘겨주는 경우
filterFruits(fruits, { fruit: Fruit -> fruit.name == "사과" })
// 가장 마지막 파라미터로 람다가 들어가는 경우 밖으로 빼줄 수도 있음
filterFruits(fruits) { fruit: Fruit -> fruit.name == "사과" }
// 타입 생략 가능
filterFruits(fruits) { fruit -> fruit.name == "사과" }
// parameter가 1개인 경우 it으로 사용 가능
filterFruits(fruits) { it.name == "사과" }
Closure
자바에서
String targetFruitName = "바나나";
targetFruitName = "수박";
filterFruits(fruits, (fruit) -> targetFruitName.equals(fruit.getName()));
// Variable used in lambda expression should be final or effectively final 오류
Kotlin에서
var targetFruitName = "바나나"
targetFruitName = "수박"
filterFruits(fruits) { it.name == targetFruitname }
- 코틀린은 람다가 시작하는 지점에 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고 있다.
- 이러한 데이터 구조를 Closure라고 부른다.
다시 try with resources
fun readFile(path: String) {
BufferedReader(FileReader(path)).use { reader ->
println(reader.readLine())
}
}
18. 코틀린에서 컬렉션을 함수형으로 다루는 방법
필터와 맵
val apples = fruits.filter { fruit -> fruit.name == "사과" }
val apples = fruits.filterIndexed { idx, fruit ->
println(idx)
fruit.name == "사과"
}
val applePrices = fruits.filter { fruit -> fruit.name == "사과" }
.map { fruit -> fruit.price }
val applePrices = fruits.filter { fruit -> fruit.name == "사과" }
.mapIndexed { idx, fruit ->
println(idx)
fruit.currentPrice
}
val values = fruits.filter { fruit -> fruit.name == "사과" }
.mapNotNull { fruit -> fruit.nullOrValue() }
다양한 컬렉션 처리 기능
// 조건 모두 만족시 true
val isAllApple = fruits.all { fruit -> fruit.name == "사과" }
// 조건을 모두 불만족시 true
val isAllApple = fruits.none { fruit -> fruit.name == "사과" }
// 조건을 하나라도 만족하면 true
val isAllApple = fruits.any { fruit -> fruit.name == "사과" }
fruits.count()
fruits.sortedBy { fruit -> fruit.currentPrice }
fruits.sortedByDescending { fruit -> fruit.currentPrice }
fruits.distinctBy { fruit -> fruit.name }.map { fruit -> fruit.name }
fruits.first() // 첫번째 값을 가져오는데 null이면 안됨 (exception)
fruits.firstOrNull() // 첫번째 값을 가져오는데 없으면 null
fruits.last()
fruits.lastOrNull()
List를 Map으로
// 과일이름 -> List<과일>
val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
// id -> 과일
val map: Map<Long, Fruit> = fruits.associatedBy { fruit -> fruit.id }
// 과일이름 -> List<출고가>
val map: Map<String, List<Long>> = fruits
.groupBy({ fruit -> fruit.name }, { fruit -> fruit.factoryPrice })
// id -> 출고가
val map: Map<Long, Long> = fruits
.associateBy({ fruit -> fruit.id }, { fruit -> fruit.factoryPrice })
중첩된 컬렉션 처리
fruitsInList.flatMap { list ->
list.filter { fruit -> fruit.factoryPrice == fruit.currentPrice }
}
// List<List<Fruit>> 을 List<Fruit>로 변경
fruitsInList.flatten()
19. 코틀린의 이모저모
Type Alias와 as import
typealias FruitFilter = (Fruit) -> Boolean
fun filterFruits(fruits: List<Fruit>, filter: FruitFilter) {}
data class UltraSuperGuardianTribe(val name: String)
typealias USGTMap = Map<String, UltraSuperGuardianTribe>
// 다른 패키지의 같은 이름 함수를 동시에 가져오고 싶은 경우
import com.a.printlnHelloWorld as aPrintlnHelloWorld
import com.b.printlnHelloWorld as bPrintlnHelloWorld
fun main() {
aPrintHelloWorld()
bPrintHelloWorld()
}
구조분해와 componentN 함수
// 구조분해: 복합적인 값을 분해하여 여러 변수를 한 번에 초기화하는 것
// data class는 componentN을 자동으로 만들어주기 때문에 별도 설정없이 이렇게 사용이 가능
val person = Person("가나다", 100)
val (name, age) = person
// 위와 같은 코드
val person = Person("가나다", 100)
val name = person.component1()
val age = person.component2()
// data class가 아닌데 구조분해를 사용하고 싶은 경우: componentN을 구현
class Person(
val name: String,
val age: Int
) {
operator fun component1(): String {
return this.name
}
operator fun component2(): Int {
return this.age
}
}
Jump와 Label
- return: 기본적으로 가장 가까운 enclosing function 또는 익명함수로 값이 반환된다
- break: 가장 가까운 루프가 제거된다
- continue: 가장 가까운 루프를 다음 step으로 보낸다
// forEach문에서는 continue, break를 쓸 수 없다.
// break를 꼭 쓰고 싶다면 다음과 같이 작성해야 한다.
run {
numbers.forEach { number ->
if (number == 2) {
return@run
}
}
}
// continue를 꼭 쓰고 싶다면 다음과 같이 작성해야 한다.
number.forEach { number ->
if (number == 2) {
return@forEach
}
}
- Label: 특정 expression에서 라벨이름@을 붙여 하나의 라벨로 간주하고 break, continue, return 등을 사용하는 기능
// break문이 가장 가까운 for문이 아닌 loop가 붙은 가장 상위의 for문을 멈춤
loop@ for (i in 1..100) {
for (j in 1..100) {
if (j == 2) {
break@loop
}
}
}
TakeIf와 TakeUnless
fun getNumberOrNull(): Int? {
return if (number <= 0) {
null
} else {
number
}
}
// takeIf: 주어진 조건을 만족하면 그 값이, 그렇지 않으면 null이 반환된다.
fun getNumberOrNull(): Int? {
return number.takeIf { it > 0 }
}
// takeUnless: 주어진 조건을 만족하지 않으면 그 값이 그렇지 않으면 null이 반환된다.
fun getNumberOrNull(): Int? {
return number.takeUnless { it <= 0 }
}
20. 코틀린의 scope function
scope function이란 무엇인가
scope function: 람다를 사용해 일시적인 영역을 형성하는 함수. 코드를 더 간결하게 만들거나 emthos chaning에 활용하는 함수를 scope function이라고 한다.
// let: 확장함수. 람다를 받아, 람다 결과를 반환한다.
fun printPerson(person: Person?) {
person?.let {
println(it.name)
println(it.age)
}
}
scope function의 분류
let, run, also, apply, with
// 결과: age, it 사용
person.let { it.age }
// 결과: age, this 사용
person.run { this.age }
// 결과: person, it 사용
person.also { it.age }
// 결과: person, this 사용
person.apply { this.age }
// this: 생략이 가능한 대신, 다른 이름을 붙일 수 없다.
person.run { age }
// it: 생략이 불가능한 대신, 다른 이름을 붙일 수 있다.
person.let { p -> p.age }
// with: this 사용
with(person) {
println(name)
println(this.age)
}
언제 어떤 scope function을 사용해야 할까
// 1. let: 하나 이상의 함수를 call chain 결과로 호출 할 때
val strings = listOf("a", "b")
strings.map { it.length }
.filter { it > 3 }
.let(::println)
// 2. let: non-null 값에 대해서만 code block을 실행시킬 때
val length = str?.let {
println(it.uppercase())
it.length
}
// 3. let: 일회성으로 제한된 영역에 지역 변수를 만들 때
val numbers = listOf("a", "b")
val modifiedFirstItem = numbers.first()
.let { firstItem -> if (firstItem.length >= 5) firstItem else '.' }.uppercase()
// run: 객체 초기화와 반환 값의 계산을 동시에 해야 할 때
val person = Person("a", 100).run(personRepository::save)
// apply: Test Fixture를 만들 때
fun createPerson(name: String, age: Int, hobby: String): Person {
return Person(
name = name,
age = age,
).apply {
this.hobby = hobby
}
}
// apply: call chain을 수정할 때
person.apply { this.growOld() }.let { println(it) }
// also: 객체를 수정하는 로직이 call chain 중간에 필요할 때
mutableListOf("a", "b")
.also { println(it) }
.add("four")
// with: 특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때
return with(person) {
PersonDto(
name = name,
age = age,
)
}
'Java & Spring' 카테고리의 다른 글
| [Kafka] 심화 개념 및 이해 (0) | 2024.06.17 |
|---|---|
| [Kafka] 기본 개념 및 이해 (0) | 2024.04.16 |
| [쿠버네티스/도커] 3 - 쿠버네티스에서의 도커, 젠킨스, 프로메테우스, 그라파나 (1) | 2024.04.14 |
| [쿠버네티스/도커] 2 - 쿠버네티스란, 쿠버네티스 기본 사용법 (0) | 2024.02.25 |
| [쿠버네티스/도커] 1 - 컨테이너 인프라 환경이란, 테스트 환경 구성 (1) | 2024.02.25 |