# 6장 코틀린 타입 시스템
### 이번장에서는...
#### 배운점, 느낀점
- 널이 될 수 있는 타입과 널을 처리하는 구문의 문법
- 코틀린 원시 타입 소개와 자바 타입과 코틀린 원시 타입의 관계
- 코틀린 컬렉션 소개와 자바 컬렉션과 코틀린 컬렉션의 관계
---
- 타입 시스템: 자바와 비교하면 코틀린의 타입 시스템은 코드의 가독성을 향상시키는 데 도움이 되는 몇 가지 특성을 새로 제공한다.
- 널이 될 수 있는 타입
- 읽기 전용 컬렉션
### 널 가능성
- 코틀린은 null에 대한 접근 방법은 가능한 한 이 문제를 실행 시점에서 컴파일 시점으로 옮기는 것이다. 널이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 여러 가지 오류를 컴파일 시 미리 감지해서
실행 시점에 발생 할 수 있는 예외의 가능성을 줄일 수 잇다.
#### 널이 될 수 있는 타입
- 코틀린과 자바의 중요한 차이는 코틀린 타입 시스템이 널이 될 수 있는 타입을 명시적으로 지원한다는 점
- 널이 될 수 있는 타입은 프로그램 안의 프로퍼티나 변수에 null을 허용하게 만드는 방법
- 어떤 변수가 널이 될 수 있다면 그 변수에 대해(그 변수를 수신 객체로) 메서드를 호출하면 `NullPointerException`이 발생할 수 있으므로 안전하지 않다. 코틀린은 그런 메서드 호출을 금지함으로써
많은 오류를 방지한다.
```java
int setLen(String s){
return s.length(); //이 함수에 null을 넘기면 NullpointerException이 발생
}
fun strLen(s: String) = s.length // 컴파일 오류
fun strLenSafe(s: String?) = s.length
fun strLenSafe(s: String?) = s.length() // X
val x: String? = null
var y: String = x // X
strLen(x)
fun strLenSafe(s: String?): Int =
if (s != null) s.length else 0 // null 검사를 추가하면 코드가 컴파일 된다.
fun main(args: Array<String>) {
val x: String? = null
println(strLenSafe(x))
}
실행 시점에 널이 될 수 있는 타입이나 널이 될 수 없는 타입의 객체는 같다. 널이 될 수 있는 타입은 널이 될 수 없는 타입을 감싼 래퍼 타입이 아니다. 모든 검사는 컴파일 시점에 수행된다. 따라서 코틀린에서는 널이 될 수 있는 타입을 처리하는 데 별도의 실행시점 부가 비용이 들지 않는다
?.
은 null 검사와 메서드 호출을 한 번의 연산으로 수행// 예제 1
fun printAllCaps(s: String?) {
val allCaps: String? = s?.toUpperCase() // allCaps는 널일 수 있다.
println("allCaps= $allCaps")
}
// 예제2
class Employee(val name: String, val manager: Employee?)
fun managerName(employee: Employee): String? = employee.manager?.name
// 예제3
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 "Unknown"
}
fun main(args: Array<String>) {
//예제 1
printAllCaps("abc")
printAllCaps(null)
// 예제 2
val ceo = Employee("Da Boss", null)
val developer = Employee("bob smith", ceo)
println(managerName(developer))
println(managerName(ceo))
// 예제 3
val person = Person("Dmitry", null)
println(person.countryName())
}
fun foo(s: String?)
val t: String = s ?: "" // 's'가 널이면 결과는 빈 문자열("")이다
// 예제 1
fun strLenSafe2(s: String?): Int = s?.length ?: 0
// 예제 2
class Address2(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company2(val name: String, val address: Address2?)
class Person2(val name: String, val company: Company2?)
fun printShippingLabel(person: Person2) {
val address = person.company?.address
?: throw IllegalAccessException("No address")
with(address) {
println(streetAddress)
print("$zipCode $city, $country")
}
}
fun main(args: Array<String>) {
// 예제 1
println(strLenSafe2("abc"))
println(strLenSafe2(null))
// 예제 2
val address = Address2("Elsestr. 47", 80687, "Munich", "Germany")
val jetbrains = Company2("JetBrains", address)
val person = Person2("Dmitry", jetbrains)
printShippingLabel(person)
}
class Person3(val firstName: String, val lastName: String) {
override fun equals(other: Any?): Boolean {
val otherPerson = other as? Person3 ?: return false // 타입이 서로 일치하지 않으면 false를 반환한다.
return otherPerson.firstName == firstName && otherPerson.lastName == lastName // 안전한 캐스트를 하고나면 otherPerson이 Person타입으로 스마트 캐스트된다.
}
override fun hashCode(): Int {
var result = firstName.hashCode()
result = 31 * result + lastName.hashCode()
return result
}
}
fun main(args: Array<String>) {
val p1 = Person3("Dmitry", "Jemerov")
val p2 = Person3("Dmitry", "Jemerov")
println(p1 == p2) //== 연산자는 equals 메서드를 호출한다
print(p1.equals(42))
}
fun ignoreNulls(s: String?) {
val sNotnull: String = s!!
println(sNotnull.length)
}
fun main(args: Array<String>) {
ignoreNulls(null) //java.lang.NullPointerException
}
person.company!!.address!!.country // 이런 식으로 코드를 작성하지 말라.
fun sendEmailTo(email: String) {
println("Sending email to $email")
}
fun main(args: Array<String>) {
var email: String? = "[email protected]"
email?.let { sendEmailTo(it) }
email = null
email?.let { sendEmailTo(it) }
}
class MyService {
fun performAction(): String = "foo"
}
class MyTest {
private val myService: MyService? = null // null로 초기화하기 위해 널이 될 수 있는 타입인 프로퍼티를 선언한다
@Before
fun setUp() {
myService = MyService() // setUp 메서드 안에서 진짜 초깃값을 지정한다.
}
@Test
fun testAction() {
Assert.assertEquals("foo", myService!!.performAction()) // 반드시 널 가능성에 신경 써야 한다 !!나 ?를 꼭 써야한다,
}
}
class MyService {
fun performAction(): String = "foo"
}
class MyTest {
private lateinit var myService: MyService // 초기화하지 않고 널이 될 수 없는 프로퍼티를 선언한다.
@Before
fun setUp() {
myService = MyService() // setUp 메서드 안에서 진짜 초깃값을 지정한다.
}
@Test
fun testAction() {
Assert.assertEquals("foo", myService.performAction()) // 널 검사를 수행하지 않고 프로퍼티를 사용한다.
}
}
fun verifyUserInput(input: String?) {
if (input.isNullOrBlank()) { // 안전한 호출을 하지 않아도 된다. 널이 될 수 있는 타입의 확장 함수는 안전한 호출 없이도 호출 가능하다
println("Please fill in the required fields")
}
}
fun main(args: Array<String>) {
verifyUserInput(" ")
verifyUserInput(null) // isNullOrBlank null을 수신 객체로 전달해도 아무런 예외가 발생하지 않는다.
}
fun String?.isNullOrBlank(): Boolean = //널이 될 수 잇는 String의 확장
this == null || this.isBlank() // 두번째 this 에는 스마트 캐스트가 적용된다.
fun <T> printHashCode(t: T) {
println(t?.hashCode()) // t가 널이 될 수 있으므로 안전한 호출을 써야만 한다.
}
fun main(args: Array<String>) {
printHashCode(null) //T의 타입은 Any?로 추론된다.
}
fun <T : Any> printHashCode(t: T) { // 이제 T는 널이 될 수 없는 타입이다.
println(t?.hashCode())
}
fun main(args: Array<String>) {
printHashCode(null) //이 코드는 컴파일 되지 않는다. 널이 될 수 없는 타입의 파라미터에 널을 넘길 수 없다.
}
val i: Int = person.name
//ERROR: Type mismatch: inferred type is String! but Int was expected
interface String processor {
void process(String value);
}
class StringPrinter : StringProcessor {
override fun process(value: String) {
println(value)
}
}
class NullableStringPrinter : StringProcessor {
override fun process(value: String?) {
if (value != null) {
println(value)
}
}
}
val i: Int = 1
val list: List<Int> = listOF(1, 2, 3)
fun showProgress(progress: Int) {
val percent = progress.coerceIn(0, 100) // 특정 범위값 제한
println("We're ${percent} % done !")
}
fun main(args: Array<String>) {
showProgress(146)
}
java.lang.Integer
객체가
들어간다data class Person(val name: String, val age: Int? = null) {
fun isOlderThan(other: Person): Boolean? {
if (age == null || other.age == null)
return null
return age > other.age
}
}
fun main(args: Array<String>) {
println(Person("Sam", 35).isOlderThan(Person("Amy", 42)))
println(Person("Sam", 35).isOlderThan(Person("Jane")))
}
val i = 1
val l: Long = i // error: type mismatch 컴파일 오류 발생
val i = 1
val l: Long = i.toLong() // 직접 변환 메서드를 호출해야한다.
val x = 1 //int 타입인 변수
val list = listOf(1L, 2L, 3L) // Long 값으로 이뤄진 리스트
x in list // 묵시적 타입 변환으로 인해 false 임
val x = 1
println(x.toLong()) in listOf(1L, 2L, 3L) // true
val x = listOf(1_234, 1_000) // 리터럴 중간에 밑줄(_)을 넣을 수 있다.
val k = 42L
val h = 42.0f
fun foo(l: Long) = pritln(l)
fun main(args: Array<String>) {
val b: Byte = 1 // 상수 값은 적절한 타입으로 해석된다
val l = b + 1L // + Byte와 Long 인자로 받을 수 있다.
foo(42) // 컴파일러는 42를 Long 값으로 해석한다,
}
interface Processor<T> {
fun process(): T
}
class NoResultProcessor : Processor<Unit> { // Unit을 반환하지만 타입을 지정할 필요는 없다
override fun process() {
TODO("Not yet implemented")
} // 여기서 return을 명시할 필요가 없다.
}
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
fun main(args: Array<String>) {
fail("Error occurred")
}
val address = company.address ?: fail("No address")
fun main(args: Array<String>) {
println(address.city)
}
fun readNumbers(reader: BufferedReader): 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
}
import java.io.BufferedReader
fun addValidNumbers(numbers: List<Int?>) {
var sumOfValidNumbers = 0
var invalidNumbers = 0
for (number in numbers) {
if (number != null) {
sumOfValidNumbers += number
} else {
invalidNumbers++
}
}
println("SUm of valid numbers: $sumOfValidNumbers")
println("Invalid numbers: $invalidNumbers")
}
fun main(args: Array<String>) {
val reader = BufferedReader(StringReader("1\\nabc\\n42"))
val numbers = readNumbers(reader)
addValidNumbers(numbers)
}
fun addValidNumbers(numbers: List<Int?>) {
val validNumbers = numbers.filterNotNull()
println("SUm of valid numbers ${validNumbers.sum()}")
println("Invalid numbers: ${numbers.size - validNumbers.size}")
}
코틀린 컬렉션과 자바 컬렉션을 나누는 가장 중요한 특성 하나는 코틀린에서는 컬렉션안의 데이터에 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다는 점이다.
컬렉션을 수정하려먼 : kotlin.collections.MutableCollection
코드에서 가능하면 항상 읽기 전용 인터페이스를 사용하는 것을 일반적인 규칙으로 삼아야한다. 코드가 컬렉션을 변경할 필요가 있을 떄만 변경 가능한 버전을 사용하라.
val 과 var의 구별과 마찬가지로 컬렉션의 읽기 전용 인터페이스와 변경 가능 인터페이스를 구별하는 이유는 프로그램에서 데이터에 어떤 일이 벌어지는지를 더 쉽게 이해하기 위함이다.
어떤 함수가 MutableCollection이 아닌 Collection 타입의 인자를 받는다면 그 함수는 컬렉션을 변경하지 않고 읽기만한다.
fun <T> copyElements(
source: Collection<T>,
target: MutableCollection
) {
for (item in source) { // source 컬렉션의 모든 원소에 대해 루프를 돈다
target.add(item) // 변경 가능한 Target 컬렉션에 원소를 추가한다.
}
}
fun main(args: Array<String>) {
val source: Collection<Int> = arrayListOf(3, 5, 7)
val target: MutableCollection<Int> = arrayListOf(1)
copyElements(source, target)
println(target)
}
컬렉션 타입 | 읽기 전용 타입 | 변경 가능 타입 |
---|---|---|
list | listOf | mutableListOf, arrayListOf |
Set | setOf | mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf |
Map | mapOf | mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf |
interface FileContentProcessor {
void processContents(File path,
byte[] binaryContents,
List<String> textContents);
}
class FileIndexer : FileContentProcessor {
override fun processContents(
path: File,
binaryContents: ByteArrat?,
textContents: List<String>?
)
}
fun main(args: Array<String>) {
for (i in args.indices) { // 배열의 인덱스 값의 범위에 대해 이터레이션 하기 위해 array.indices 확장 함수를 사용한다.
println("Argument $i is ${args[i]}") // array[index]로 인덱스를 사용해 배열 원소에 접근한다.
}
}
val letters = Array<String>(26) { i -> ('a' + i).toString() }
fun main(args: Array<String>) {
println(letters.joinToString(""))
}
val strings = listOf("a", "b", "c")
fun main(args: Array<String>) {
println("%s/%s/%s".format(*strings.toTypedArray())) // vararg 인자를 넘기기 위해 스프레드 연산자*를 써야 한다.
}
val fiveZeros = IntArray(5)
val ficeZerosTwo = intArrayOf(0, 0, 0, 0, 0)
val squares = IntArray(5) { i -> (i + 1) * (i + 1) }
println(squares.joinToString())
fun main(args: Array<String>) {
args.forEachIndexed { index, element ->
println("Argument $index is : $element")
}
}