Java 개발자의 Kotlin 기본 문법 정복기

Joshua_Kim·2023년 11월 18일
22

Kotlin 정복기

목록 보기
1/1
post-thumbnail

🌱 서론

원하던 회사에 이직을 했다. 야호! ✈️
(여담이지만 이직을 준비하면서, 그리고 이직한 회사에서 얻은 인사이트를 정리하여 조만간 블로그에 올릴 예정이다.)

이직한 회사는 현재 Java로 만들어진 레거시 코드들을 Kotlin 으로 전환하는 중이다.
이미 서비스가 굴러가고 있는 터라 달리는 자동차의 바퀴를 바꾸는 과정에 있다.

안그래도 Kotlin 을 배우고 싶었는데,
Kotlin 보다 아직 더 우선적으로 공부할 것들이 많아서 뒤로 미루고있었다. (게을러서다)

입사하고 코드들을 봤는데 일단 Kotlin 이었다. 👀
들어보니 인텔리제이의 도움을 받아 Java 코드들이 일단 Kotlin 으로 돌아는 가게끔 만들었다고 했다.

이제 이 코드들을 Kotlin 스럽고 '우아하게' 만들어야 한다. 🍬

코드들을 들여다보는데 역시나 익숙하지 않아서인지 한눈에 들어오지 않았다.

Kotlin 을 공부한다면 꼭 들어야겠다고 벼르고 있었던 '인프런''최태현' 님의 강좌를 새로운 회사에 출근한지 일주일만에 신청했다. (교육 복지 만세 🎉)

강의 링크 : 👉🏻 자바 개발자를 위한 코틀린 입문
(어떠한 광고비도 받지 않았다 ㅎ.. 강의가 너무 좋았음 👍🏻)

퇴근 후 그리고 주말을 이용해 일주일만에 기초강의를 들었다.
그만큼 강의가 재미있었고, 배우는 즐거움이 컸다.

강의를 들은 후, 이 포스팅을 적기 시작했다.
포스팅을 정리하는 것도 약 일주일이 걸렸다. 🥳

이 포스팅의 목적은 다음과 같다.

목적
1. 강의에서 배운 내용을 바탕으로 복습Kotlin 문법을 정리한다.
2. Java 에 익숙한 개발자들이 이해하기 편하도록 정리한다.
3. 실무에서 문법이 헷갈리거나 개념이 헷갈릴 때 언제든 찾아 볼 수 있도록 기초 문법 사전으로 사용하도록 정리한다.


📖 강의 정리

ITEM 1. 변수

# 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 쓰고 다시 지우곤 한다 ㅎ


ITEM 2. null

Safe Call 과 Elvis 연산자

Safe Call

  • 말 그대로 안전하게 불러오는 것을 의미함
  • ?. 을 통해 null 이 들어올 수 있는 값이 npe 터지지 않도록 함.
  • null 이라면 null 그대로를 반환한다.
# kotlin - safe call 예시
val str1: String? = "ABC"
val str2: String? = null
println(str1?.length) //-> 3
println(str2?.length) //-> null

Elvis 연산자

  • 앞의 연산 결과가 null 이면 Elvis 연산자 뒤의 값을 사용한다.
  • ?: 형태
  • 왜 Elvis 냐? 저걸 시계방향으로 돌리면 꼬브랑 머리가 엘비스 프레슬리 같다고 ..ㅎ..
# kotlin - Elvis 연산자 예시
val nullValueUsingSafeCall: Int? = safeCallMethod(null)
println(nullValueUsingSafeCall?.dec() ?: 10) // '?:' 요게 Elvis 연산자

Safe Call 과 Elvis 연산자를 활용하여 Java -> Kotlin

# 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
}
  • 직관적이면서도 간결하다. 만세 !

null 을 안전하게 다루는 Kotlin

fun startWithA4 (str: String): Boolean {
    return str.startsWith("A")
}
  • strString 타입이므로 (String? 타입이 아니므로) null 이 들어가지 않는다고 코틀린이 간주한다.
  • 그러므로 startsWith() 메서드 콜이 바로 가능하다.

