코틀린 스터디

코틀린 - 예외처리, Type System, 컬렉션

막이86 2023. 11. 10. 15:52
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 인터페이스를 상속한 것처럼 취급
      • 자바 호환성을 제공
    • 읽기 전용 인터페이스와 변경가능한 인터페이스를 분리

  • 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