코틀린에서 컬렉션 만들기

  • 코틀린은 자신만의 컬렉션 기능을 제공하지 않는다.
    • 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉽다.
  • 코틀린에서는 자바보다 더 많은 기능을 쓸 수 있다.

함수를 호출하기 쉽게 만들기

이름 붙인 인자

  • 코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있다.
  • 호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.
  • 자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없다.
    • 클래프 파일에 함수 파라미터정보를 넣은 것은 자바 8 이후 추가된 선택적 특징인데, 코틀린은 JDK 6와 호환된다.

디폴트 파라미터 값

  • 코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있으므로 자바의 오버로딩이 많아지는 문제를 피할 수 있다.
  • 자바에는 디폴트 파라미터 값이라는 개념이 없어서 코틀린 함수를 자바에서 호출하는 경우에는 코틀린 함수가 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다.
    • 코틀린 함수에 @JvmOverloads 애노테이션을 추가하면 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 파라미터를 하나씩 생략한 오버로딩한 자바 메소드를 추가해준다.

정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티

  • 코틀린에서는 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시킬 수 있다.
  • 코틀린 컴파일러는 컴파일할 때, 새로운 클래스를 정의해준다. 최상위 함수는 이 클래스의 정적인 메소드가 된다.
  • 코틀린 최상위 함수가 포함되는 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 애노테이션을 추가하면 된다.
    • @JvmName 애노테이션은 파일의 맨 앞, 패키지 이름 선언 이전에 위치해야 한다.
  • 함수와 마찬가지로 프로퍼티도 파일의 최상위에 놓을 수 있다.
    • 이런 프로퍼티 값은 정적 필드에 저장된다.
    • 겉으로는 상수처럼 보이는데, 실제로는 getter를 사용해야 한다면 자연스럽지 못하다.
    • const 변경자를 추가하면 프로퍼티를 public static final 필드로 컴파일하게 만들 수 있다.

메소드를 다른 클래스에 추가: 확장 함수와 프로퍼티

확장 함수

  • 확장 함수: 어떤 클래스의 멤버 메소드인 것 처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수
  • 수신 객체 타입(receiver type): 확장 함수의 클래스 이름
  • 수신 객체(receiver object): 확장 함수가 호출이 되는 대상이 되는 객체
  • 확장 함수가 캡슐화를 깨지는 않는다. 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버와 protected 멤버를 사용할 수 없다.
fun String.middle() = get(this.length / 2)

fun main() {
	val name = "junroot"
	print(name.middle())
}

임포트와 확장 함수

  • 확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야만 한다.
  • 한 파일 안에서 다른 여러 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야 하는 경우 이름을 바꿔서 임포트하면 이름 충돌을 막을 수 있다.
import extension.middle as m

fun main() {
	val name = "junroot"
	name.m()
}

자바에서 확장 함수 호출

  • 내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드다.
  • 그래서 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않는다.

확장 함수로 유틸리티 함수 정의

  • 확장 함수는 단지 정적 메소드 호출에 대한 syntatic sugar일 뿐이다.

  • 그래서 클래스가 아닌 더 구체적인 타입을 수신 객체 타입으로 지정할 수도 있다.

  • 아래와 같이 문자열의 컬렉션에 대해서만 호출할 수 있는 확장 함수도 만들 수 있다.

    fun Collection<String>.join(
    	separator: String = ", ", prefix: String = "", postfix: String = ""
    ) = joinToString(separator, prefix, postfix)
    

확장 함수는 오버라이드할 수 없다

  • 확장 함수는 클래스의 일부가 아니다. 확장 함수는 클래스 밖에 선언된다.
  • 이름과 파라미터가 완전히 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의해도 실제로는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되지, 그 변수가 저장된 객체의 동적인 타입에 의해 확장 함수가 결정되지 않는다.
  • 어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다.

확장 프로퍼티

  • 확장 프로퍼티도 일반적인 프로퍼티와 같은데, 단지 수신 객체 클래스가 추가됐을 뿐이다.
  • backing field가 없어서 기본 getter 구현을 제공할 수 없으므로 최소한 getter는 꼭 정의를 해야 한다.
  • 마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.

컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원