fun safeCallMethod (str: String?): Int? {
    return str?.length
}
  • strString? 타입이므로 null 이 들어갈 수 있다.
  • 이 경우 메서드 콜을 위해 safe call 을 한다.
  • str?.lengthstrnull 이 아니면 실행하고, null 이면 실행자체를 하지 않고 null 을 반환한다.

💡 Key Point
1. 코틀린은 null 이 가능한 타입(?가 붙은 타입)을 다른 타입으로 취급한다.
2. 언어 단에서 null 을 안전하게 다뤄준다는 것만으로도 코틀린을 써야할 이유가 충분하다. ㅎ..


nullable 이지만, 절대 null 일 수 없는 경우

이런 경우는 어떤 경우일까? 🤔

DB 에 처음 데이터가 들어올 때는 null 일 수 있어서 nullable 이지만,
한번 어떠한 비지니스 로직을 통해 해당 데이터가 업데이트 되면 그 이후로는 절대 null 이 아닌 경우

fun neverNullValue (str: String?): Boolean {
    return str!!.startsWith("A") 
}
  • 절대 null 이 아니야!! 그러니까 startWith() 메서드를 실행해줘!!!!

💡 Key Point
1. 절대 null 이 아니다!! 라고 소리친다고 이해하면 좋음
2. 개인적으로는 이 방법이 썩 좋은 방법 같지는 않다. (어떤일이 터질지 모르니까.. 프로그래밍 세계는 절대적인게 없거든. 웬만하면 안써야겠다.)


ITEM 3. Type

타입 캐스팅과 (개쩌는) 스마트 캐스팅

# 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)
    }
  • instanceofis 로 사용된다.
  • java 에서 캐스팅을 위한 (Person)as 를 사용하며, 생략이 가능하다.

version 2

fun printAgeIfPerson (obj: Any) {
    if (obj is Person) { 
        println(obj.age) 
    }
  • 개쩌는 스마트 캐스팅 🫢
  • 코틀린이 if 절을 컴파일에서 이미 이해하고, objPerson 타입인것을 인지하여 age 메서드 콜이 가능하다.

version 3

fun printAgeIfPerson (obj: Any) {
	val person: Person? = obj as? Person 
    println(person?.age)
}
  • 안전한 캐스팅
  • Personnull 이 들어올 수 있을 때, nullable 로 선언이 가능하다.
  • 이 타입이 아닌경우에도 castException 이 아니라 null 이 반환된다.

💡 Key Point
1. instanceof -> is
2. 캐스팅은 as 로 사용하며, 생략이 가능.
3. 코틀린은 스마트 캐스팅이 가능하다.


Kotlin 의 특이한 타입 3가지

Any

  • JavaObject 역할. 즉, 모든 객체의 최상위 타입
  • Any 자체로는 null 을 포함하지 않는다.
    - null 포함하려면 Any? 로 표현해야한다. ㅎ
  • Anyequals, hashCode, toString 이 존재한다

Unit

  • Javavoid 와 동일한 역할
  • 하나 다른 점은, unit 은 그 자체로 타입 인자로 사용 가능하다.
  • 이게 무슨말이냐면 코틀린의 unit 은 실제 존재하는 타입이라는 것! (Javavoid 는 타입이 없었..)

Nothing

  • 함수가 정상적으로 끝나지 않았다는 사실을 표현할 때 쓰인다.
  • '무조건 예외를 반환하는 함수', '무한루프 함수' 등..
  • 실무에서는 잘 안쓰인다.

ITEM 4. 연산자

연산자 공부를 위해 만들어진 Money 클래스

# 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 
  • money1money3 은 주소 값이 같은 동일한 객체다. (==)
  • compareTo() 가 구현되어 있다면, money1money4equals() 즉, 값이 같은 동등한 객체다
  • 좀 헷갈림..

# 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)


ITEM 5. 제어문

if

# 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보다 작을 수 없습니다.")
    }
}
  • 위의 코드는 사실 별다를게 없다.

