diff --git a/Chapter07/babi/README.md b/Chapter07/babi/README.md new file mode 100644 index 0000000..96bfc16 --- /dev/null +++ b/Chapter07/babi/README.md @@ -0,0 +1,350 @@ +## 7장 + +### Null 네이놈 + +- `NullPointerException` +- 런타임에서 발견할수 있었던 오류를 컴파일시 발견할수 있도록 null 을 다룬다. +- 개인적으로 자바에서 해당 에러가 발생하지 않도록 많은 노력을 했었다. +```java +import java.util.Objects; + +Objects.equals(a, b); +Objects.isNull(a); +``` +- 그럼에도 불구하고 간헐적으로 발생... + +--- + +#### Null 이 될 수 있는 변수 + +- 자바에서 `Wrapper Type` 데이터타입으로는 `null`이 들어갈 수 있다. +- 예제로 다음 함수에서 들어온 변수에 대해 널 체크를 하지 않는다면 `NullPointerException` 이 발생한다. +```java +int strLen(String s){ + return s.length(); +} +``` +- `NotNull`을 명시하더라도 `warning` 표시만 날뿐, 못쓰도록 강제화되지 않는다. +```java +int strLen(@NotNUll String s){ + return s.length(); +} + +void getLength(){ + // Passing 'null' argument to parameter annotated as @NotNull 주의 발생은 하는데 강제화하여 막지는 않는다 + strLen(null); +} +``` +- 코틀린에서는 `?` 기호를 데이터타입 뒤에 붙임으로써, 해당 변수의 `null` 가능성을 표시한다. +```kotlin +// str 변수는 null 가능성이 없다. +fun strLen(str: String){ + // Do it +} + +// str 변수는 null 일수도 있다. +fun strLen(str: String?){ + // Do it +} +``` +- 그리고 자바와 다르게 `?` 기호가 없는 변수에 `null` 을 넣는다면 컴파일시점에서 부터 에러가 발생한다. +```kotlin +// str 변수는 null 가능성이 없다. +fun strLen(str: String){ + // Do it +} + +fun getTest(){ + // 에러발생 : Null can not be a value of a non-null type String + strLen(null) +} +``` +- 또한 `null` 가능성이 있는 타입은 바로 쓸수없는데, `String` 과 `String?` 은 다른 변수로 취급이된다. +```kotlin +fun main() { + val x: String? = null + // Type mismatch. Required: String, Found: String? + strLen(x) +} + +fun strLen(str: String){ + // Do it +} +``` +--- +### null 을 다루기 위한 도구들 +- 코틀린에서 스마트 캐스트가 잘된다는걸 앞전에 봤었는데, `null` 관련된 작업들도 한번 비교하면 컴파일러가 `null` 이 아님을 기억한다 +- 그리고 null 을 다루기 위한 도구들로는 다음처럼 있다. +#### if +```kotlin +fun strLenSafe(s: String?): Int { + // 가능 + if (s != null) s.length else 0 +} +``` + +--- +#### ?. +- 호출연산자로써, `null` 검사와 메서드 호출을 하나로 수행한다. +- 예로, `str?.uppercase()` 은 `str` 변수가 `null` 인지 검사를 하고, `null` 이 아니라면 `uppercase()` 메서드까지 실행한다. + - `str != null` -> `str.uppercase()` + - `str == null` -> `null` +- `str` 이 `null` 이 아니라면 `String` 이 반환되고, `str` 이 `null` 이라면 `null` 이 반환되기 때문에, 해당 식의 데이터타입은 `String?` 이다. +- 연속해서 다음처럼 사용할 수도 있다. +```kotlin +class Address(val company: Company?) +class Company(val person: Person?) +class Person(val name: String?) + +fun getName(address: Address){ + // 연속해서 사용할 수 있긴하지만 좋은코드인지는 음... + val name = address?.company?.person?.name +} +``` +--- +#### ?: +- 엘비스 연산자라고 불린다. +- `nullable` 인 변수를 `nullable` 하지 않게 만들어 준다. +- 예로, `nullable` 한 변수인 `val name: String?` 을 엘버스 연산자를 사용하여 `val notNullName = name ?: "unnamed"` 이라고 한다면 결과는 다음과 같다 + - `name != null` -> name + - `name == null` -> "unnamed" +- `str` 이 `null` 인 경우에도 항상 `String` 데이터타입으로 반환되기 때문에 `nullable` 하지 않아진다. +- 엘버스 연산자 뒤로 동일한 데이터타입만 들어가야하는건 아니다. `throw`, `return` 등도 식이기 때문에 들어갈 수 있다 + +--- + +#### as? +- 타입 캐스트 연산자인 `as` 에 `?` 를 붙여 안전하게 변환할 수 있다. +- `other as? Person` + - `other is Person` -> `other as Person` + - `other !is Person` -> `null` +- 일반적으로 엘비스 연산자와 함께 사용하여 `null` 일때 특정 작업을 하면 유용하게 사용할 수 있다 +```kotlin +class Person(val firstName: String, val lastName: String) { + override fun equals(other: Any?): Boolean { + val otherPerson = o as? Person ?: return false // 타입이 서로 맞지 않다면 바로 return false + // equals 연산 + } +} +``` +--- + +#### !! +- 널 아님 단언 코드로, `?` 연산자가 사용되어 `null` 이 될수있는 변수이지만 절대 `null` 이 들어오지 않으니 널이 아닌 타입으로 강제로 바꾸는 연산자. +```kotlin +fun strLength(str: String?){ + str!!.length // String? 을 String 으로 강제 캐스트 +} +``` + +- 하지만 `!!` 연산자를 통해 강제 캐스트를 했는데, 실제로 `null` 이 들어온다면, `NullPointerException`이 발생하게 된다. +- `!!` 연산자를 사용하지 않으려면 다음과 같이 사용할 수도 있다 `val value = list.selectedValue ?: return` + - `list.selectedValue` 의 값에 따라 조기종료가 되어 이후 로직에서는 `null` 이 아님을 보장 받는다. +- 혹시 사용을 한다면 `!!` 연산자를 한 줄에 함께 쓰는 일을 피하는것이 좋다. **스택 트레이스**에서 어떤 파일의 몇 번째 줄인지는 나오지만, 어떤 식에서 발생했는지 나오지 않는다. + - `person!!.company!!.address!!.country // 추천하지 않는다.` + +--- + +#### let +- `null` 이 될 수 있는 식을 더 쉽게 다룰 수 있다. 널이 될 수 있는 값을 검사한 다음 그 결과에 변수를 넣는 작업을 한번에 처리할 수 있다. +- 흔한 용례로, 함수의 파라미터로 널이 아닌 값을 보내야할때, 널이될수 있는 변수를 가지고 해당 함수를 호출하는 경우가 있다. +```kotlin +// null 이 아닌 값을 파라미터로 받는 함수 +fun sendEmailTo(email: String) { } + +// 1. 바로 호출하면 에러 발생 +fun main() { + val email: String? = "mail" + // Error: Type mismatch + sendEmailTo(email) +} + +// 2. 조건문으로 null 여부 검사 후 호출 +fun main() { + val email: String? = "mail" + // 정상동작 + if(email != null) sendEmailTo(email) +} + +// 3. let 을 사용하여 null 여부 검사 후 호출 +fun main() { + val email: String? = "mail" + // 정상동작 + email?.let { sendEmailTo(email) } +} +``` +- `email?.let { sendEmailTo(email) }` 은 다음과 같이 분기되어 실행된다 + - `email != null` -> let 구문에서 `it`은 널이 아니다 + - `email == null` -> let 구문 안타고 종료 + +--- + +#### also + +- 객체를 수정하거나 로깅등을 수행할 때 사용한다. +- also 블록 안에서는 it 키워드를 통해 주체 객체에 접근할 수 있다. +- 주체 객체를 그대로 반환하기 때문에, 체이닝을 이어갈 수 있다. +```kotlin +fun main() { + val list = mutableListOf("a", "b") + .also { println("초기 리스트: $it") } + .also { it.add("c") } + .also { println("변경된 리스트: $it") } +} +``` + +--- + +#### apply +- 객체를 생성하고 객체의 속성을 설정할 때 사용한다. +- apply 블록 안에서는 this 키워드를 통해 객체 멤버에 접근할 수 있으며, this는 생략 가능하다. +- 주체 객체를 그대로 반환한다. + +```kotlin +fun main() { + val user = User().apply { + name = "민수" + age = 20 + email = "minsu@example.com" + } +} +``` + +--- +#### run +- 객체에 대해 작업을 수행하고, 최종 결과를 얻고 싶을 때 사용한다. +- run 블록 안에서는 this 키워드를 통해 객체 멤버에 접근할 수 있다. +- run 블록의 마지막 표현식이 최종 반환된다. +```kotlin +fun main() { + val length = "Hello Kotlin".run { + println("문자열 출력: $this") + length // 마지막 표현식이 반환된다 + } + println(length) // 12 +} +``` + +--- +#### with +- 주어진 객체에 대해 여러 작업을 수행할 때 사용한다. +- with는 확장 함수가 아니라 일반 함수이며, 첫 번째 파라미터로 객체를 받고 블록 내부에서 this를 통해 접근한다. +- 블록 마지막 표현식이 반환된다. + + +```kotlin + +fun main() { + val result = with(StringBuilder()) { + append("Hello, ") + append("World!") + toString() + } + println(result) // Hello, World! +} +``` + +---- + +### 영역 함수 비교 + +| 구분 | 대상 (수신 객체) | 반환값 | 블록 내 키워드 | 주요 용도 | 주 사용 시점 | +|-------|------------|---------------|--------------|-----------------------------|-----------------------| +| let | it | 마지막 식 | it | null 체크, 안전 호출 | 값이 null이 아닐 때 코드 실행 | + | also | it | 수신 객체 (자기 자신) | it | 부수 효과(side effect), 로깅, 디버깅 | 객체를 그대로 반환하며 중간 작업 삽입 | +| apply | this | 수신 객체 (자기 자신) | this (생략 가능) | 객체 초기화, 빌더 패턴 | 객체 프로퍼티를 설정하고 반환할 때 | + | run | this | 마지막 식 | this (생략 가능) | 결과 생성, 계산 후 결과 반환 | 객체 조작 후 결과가 필요할 때 | +| with | this | 마지막 식 | this (생략 가능) | 객체에 대해 여러 작업 수행 | 여러 메서드를 호출해야 할 때 | + + +--- + +### 지연 초기화 프로퍼티 +- 객체를 생성한 이후 나중에 전용 메서드를 통해 초기화하는 프레임워크가 많다. 당장 테스트에서 사용하는 `JUnit` 에서도 `@BeforeAll`, `@BeforeEach` 어노테이션 메서드 안에서 초기화 로직을 수행한다. +- 코틀린에서는 클래스 안의 널이 아닌 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메서드 안에서 초기화할 수 없다. 일반적으로 생성자에서 모든 프로퍼티를 초기화해야한다. +- 초기화 값을 제공할 수 없으면 널이 될 수 있는 타입을 사용해야하고, 해당 타입은 `null` 검사 혹은 `!!` 연산자를 사용해야한다. +- 다음은 못생긴 코드의 예시다 +```kotlin +class MyService{ + fun action(): String = "Actions" +} + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MyTest { + private var myService: MyService? = null + + @BeforeAll + fun setUp() { + myService = MyService() + } + + // 못생긴 코드들 + @Test + fun test(){ + // `!!` 사용 + assertEquals("Actions", myService!!.action()) + // null 검사 + if(mySevice != null) + assertEquals("Actions", myService.action()) + } +} +``` + +--- +- 위와같은 상황을 해결하기 위해 지연 초기화를 할 수 있다. +- `lateinit` 변경자를 붙임으로써 프로퍼티를 나중에 초기화할 수 있다. +```kotlin + +class MyService{ + fun action(): String = "Actions" +} + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MyTest { + // lateinit 사용을 통해 지연 초기화 + private lateinit var myService: MyService + + @BeforeAll + fun setUp() { + myService = MyService() + } + + // 못생긴 코드들 + @Test + fun test(){ + // null 검사를 하지 않아도 상관없다! + assertEquals("Actions", myService.action()) + } +} +``` +--- +- `val` 프로퍼티는 파이널 필드로 컴파일되고, 반드시 생성자 안에서 초기화돼야 하기 때문에 지연 초기화 프로퍼티는 항상 **`var`** 여야 한다 +- 그리고 프로퍼티를 초기화하기 전에 접근하면 `UniitializedPropertyAccessException`이 발생한다. +- `lateinit` 프로퍼티는 반드시 클래스의 멤버가 아니어도 된다. 함수 본문 안의 지역변수, 최상위 프로퍼티도 가능하다. + +--- + +### 타입 파라미터의 널 +- 코틀린 함수, 클래스의 모든 타입 파라미터는 `null` 이 될 수 있다. +- 파라미터 `T`를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 물음표가 없더라도 `T`가 `null`이 될 수 있는 타입이다. + +```kotlin +fun printHashCode(t: T) { + println(t?.hashCode()) // t가 null이 될 수 있으므로, 안전한 호출 사용 +} + +fun main() { + printHashCode(null) // T 의 타입은 `Any?` 로 추론 +} +``` +- 타입이름 `T` 에는 물음표가 붙어있지 않지만, t 는 `null`을 받을 수 있다. +- 타입 파라미터가 null 이 아님을 확실히 하려면 타입 상계를 지정해야한다. +```kotlin +fun printHashCode(t: T) { // t는 더이상 null이 될 수 없다 + println(t.hashCode()) +} + +fun main() { + printHashCode(null) // Error +} +``` diff --git a/Chapter08/babi/README.md b/Chapter08/babi/README.md new file mode 100644 index 0000000..aad7f8f --- /dev/null +++ b/Chapter08/babi/README.md @@ -0,0 +1,149 @@ +## 8장 + +### 원시타입과 래퍼타입? +- 자바에서는 원시타입과 참조타입을 구분한다. +- 원시타입은 값을 직접 저장하고, 참조타입은 메모리상의 객체 위치가 들어간다. +- 원시타입은 메서드를 호출하거나, 컬렉션에 담을 수 없기 때문에 참조 타입이 필요한 경우, 래퍼 타입으로 원시 타입을 감싸서 사용한다 (`int` -> `Integer` 등) +- 하지만 코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다. +- 항상 참조타입을 사용하는것이 아니라, 실행 시점에 가장 효율적인 값으로 표현된다 + - 대부분의 경우 원시타입으로 사용 + - 제네릭 클래스, 컬렉션은 래퍼 타입으로 사용 + +| 항목 | 원시 타입 (Primitive) | 래퍼 타입 (Wrapper) | +|-------------|-------------------|--------------------| +| 저장 방식 | 값 자체 저장 | 객체로 감싸서 저장 | +| 메모리 사용량 | 적음 | 상대적으로 많음 | +| null 허용 여부 | 불가 | 가능 | +| 메서드/프로퍼티 사용 | 불가 | 가능 | +| 컬렉션/제네릭 클래스 | 불가 | 가능 | +| 예시 | int, boolean 등 | Integer, Boolean 등 | + +--- + +#### 부호 없는 숫자 타입 +- 동일한 메모리를 사용하지만, 부호 구분이 아닌 더 큰 양수 범위를 표현하도록 한다. +- `Int` 가 약 -21억에서 21억까지를 표현한다고 한다면, `UInt`는 0에서 약 42억까지를 표현한다. +- 하지만 명시적으로 전체 비트 범위가 필요한 경우가 아니라면 일반적으로 보통의 정수를 사용하고, 음수 범위가 함수에 전달되었는지 검사하는게 낫다. + +--- + +#### 널이 될수 있는 기본 타입 +- 코틀린에서는 원시 타입과 래퍼 타입을 구분하지 않는다고 했다. +- 따라서 코틀린에서 `null`이 들어갈 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일 된다. +```kotlin +val a: Int = 1 // 원시타입인 int로 컴파일 +val b: Int? = 1 // null이 가능하므로 Integer 객체로 컴파일 +``` + +--- +#### 수 변환 +- 코틀린은 한 타입의 수를 다른 타입의 수로 자동 변환하지 않는다. +- 변환할 타입의 범위가 더 넓더라도 자동 변환은 불가능하다 +- 코틀린은 명시적 변환만 허용하여, 의도하지 않은 버그가 생기는걸 방지한다. +- 대신 원시 타입에 대해 변환 함수를 제공한다 (`toByte()`, `toShort()`, `toChar()` 등) + - 표현 범위가 더 넓은 타입으로 변환하는 함수도 있고(`Int.toLong()`), 범위가 더 좁은 범위로 변환하며 일부를 잘라내는 함수도 있다(`Long.toInt()`) + - 변환함수를 사용하다보면 각 타입 포맷에 맞지 않아 `NumberFormatException` 등이 발생할 수 있는데, 변환 실패시 `null`을 반환하는 함수들도 있다 (`toIntOrNull`, `toByteOrNull`) + +--- + +### Any, Unit, Nothing +- 자바를 했더라도 추측하기가 힘든 3인방이다. + + +#### Any +- Kotlin 의 `null` 이 될수 없는 모든 타입의 최상위 타입. +- Java 의 Object 와 비슷하지만, `equals`, `hashCode`, `toString`만 기본으로 제공한다. +- Java 의 Object 는 원시 타입은 포함되지 않지만, 코틀린의 Any 는 자동으로 값을 객체로 감싼다 + +--- + +#### Unit +- 자바에서의 `void` 역할을 하며, 반환 타입을 명시하지 않더라도 `Unit` 반환형을 사용했다고 볼 수 있다 +- 자바의 `void` 와 달리 타입 인자로 사용할 수 있다. + +```kotlin +interface Processor { + fun process(): T +} + +class NoResultProcessor : Processor { + override fun process() { // Unit 반환 타입은 생략할 수 있다. void 와 동일하기 때문에 return 이 없다 + // Todo + } +} +``` + +--- + +#### Nothing +- 절대 정상적으로 반환하지 않는 함수의 반환 타입 +- 리턴값이 없다는 뜻이 아니라, 예를들어 무조건 예외가 발생하거나 무한 루프를 도는 함수로, 값을 반환하며 정상적으로 종료되지 않는다는 뜻. +```kotlin +fun fail(message: String): Nothing { + throw IllegalArgumentException(message) +} + +val address = company.address ?: fail("No Address") +print(address.city) +``` +- `Nothing` 타입은 아무 값도 포함하지 않고, 변수를 선언하더라도 아무 값도 저장할 수 없기에, 위와같은 식은 항상 값이 존재한다고 볼 수 있다. + +--- + +### 컬렉션과 배열 +- `null`이 될 수 있는 값의 컬렉션과 `null`이 될 수 없는 컬렉션은 당연하게도 데이터 타입에 따른다. +- `?` 기호가 존재한다면 `null` 혹은 해당 데이터타입이 들어갈 수 있는 컬렉션이다. + - `List` -> `null` 불가 + - `List` -> `null` 가능 +- 그런데 컬렉션 원소가 아니라, 컬렉션 자체가 `null` 이 될수 있냐는 전혀 다른 이야기이다. + - `List` -> 컬렉션은 null이 될수 없고, 컬렉션 내부 원소는 null이 들어갈 수 있다. + - `List?` -> 컬렉션은 null이 될수 있고, 컬렉션 내부 원소는 null이 들어갈 수 없다. + +--- +- 코틀린에서 `null` 을 다루는게 중점적인것 처럼, 표준 라이브러리에서도 다양하게 `null` 인 원소를 거르는 함수를 제공한다. +- `filterNotNull` -> null 이 아닌 리스트 반환 +```kotlin +fun printValidNumbers(numbers: List) { + numbers.filterNotNull().map { println(it) } +} + +// 1, 3 만 출력 +fun call() = printValidNumbers(listOf(1, null, 3)) +``` + +--- +#### 읽기 전용과 변경 가능한 컬렉션 +- 코틀린에서 `var` 와 `val` 가 있고, 그중에서 불변 타입인 `val` 을 지향하라는것 처럼, 컬렉션에서도 불변 사용을 지향한다. +- 변경 가능한 컬렉션인 `MutableCollection` 은 읽기 전용 인터페이스인 `Collection` 을 확장하여 추가, 삭제, 전체삭제 등의 메서드를 지원한다. + +```kotlin +val readOnly = listOf(1, 2, 3) +val mutable = mutableListOf(1, 2, 3) + +mutable.add(4) // 가능 +// readOnly.add(4) // 컴파일 에러 +``` + +--- + +#### 코틀린 컬렉션과 자바컬렉션 +- 코틀린의 컬렉션 기본 구조는 자방 컬렉션 인터페이스 구조와 같다. +- 변경 가능 인터페이스는 자신과 대응하는 읽기 전용 인터페이스를 상속하고 있다. +- 코틀린은 자바 표준클래스가 코틀린의 변경 가능한 컬렉션을 상속한 것 처럼 취급한다. +- 이로, 자바 호환성을 제공하는 한편 읽기 전용 인터페이스와 변경 가능 인터페이스를 분리한다. + + +| 컬렉션 타입 | 읽기 전용 타입 | 변경 가능 타입 | +|--------|--------------|-------------------------------------------------------------| +| List | listOf, List | mutableListOf, MutableList, arrayListOf, buildList | +| Set | setOf | mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf, buildSet | +| Map | mapOf | mutableMapOf, hashMapOf, linkedMapOf, sortedmapOf, buildMap | + +- 자바는 코틀린과 달리 읽기 전용 컬렉션과, 변경 가능 컬렉션을 구분하지 않기 때문에, 코틀린에서 읽기전용으로 선언되더라도 자바에서는 변경 가능하다. +- 자바 코드가 컬렉션을 변경할지 여부에 따라 파라미터 타입 사용의 책임은 사용자에게 있다.... +- `toList()`, `toMutableList()` 등 복사본을 사용하여 별도의 복사본으로 만드는것이 안전하다. +--- +- 자바에서 코틀린으로 넘어와서, `null` 인지 알수없는 플랫폼타입들이 존재한다. +- 해당 타입은 컴파일러가 `null` 관련 오류를 강제하지 않는다. +- 사용자가 명시적으로 `?` 기호를 추가하여 `null` 가능 타입으로 받거나, `null` 검사 방어로직을 추가해야한다. +- \ No newline at end of file