728x90
Kotlin in Action 을 요약한 내용입니다.
예외처리
- 예외 처리는 자바나 다른 언어와 비슷
- throw는 식이므로 다른 식에 포함될 수 있다.
val percentage = if (number in 0..100) number else throw IllegalArgumentException("A percentage value must be between 0 and 100 : $number")
try, catch, finally
- 자바 코드와 가장 큰 차이는 throws 절이 코드에 없다
- 자바에서는 함수 선언 뒤에 throws IOException 을 붙여야 한다
fun readNumber(reader: BufferedReader): Int? { try { val line = reader.readLine() return Integer.parseInt(line) } catch (e: NumberFormatException) { return null } finally { reader.close() } } fun main() { val reader = BufferedReader(StringReader("239")) println(readNumber(reader)) }
try를 식으로 사용
- finally 절을 없애고 파일에서 읽은 수를 출력
- try 키워드는 if나 when과 마찬가지로 식
fun readNumber(reader: BufferedReader) { val number = try { Integer.parseInt(reader.readLine()) } catch (e: NumberFormatException) { return } println(number) } fun main() { val reader = BufferedReader(StringReader("nat a number")) readNumber(reader) }
Type System (Null 처리)
널 가능성
- Nullability는 NullPointerException 오류를 피할 수 있게 도와 줌
- Null이 될 수 있는지 여부를 타입 시스템에 추가
- 컴파일러가 여러 가지 오류를 미리 감지 가능
널이 될 수 있는 타입
- 널이 될 수 있는 타입을 명시적으로 지원
- 널이 인자로 들어 올수 없는 함수
- strLen에 null이거나 널이 될 수 있는 인자를 넘기는 것은 금지
- 컴파일 시 오류 발생
fun strLen(s: String) = s.length
- 널과 문자열을 인자로 받을 수 있는 함수
- ? 사용하여 null을 인자로 받을 수 있음
- s?.length에 ?를 붙이지 않으면 컴파일 오류 발생
fun strLen(s: String?) = s?.length
- if를 통해 null 값 다루기
- null과 비교하고 나면 컴파일러는 null이 아님이 확실한 영역에서는 널이 될 수 없는 타입처럼 사용 가능
fun strLenSafe(s: String?): Int = if (s != null) s.length else 0 fun main() { val x: String? = null println(strLenSafe(x)) println(strLenSafe("abc")) }
안전한 호출 연산자: ?.
- 안전한 호출 연산자 ?.를 제공
- null 검사와 메소드 호출을 한 번의 연산으로 수행
- s?.toupperCase() ⇒ if (s ≠ null) s.toUpperCase() else null 과 같다
- 널이 될수 있는 타입인 경우 결과 타입도 널이 될 수 있다
fun printAllCase(s: String?) {
val allCaps: String? = s?.toUpperCase()
println(allCaps)
}
fun main() {
printAllCase("abc")
printAllCase(null)
}
- 안전한 호출 연쇄시키기
- 간결하게 널 검사 가능
class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String) class Company(val name: String, val address: Address?) class Person(val name: String, val company: Company?) fun Person.countryName(): String { val country = this.company?.address?.country return if (country != null) country else "Unkown" } fun main() { val person = Person("Dmitry", null) println(person.countryName()) }
엘비스 연산자: ?:
- null 대신 사용할 디폴트 값을 지정 할 때 사용
fun foo(s: String) {
val t: String = s ?: ""
}
fun strLenSafe(s: String?): Int = s?.length ?: 0
fun Person.countryName() = company?.address?.country ?: "Unknown"
- return 이나 throw 등의 연산도 식
- 엘비스 연산자의 우항에 return, throw를 넣을 수 있음
class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String) class Company(val name: String, val address: Address?) class Person(val name: String, val company: Company?) fun printShippingLabel(person: Person) { val address = person.company?.address ?: throw IllegalArgumentException("No address") with(address) { println(streetAddress) println("$zipCode $city, $country") } } fun main() { val address = Address("Elsestr. 47", 80687, "Munich", "Germany") val jetbrains = Company("JetBrains", address) val person = Person (" Dmitry " , jetbrains ) printShippingLabel(person) }
안전한 캐스트: as?
- as? 연산자는 지정한 타입으로 변환 할 수 없으면 null 반환
class Person(val firstName: String, val lastName: String) {
override fun equals(other: Any?): Boolean {
val otherPerson = other as? Person ?: return false
return otherPerson.firstName == firstName && otherPerson.lastName == lastName
}
override fun hashCode(): Int = firstName.hashCode() * 37 + lastName.hashCode()
}
fun main() {
val p1 = Person("Dmitry", "Jemerov")
val p2 = Person("Dmitry", "Jemerov")
println(p1 == p2)
}
널 아임 단언: !!
- 널이 될수 타입의 값을 널이 될 수 없는 타입으로 강제로 바꿀 수 있다
- 널에 대해서 !!를 적용하면 NPE가 발생
fun ignoreNulls(s: String?) {
val notNull: String = s!!
println(notNull.length)
}
fun main() {
ignoreNulls(null)
}
- 어떤 값이 널이었는지 확실히 하기 위해 여러 !! 단언문을 한줄에 함께 사용하지 않기
let 함수
- let 함수를 사용하면 널이 될 수 있는 식을 더 쉽게 다룰 수 있음
- let 함수는 자신의 수신 객체를 인자로 전달받은 람다에게 넘김
fun sendEmailTo(email: String) {
println("Sending email to $email")
}
fun main() {
var email: String? = "yole@example.com"
email?.let { sendEmailTo(it) }
}
- let을 사용하면 식의 결과를 저장하는 변수를 따로 만들 필요가 없다
var person: Person? = getTheBestPersonInTheWorld()
if (person != null) sendEmailTo(person.email)
// let 함수 사용
getTheBestPersonInTheWorld()?.let { sendEmailTo(it.email) }
- let 함수를 중첩시켜 사용할 수 있지만 권장하지 않음
- 가독성이 낮아져 if를 사용하는게 더 가독성이 높음
나중에 초기화할 프로퍼티
- lateinit 변경자를 사용하면 나중에 초기화 가능
- 나중에 초기화하는 프로퍼티는 항상 var 사용
- val 프로퍼티는 파이널 필드로 컴파일 됨
- 생성자 안에서 반드시 초기화 해야함
class MyService { fun performAction(): String = "foo" } class MyTest { private lateinit var myService: MyService @Before fun setUp() { myService = MyService() } @Test fun testAction() { Assert.assetEquals("foo", myService.performAction()) } }
- 프로퍼티가 초기화 전에 접근 하면 오류 발생
- "lateinit property myService has not been ini tialized”
널이 될 수 있는 타입 확장
- 메소드를 호출하기 전에 수식 객체 역활을 하는 변수가 널이 될 수 없다고 보장하는 대신 확장 함수가 알아서 널 처리
- String?. 타입의 수신 객체에서 호출할 수 있는 isNullOrEmpty, isNullOrBlank 메소드
- fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()
fun verifyUserInput(input: String?) { if (input.isNullOrBlank()) { println("Please fill in the required fields") } } fun main() { verifyUserInput(" ") verifyUserInput(null) }
타입 파라미터의 널 가능성
- 타입 파라미터 T를 클래스나 함수 안에서 사용하면 ?가 없더라도 T가 널이 될 수 있다
- T에 대한 추론한 타입은 Any?
fun <T> printHashCode(t: T) { println(t?.hashCode()) } fun main() { printHashCode(null) }
- 타입 파라미터가 널이 아님을 확실히 하려면 타입 상한을 지정
- fun <T: Any> printHashCode(t: T) { println(t.hashCode()) } fun main() { printHashCode(null) // 오류 printHashCode(42) }
널 가능성과 자바
- 코틀린과 자바를 사용할 때 모든 값에 null 검사를 해야 할까?
- 자바 코드에도 애노테이션으로 표시된 널 가능성 정보가 있다.
- @Nullable String ⇒ String?
- @NotNull String ⇒ String
컬렉션과 배열
널 가능성과 컬렉션
- 타입 인자로 쓰인 타입에 ?를 붙이면 널을 저장할 수 있다
- List<Int?>는 Int? 타입의 값을 저장
fun readNumbers(reader: BufferdReader): List<Int?> { val result = ArrayList<Int?>() for (line in reader.lineSequence()) { try { val number = line.toInt() result.add(number) } catch(e: NumberFormatException) { result.add(null) } } return result }
- List<Int>? 컬렉션 자체가 널이 될 수 있다
- filterNotNull 함수를 사용하여 null값을 제거
읽기 전용과 변경 가능한 컬렉션
- kotlin.collections.Collection 인터페이스를 사용하면 데이터를 읽는 여러 연산 수행 가능
- 읽기 전용 인터페이스
- kotlin.collections.MutableCollection 인터페이스를 사용하면 추가, 제거 메소드 사용 가능
- 가능한 읽기 전용 인터페이스를 사용
- 읽기 전용과 변경 가능한 컬렉션 인터페이스
- target에 읽기 전용 컬렉션을 넘길 수 없다
fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) { for (item in source) { target.add(item) } } fun main() { val source: Collection<Int> = arrayListOf(3, 5, 7) val target: MutableCollection<Int> = arrayListOf(1) copyElements(source, target) println(target) }
코틀린 컬렉션과 자바
- 그림에 표시된 모든 컬렉션 인터페이스는 코틀린에서 정의
- ArrayList, HashSet 인터페이스를 상속한 것처럼 취급
- 자바 호환성을 제공
- 읽기 전용 인터페이스와 변경가능한 인터페이스를 분리
- ArrayList, HashSet 인터페이스를 상속한 것처럼 취급
- java.util.Collection을 파라미터로 받는 자바 메소드가 있다면 아무 Collection 이나 MutableCollection 값을 인자로 넘길 수 있다
- 코틀린에서 읽기 전용이여도 자바에서는 변경이 가능
- 코틀린 → 자바로 넘기는 코틀린 코드의 경우 주의 해야함
컬렉션을 플랫폼 타입으로 다루기
- 플랫폼 타입의 경우 코틀린 쪽에서는 널 관련 정보가 없다
- 자바에서 선언한 컬렉션 타입의 변수를 코틀린에서는 플랫폼 타입으로 본다
- 플랫폼 타입은 코틀린에서 읽기 전용이나 변경 가능한 컬렉션 어느 쪽으로든 다룰 수 있다
- 컬렉션 타입이 시그니처에 들어간 자바 메소드 구현을 오버라이드 하는 경우 문제가 될 수 있음
- 읽기 전용, 변경 가능 컬렉션의 차이로 인해
- 코틀린에서 어떤 컬렉션 타입으로 표현 해야할지…
- 고려해야 할 사항
- 컬렉션이 널이 될 수 있는가?
- 컬렉션의 원소가 널이 될 수 있는가?
- 오버라이드하는 메소드가 컬렉션을 변경할 수 있는가?
- 컬렉션 파라미터가 있는 자바 인터페이스
- 일부 파일은 이진 파일이며 이진 파일 안의 내용은 텍스트로 표현할 수 없는 경우가 있음
- 리스트는 널이 될 수 있다
- 파일의 각 줄은 널이 수 없음
- 리스트의 원소는 널이 될 수 없다
- 리스트의 파일 내용을 표현
- 내용을 바꿀 필요가 없으므로 읽기 전용
interface FileContentProcesseor { void processContents(File path, byte[] binaryContents, List<String> textContnets); }
class FileIndexer: FileContentProcesseor { override fun processContents(path: File, binaryContents: ByteArray?, textContents: List<String>?) { ... } }
- 일부 파일은 이진 파일이며 이진 파일 안의 내용은 텍스트로 표현할 수 없는 경우가 있음
- 컬렉션 파라미터가 있는 자바 인터페이스
- 호출하는 쪽에서 항상 오류 메세지를 받아야 함
- List<String> 널이 되면 안된다.
- errors의 원소는 널이 될 수 있다
- output에 들어가는 정보를 파싱하는 과정에서 오류가 발생하지 않으면 메시지는 널
- 원소를 추가할 수 있음
- List<String>은 변경 가능해야 함
interface DataParser<T> { void parseData(String input, List<T> output, List<String> errors); }
class PersonParser: DataParser<Person> { override fun parseData(input: String, output: MutableList<Parson>, errors: MutableList<String?>) { ... } }
- 호출하는 쪽에서 항상 오류 메세지를 받아야 함
객체의 배열과 원시 타입의 배열
- 코틀린에서 배열을 만드는 방법
- arrayOf 함수에 원소를 넘기면 배열을 만들 수 있다
- arrayOfNulls 함수에 정수 값을 인자로 넘기면 모든 원소가 null이고 인자로 넘긴 값고 크기가 같은 배열을 만들 수 있다.
- 원소 타입이 널이 될 수 있는 타입인 경우에만 사용 가능
- Array 생성자는 배열 크기와 람다를 인자로 받아서 람다를 호출해서 각 배열의 원소를 초기화 해준다
- arrayOf를 쓰지 않고 각 원소가 널이 아닌 배열을 만들어야 하는 경우 사용
- 배열 만들기
val letters = Array<String>(26) { i -> ('a' + i).toString() }
println(letters)
- 컬렉션을 vararg 메소드에 넘기기
- toTypedArray 사용하면 쉽게 컬렉션을 배열로 바꿀 수 있다
val strings = listOf("a", "b", "c") println("%s/%s/%s".format(*strings.toTypedArray())
- 코틀린은 ByteArray, CharArray, BooleanArray 등 원시 타입 배열을 제공
- 배열의 값은 박싱하지 않고 가장 효율적인 방식으로 저장
- 원시 타입의 배열을 만드는 방법
- 각 배열 타입의 생성자는 size 인자를 받아서 해당 원시 타입의 디폴트 값으로 초기화된 size 크기의 배열을 반화
- 팩토리 함수는 여러 값을 가변 인자로 받아 값이 들어간 배열을 반환
- 크기와 람다를 인자로 받는 생성자를 사용
val fiveZeros = IntArray(5) val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0) val squares = IntArray(5) { i -> (i+1) * (i+1) } println(squares.jonToString())
- 코틀린 표준 라이브러리는 배열 기본 연산에 더해 컬렉션에 사용할 수 있는 모든 확장 함수를 배열에도 제공
fun main(args: Array<String>) {
args.forEachIndexed { index, element ->
println("Argument $index is: $element")
}
}
728x90
'코틀린 스터디' 카테고리의 다른 글
채널 (0) | 2023.12.04 |
---|---|
코루틴 스코프 만들기 (1) | 2023.11.24 |
코루틴 기반 동시성 프로그래밍 (0) | 2023.11.24 |
코틀린 - 클래스, 인터페이스, 상속 (0) | 2023.11.10 |
코틀린 - 변수, 연산자, 반복문, 함수 (1) | 2023.11.10 |