가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의

  • 가변 인자 함수 정의 방법: 함수 파라미터 앞에 vararg 변경자를 붙인다.

    public fun <T> listOf(vararg elements: T): kotlin.collections.List<T> { /* compiled code */ }
    
  • 스프레드 연산자(*): 이미 배열에 들어있는 원소를 가변 길이 인자로 넘기는 방법

    val list = listOf("args: ", *args)
    

값의 쌍 다루기: 중위 호출과 구조 분해 선언

  • 중위 호출(infix call): 수신 객체와 메소드 인자 사이에 메소드 이름을 넣는 호출 방식. 인자가 하나뿐인 일반 메소드나 확장 함수에 중위 호출을 할 수 있다.

    • 함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.

      public infix fun <A, B> A.to(that: B): kotlin.Pair<A, B> { /* compiled code */ }
      
    • 아래의 예에서 to가 중위 호출이다. to 함수를 호출하면 Pair 타입을 반환한다.

      mapOf(1 to 2, 2 to 3)
      
  • 구조 분해 선언: Pair 인스턴스 뿐만 아니라 여러 가지 객체에서 두 변수를 즉시 초기화 할 수 있다.

    • withIndex의 경우도 구조 분해 선언과 조합해서 원소의 인덱스와 값을 따로 변수에 담을 수 있다.

      for ((index, value) in list.withIndex()) {
      
      }
      

문자열과 정규식 다루기

문자열 나누기

  • 자바의 split() 메소드는 정규식으로만 표현이 가능하다.

  • 코틀린의 split() 함수는 기본적으로 String으로 문자열을 나누고, 정규식을 파라미터로 받는 함수는 Regex 타입을 받는 함수가 따로 있다.

  • 문자열을 정규식으로 바꾸기 위해서는 .toRegex() 함수를 사용하면 된다.

    printin("12.345-6.A".split("\\.|-".toRegex()))
    
  • 또한 split() 확장 함수를 오버로딩한 버전 중에 구분 문자열을 하나 이상 인자로 받을 수도 있다.

    printIn("12.345-6.A".split(".", "-"))
    

정규식과 3중 따옴표로 묶은 문자열

  • 3중 따옴표 문자열에서는 역슬래시(\)를 포함한 어떤 문자도 이스케이프할 필요가 없다.
  • Regex 타입의 matchEntire() 함수를 사용하면 매치가 된다. 매치에 실패하면 null이 반환된다.
  • 매치된 결과에 destructured 프로퍼티는 구조 분해 선언이 가능하다.

fun parsePath (path: String) {
	val regex = """(.+)/(.+)\.(.+)""".toRegex () 
  val matchResult = regex.matchEntire (path)
  if (matchResult != null) (
    val (directory, filename, extension) = matchResult.destructured
    printIn ("Dir: $directory, name: $filename, ext: Sextension")
)

여러 줄 3중 따옴표 문자열

  • 3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이나 이스케이프 없이 그대로 들어간다.

  • 여러 줄 문자열을 코드에서 더 보기 좋게 표현하고 싶다면 들여쓰기를 하되 들여쓰기의 끝부분을 특별한 문자열로 표시하고, trimMargin 을 사용해 그 문자열과 그직전의 공백을 제거한다.

  • 3중 따옴표 문자열 안에 문자열 템플릿을 사용할 수도 있다.

  • 3중 따옴표 문자열 안에서는 이스케이프를 할 수 없기 때문에 문자열 템플릿의 시작을 표현하는 $를 3중 따옴표 문자열 안에 넣어야한다면 아래 처럼 문자열 템플릿 안에 ‘$’ 문자를 넣어야 한다.

    val price = """${'$'}99.9"""
    

코드 다듬기: 로컬 함수와 확장

  • 로컬 함수: 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다.
    • 문법적인 부가 비용을 들이지 않고 코드 중복을 제거할 수 있다.
import java.util.*

fun Person.validateBeforeSave() {
	fun <T> validateNotNull(value: T) {
		if (Objects.isNull(value)) {
			throw IllegalArgumentException()
		}
	}

	validateNotNull(this.id)
	validateNotNull(this.name)
}

fun save(person: Person) {
	person.validateBeforeSave()
	// save
}