원하던 회사에 이직을 했다. 야호! ✈️
(여담이지만 이직을 준비하면서, 그리고 이직한 회사에서 얻은 인사이트를 정리하여 조만간 블로그에 올릴 예정이다.)
이직한 회사는 현재 Java
로 만들어진 레거시 코드들을 Kotlin
으로 전환하는 중이다.
이미 서비스가 굴러가고 있는 터라 달리는 자동차의 바퀴를 바꾸는 과정에 있다.
안그래도 Kotlin
을 배우고 싶었는데,
Kotlin
보다 아직 더 우선적으로 공부할 것들이 많아서 뒤로 미루고있었다. (게을러서다)
입사하고 코드들을 봤는데 일단 Kotlin
이었다. 👀
들어보니 인텔리제이의 도움을 받아 Java
코드들이 일단 Kotlin
으로 돌아는 가게끔 만들었다고 했다.
이제 이 코드들을 Kotlin
스럽고 '우아하게' 만들어야 한다. 🍬
코드들을 들여다보는데 역시나 익숙하지 않아서인지 한눈에 들어오지 않았다.
Kotlin
을 공부한다면 꼭 들어야겠다고 벼르고 있었던 '인프런' 의 '최태현' 님의 강좌를 새로운 회사에 출근한지 일주일만에 신청했다. (교육 복지 만세 🎉)
강의 링크 : 👉🏻 자바 개발자를 위한 코틀린 입문
(어떠한 광고비도 받지 않았다 ㅎ.. 강의가 너무 좋았음 👍🏻)
퇴근 후 그리고 주말을 이용해 일주일만에 기초강의를 들었다.
그만큼 강의가 재미있었고, 배우는 즐거움이 컸다.
강의를 들은 후, 이 포스팅을 적기 시작했다.
포스팅을 정리하는 것도 약 일주일이 걸렸다. 🥳
이 포스팅의 목적은 다음과 같다.
목적
1. 강의에서 배운 내용을 바탕으로 복습겸Kotlin
문법을 정리한다.
2.Java
에 익숙한 개발자들이 이해하기 편하도록 정리한다.
3. 실무에서 문법이 헷갈리거나 개념이 헷갈릴 때 언제든 찾아 볼 수 있도록 기초 문법 사전으로 사용하도록 정리한다.
# Java
long number1 = 10L;
final long number2 = 10L;
# Kotlin
var number1: Long = 10L
val number2 = 10L
모든 변수는 우선 val
로 선언하는 것을 추천한다.
꼭 필요한 경우. 즉, 값이 변해야 하는 경우에만 var
로 선언한다.
💡 Key Point
1. val : value 값 자체를 의미한다. 변하지 않는다.
2. var : variable 변할 수 있는 값 즉, 변수를 의미한다.
# Java
Long number3 = 1_000L;
# Kotlin
var number3: Long? = 1_000L
💡 Key Point
1. 코틀린에서는 Primitive Type 과 Reference Type 차이가 없다.
2. 자바의 Reference Type 은null
이 들어갈 수 있으므로Kotlin
에서는 타입에?
를 붙여주었다.
-null
은 다음 챕터에서 더 다뤄보자.
# Java
Person person = new Person("Joshua");
# Kotlin
var person = Person("Joshua")
💡 Key Point
1. 코틀린에서는 객체를 인스턴스화 할 때new
를 사용하지 않는다.
- 개인적으로는 이게 아직 어색하더라 ..new
쓰고 다시 지우곤 한다 ㅎ
?.
을 통해 null
이 들어올 수 있는 값이 npe
터지지 않도록 함.null
이라면 null
그대로를 반환한다.# kotlin - safe call 예시 val str1: String? = "ABC" val str2: String? = null println(str1?.length) //-> 3 println(str2?.length) //-> null
null
이면 Elvis 연산자 뒤의 값을 사용한다.?:
형태# kotlin - Elvis 연산자 예시 val nullValueUsingSafeCall: Int? = safeCallMethod(null) println(nullValueUsingSafeCall?.dec() ?: 10) // '?:' 요게 Elvis 연산자
# java
public boolean startsWithA1(String str) {
if (str == null) {
throw new IllegalArgumentException("null이 들어왔습니다");
}
return str.startsWith("A");
}
public Boolean startsWithA2(String str) {
if (str == null) {
return null;
}
return str.startsWith("A");
}
public boolean startsWithA3(String str) {
if (str == null) {
return false;
}
return str.startsWith("A");
}
# kotlin
fun startsWithA1 (str: String?): Boolean{
return str?.startsWith("A") ?: throw IllegalArgumentException("null 이 들어왔습니다.")
}
fun startWithA2 (str: String?): Boolean? {
return str?.startsWith("A")
}
fun startWithA3 (str: String?): Boolean {
return str?.startsWith("A") ?: false
}
fun startWithA4 (str: String): Boolean {
return str.startsWith("A")
}
str
이 String
타입이므로 (String?
타입이 아니므로) null
이 들어가지 않는다고 코틀린이 간주한다.startsWith()
메서드 콜이 바로 가능하다.fun safeCallMethod (str: String?): Int? {
return str?.length
}
str
은 String?
타입이므로 null
이 들어갈 수 있다.str?.length
는 str
이 null
이 아니면 실행하고, null
이면 실행자체를 하지 않고 null
을 반환한다.💡 Key Point
1. 코틀린은null
이 가능한 타입(?
가 붙은 타입)을 다른 타입으로 취급한다.
2. 언어 단에서null
을 안전하게 다뤄준다는 것만으로도 코틀린을 써야할 이유가 충분하다. ㅎ..
이런 경우는 어떤 경우일까? 🤔
DB 에 처음 데이터가 들어올 때는
null
일 수 있어서nullable
이지만,
한번 어떠한 비지니스 로직을 통해 해당 데이터가 업데이트 되면 그 이후로는 절대null
이 아닌 경우
fun neverNullValue (str: String?): Boolean {
return str!!.startsWith("A")
}
null
이 아니야!! 그러니까 startWith()
메서드를 실행해줘!!!!💡 Key Point
1. 절대 null 이 아니다!! 라고 소리친다고 이해하면 좋음
2. 개인적으로는 이 방법이 썩 좋은 방법 같지는 않다. (어떤일이 터질지 모르니까.. 프로그래밍 세계는 절대적인게 없거든. 웬만하면 안써야겠다.)
# java
public void printAgeIfPerson(Object obj) {
if (obj instanceof Person) {
Person person = (Person) obj;
System.out.println(person.getAge());
}
}
version 1
# kotlin
fun printAgeIfPerson (obj: Any) {
if (obj is Person) {
val person = obj as Person
println(person.age)
}
instanceof
는 is
로 사용된다.java
에서 캐스팅을 위한 (Person)
은 as
를 사용하며, 생략이 가능하다.version 2
fun printAgeIfPerson (obj: Any) {
if (obj is Person) {
println(obj.age)
}
if
절을 컴파일에서 이미 이해하고, obj
가 Person
타입인것을 인지하여 age
메서드 콜이 가능하다.version 3
fun printAgeIfPerson (obj: Any) {
val person: Person? = obj as? Person
println(person?.age)
}
Person
에 null
이 들어올 수 있을 때, nullable
로 선언이 가능하다.castException
이 아니라 null
이 반환된다.💡 Key Point
1.instanceof
->is
2. 캐스팅은as
로 사용하며, 생략이 가능.
3. 코틀린은 스마트 캐스팅이 가능하다.
Java
의 Object
역할. 즉, 모든 객체의 최상위 타입Any
자체로는 null
을 포함하지 않는다.null
포함하려면 Any?
로 표현해야한다. ㅎAny
에 equals, hashCode, toString
이 존재한다Java
의 void
와 동일한 역할unit
은 그 자체로 타입 인자로 사용 가능하다.unit
은 실제 존재하는 타입이라는 것! (Java
의 void
는 타입이 없었..)# java
public class JavaMoney implements Comparable<JavaMoney> {
private final long amount;
public JavaMoney(long amount) {
this.amount = amount;
}
public JavaMoney plus(JavaMoney other) {
return new JavaMoney(this.amount + other.amount);
}
@Override
public int compareTo(@NotNull JavaMoney o) {
return Long.compare(this.amount, o.amount);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JavaMoney javaMoney = (JavaMoney) o;
return amount == javaMoney.amount;
}
@Override
public int hashCode() {
return Objects.hash(amount);
}
@Override
public String toString() {
return "JavaMoney{" +
"amount=" + amount +
'}';
}
}
# kotlin
data class Money (
val amount: Long
) {
operator fun plus(other: Money): Money {
return Money(this.amount + other.amount)
}
}
💡 Key Point
1. 위랑 아래 코드 진짜 똑같은 코드가 맞다. 놀라울따름 ㅎ..
2. 해당 내용은 클래스 부분에서 다시 더 자세하게 다루겠다.
# java
JavaMoney money1 = new JavaMoney(10_000L);
JavaMoney money2 = new JavaMoney(2_000L);
if (money1.compareTo(money2) > 0) {
System.out.println("Money1 이 Money2 보다 금액이 큽니다.");
}
# kotlin
val money1 = JavaMoney(10_000L)
val money2 = JavaMoney(2_000L)
if (money1 > money2) {
println("Money1 이 Money2 보다 금액이 큽니다.")
}
compareTo
를 호출한다.compareTo
메서드를 만들어 주기 때문에 위의 코드에서 보이지 않는다.동등성 : '두 객체의 값이 같은가'
Java
:equals()
로 비교Kotlin
:==
로 비교
동일성 : '완전히 동일한 객체인가' 즉, '주소' 값이 같은가
Java
:==
로 비교Kotlin
:===
로 비교
# java
JavaMoney money3 = money1;
JavaMoney money4 = new JavaMoney(1_000L);
System.out.println(money1 == money3); // true
System.out.println(money1.equals(money4)); // true
money1
와 money3
은 주소 값이 같은 동일한 객체다. (==
)compareTo()
가 구현되어 있다면, money1
과 money4
는 equals()
즉, 값이 같은 동등한 객체다# kotlin
val money3 = money1
val money4 = JavaMoney(2_000L)
println(money1 === money3)
println(money1 == money4)
=
이 하나 더 붙으면 '주소까지 같은지 완전 비교하자!' 라는 의미로 이해하면 좋다.operator
키워드를 통해 가능하다.# Money.kt
data class Money (
val amount: Long
) {
operator fun plus(other: Money): Money {
return Money(this.amount + other.amount)
}
}
Money
클래스다.operator
키워드를 통해 연산자를 직접 정의했다. val kMoney1 = Money(1_000L)
val kMoney2 = Money(2_000L)
println(kMoney1 + kMoney2) // 3_000
plus
라는 연산자를 오버로딩 하면, +
로 객체끼리 연산을 하게 되면 정의한 대로 연산이 된다. 💡 Key Point
1.operator
연산자를 정해진 연산자 메서드 네이밍에 맞춰 커스텀할 수 있다.
2. 사용할 때, 해당 연산자를 사용하면 오버로딩된 연산이 가능하다. (+
->plus
)
# java
private void validateScoreIsNotNegative(int score) {
if (score < 0) {
throw new IllegalArgumentException(String.format("%s는 0보다 작을 수 없습니다", score));
}
}
# kotlin
fun validateScoreIsNotNegative(score: Int) {
if (score < 0) {
throw IllegalArgumentException("${score} 는 0보다 작을 수 없습니다.")
}
}
Statement vs Expression
- Statement : 프로그램의 문장. 하나의 값으로 도출되지 않는다. 즉, return 값으로 사용 불가.
- Expression : 하나의 값으로 도출되는 문장. (
Java
에서는 3항 연산자 같은 것)
# java
private String getPassOrFail (int score) {
if (score >= 50) {
return "P";
} else {
return "F";
}
}
# kotlin
fun getPassOrFail (score: Int): String {
return if (score >= 50) {
"P"
} else {
"F"
}
}
💡 Key Point
1. 코틀린에서 if-else 문은 Expression 이다.
2. 코틀린에서는 if-else 가 Expression 이므로 3항연산자가 없다. 사실 쓸 필요가 없음..ㅎ
# java
private String getGrade (int score) {
if (score >= 90) {
return "A";
} else if (score >= 80) {
return "B";
} else if (score >= 70) {
return "C";
} else {
return "D";
}
}
# kotlin
fun getGrade (score: Int): String {
val calculatedScore: String = if (score >= 90) {
"A"
} else if (score >= 80) {
"B"
} else if (score >= 70) {
"C"
} else {
"D"
}
return calculatedScore
}
switch
가 사라지고 when
이 생겼다.when
역시 Expression 이다. # java
private String getGradeWithSwitch (int score) {
switch (score / 10) {
case 9:
return "A";
case 8:
return "B";
case 7:
return "C";
default:
return "D";
}
}
# kotlin
fun getGradeWithSwitch1(score: Int): String {
return when (score / 10) {
9 -> "A"
8 -> "B"
7 -> "C"
else -> "D"
}
}
default
대신 else
를 사용한다.# kotlin
fun getGradeWithSwitch2(score: Int): String {
return when (score) {
in 90..99 -> "A"
in 80..89 -> "B"
in 70..79 -> "C"
else -> "D"
}
}
# java
private boolean startsWithA (Object obj) {
if (obj instanceof String) {
return ((String) obj).startsWith("A");
} else {
return false;
}
}
# kotlin
fun startsWithA(obj: Any): Boolean {
return when (obj) {
is String -> obj.startsWith("A") // 스마트 캐스팅
else -> false
}
}
is
를 사용할 수 있다.# java
private void judgeNumber1 (int number) {
if (number == 1 || number == 0 || number == -1) {
System.out.println("어디서 많이 본 숫자입니다.");
} else {
System.out.println("1, 0, -1 이 아닙니다.");
}
}
# kotlin
fun judgeNumber1(number: Int) {
when (number) {
1, 0, -1 -> println("어디서 많이 본 숫자입니다.")
else -> println("1, 0, -1 이 아닙니다.")
}
}
,
로 들어간다면 or
의 의미다.# java
private void judgeNumber2(int number) {
if (number == 0) {
System.out.println("주어진 숫자는 0입니다");
return;
}
if (number % 2 == 0) {
System.out.println("주어진 숫자는 짝수입니다");
return;
}
System.out.println("주어지는 숫자는 홀수입니다");
}
# kotlin
fun judgeNumber2(number: Int) {
when {
number == 0 -> println("주어진 숫자는 0입니다.")
number % 2 == 0 -> println("주어진 숫자는 짝수입니다.")
else -> println("주어진 숫자는 홀수입니다.")
}
}
# java
private void exampleClassicFor1 () {
for (int i = 1 ; i <= 3; i++) {
System.out.println(i);
}
}
private void exampleClassicFor2 () {
for (int i = 3 ; i >= 1; i--) {
System.out.println(i);
}
}
private void exampleClassicFor3 () {
for (int i = 1; i <= 5; i += 2) {
System.out.println(i);
}
}
# kotlin
fun exampleClassicFor1 () {
for (i in 1..3) { // for (i in 1..3)
println(i)
}
}
fun exampleClassicFor2 () {
for (i in 3 downTo 1) {
println(i)
}
}
fun exampleClassicFor3 () {
for (i in 1..5 step 2) {
println(i)
}
}
💡 Key Point
1. 코틀린에서는 range 를..
으로 나타낸다.
2. 코틀린에서downTo
와step
은 '중위함수' 인데, 아래의 '함수' 파트에서 더 자세하게 다룬다.
# java
private void exampleForEach () {
List<Long> numbers = Arrays.asList(1L, 2L, 3L);
for (Long number: numbers) {
System.out.println(number);
}
}
# kotlin
fun exampleForEach () {
val numbers = listOf(1L, 2L, 3L)
for (number in numbers) {
println(number)
}
}
💡 Key Point
1. 코틀린에서는:
대신in
을 사용한다.
# java
private void exampleWhile () {
int i = 1;
while (i <= 3) {
System.out.println(i);
i++;
}
}
# kotlin
fun exampleWhile () {
var i = 1 // 가변이니까 var
while (i <= 3) {
println(i)
i++
}
}
💡 Key Point
1. while 은 자바와 코틀린 동일하다.
# java
private int parseIntOrThrow (@NotNull String str) {
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("주어진 %s 는 숫자가 아닙니다.", str));
}
}
# kotlin
fun parseIntOrThrow (str: String): Int {
try {
return str.toInt()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("주어진 ${str} 은 숫자가 아닙니다.")
}
}
💡 Key Point
1. 구조는 문법적으로 완전히 똑같다.
2. 깨알 팁 : 코틀린에서의 형변환은변환할변수.to타입()
형태의 메서드로 이루어진다.
# java
private Integer parseIntOrThrow2 (@NotNull String str) {
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
return null;
}
}
# kotlin
fun parseIntOrThrow2 (str: String): Int? {
return try {
str.toInt()
} catch (e: NumberFormatException) {
null
}
}
💡 Key Point
1.null
이 반환 될 수 있는 경우 코틀린에서는 반환타입을 신경써야 한다.?
를 꼭 잊지말기
2. 코틀린에서는 try - catch 문도 Expression 이다.
# java
public void readFile1 () 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();
}
# kotlin
fun readFile1() {
val currentFile = File(".")
val file = File(currentFile.absolutePath + "/a.txt")
val reader = BufferedReader(FileReader(file))
println(reader.readLine())
reader.close()
}
💡 Key Point
1. 코틀린에서는 모두 Unchecked Exception 이다.
2. 즉,IOException
등의 Checked Exception 을 코드를 짤 때 신경 써줄 필요가 없다. 🫢
# java
public void readFile2(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
System.out.println(reader.readLine());
}
}
# kotlin
fun readFile2 (path: String) {
BufferedReader(FileReader(path)).use {reader ->
println(reader.readLine())
}
}
💡 Key Point
1. 코틀린에서는 try with resources 구문이 없다.
2. 대신use
라는inline
확장함수를 사용한다. 아래에 함수 부분에서 자세하게 정리되어있다.
# java
public int max (int a, int b) {
if (a > b) {
return a;
}
return b;
}
# kotlin
fun max (a: Int, b: Int) = if (a > b) a else b
💡 Key Point
1. 함수 자체의 결과값이 하나라면 결과값을=
을 통해 바로 넣어 줄 수 있다.
2. 파라미터가 모두Int
이므로, 함수의 반환타입이Int
라는 것을 타입추론을 통해 알수 있으므로 이 함수의 반환 타입은 생략 가능하다.🫢
3. 만약 중괄호를 사용했다면 명시적으로 반환타입을 적는 것이 좋다.
# java
public void repeat (String str, int num, 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);
}
repeat
함수가 3개. 오버로딩이 되어있다.useNewLine
이 true
로 사용되는 경우가 많아서!num
이 3
인 경우도 꽤 많아서 또 오버로딩을 했다.# kotlin
fun repeat (
str: String,
num: Int = 3,
useNewLine: Boolean = true
) {
for (i in 1..num) {
if (useNewLine) {
println(str)
} else {
print(str)
}
}
}
# kotlin
repeat("룰루", useNewLine = false)
repeat(useNewLine = true, str = "후후후")
repeat
함수에서 값을 넣지 않으면 기본값을 사용하고, 임의로 값을 넣어주고 싶다면 골라서 파라미터의 이름을 key 로 넣고 값을 줄 수 있다.# kotlin
fun printNameAndGender (name: String, gender: String) {
println(name)
println(gender)
}
printNameAndGender("", "")
printNameAndGender(name = "이름", gender = "남자")
# java
public static void printAll(String... strings) {
for (String str : strings) {
System.out.println(str);
}
}
# kotlin
fun printAll (vararg strings: String) {
for (str in strings) {
println(str)
}
}
💡 Key Point
1.Java
:...
을 사용
2.Kotlin
:vararg
를 사용
# kotlin
val array = arrayOf("A", "B")
printAll("A", "B")
printAll(*array)
💡 Key Point
1. 스프레드 연산자 :*
배열안에 있는 것들을 마치 그냥 ',' 쓰는 것처럼 요소들을 펼쳐서 꺼내준다.
# java
public class JavaPerson {
private final String name;
private int age;
public JavaPerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
# kotlin
class Person (
val name: String,
var age: Int
)
💡 Key Point
1.val
은 불변이므로java
의final
이 붙은 것과 같다.
또한,final
은 수정될 수 없으므로getter
만을 생성한다.
2.age
는var
로 선언되었으므로,getter
와setter
모두 생성해준다.
3. 코틀린에서는 필드선언과 생성자를 동시에 선언한다.
# kotlin
class Person (
name: String = "김민재",
var age: Int = 10
)
# java
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;
}
...
# kotlin
class Person (
name: String = "김민재",
var age: Int = 10
) {
init {
if (age <= 0) {
throw IllegalArgumentException("나이는 ${age}일 수 없습니다." )
}
}
}
💡 Key Point
1.init
을 통해 초기화 블록을 만들 수 있다.
2. 객체가 생성되었을 때 해당 로직이 실행 된다.
예시 1>
# kotlin
class Person (
val name: String = "김민재",
var age: Int = 10
) {
val uppercaseName: String
get() = this.name.uppercase()
}
fun main() {
val testPerson = Person("abc")
println(testPerson.uppercaseName) // ABC
println(testPerson.name) // abc
}
예시 2>
# java
public class JavaPerson {
private final String name;
private int age;
...
public boolean isAdult() {
return this.age >= 20;
}
...
# kotlin
class Person (
name: String = "김민재",
var age: Int = 10
) {
...
val isAdult: Boolean
get() = this.age >= 20
...
💡 Key Point
1.Person
클래스에upperCaseName
이라는 필드가 있는 것처럼 사용할 수 있다.
2.getter
를 사용 하는 것처럼 객체에서 바로 필드 이름으로 접근이 가능하다. 🫢
3.isAdult
또한 마찬가지 !
# java
public abstract class JavaAnimal {
protected final String species;
protected final int legCount;
public JavaAnimal(String species, int legCount) {
this.species = species;
this.legCount = legCount;
}
abstract public void move();
public String getSpecies() {
return species;
}
public int getLegCount() {
return legCount;
}
}
----------
public class JavaCat extends JavaAnimal {
public JavaCat(String species) {
super(species, 4);
}
@Override
public void move() {
System.out.println("고양이가 사뿐 사뿐 걸어가~");
}
}
# kotlin
abstract class Animal (
protected val species: String,
protected val legCount: Int
){
abstract fun move()
}
------------
class Cat (
species: String
) : Animal (species, 4){
override fun move() {
println("고양이가 사뿐 사뿐 걸어가~")
}
}
💡 Key Point
1. 코틀린에서 상속은extends
대신에:
를 사용한다.
2. 상속받은 상위 클래스의 생성자를 반드시 바로 호출해야한다.
3. 코틀린은@Override
어노테이션이 없다. 대신override
라는 키워드를 '필수적'으로 붙여줘야 한다.
# java
public final class JavaPenguin extends JavaAnimal {
private final int wingCount;
public JavaPenguin(String species) {
super(species, 2);
this.wingCount = 2;
}
@Override
public void move() {
System.out.println("펭귄이 움직입니다~ 꿱꿱");
}
@Override
public int getLegCount() {
return super.legCount + this.wingCount;
}
}
# kotlin
class Penguin (
species: String
) : Animal(species, 2) {
private val wingCount: Int = 2
override fun move() {
println("펭귄이 움직입니다~ 꿱꿱")
}
override val legCount: Int
get() = super.legCount + this.wingCount
}
...
abstract class Animal (
protected val species: String,
protected open val legCount: Int // open 으로 변경
){
abstract fun move()
💡 Key Point
1.legCount
를override
하기 위해서는Animal
의legCount
를open
해줘야 한다.
- 추상 프로퍼티가 아니면, 상속받을 때 open 을 꼭 붙여야 한다.
- 추상 클래스에서 자동으로 만들어진
getter
를override
하기 위함이다.
# java
public interface JavaSwimable {
default void act() {
System.out.println("어푸 어푸");
}
}
---
public interface JavaFlyable {
default void act() {
System.out.println("파닥 파닥");
}
}
---
public final class JavaPenguin extends JavaAnimal implements JavaSwimable, JavaFlyable {
...
@Override
public void act() {
JavaSwimable.super.act();
JavaFlyable.super.act();
}
...
# kotlin
interface Swimable {
fun act() {
println("어푸어푸")
}
}
---
interface Flyable {
fun act () {
println("파닥파닥")
}
}
---
class Penguin (
species: String
) : Animal(species, 2), Swimable, Flyable{
...
override fun act() {
super<Swimable>.act()
super<Flyable>.act()
}
...
💡 Key Point
1. 코틀린에서는 인터페이스의 구현도:
을 사용한다.
2.super
의 위치가 다르다.super<타입>.함수
# kotlin
interface Flyable {
// 인터페이스의 프로퍼티는 디폴트 값을 가질 수도 있다.
val flyAbility : Int
get() = 10
fun act () {
println("파닥파닥")
}
}
---
interface Swimable {
val swimAbility: Int
fun act() {
println("어푸어푸")
}
}
---
class Penguin (
species: String
) : Animal(species, 2), Swimable, Flyable{
...
/**
* Swimable 에서 프로퍼티를 가질 수 있다.
* val 프로퍼티는 getter 를 구현하길 기대하므로, 구현 클래스에서 getter 를 구현해준다.
*/
override val swimAbility: Int
get() = 5
/**
* Flayble 의 fiyAbility 는 디폴트 값을 가지고 있으므로 구현하지 않아도된다.
* 하지만, 오버라이드하고 싶다면 아래와 같이 가능하다.
*/
override val flyAbility: Int
get() = super.flyAbility * 10
...
💡 Key Point
1. 코틀린 인터페이스의 프로퍼티는 디폴트 값을 가질 수 있다. ->flyAbility
는 10의 값을 가진다.
2.Swimable
인터페이스의swimAbilty
프로퍼티는 디폴트 값이 없으므로 구현할 때getter
를override
하여 구현해줘야 한다.
3.Flyable
인터페이스의flyAbility
프로퍼티는 디폴트 값이 있으므로 구현하지 않아도 된다.
- 하지만 디폴트 값이 아닌 다른 값을 넣고 싶다면
override
할 수 있다.
# java
public class JavaBase {
public JavaBase() {
System.out.println(getMember());
}
public int getMember() {
return 1;
}
}
---
public class JavaDerived extends JavaBase {
public JavaDerived() {
super();
}
@Override
public int getMember() {
return 10;
}
}
# kotlin
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")
}
}
---
fun main () {
Derived(300) // 여기서 무슨 값이 나올까?
}
💡 Key Point
1. 일반 클래스를 상속 하려면, 상위 클래스는open
이라는 키워드로 열려있어야 한다. 그래야 상속이 가능하다.
2. 상위 클래스에서 생성자 혹은 초기화 블록에 사용되는 프로퍼티에는open
을 피해야한다.
- 현재
Base
클래스의number
프로퍼티는open
으로 열려있다.Derived
는number
를override
하고 있다.main
함수에서Derived(300)
으로 객체를 생성하면Base
초기화 블록에서는number
가 뭐라고 나올까?- 0 이 나오게 된다.
Derived
를 호출할 때,Base
클래스를 먼저 호출을 하는데, 이 시점에서는number
가 아직 주입되지 않았기 때문이다.- 100이 나오지도 않는다. 왜냐하면
override
했기 때문에, 주입된 값을 받으려고 하기 때문이다.- 그러므로 상위 클래스의 생성자 혹은 초기화 블록의 프로퍼티는
open
으로 열어 두지 말자!
# kotlin
open class Base (
val number: Int = 100
) { ... }
---
class Derived (
number: Int
) : Base(number){ ... }
Derived(300)
을 호출 했을 때, Base
클래스 생성시 300이 출력된다.# java
StringUtils.java
public abstract class StringUtils {
private StringUtils() {}
public boolean isDirectoryPath(String path) {
return path.endsWith("/");
}
}
# kotlin
StringUtils.kt
fun isDirectoryPath(path: String): Boolean {
return path.endsWith("/")
}
💡 Key Point
1. 코틀린에서는 유틸성 함수를 만들 때 클래스 생성할 필요 없이 파일 최상단에 만들면 편하다.
# kotlin
class Car (
internal val name: String,
private var owner: String,
_price: Int
) {
var price= _price
private set
}
💡 Key Point
1.name
은getter
의 접근제어자가internal
이다. (val
이므로getter
뿐이다)
2.owner
는getter
와setter
모두private
이다.
3.price
는getter
는public
이며,setter
는 명시적으로private
이 되었다.
- 코틀린에서는 기본 접근제어자가
public
이다.
# java
public class JavaPerson {
private static final int MIN_AGE = 1;
public static JavaPerson newBaby(String name) {
return new JavaPerson(name, MIN_AGE);
}
private String name;
private int age;
private JavaPerson(String name, int age) {
this.name = name;
this.age = age;
}
}
# kotlin
class Person private constructor(
var name: String,
var age: Int
) {
companion object{
private val MIN_AGE = 1
fun newBaby(name: String): Person {
return Person(name, MIN_AGE)
}
}
}
💡 Key Point
1. 코틀린에서는static
이 없다.
2.companion object
를 대신 사용한다.
- 이 블럭안에 넣어둔 변수와 함수가
static
변수와 함수가 된다.- static : 클래스가 인스턴스화 될 때 새로운 값이 복제되지 않고, 정적으로 인스턴스끼리의 값을 공유한다.
- companion object :
- 클래스와 동행하는 유일한 오브젝트.
- 이것도 클래스라는 설게도와 동행하는 유일한 오브젝트로 정적이다.
- 한 클래스에는 하나의
companion object
를 만들 수 있다.companion object
에 유틸성 함수를 넣어도 되지만, 유틸 함수는 최상단 파일을 활용하는 것이 좋다.
# kotlin
interface Log {
fun log ()
}
---
companion object Factory : Log{
...
override fun log() {
println("companion object 는 인터페이스를 구현할 수 있다. 또한 여기에 선언된 상수 값은 $MIN_AGE 이다.")
}
...
}
💡 Key Point
1.companion object
는하나의 '객체'로 간주된다.
- 그렇기 때문에, 이름을 붙일 수 있고,
interface
를 구현할 수도 있다.- 위의
companion object
는Factory
라는 이름을 가지며,Log
인터페이스를 구현한다.Log
인터페이스의log()
함수를override
를 통해 구현한다.
# kotlin
companion object Factory : Log{
private val MIN_AGE = 1
}
---
companion object Factory : Log{
private const val MIN_AGE = 1
}
💡 Key Point
1. val : '런타임' 시에 변수가 할당된다.
2. const val : '컴파일' 시에 변수가 할당된다.
- const 는 진짜 상수에 붙이기 위한 용도다.
- const 없이 그냥 val 만 사용하면 런타임 때 변수가 할당 된다. (물론 val 은 final 이므로 런타임시에 최초 할당 이후로 변경 불가)
- 기본 타입과 String 에도 붙일 수 있다.
# java
public class JavaSingleton {
private static final JavaSingleton INSTANCE = new JavaSingleton();
private JavaSingleton() { }
public static JavaSingleton getInstance() {
return INSTANCE;
}
}
# kotlin
object Singleton
# java
public interface Movable {
void move();
void fly();
}
---
public class Lec12Main {
public static void main(String[] args) {
moveSomething(new Movable() { // 일회성으로 사용하기 위해 익명클래스 사용.
@Override
public void move() {
System.out.println("움직인다.");
}
@Override
public void fly() {
System.out.println("난다.");
}
});
}
private static void moveSomething (Movable movable) {
movable.move();
movable.fly();
}
}
# kotlin
public interface Movable { // Java 인터페이스를 그대로 사용
void move();
void fly();
}
---
fun main () {
moveSomething(object : Movable {
override fun move() {
println("움직인다.")
}
override fun fly() {
println("난다.")
}
})
}
private fun moveSomething (movable: Movable) {
movable.move()
movable.fly()
}
💡 Key Point
1. 코틀린에서의 익명클래스는 문법이 조금 다르다.
- Java : `new 타입이름() { ... }
- Kotlin : `object : 타입이름 { ... }
override
키워드를 통해 인터페이스의 메서드를 익명클래스로 구현한다.
Inner 클래스 : 바깥 클래스를 참조하고 있다.
static inner 클래스 : 바깥 클래스를 호출 할 수 없다.
static inner 클래스를 사용하자!
- static 을 사용하지 않은 Inner 클래스는 숨겨진 외부 클래스 정보를 가지고 있기때문에 참조를 끊어주지 못할 경우 메모리 누수가 생긴다.
- 이런 경우 디버깅도 어렵다.
- 내부 클래스 직렬화 형태가 명확히 정의 되지 않아서 직렬화에 있어 제한이 있다.
클래스 내부에 클래스를 만들 때는 static 클래스를 사용하라
- Effective Java
# java
public class JavaHouse {
private String address;
private LivingRoom livingRoom;
private BedRoom bedRoom;
public JavaHouse(String address) {
this.address = address;
this.livingRoom = new LivingRoom(10);
this.bedRoom = new BedRoom(20);
}
public LivingRoom getLivingRoom() {
return livingRoom;
}
/**
* Inner Class
*
*/
public class LivingRoom {
private double area;
public LivingRoom(double area) {
this.area = area;
}
public String getAddress() {
return JavaHouse.this.address;
}
}
/**
* static inner class
*/
public static class BedRoom {
private double area;
public BedRoom(double area) {
this.area = area;
}
// public String getAddress() { 참조 불가
// return JavaHouse.this.address;
// }
}
}
# kotlin
class House (
private val address: String,
private val livingRoom: LivingRoom,
private val bedRoom: BedRoom
) {
/**
* Inner class
*/
inner class LivingRoom (
private val area: Double
) {
val address: String
get() = this@House.address
}
/**
* static inner class
*/
class BedRoom (
private val area: Double
)
}
💡 Key Point
1. 코틀린은 권장되지 않는 Inner class 를 만드는 것을 번거롭게 했다.
inner
라는 키워드를 굳이!! 써줘야 한다.- 즉 바깥 클래스를 참조하는 (Anti pattern) 클래스를 만들기 위해서 굳이
inner
키워드를 써야한다.- 바깥 클래스를 참조하여 값을 가져오려면 굳이
this@바깥클래스.값
의 형태로 가져와야 한다.(this@House.address)
- 코틀린은 class 내부에 class 를 만들면 권장되는
static inner class
가 된다.
- 그냥 편하게 내부에 class를 만들어 주면된다.
- 기본적으로 바깥 클래스에 대한 참조가 없는 클래스가 만들어진다.
DTO (Data Transfer Object)
- Data Class 는 주로 DTO 로 사용된다.
- 계층간의 데이터를 전달하기 위한 클래스를 의미한다.
# java
public class JavaPersonDto {
private final String name;
private final int age;
public JavaPersonDto(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JavaPersonDto that = (JavaPersonDto) o;
return age == that.age && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "JavaPersonDto{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
# kotlin
data class PersonDto (
val name: String,
val age: Int
)
💡 Key Point
1. 'That's ENOUGH' :data
만class
앞에 적어두면dto
클래스로서 완벽하다.
equals(), hashCode(), toString()
을 자동으로 만들어 준다.
- 여기에
named argument
까지 활용한다면,builder
패턴을 사용하는 것과 같은 효과도 가진다.
- 사실상
data
키워드에builder
까지 있는 격이다.
Enum
- 추가적인 클래스를 상속받을 수 없다.
- 인터페이스는 구현할 수 있다.
- 각 값들은 싱글톤이다.
# java
public enum JavaCountry {
KOREA("KO"),
AMERICA("US"),
;
private final String code;
JavaCountry(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
---
public static void main(String[] args) {
handleCountry(JavaCountry.KOREA);
}
private static void handleCountry(JavaCountry country) {
if (country == JavaCountry.KOREA) {
// 로직 처리
}
if (country == JavaCountry.AMERICA) {
// 로직 처리
}
}
# kotlin
enum class Country(
private val code: String
) {
KOREA ("KO"),
AMERICA ("US")
;
}
---
fun handleCountry (country: Country) {
when (country) {
Country.KOREA -> TODO() // 로직처리
Country.AMERICA -> TODO() // 로직처리
}
}
💡 Key Point
1. enum 은 java 와 kotlin 은 큰 차이는 없다.
2. 사용에 있어서 진가를 발휘하는데, kotlin 의when
문법에서 진가를 발휘한다.handleCountry
함수를 보자.
when
과enum
을 활용하면 더 읽기 쉬운 코드를 사용할 수 있다.- 컴파일러가
Country
의 모든 타입을 알고 있어서else
를 추가로 작성할 필요가 없다.- 코딩을 하면서
enum
클래스에 변화가 있으면 컴파일러에서 경고를 바로 날려서 개발자에게 알려준다.🫢 개쩐다!
- '님 뭐 잊은거 없음? Enum 에 France 추가 되었음!' 이라고 알려줌 👍🏻- java 에서는
enum
클래스에 값이 추가되어도 알 방법이 없다.
Sealed : 봉인된
- 상속이 가능하도록 추상클래스를 만들었지만, 외부에서는 이 클래스를 상속받지 못하게 '봉인' 하는 것
- 컴파일 타임 때 하위 클래스 타입을 모두 기억한다.
- 즉, 런타임 때 클래스 타입이 추가될 수 '없다'- 하위 클래스는 같은 패키지에 있어야 한다.
# kotlin
sealed class HyndaiCar (
val name: String,
val price: Long
)
class Avante : HyndaiCar ("아반테", 1000L)
class Sonata : HyndaiCar ("소나타", 2000L)
---
fun main() {
handleCar(Avante())
}
private fun handleCar (car: HyundaiCar) {
when(car) {
is Avante -> TODO()
is Sonata -> TODO()
}
}
💡 Key Point
1. Enum 과 차이
- 클래스를 상속받을 수 '있다.'
- 하위 클래스는 멀티 인스턴스가 가능하다.
- Enum 과 마찬가지로
when
에서 매우 효과적으로 사용이 가능하다.- 추상화가 필요한 Entity 나 DTO 에 활용된다.
배열은 잘 사용되지 않는다.
- 하지만 기본 문법은 알아두잣!
# java
private void arraySample (){
int [] array = {100, 200};
for (int i = 0; i < array.length; i++) {
System.out.printf("%s %s", i, array[i]);
}
}
# kotlin
fun arraySample () {
val array: Array<Int> = arrayOf(100, 200)
for (i in array.indices) {
println("${i} ${array[i]}")
}
for ((idx, value ) in array.withIndex()) {
println("$idx, $value")
}
array.plus(300)
}
💡 Key Point
1. 배열의 선언
arrayOf()
를 통해 선언해준다.
- 배열의 범위 -
indices
- 0 부터 마지막 index 까지의 범위를 의미한다.
- indeces 는 index 의 복수형이다.
- 인덱스와 값을 동시에 가져오기 - `withIndex()
- 배열의 인덱스와 그에 해당하는 값을 동시에 가져올 수 있다.
- 배열에 값 넣기
plus()
를 통해 값을 쉽게 넣을 수 있다.
불변, 가변
- 코틀린에서는 컬렉션을 만들 때, '불변' 인지 '가변' 인지를 설정해야한다.
- 언어 레벨에서 개발자에게 해당 부분을 미리 신경쓰도록 한다. 🫢
- 가변 컬렉션 (Mutablexxx) : 컬렉션에 element 를 추가, 삭제할 수 있다.
- 불변 컬렉션 : 컬렉션에 element 를 추가, 삭제할 수 없다.
- 우리가 익숙한 이름들 (List, Set, Map 등) 이 코틀린에서는 불변 컬렉션이다.
# kotlin
# List
fun listSample () {
// 불변 리스트 만들기
val numbers = listOf(100, 200)
// 비어있는 리스트를 만들 때. 이때는 꼭 들어올 타입이 무엇인지 명시해줘야 한다.
val emptyList = emptyList<Int>()
// 비어있는 리스트라도 만약 타입 추론이 가능하다면, 명시 해주지 않아도 된다.
estimatableEmptyList(emptyList())
// 값 가져오기
println(numbers[0]) // 하나만 가져오기
for (number in numbers) {
println(number)
}
for ((idx, value) in numbers.withIndex()) {
println("$idx $value")
}
// 가변 리스트
val mutableNumbers = mutableListOf(200, 300)
mutableNumbers.add(400) // 가변이므로 add 가 가능함.
for (number in mutableNumbers) {
println(number)
}
}
private fun estimatableEmptyList (numbers: List<Int>) {
}
💡 Key Point
1.listOf()
는 기본적으로 불변 리스트를 만든다.
2.mutableListOf()
를 통해 가변리스트를 만들 수 있다.
3.emptyList()
를 통해 빈 리스트를 만들 수 있다. 이 또한 불변이다.
# kotlin
# Set
private fun sampleSet () {
// 불변
val numbers = setOf(100, 200, 200, 300, 400, 500, 670)
for (number in numbers) {
println(number)
}
for ((idx, value) in numbers.withIndex()) {
println("$idx $value")
}
// 가변
val mutableSetOf = mutableSetOf(100, 200)
}
# kotlin
# Map
fun mapSample () {
// 가변
val oldMap = mutableMapOf<Int, String>()
oldMap[1] = "MON"
oldMap[2] = "TUES"
// 불변
val mapOf: Map<Int, String> = mapOf(1 to "MON", 2 to "TUES")
for (key in oldMap.keys) {
println(key)
println(oldMap[key])
}
for ((key, value) in oldMap.entries) {
println("$key $value")
}
}
💡 Key Point
1.mapOf(key to value)
형식을 통해 불변 map 을 만들 수 있다.
?
의 위치에 따라 이야기가 달라짐
1.List<Int?>
- 리스트에 null 이 들어갈 수 있지만, 리스트 자체는 절대 null 이 아님
List<Int>?
- 리스트에 null 이 절대 들어갈 수 없지만, 리스트 자체가 null 일 수 있다.
List<Int?>?
- 리스트에 null 이 들어갈 수도 있고, 리스트 자체가 null 일 수도 있다.
배경
1. kotlin 은 java 와 완벽하게 호환하는 것을 목표로 했다.
- 🤔 : "Java 코드 위에 자연스럽게 코틀린을 살포시 얹을 수 없을까?"
- "Java 코드는 그대로 두고 싶은데.."
- "Java 로 만들어진 라이브러리로 유지보수 하면서 동시에 확장할 때는 코틀린을 '덧붙이고' 싶어!"
- 어떤 클래스 안에 있는 메서드 처럼 호출할 수 있지만, 함수 밖에 만들 수 있게 하려고 함.
- 즉, 함수의 코드 자체는 클래스 밖에 있는데, '마치 클래스 안에 있는 멤버 함수 처럼' 사용하려는 것!
# kotlin
fun String.customLastChar(): Char {
return this[this.length - 1]
}
fun main() {
val str: String = "ABC"
println(str.customLastChar()) // 'C' 출력
}
customLastChar()
라는 함수가 없다.!! 🫢this
를 통해 '실제 클래스 안의 값에 접근` 이 가능하다 🫢 개쩐다..!"String 을 확장하는구나!!"
확장 프로퍼티
- 확장 함수의 개념은 여기서 멈추지 않았다.
- '확장 프로퍼티' 라는 신박한 녀석이 생겼다.
# kotlin
# 확장 프로퍼티
val String.lastChar: Char
get() = this[this.length - 1]
val Person.customLastNamePlusMj: String
get() = this.lastName + "Mj"
fun main() {
val str: String = "ABC"
println(str.lastChar) // 'C' 출력
val person = Person("MJ", "Kim", 20)
println(person.customLastNamePlusMj) // 'KimMj' 출력
}
멤버 함수 vs 확장함수
클래스의 멤버함수와 확장함수의 시그니처가 같다면 어떻게 될까?
- 결론 : 원조가 승리함. 즉, 멤버함수가 우선적으로 호출된다.
- 그러므로, 확장함수를 만들었는데 다른 기능의 똑같은 시그니처의 멤버함수가 생기면 생각지 못한 오류가 생길 수 있으므로 주의!
캡슐화가 깨질 염려는 없나?
- 확장함수가 public 이고, 확장하는 클래스의 private 함수를 가져오면 캡슐화가 깨지는 거 아닌가?
-> 확장함수는 클래스에 있는 private 또는 protected 멤버를 가져 올 수 없다!
확장함수가 override 된다면?
- 사용하는 측면에서, 해당 변수의 '현재 타입' 에 따라 어떤 확장 함수가 호출 될지 결정된다.
# kotlin
open class Train (
val name: String = "새마을 기차",
val price: Int = 5000
)
fun Train.isExpensive(): Boolean {
println("Train 의 확장함수")
return this.price >= 10000
}
class Srt : Train (
"SRT",
40_000
)
fun Srt.isExpensive(): Boolean {
println("SRT 의 확장함수")
return this.price >= 10000
}
---
fun main () {
# case 1
val train: Train = Train()
train.isExpensive() // Train 의 확장함수 호출
# case 2
val srt1: Train = Srt()
srt1.isExpensive() // Train 의 확장함수 호출
# case 3
val srt2: Srt = Srt()
srt2.isExpensive() // Srt 의 확장함수 호출
}
💡 Key Point
1. 확장함수가override
되는 경우 사용하는 변수의 타입에 따라 결정된다.
2. case 2 가 특이한데, Srt() 로 변수가 생성 되었으나, 선언된 타입은 부모타입인 Train 이므로, 확장함수는 Train 의 확장함수를 호출한다.
"함수를 호출하는 새로운 방법!"
- 신박하다. 개쩐다. 🫢
# kotlin
fun Int.add(other: Int): Int {
return this + other
}
# 중위함수
infix fun Int.add2(other: Int): Int {
return this + other
}
---
fun main () {
3.add(4)
3.add2(4)
// 중위 함수
3 add2 4
print(3 add2 5)
}
💡 Key Point
1. 중위함수는infix
키워드로 선언한다.
2.변수 함수이름 arg
형태로 함수를 호출한다.
사용처
1. 함수 호출 대신, 함수를 호출한 지점에 함수 본문을 그대로 복붙 하고 싶은 경우에 사용된다.
2. 함수를 호출하는 것이 아니라, 말그대로 함수 본문 자체가 사용하는 곳에 붙여 넣게 된다.
3. 함수를 파라미터로 전달할 때의 오버헤드를 줄일 수 있다.
4. ⛔️ 주의 ! 성능 측정이 꼭 필요하고, 신중하게 사용되어야 한다.
# kotlin
inline fun Int.addCustom(other: Int): Int {
return this + other
}
함수 안에 함수를 넣을 수 있다!
- 그런데 사실 잘 사용되지 않는다.
- 이유 1. depth 가 깊어진다.
- 이유 2. 코드가 깔끔하지 않다.
- 이유 3. 코드의 책임이 많아진다. -> private 으로 다른 함수로 빼는 것이 좋다.
# kotlin
fun createPersonUsingLocalFunction (firstName: String, lastName: String): Person {
fun validateName(name: String, fieldName: String) {
if(name.isEmpty())
throw IllegalArgumentException("$fieldName is empty. check: $name")
}
validateName(firstName, "first name")
validateName(lastName, "last name")
return Person(firstName, lastName, 1)
}
Java 에서의 Lambda
- Java 에서 함수는 '2급 시민' 이다. 😭
2급 따위..- 즉, Java 에서 함수는 '변수에 할당' 되거나 '파라미터로 전달 될 수 없다.'
- 단지, 함수를 직접 넘겨주는 '것 처럼' 쓸 수 있다.
- Java 에서는 Lambda 가 그렇게 사용된다.
Kotlin 에서의 Lambda
- Java 와는 근본적으로 다르다.
- Kotlin 에서는 함수가 '그 자체로 값' 이 될 수 있다. 즉, 변수에 할당 이 가능하다.
- Kotlin 에서는 함수가 파라미터로 전달 될 수 있다.
# kotlin
# 방법 1
val isApple = fun(fruit: Fruit): Boolean {
return fruit.name == "사과"
}
# 방법 2
val isApple2 = { fruit: Fruit -> fruit.name == "사과"}
💡 Key Point
1. 함수를 변수에 할당이 가능하다. 🫢
2. 방법 2 처럼 '이름 없는 함수' 즉, Lambda 를 변수에 할당 가능하다.
# kotlin
fun main () {
val fruits = listOf(
Fruit("사과", 1000),
Fruit("사과", 1200),
Fruit("사과", 1200),
Fruit("사과", 1500),
Fruit("바나나", 3000),
Fruit("바나나", 3200),
Fruit("바나나", 2500),
Fruit("수박", 10000)
)
isApple(fruits[0]) // true
isApple(Fruit("메론", 2000)) // false
isApple.invoke(fruits[0]) // true
isApple.invoke(Fruit("수박", 3000)) // false
}
💡 Key Point
1. 할당한 변수명에 할당된 함수의 파라미터를 넘겨주면 된다.
2.invoke
를 통해 명시적으로 함수를 호출 할 수도 있다.
함수의 타입
1. Java 개발자로서는 '함수의 타입' 이라는 것이 좀 낯설다.
- 함수의 타입 이라면 '리턴 타입'이 익숙한데, 여기서 말하는 것은 '함수 자체의 타입' 을 의미한다.
- isApple() 의 타입은
(Fruit) -> Boolean
이다.
# kotlin
val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
return fruit.name == "사과"
}
val isApple2: (Fruit) -> Boolean = { fruit: Fruit -> fruit.name == "사과"}
💡 Key Point
1. 함수의 타입은(함수 파라미터 타입, ...) -> 함수의 반환 타입
이다.
- 즉,
isApple
함수는Fruit
타입의 변수가 파라미터로 들어가며,Boolean
을 리턴하므로,
isApple
함수는(Fruit) -> Boolean
타입이다.
1급 시민과 2급 시민의 차이.. 🫢
# java
private List<Fruit> filterFruits(List<Fruit> fruits, FruitFilter fruitFilter) {
return fruits.stream()
.filter(fruitFilter::isSelected)
.collect(Collectors.toList());
}
---
public interface FruitFilter {
boolean isSelected(Fruit fruit);
}
# kotlin
private fun filterFruits(
fruits: List<Fruit>,
filter: (Fruit) -> Boolean
): List<Fruit> {
return fruits.filter(filter) // filter 함수 사용
}
💡 Key Point
1. 코틀린에서는 함수가 1급 시민이므로, 파라미터로 넘길 수 있다.
filter: (Fruit) -> Boolean
:(Fruit) -> Boolean
타입의 함수인 filter 를 파라미터로 받는다.
- 참고로
fruits.filter(..)
에서 호출된 filter 는 Collection 에 있는 filter 다.
- java 의 stream 에 있는 것과 같다고 보면 된다.
- 혹시가 헷갈릴까봐 ㅎㅎ 🫠
# kotlin
# case 1
filterFruits(fruits, isApple)
# case 2
filterFruits(fruits) { fruit: Fruit -> fruit.name == "사과"}
# case 3
pfilterFruits(fruits) { fruit -> fruit.name == "사과"})
# case 4
println(filterFruits(fruits) { it.name == "사과"})
💡 Key Point
1. case 1
- 함수를 할당한 변수
isApple
을 넣어 줄 수있다.
- case 2
filterFruits()
함수의 마지막 파라미터가 함수일 경우 중괄호를 바깥으로 빼서 넘길 수 있다.- 소괄호 안에 해당 함수가 들어가도 되지만, 소괄호 안에 중괄호 들어가는게 조금 어색하기 때문이다.
- 물론, 소괄호 안에 들어가 있어도 상관없다.
- case 3
- 함수를 선언한 곳에서 파라미터로 들어올 함수의 파라미터 타입이 무엇인지 알기 때문에
: Fruit
를 생략할 수 있다.
- case 4
it
으로 대체될 수 있다.
# java
String targetFruitName = "바나나";
targetFruitName = "수박";
// Variable used in lambda expression should be final or effectively final
filterFruitsUsingLambda(fruits, fruit -> targetFruitName.equals(fruit.getName())); // 컴파일 에러
Java 에서는 문제가 있다
- 람다 로직 내부에서 예외가 난다.
- Java 에서는 람다를 쓸 때 사용할 수 있는 '변수'에 제약이 있다.
-final
인 변수 혹은 실질적으로final
인 변수만 사용할 수 있다.
- 즉,
targetFruitName
의 값이 변경되었으므로 람다에서 사용이 불가능해 진 것이다.- Variable used in lambda expression should be final or effectively final
# kotlin
var targetFruitName = "바나나"
targetFruitName = "수박"
println(filterFruits(fruits) {it.name == targetFruitName})
Kotlin 에서는 아무 문제 없다.
- 아무 문제 없이 사용이 가능하다!
- 코틀린은 '람다가 시작하는 지점 (즉,
it.name == targetFruitName
이 실행되는 시점) 에 참조하고 있는 변수들을 모두 '포획'한다.- 그래서 포획된 모든 변수들의 정보를 가지고 있다.
- 이렇게 해야만 람다를 진정한 '일급 시민' 으로 간주 할 수 있다.
- 이러한 데이터 구조를 '클로저 (Closure)' 라고 한다.
- 람다가 실행되고 있는 시점에 쓰고 있는 모든 변수들을 포획한 데이터 구조
# kotlin
data class Fruit (
val id: Long,
val name: String,
val factoryPrice: Long,
val currentPrice: Long?
) {
val isSamePrice: Boolean
get() = factoryPrice == currentPrice
}
# kotlin
val apples = fruitList.filter { fruit -> fruit.name == "사과" }
val applesUsingIt = fruitList.filter { it.name == "사과" }
💡 Key Point
1. filter 에 조건을 함수로 넘겨준다.
2. it 을 사용할 수도 있다.
# kotlin
val applesUsingIndex = fruitList.filterIndexed {
idx, fruit ->
println(idx)
fruit.name =="사과"
}
💡 Key Point
1. 필터에 인덱스가 필요하다면filterIndexed
를 사용할 수 있다.
# kotlin
val applesUsingFilterAndMap =
fruitList
.filter { it.name == "사과" }
.map { it.currentPrice }
💡 Key Point
1. 매핑을 하고 싶다면map
을 사용할 수 있다.
2.currentPrice
정보들로 리스트가 매핑된다.
# kotlin
val applesUsingFilterAndMapIndex
= fruitList.filter { it.name == "수박" }
.mapIndexed{ idx, fruit ->
println("수박 인덱스 $idx")
fruit.currentPrice
}
💡 Key Point
1. 매핑과 동시에 인덱스를 가지고 오고 싶다면mapIndexed
를 사용할 수 있다.
2. 리스트가 매핑되는 값은 가장 아래의fruit.currentPrice
로 매핑이 된다.
# kotlin
val applesUsingMapNotNull
= fruitList.filter { it.name == "사과" }
.mapNotNull { it.currentPrice }
💡 Key Point
1. 매핑의 결과가null
이 아닌 것만 가지고 오고 싶을 때mapNotNull
을 사용한다.
2. 여기서 매핑의 결과가null
이라는 의미는, 매핑된 결과 즉, 객체의currentPrice
가null
인 경우를 의미한다.
# kotlin
# all
val isAllApple = fruitList.all { it.name == "사과" }
# none
val isNoApple = fruitList.none { it.name == "사과" }
# any
val isAnyApple = fruitList.any {it.name == "사과"}
💡 Key Point
1. all : '모든 조건을 만족'하면 true, 하나라도 아니면 false
2. none : all 과 반대. 조건을 '모두 만족하지 않으면' true, 하나라도 만족하면 false
3. any : 조건을 '하나라도 만족하면' true, 모두 만족하지 않으면 false
# kotlin
# count
val countFruit = fruitList.count()
# sortedBy
val ascFruit = fruitList.sortedBy { it.id }
# sortedByDescending
val decFruit = fruitList.sortedByDescending { it.id }
#distinctBy
val distinctFruitNames = fruitList.distinctBy { it.name }.map { it.name }
💡 Key Point
1. count : 컬렉션의 개수를 리턴한다.
2. sortedBy : 함수에 들어온 값을 기준으로 정렬한다. 기본은 오름차순 (asc) 이다.
3. sortedByDescending : 내림차순으로 정렬한다. (desc)
4. distinctBy : 함수에 들어온 값을 기준으로 중복을 제거한다.
# kotlin
# first
val firstFruitPrice = fruitList.first()
# firstOrNull
val firstOrNullFruitPrice = fruitList.map { it.currentPrice }.firstOrNull()
# last
val lastFruitPrice = fruitList.last()
# lastOrNull
val lastOrNullFruitPrice = fruitList.map { it.currentPrice }.lastOrNull()
💡 Key Point
1. first : 첫번째 값을 가져온다. (null
이 아니어야 한다.)
2. firstOrNull : 첫번째 값 또는null
을 가져온다.
3. last : 마지막 값을 가져온다. (null
이 아니어야 한다.)
4. lastOrNull: 마지막 값 또는null
을 가져온다.
# kotlin
val fruits = listOf(
Fruit(1,"사과", 1000,null),
Fruit(2,"사과", 1200,3000),
Fruit(3,"사과", 1200,3400),
Fruit(4,"사과", 1500, 5000),
Fruit(5,"바나나", 3000, 7000,),
Fruit(6,"바나나", 3200, 8000,),
Fruit(7,"바나나", 2500, 7090),
Fruit(8,"수박", 10000, 12800)
)
--
# groupBy
val map: Map<String, List<Fruit>>
= fruits.groupBy { it.name }
val mapKeyAndValue: Map<String, List<Long>>
= fruits.groupBy({it.name}, {it.factoryPrice})
# associatedBy
val mapAssociatedBy: Map<String, Fruit>
= fruits.associateBy { it.name }
val mapKeyAndValueAssociatedBy: Map<String, Long>
= fruits.associateBy({it.name}, {it.factoryPrice})
💡 Key Point
1. groupBy
- list (fruits) 를 groupBy 에 명시한 것 기준으로 묶어서 Map 컬렉션이 된다.
- value 값도 함께 기준을 정하고 싶다면 groupBy 에 순차적으로 key, value 순으로 정해주면 된다.
- associatedBy
- value 가 list 가 아닌 경우에는 associatedBy 를 사용한다.
- associatedBy 는 말 그대로, 함수에 값들을 '모아주는' 기준점을 제시하는 것.
println(mapAssociatedBy)
출력값 :
-{사과=Fruit(id=4, name=사과, factoryPrice=1500, currentPrice=5000), 바나나=Fruit(id=7, name=바나나, factoryPrice=2500, currentPrice=7090), 수박=Fruit(id=8, name=수박, factoryPrice=10000, currentPrice=12800)}
# kotlin
data class Fruit (
val id: Long,
val name: String,
val factoryPrice: Long,
val currentPrice: Long?
) {
val isSamePrice: Boolean
get() = factoryPrice == currentPrice
}
--
val fruitsInList: List<List<Fruit>> = listOf(
listOf(
Fruit(1, "사과", 1000, 1500),
Fruit(2, "사과", 1200, 2000),
Fruit(3, "사과", 1500, 2300),
Fruit(4, "사과", 1400, 2500)
),
listOf(
Fruit(5, "바나나", 2500, 2500),
Fruit(6, "바나나", 3000, 4900),
Fruit(7, "바나나", 3200, 5000),
),
listOf(
Fruit(8, "수박", 20000, 34000)
)
)
---
# flatMap
val samePriceFruits = fruits.flatMap { list ->
list.filter { it.factoryPrice == it.currentPrice }
}
# flatMap + 확장함수
val luxurySamePriceFruits =
fruits.flatMap { it.samePriceFilter }
val List<Fruit>.samePriceFilter: List<Fruit>
get() = this.filter { it.isSamePrice }
# flatten
val flattenList = fruits.flatten()
💡 Key Point
1. flatMap
flatMap
을 사용하면list<list<Fruit>>
가list<Fruit>
가 된다.- 위 예제는, 중첩 리스트를 한번 flat 하게 핀 후에, filter 를 통해 factoryPrice 와 currentPrice 가 같은 경우를 뽑아내는 것.
- 확장함수를 이용하면 더 깔끔하고 가독성 있게 만들 수 있다.
List<Fruit>
라는 타입에다가samePricefilter
라는 확장함수를 추가한다.- 해당 확장함수는 Fruit 클래스에 선언되어있는
isSamePrice
를 filter 로 거른List<Fruit>
를 리턴한다.
- flatten
- 그냥 어떠한 조건 없이 중첩 리스트를 피는 것!
list<list<Fruit>>
가list<Fruit>
로 순수하게 변환!
# kotlin
typealias FruitFilter = (Fruit) -> Boolean
private fun filterFruits(
fruits: List<Fruit>,
filter: FruitFilter
): List<Fruit> {
return fruits.filter(filter)
}
💡 Key Point
1. 긴 이름의 클래스 혹은 함수 타입이 있을 때, 축약하거나 더 좋은 이름을 사용하고 싶을 때 유용하다.
(Fruit) -> Boolean
타입의 함수타입을FruitFilter
로 이름을 주고 사용
# kotlin
data class UltraSuperGuardianTribeBlaBlaBla (
val name: String
)
typealias USGTMap = Map<String, UltraSuperGuardianTribeBlaBlaBla>
--
fun main () {
val usgtMap: USGTMap
{
💡 Key Point
1. 위와같이 이름이 긴 클래스를 컬렉션에 담아서 사용할 때도 유용하게 사용이 가능하다.
# kotlin
import com.lannstark.lec19.a.printHelloWorld as printHelloWorldA
import com.lannstark.lec19.b.printHelloWorld as printHelloWorldB
fun main () {
printHelloWorldA()
printHelloWorldB()
}
💡 Key Point
1. 다른 패키지의 같은 이름인 함수를 동시에 가지고 오고 싶을 때 사용한다.
구조분해
1. 의미
- 복합적인 값을 분해하여 여러 변수를 한 번에 초기화 하는 것을 의미함.
- 흠.. 🤔 뜻이 이해하기 상당히 어렵다.
아래의 예제를 보자!
# kotlin
data class Person (
val name: String,
val age: Int,
val gender: String
)
--
fun decompostion () {
val person = Person("김민재", 30, "남자")
# 구조 분해 할당
val (name, age) = person
val (ff, dd) = person
# 할당 된 변수 출력
println("이름 : $name 나이 : $age")
println("이름 : $ff 나이 : $dd")
}
💡 Key Point
1. 예제를 통해 보면 별거 없다.
- 변수를 초기화 할 때, 복합적인 값 (여기서는 person 객체) 을 분해해서 한번에 초기화 하는 것!
- 이 예제를 보고 위의 구조분해의 의미를 다시 보면 이해된다. 별거없다.
- ⛔️주의
- 구조분해는
data class
에서만 가능하다.- 그 이유는
componentN
함수라는 비밀에 있다.
componentN 함수
1.data class
는componentN
이라는 함수를 자동으로 만들어준다.
2. componentN 함수란?
- data class 는 선언된 필드의 순서대로 component 함수를 만들어준다.
- N 의 의미는 선언된 프로퍼티의 순서를 의미한다.
- 위의
Person
클래스의name
은 1번,age
는 2번 .. 이런 식이다.name
이나age
라는 이름으로 인식하지 않고 프로퍼티 순서로 값을 가져온다.
- 일반 클래스에서 구조분해 할당을 하고 싶다면
componentN
함수를 만들어주면 된다.
# kotlin
class PersonNotData (
val name: String,
val age: Int
) {
operator fun component1(): String {
return this.name
}
operator fun component2(): Int {
return this.age
}
}
--
val personNotData = PersonNotData("김민재", 100)
val (name1, age1) = personNotData
💡 Key Point
1. 일반 클래스에서componentN
함수를 구현 했다.
그리고 나서 구조분해 할당이 가능해졌다.
2. 여기서 주의해야 할 점은componentN
함수를 구현할 때,operator
연산자를 붙여야 한다는 것이다.
즉, 코틀린은componentN
함수를 연산자로 인식한다는 것이다.
# kotlin
fun getNumberOrNull(number: Int): Int? {
return if (number <= 0) {
null
} else {
number
}
}
fun getNumberOrNullTakeIf (number: Int): Int? {
return number.takeIf { it > 0 }
}
💡 Key Point
1. 주어진 조건을 만족하면 그 값이 반환되며, 그렇지 않으면 null 이 반환된다.
2.takeIf
를 사용한 값을 return 하게 되면, return 타입은 nullable 한 곳에 사용되어야 한다.
3.takeUnless
- takeIf 와 반대
- 주어진 조건을 만족하지 않으면 그 값이 반환되고, 만족한다면 null 이 반환된다.
개념을 잡자
- 의미 : '일시적인 영역' 을 형성하는 '함수'
- 람다를 사용해서 일시적인 영역을 만들고 코드를 더 간결하게 만든다.
- 메서드 체이닝을 활용하여 코드를 보다 간결하게 만든다.
# kotlin
fun printPersonV1 (person: Person?) {
if (person != null) {
println(person.name)
println(person.age)
}
}
---
# scope function 'let' 사용
fun printPersonV2 (person: Person?) {
person?.let {
println(it.name)
println(it.age)
}
}
💡 Key Point
1.let
을 통해 '일시적인 영역' 이 생겼다.
- 이 영역은 람다가 된다.
- 람다 안에서
it
을 통해person
에 접근한다.
let
은 람다를 받고, 그 람다의 결과를 반환 한다. (밑에 더 자세하게 서술한다.)
[it 사용] [this 사용]
[람다의 결과] 'let' 'run'
[객체 그 자체] 'also' 'apply'
'with'
💡 Key Point
1. Scope Function 은 총 5가지
- let, run, also, apply, with
- with 를 제외한 4가지는 모두 '확장함수'로 사용한다.
- Scope Function 은 모두 람다 형식으로 사용된다.
- let, run : 람다의 결과를 반환한다.
- also, apply : 람다 내에서 뭔가를 해도 객체 자체를 반환한다.
- let, also : 람다 내에서 it 을 사용한다.
- run, apply : 람다 안에서 this 를 사용한다.
# kotlin
val letPerson = person.let {
it.age
}
println(letPerson)
val runPerson = person.run {
this.age
}
println(runPerson)
💡 Key Point
1. 람다에서는 가장 마지막 줄이 return 된다.
2. let, run 은 람다의 결과가 반환된다.
3. 즉, person 의 age 가 반환된다.
# kotlin
val alsoPerson = person.also {
it.age
}
println(alsoPerson)
val applyPerson = person.apply {
this.age
}
println(applyPerson)
💡 Key Point
1. also, apply 에서 반환되는 람다는 객체 그 자체가 반환된다.
2. 그러므로 println 을 통해 콘솔에 찍히는 값은 객체의 주소값이다.
람다의 결과는 해당 scope function 의 return 값과는 무관하다.
확장함수로 사용된 객체의 클래스 자체가 반환된다.
# kotlin
val with: String = with(person) {
println(name)
println(this.age)
this.name
}
println("with : $with")
💡 Key Point
1. with 는 확장함수로 사용되지 않으므로 사용법이 조금 다르다.
2. 객체를 with 의 파라미터로 받아서 사용한다.
3.this
를 통해 객체에 접근이 가능하다.
# kotlin
fun usingLet1 () {
val strings = listOf("Apple", "car", "banana")
strings.map { it.length }.filter { it > 3 }.let { println(it) }
}
# kotlin
fun usingLet2(str: String?) {
val length = str?.let {
println(it.uppercase())
it.length
}
println(length)
}
?.
을 통해 non-null 인 경우에 let 을 사용한 함수가 실행 된다.# kotlin
fun usingLet3 () {
val numbers = listOf("one", "two", "three", "four")
val changedFirstItem = numbers.first()
.let { firstNumber ->
if (firstNumber.length >= 5) firstNumber else "!$firstNumber!"
}.uppercase()
println(changedFirstItem)
}
firstNumber
라는 지역변수를 let 을 통한 람다의 영역에 만들고, 이 변수를 통해 편하게 return 값을 요리한다.# kotlin
fun usingRun1 (personRepository: PersonRepository) {
val person = Person("김민재", 100)
.run { personRepository.save(this) }
}
person
객체를 초기화 함과 동시에 '객체를 만들어서 DB 에 저장' 한다.# kotlin
fun createPerson (
name: String,
age: Int,
): Person {
return Person(
name = name
).apply {
this.age = age
}
}
# kotlin
fun alsoTest1 () {
mutableListOf("one", "two", "three")
.also { println("four 추가 이전 지금 값 : $it") }
.add("four")
}
# kotlin
fun usingWith (person: Person): PersonDto {
return with(person) {
PersonDto(
name = this.name,
age = this.age
)
}
}
---
data class PersonDto (
val name: String,
val age: Int
)
---
fun main() {
val dto: PersonDto = usingWith(Person("김민재", 32))
println("dto : ${dto.name} + ${dto.age}")
}
💡 Key Point
1. 특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 메서드 혹은 toClass 함수를 만들기 어려울 때 사용.
2. 객체 컨버팅할 때 많이 사용된다.
3. 위의 예시에서는this
를 사용했으나, 생략할수 있다.
this
를 생략할 수 있기에 필드가 많아도 코드가 간결해진다.
과한 Scope Function 은 숙련된 코틀린 개발자가 아닌 경우 보기 힘들다.🫠
디버깅이 힘들다.
그로 인한 수정이 힘들 수 있다.
하지만, 적절하게 사용하면 아주 유용하게 활용이 가능하다.
💡 팀의 코틀린 숙련도와 선호도에 따른 컨벤션을 정하는 것이 중요하다.
이제, 코틀린의 기초를 끝냈다. 만세!!
다시 공부하면서 글을 정리하니까 생각보다 시간이 꽤 걸렸다.
Java 에 익숙한 개발자들이 이 글을 통해 Kotlin 기초 문법을 학습하는데 도움이 되면 좋겠다.
그리고 위의 서론에서 이야기했지만, 최태현님 코틀린 강의를 듣고 이 글을 보면 좋겠다.
강의를 통해 공부하고 이 글은 복습과 까먹었을 때 다시 상기시키는 사전 처럼 사용되면 좋겠다.
그럼, 이제 다음 강의 부수러 가보잣! 🍰
수강하면서 자세히 정리한 게 인상적입니다. 언제나 개발을 대하는 자세를 배웁니다! 좋은 포스팅 잘 봤어요