if-else

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
}
  • 코틀린에서는 if-else 가 Expression 이므로 변수로도 할당이 가능하다. 🫢

when (switch 보다 강력한..!)

  • 코틀린에서는 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"
    }
}
  • when 을 사용하면 어떠한 범위에 있거나 다른 기타 조건을 사용해서 분기를 치는 것도 가능하다.
  • 즉, '조건부' 를 사용할 수 있다는 것.
  • 이 조건부에는 어떠한 Expression 도 들어갈 수 있다. 매우 강력하다.

# 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("주어진 숫자는 홀수입니다.")
    }
}
  • when 절에 값을 넘기지 않고 바로 조건부에 조건문을 넣을 수 있다.

ITEM 6. 반복문

전통적인 for 문

# 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. 코틀린에서 downTostep 은 '중위함수' 인데, 아래의 '함수' 파트에서 더 자세하게 다룬다.


향상된 for 문

# 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 을 사용한다.


while

# 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 은 자바와 코틀린 동일하다.


ITEM 7. 예외

try - catch - finally

# 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 이다.


Checked Exception 과 Unchecked Exception

# 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 을 코드를 짤 때 신경 써줄 필요가 없다. 🫢


try with resources

  • jdk 7에서 추가 됨.
  • 외부 리소스를 try 문 안에서 생성하고 자동으로 close 해주는 것!
# 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 확장함수를 사용한다. 아래에 함수 부분에서 자세하게 정리되어있다.


ITEM 8. 함수

바디{} 가 필요없다면 굳이?

# 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. 만약 중괄호를 사용했다면 명시적으로 반환타입을 적는 것이 좋다.


Default Parameter

# 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개. 오버로딩이 되어있다.
  • 이유는 useNewLinetrue 로 사용되는 경우가 많아서!
  • 그런데 또 쓰다보니까 num3 인 경우도 꽤 많아서 또 오버로딩을 했다.
  • 흠.. 그런데 이렇게되면 계속 오버로딩을 하게 되는데...
# kotlin

fun repeat (
    str: String,
    num: Int = 3,
    useNewLine: Boolean = true
) {
    for (i in 1..num) {
        if (useNewLine) {
            println(str)
        } else {
            print(str)
        }
    }
}
  • 코틀린에서는 Default Parameter 라는 개념이 존재한다. 🫢
  • 값을 넣지 않으면 기본 값을 주어주는 것이다.

Named Argument

# kotlin

repeat("룰루", useNewLine = false)
repeat(useNewLine = true, str = "후후후")
  • repeat 함수에서 값을 넣지 않으면 기본값을 사용하고, 임의로 값을 넣어주고 싶다면 골라서 파라미터의 이름을 key 로 넣고 값을 줄 수 있다.
  • 이건 정말 아름답고 개쩐다고 말할 수 밖에 없다.🍬
  • 어떤 값은 default parameter 에 맡기고, 변경되는 값만 쏙 넣어줄 수 있는 것 🫢
  • builder 를 만들지 않고 builder 의 장점을 그대로 가져 올 수 있다.
# kotlin

fun printNameAndGender (name: String, gender: String) {
    println(name)
    println(gender)
}

printNameAndGender("", "")
printNameAndGender(name = "이름", gender = "남자")
  • 동일한 타입의 parameter 가 있을 경우에도 named argument 는 매우 큰 장점을 가진다.
  • builder 를 사용할 때 이런 것을 확인하기 쉽다는 장점이 있는데, 이와 같은 장점을 누릴 수 있다.

가변인자

# 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. 스프레드 연산자 : * 배열안에 있는 것들을 마치 그냥 ',' 쓰는 것처럼 요소들을 펼쳐서 꺼내준다.


ITEM 9. 클래스

클래스의 선언

# 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 은 불변이므로 javafinal 이 붙은 것과 같다.
또한, final 은 수정될 수 없으므로 getter 만을 생성한다.
2. agevar 로 선언되었으므로, gettersetter 모두 생성해준다.
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. 객체가 생성되었을 때 해당 로직이 실행 된다.


Custom Getter

예시 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 또한 마찬가지 !


ITEM 10. 상속

추상클래스 상속

# 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. legCountoverride 하기 위해서는 AnimallegCountopen 해줘야 한다.

  • 추상 프로퍼티가 아니면, 상속받을 때 open 을 꼭 붙여야 한다.
  • 추상 클래스에서 자동으로 만들어진 getteroverride 하기 위함이다.

인터페이스 구현

# 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 프로퍼티는 디폴트 값이 없으므로 구현할 때 getteroverride 하여 구현해줘야 한다.
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 으로 열려있다.
  • Derivednumberoverride 하고 있다.
  • 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이 출력된다.

ITEM 11. 접근 제어

유틸성 함수를 만들 때

# 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. namegetter 의 접근제어자가 internal 이다. (val 이므로 getter뿐이다)
2. ownergettersetter 모두 private 이다.
3. pricegetterpublic 이며, setter 는 명시적으로 private 이 되었다.

  • 코틀린에서는 기본 접근제어자가 public 이다.

ITEM 12. object 키워드

static 함수와 변수

# 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 :
    - 클래스와 동행하는 유일한 오브젝트.
    • 이것도 클래스라는 설게도와 동행하는 유일한 오브젝트로 정적이다.
  1. 한 클래스에는 하나의 companion object 를 만들 수 있다.
  2. 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 objectFactory 라는 이름을 가지며, 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 에도 붙일 수 있다.

Singleton

# 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 : 타입이름 { ... }
  1. override 키워드를 통해 인터페이스의 메서드를 익명클래스로 구현한다.

ITEM 13. 중첩 클래스

Inner 클래스와 static inner 클래스

💡 개념

  • 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)
  1. 코틀린은 class 내부에 class 를 만들면 권장되는 static inner class 가 된다.
  • 그냥 편하게 내부에 class를 만들어 주면된다.
  • 기본적으로 바깥 클래스에 대한 참조가 없는 클래스가 만들어진다.

ITEM 14. 다양한 클래스 (Data, Enum, Sealed)

📀 Data 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' : dataclass 앞에 적어두면 dto 클래스로서 완벽하다.

  • equals(), hashCode(), toString() 을 자동으로 만들어 준다.
  1. 여기에 named argument 까지 활용한다면, builder 패턴을 사용하는 것과 같은 효과도 가진다.
  • 사실상 data 키워드에 builder 까지 있는 격이다.

Enum Class

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 함수를 보자.

  • whenenum 을 활용하면 더 읽기 쉬운 코드를 사용할 수 있다.
  • 컴파일러가 Country 의 모든 타입을 알고 있어서 else 를 추가로 작성할 필요가 없다.
  • 코딩을 하면서 enum 클래스에 변화가 있으면 컴파일러에서 경고를 바로 날려서 개발자에게 알려준다.🫢 개쩐다!
    - '님 뭐 잊은거 없음? Enum 에 France 추가 되었음!' 이라고 알려줌 👍🏻
  • java 에서는 enum 클래스에 값이 추가되어도 알 방법이 없다.

Sealed Class, Sealed Interface

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 과 차이

  • 클래스를 상속받을 수 '있다.'
  • 하위 클래스는 멀티 인스턴스가 가능하다.
  1. Enum 과 마찬가지로 when 에서 매우 효과적으로 사용이 가능하다.
  2. 추상화가 필요한 Entity 나 DTO 에 활용된다.

ITEM 15. 배열과 컬렉션

배열

배열은 잘 사용되지 않는다.

  • 하지만 기본 문법은 알아두잣!
# 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() 를 통해 선언해준다.
  1. 배열의 범위 - indices
  • 0 부터 마지막 index 까지의 범위를 의미한다.
  • indeces 는 index 의 복수형이다.
  1. 인덱스와 값을 동시에 가져오기 - `withIndex()
  • 배열의 인덱스와 그에 해당하는 값을 동시에 가져올 수 있다.
  1. 배열에 값 넣기
  • plus() 를 통해 값을 쉽게 넣을 수 있다.

Collection

불변, 가변

  • 코틀린에서는 컬렉션을 만들 때, '불변' 인지 '가변' 인지를 설정해야한다.
  • 언어 레벨에서 개발자에게 해당 부분을 미리 신경쓰도록 한다. 🫢
  • 가변 컬렉션 (Mutablexxx) : 컬렉션에 element 를 추가, 삭제할 수 있다.
  • 불변 컬렉션 : 컬렉션에 element 를 추가, 삭제할 수 없다.
    - 우리가 익숙한 이름들 (List, Set, Map 등) 이 코틀린에서는 불변 컬렉션이다.

💡 Tip : 불변 컬렉션으로 우선 만들고, 꼭 필요한 경우에 가변 리스트로 변경하자.

# 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)
}
  • List 를 이해 했다면 Set 또한 이해하기 쉽다.
# 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 을 만들 수 있다.


Colleciton 의 null 가능성 정리

? 의 위치에 따라 이야기가 달라짐
1. List<Int?>

  • 리스트에 null 이 들어갈 수 있지만, 리스트 자체는 절대 null 이 아님
  1. List<Int>?
  • 리스트에 null 이 절대 들어갈 수 없지만, 리스트 자체가 null 일 수 있다.
  1. List<Int?>?
  • 리스트에 null 이 들어갈 수도 있고, 리스트 자체가 null 일 수도 있다.

ITEM 16. 다양한 함수 (확장함수, 중위함수, inline 함수)

확장함수

⛔️ 여기서 부터 좀 '개쩐다' 라는 표현이 많이 나옵니다 ㅎ..

배경
1. kotlin 은 java 와 완벽하게 호환하는 것을 목표로 했다.

  • 🤔 : "Java 코드 위에 자연스럽게 코틀린을 살포시 얹을 수 없을까?"
  • "Java 코드는 그대로 두고 싶은데.."
  • "Java 로 만들어진 라이브러리로 유지보수 하면서 동시에 확장할 때는 코틀린을 '덧붙이고' 싶어!"
  1. 어떤 클래스 안에 있는 메서드 처럼 호출할 수 있지만, 함수 밖에 만들 수 있게 하려고 함.
  • 즉, 함수의 코드 자체는 클래스 밖에 있는데, '마치 클래스 안에 있는 멤버 함수 처럼' 사용하려는 것!
# kotlin

fun String.customLastChar(): Char {
    return this[this.length - 1]
}

fun main() {
    val str: String = "ABC"
    
    println(str.customLastChar()) // 'C' 출력 

}
  • String 클래스 안에 있는 메서드 처럼 호출 한다.
  • 하지만 실제로 String 클래스 안에는 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 형태로 함수를 호출한다.


inline 함수

사용처
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)
}
  • 🙄 딱 봐도 별로다. 따로 함수를 분리하고 싶다. ㅎ..

ITEM 17. 람다(Lambda)

람다 문법

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 개발자로서는 '함수의 타입' 이라는 것이 좀 낯설다.

  • 함수의 타입 이라면 '리턴 타입'이 익숙한데, 여기서 말하는 것은 '함수 자체의 타입' 을 의미한다.
  1. 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 타입이다.

Java 와 Kotlin 비교

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 를 파라미터로 받는다.
  1. 참고로 fruits.filter(..) 에서 호출된 filter 는 Collection 에 있는 filter 다.
  • java 의 stream 에 있는 것과 같다고 보면 된다.
  • 혹시가 헷갈릴까봐 ㅎㅎ 🫠

위의 filterFruits 함수를 사용해보자.

# 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 을 넣어 줄 수있다.
  1. case 2
  • filterFruits() 함수의 마지막 파라미터가 함수일 경우 중괄호를 바깥으로 빼서 넘길 수 있다.
  • 소괄호 안에 해당 함수가 들어가도 되지만, 소괄호 안에 중괄호 들어가는게 조금 어색하기 때문이다.
  • 물론, 소괄호 안에 들어가 있어도 상관없다.
  1. case 3
  • 함수를 선언한 곳에서 파라미터로 들어올 함수의 파라미터 타입이 무엇인지 알기 때문에 : Fruit 를 생략할 수 있다.
  1. 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)' 라고 한다.
    - 람다가 실행되고 있는 시점에 쓰고 있는 모든 변수들을 포획한 데이터 구조

ITEM 18. 컬렉션을 함수형으로 다루기

예제의 대상이 될 Fruit 클래스 🍎

# kotlin

data class Fruit (
    val id: Long,
    val name: String,
    val factoryPrice: Long,
    val currentPrice: Long?
) {
    val isSamePrice: Boolean
        get() = factoryPrice == currentPrice
}

Filter 와 Map

# 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 이라는 의미는, 매핑된 결과 즉, 객체의 currentPricenull 인 경우를 의미한다.


다양한 컬렉션 기능 (all, none, any, count, sorted, distinctBy, first, last)

# 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 을 가져온다.


List 를 Map 으로 변환

# 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 순으로 정해주면 된다.
  1. 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 가 같은 경우를 뽑아내는 것.
  1. 확장함수를 이용하면 더 깔끔하고 가독성 있게 만들 수 있다.
  • List<Fruit> 라는 타입에다가 samePricefilter 라는 확장함수를 추가한다.
  • 해당 확장함수는 Fruit 클래스에 선언되어있는 isSamePrice 를 filter 로 거른 List<Fruit> 를 리턴한다.
  1. flatten
  • 그냥 어떠한 조건 없이 중첩 리스트를 피는 것!
  • list<list<Fruit>>list<Fruit> 로 순수하게 변환!

ITEM 19. 코틀린의 이모저모

Type Alias 와 as import

Type Alias

# 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. 위와같이 이름이 긴 클래스를 컬렉션에 담아서 사용할 때도 유용하게 사용이 가능하다.


as import

# kotlin

import com.lannstark.lec19.a.printHelloWorld as printHelloWorldA
import com.lannstark.lec19.b.printHelloWorld as printHelloWorldB

fun main () {

    printHelloWorldA()
    printHelloWorldB()
}

💡 Key Point
1. 다른 패키지의 같은 이름인 함수를 동시에 가지고 오고 싶을 때 사용한다.


구조분해와 componentN 함수

구조분해
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 객체) 을 분해해서 한번에 초기화 하는 것!
  • 이 예제를 보고 위의 구조분해의 의미를 다시 보면 이해된다. 별거없다.
  1. ⛔️주의
  • 구조분해는 data class 에서만 가능하다.
  • 그 이유는 componentN 함수라는 비밀에 있다.

componentN 함수
1. data classcomponentN 이라는 함수를 자동으로 만들어준다.
2. componentN 함수란?

  • data class 는 선언된 필드의 순서대로 component 함수를 만들어준다.
  • N 의 의미는 선언된 프로퍼티의 순서를 의미한다.
  • 위의 Person 클래스의 name 은 1번, age 는 2번 .. 이런 식이다.
  • name 이나 age 라는 이름으로 인식하지 않고 프로퍼티 순서로 값을 가져온다.
  1. 일반 클래스에서 구조분해 할당을 하고 싶다면 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 함수를 연산자로 인식한다는 것이다.


takeIf 와 takeUnless

# 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 이 반환된다.

ITEM 20. Scope Function

⛔️ 주의 : 외우지 말고 느낄 것

Scope Function 이란

개념을 잡자

  • 의미 : '일시적인 영역' 을 형성하는 '함수'
  • 람다를 사용해서 일시적인 영역을 만들고 코드를 더 간결하게 만든다.
  • 메서드 체이닝을 활용하여 코드를 보다 간결하게 만든다.
# 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 에 접근한다.
  1. let 은 람다를 받고, 그 람다의 결과를 반환 한다. (밑에 더 자세하게 서술한다.)

Scope Function 의 종류

💡 외우지는 말되, 이해하자

			   [it 사용]			  [this 사용]
               
[람다의 결과]		'let'				'run'

[객체 그 자체]		'also'				'apply'

						  'with'

💡 Key Point
1. Scope Function 은 총 5가지

  • let, run, also, apply, with
  1. with 를 제외한 4가지는 모두 '확장함수'로 사용한다.
  2. 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 를 통해 객체에 접근이 가능하다.


let 의 사용

# kotlin

fun usingLet1 () {
    val strings = listOf("Apple", "car", "banana")
    strings.map { it.length }.filter { it > 3 }.let { println(it) }
}
  • 하나 이상의 함수를 call chain 의 결과로 호출할 때 사용.

# kotlin

fun usingLet2(str: String?) {
    val length = str?.let {
        println(it.uppercase())
        it.length
    }
    println(length)
}
  • non-null 값에 대해서만 코드블럭을 실행시키고 싶을 때 사용.
  • ?. 을 통해 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 값을 요리한다.

run 의 사용

# kotlin

fun usingRun1 (personRepository: PersonRepository) {
    val person = Person("김민재", 100)
        .run { personRepository.save(this) }
}
  • '객체 초기화' 와 반환 값의 계산을 동시에 해야 할 때 사용.
  • 위의 예제는 person 객체를 초기화 함과 동시에 '객체를 만들어서 DB 에 저장' 한다.
  • 하지만 이런 방법은 잘 사용되지 않는다.

apply 의 사용

# kotlin

fun createPerson (
    name: String,
    age: Int,
): Person {
    return Person(
        name = name
    ).apply {
        this.age = age
    }
}
  • apply 는 실행된 람다에서 객체 그 자체가 반환된다.
  • 객체를 설정할 때, 객체를 '수정' (즉, apply - 적용) 하는 로직이 메서드 call chain 의 중간에 필요할 때 사용.

also 의 사용

# kotlin

fun alsoTest1 () {
    mutableListOf("one", "two", "three")
        .also { println("four 추가 이전 지금 값 : $it") }
        .add("four")
}
  • also 는 apply 와 마찬 가지로, 실행된 람다에서 객체 그 자체가 반환된다.
  • 객체를 수정하는 로직이 call chain 의 중간에 필요할 때 사용된다.

with 의 사용

# 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 을 활용한 함수형 프로그래밍

과한 Scope Function 의 사용을 주의할 것

  • 과한 Scope Function 은 숙련된 코틀린 개발자가 아닌 경우 보기 힘들다.🫠

  • 디버깅이 힘들다.

  • 그로 인한 수정이 힘들 수 있다.

  • 하지만, 적절하게 사용하면 아주 유용하게 활용이 가능하다.

  • 💡 팀의 코틀린 숙련도와 선호도에 따른 컨벤션을 정하는 것이 중요하다.


🎉 결론

이제, 코틀린의 기초를 끝냈다. 만세!!

다시 공부하면서 글을 정리하니까 생각보다 시간이 꽤 걸렸다.

Java 에 익숙한 개발자들이 이 글을 통해 Kotlin 기초 문법을 학습하는데 도움이 되면 좋겠다.

그리고 위의 서론에서 이야기했지만, 최태현님 코틀린 강의를 듣고 이 글을 보면 좋겠다.

강의를 통해 공부하고 이 글은 복습과 까먹었을 때 다시 상기시키는 사전 처럼 사용되면 좋겠다.

그럼, 이제 다음 강의 부수러 가보잣! 🍰

profile
인문학 하는 개발자 💻

6개의 댓글

comment-user-thumbnail
2023년 11월 18일

수강하면서 자세히 정리한 게 인상적입니다. 언제나 개발을 대하는 자세를 배웁니다! 좋은 포스팅 잘 봤어요

1개의 답글
comment-user-thumbnail
2023년 11월 20일

광기가 느껴집니다...

1개의 답글
comment-user-thumbnail
2023년 11월 23일

자바 개발자로 일하면서 다음 스터디를 코틀린 생각중인데 잘 정리해주신 글 덕분에 코틀린 맛보기 해봤습니다! 코틀린 재밌어 보이네요!😁

1개의 답글