728x90
반응형
1. 코틀린에서 컬렉션 만들기
https://wooded-aphid-86c.notion.site/3-80871557a2d347b6a2593b692411cf7f?pvs=4
(1) 코틀린의 컬렉션
- 코틀린 자체 컬렉션은 없다. 기존 자바 컬렉션을 사용한다.
- 자체 컬렉션을 제공하지 않는 이유는 자바와의 호환성때문에
fun main() {
val set = hashSetOf(1, 2, 3)
val list = arrayListOf(1, 2, 3)
val map = hashMapOf(1 to "one", 7 to "seven", 10 to "ten")
println(set.javaClass)
println(list.javaClass)
println(map.javaClass)
}
//출력값
//class java.util.HashSet
//class java.util.ArrayList
//class java.util.HashMap
2. 함수를 호출하기 쉽게 만들기 : joinToString
val list = listOf(1, 2, 3)
println(list) //출력값 [1, 2, 3]
- 자바 컬렉션에는 toString() 가 디폴트로 구현되어있다.
- 출력 형식은 이렇게 고정이다. [1, 2, 3]
- 출력형식을 우리에게 맞게끔 커스텀하고싶다면?
- 구아바나 아파치커먼즈 같은 서드파티 프로젝트를 추가하거나 직접 관련 로직을 구현해야한다.
- 코틀린에는 이미 이런 요구사항을 들어주는 함수가 표준 라이브러리에 들어있다.
(1) joinToString 함수를 직접 구현해보자.
fun main() {
val list = listOf(1, 2, 3)
println(jointoString(list, "; ", "(", ")"))
//출력값
result start: (
result end: (1
result start: (1
result end: (1; 2
result start: (1; 2
result end: (1; 2; 3
(1; 2; 3)
}
//joinToString 함수를 직접 구현해보자
fun <T> jointoString(
collection: Collection<T>,
separator: String,
prefix: String,
postfix: String
): String {
val result = StringBuilder(prefix)
for ((index, element) in collection.withIndex()) {
println("result start: ${result}")
if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다
result.append(element)
println("result end: ${result}")
}
result.append(postfix)
return result.toString()
}
- 이 함수는 제네릭하다. 즉 이 함수는 어떤 타입의 값을 원소로하는 컬렉션이든 처리할 수 있다.
(2) joinToString 함수를 덜 번잡하게, 이름 붙인 인자
jointoString(list, "; ", "(", ")") //가독성이 매우 떨어진다.
jointoString(list, separator = " ", prefix = "(", postfix = ")") //인자의 이름을 명시할 수 있다.
- 함수에 전달하는 인자에 이름을 명시적으로 붙일 수 있다.
- 인자를 다 붙이거나 안붙이거나 (인자를 붙였으면 나머지 인자에도 이름을 꼭 명시해야한다.)
- 함수의 파라미터 이름을 변경 할때에는 에디터에서 직접 변경하는게아니라 refactor를 이용해야한다.
- 자바로 작성한 코드를 호출 할 때는 이름 붙인 인자를 사용할 수 없다. 자바 8 이후 추가된 선택사항
- 그러나 코틀린은 JDK 6과 호환되서 비교불가함.
(3) 디폴트 파라미터 값
- 자바에서는 일부클래스에서 오버로딩한 메소드가 너무 많아진다는 문제가 있다.
- 이런식으로 오버로딩 메소드들은 하위호환성을 유지하거나 API 사용자에게 편의를 더하는 등 이유로 만들어진다.
- 단점 : 반복되고 모든 오버로딩 함수에 대해 주석이 필요할 수 도 있다.
Constructors Constructor and Description Thread() Allocates a new Thread object. Thread(Runnable target) Allocates a new Thread object. Thread(Runnable target, String name) Allocates a new Thread object. Thread(String name) Allocates a new Thread object. Thread(ThreadGroup group, Runnable target) Allocates a new Thread object. Thread(ThreadGroup group, Runnable target, String name) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group. Thread(ThreadGroup group, Runnable target, String name, long stackSize) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size. Thread(ThreadGroup group, String name) Allocates a new Thread object.
- 디폴트 파라미터를 설정하여 오버로드 상당수를 피할 수 있다.
fun main() { val list = listOf(1, 2, 3) println(jointoString(list, "; ", "(", ")")) println(jointoString(list, prefix = "; ", separator = "(", postfix = ")")) //이름 붙인 인자면 순서 변경해도 상관없다. println(jointoString(list)) //다 생략 println(jointoString(list, "; ")) //separator만 작성 } //출력값 (1; 2; 3) ; 1(2(3) 1, 2, 3 1; 2; 3
- fun <T> jointoString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String ="" ): String {
- 디폴드값과 자바
- 자바에는 디폴트 파라미터 값이라는 개념이 없다. 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터를 제공하더라도 모든 인자를 명시해야한다.
- 자바에서 코틀린 함수를 자주 호출하는 경우 @JvmOverloads 를 추가하자.
- 추가하면 코틀린 컴파일러가 맨 마지막 파라미터부터 하나씩 생략한 오버로딩한 자바 메소드를 추가한다.
public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit, CharSequence truncated, Function1<? super T, ? extends CharSequence> transform ) { // 함수 구현 } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit, CharSequence truncated ) { return joinToString($this$joinToString, separator, prefix, postfix, limit, truncated, (Function1)null, 64, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit ) { return joinToString($this$joinToString, separator, prefix, postfix, limit, (CharSequence)null, (Function1)null, 48, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix ) { return joinToString($this$joinToString, separator, prefix, postfix, -1, (CharSequence)null, (Function1)null, 48, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator ) { return joinToString($this$joinToString, separator, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 124, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString ) { return joinToString($this$joinToString, (CharSequence)null, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 124, (Object)null); }
(4) 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
- 객체지향언어 자바에서는 모든 코드를 클래스의 메소드로 작성해야한다.
- 하지만, 실전에서는 어느 클래스에 포함시키기 어려운 코드도 많다.
- 일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있다.
- 중요한 객체는 하나뿐이지만, 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지 않은 경우도 있다.
- 이 결과, 다양한 정적 메소드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메소드는 없는 클래스가 생겨난다. (ex JDK Colledtions)
- Util
- 코틀린에서는 이런 무의미한 클래스가 필요없다.
- 함수를 소스파일의 최상위 수준, 다른 클래스 밖에 위치시키면 된다.
- 이런 함수들은 그 파일의 맨앞에 정의된 패키지의 멤버함수이다. 다른 패키지에서 사용하고 싶다면 import하면 된다.
- 추가로 유틸리티 클래스 이름이 들어갈 필요는 없다.
- 이런 함수들은 그 파일의 맨앞에 정의된 패키지의 멤버함수이다. 다른 패키지에서 사용하고 싶다면 import하면 된다.
- 함수를 소스파일의 최상위 수준, 다른 클래스 밖에 위치시키면 된다.
- 내가 만든 joinToString() 함수를 join.kt를 만들어서 package strings에 넣어보자.
- 다른 파일에서 내가 만든 joinToString() 함수를 사용할 수 있다.
- 실행가능한 이유
- JVM은 클래스안에 들어있는 코드만 실행 할 수 있다. 때문에, 컴파일러는 다른 실행파일을 컴파일할때 새로운 클래스를 정의해준다.
- package strings fun <T> jointoString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String ="" ): String { val result = StringBuilder(prefix) for ((index, element) in collection.withIndex()) { //println("result start: ${result}") if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) //println("result end: ${result}") } result.append(postfix) return result.toString() }
- 어떻게 코틀린이 join.kt를 컴파일하는가?
- join.kt를 컴파일한 결과를 자바 코드로 살펴보자
- package strings; public class JoinKt { //내가 작성한 joinToString() }
- 코틀린 컴파일러가 생성하는 클래스의 이름은 최상위 함수가 틀어있던 코틀린 소스파일의 이름과 대응한다.
- 코틀린 파일의 최상위 함수는 이 클래스의 정적인 메소드가 된다.
- //호출 import strings.JoinKt; ... JoinKt.jointToString(list,", ","","");
- 코틀린 최상위함수가 포함된 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 어노테이션을 붙여라.
- //join.kt에 어노테이션을 추가하자. @file:JvmName("StringFunctions") //클래스 이름을 지정하는 어노테이션 package strings fun joinToString(...) { ... } /* 자바 */ import strings.StringFunctions; StringFunctions.joinToString(list,", ","","");
- 최상위 프로퍼티
- 함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. (흔하진않음)
3. 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
- 완전히 코틀린으로 이뤄진 프로젝트라 하더라도 JDK나 안드로이드 프레임워크, 서드파티 프레임워크 등의 자바 라이브러리를 기반으로 만들어진다.
- 기존 자바 프로젝트를 통합하는 경우, 코틀린으로 변환할 수 없거나 미쳐 변환못한 자바코드도 처리 할 수 있어야한다.
- 기존 자바 API를 재작성하지 않고도 코틀린의 확장함수를 이용하면 가능하다.
- 확장함수란?
- 어떤 클래스의 멤버 메소드인 것처럼 호출 할 수 있지만, 사실 그 클래스의 밖에 선언된 함수이다.
- strings 패키지에 마지막 문자를 돌려주는 메소드를 추가해보자.
package strings fun String.lastChar(): Char = this.get(this.length - 1)
- 함수 이름앞에 그 함수가 확장할 클래스의 이름을 덧붙인다.
- 클래스 이름을 수신 객체 타입(receiver type)이라 하고, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신객체(receiver object)라고 한다.
- 어떤 클래스의 멤버 메소드인 것처럼 호출 할 수 있지만, 사실 그 클래스의 밖에 선언된 함수이다.
- println("Kotlin".lastChar()) //수신 객체 타입은 String, 수신객체는 "Kotlin"이다.
- String이 자바나 코틀린 등의 언어 중 어떤것으로 작성된건 중요하지않다. 그루비와 같은 JVM 언어로 작성된 클래스도 확장 가능하다.
- 자바 클래스로 컴파일한 클래스 파일이 있는 한, 그 클래스에 원하는대로 확장을 추가할 수 있다.
- this 생략가능 fun String.lastChar(): Char = get( length - 1 )
- 확장 함수 내부에서는 수신객체의 메소드나 프로퍼티를 가져올 수 있다. (일반적인 인스턴스 메소드와 비슷하게)
- 캡슐화는 하지 않는다.
- 즉, private , protecte를 사용할 수 없다.
- 이제부터는 클래스의 멤버메소드, 확장함수 모두를 메소드라 부를것이다.
- 함수를 호출하는 쪽에서는 멤버 메소드, 확장 함수를 구별할 수 없다. 또한 중요하지도 않음.
(1) 임포트와 확장함수
- 확장함수를 사용하기 위해서는 클래스나 함수와 마찬가지로 import해야한다.
- 코틀린에서는 클래스를 임포트할 때와 동일한 구문을 사용해서 함수도 임포트 할 수 있다.
- 코틀린 문법상, 확장함수는 반드시 짧은 이름을 써야한다.
- 한 파일안에서, 여러 다른 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야하는 경우 이름을 바꿔서 import하면 충돌을 막을 수 있다.
//기본 import strings.lastChar val c = "Kotlin".lastChar() //*를 이용하기 import strings.* val c = "Kotlin".lastChar() //as를 이용해서 임포트한 클래스나 함수이름을 다른 이름으로 부르기 import strings.lastChar as last val c = "Kotlin".last()
(2) 자바에서 확장 함수 호출
- 내부적으로 확장함수는 수신객체를 첫번째 인자로 받는 정적 메소드이다.
- 그래서 확장함수를 호출해도 다른 아탑터 객체나 실행시점 부가비용이 들지 않는다.
- 이런 설계로 인해 자바에서 확장함수를 사용하기도 편하다.
- 다른 최상위 함수와 마찬가지로, 확장 함수가 들어있는 자바클래스 이름도 확장함수가 들어있는 파일 이름에 따라 결정된다.
/* 자바 */ char c = StringUtilKt.lastChar("Java");
(3) 확장 함수로 유틸리티 함수 정의 : joinToString 최종버전
fun <T> Collection<T>.jointoString( //Colletion에 대한 확장함수로 변경
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) { //수신객체를 this로 나타낸다. 여기서 this는 T타입으로 이뤄진 컬렉션이다.
//println("result start: ${result}")
if (index > 0) result.append(separator)
result.append(element)
//println("result end: ${result}")
}
result.append(postfix)
return result.toString()
}
- 확장함수는 단지 정적 메소드 호출에 대한 문법적인 편의일 뿐이다.
- 구체적인 타입을 수신 객체 타입으로 지정할 수 있다. T타입을 String으로 바꿔보자.
//문자열 컬렉션에 대해서만 호출 가능하다. //Int 컬렉션에서는 호출불가 fun Collection<String>.join( separator: String = " ", prefix: String = "", postfix: String = "" ) = jointoString(separator, prefix, postfix) println(listOf("a","b","c").join(" ")) //출력값 a b c
(4) 확장 함수는 오버라이드 할 수 없다.
- 확장 함수가 정적 메소드와 같은 특징을 가지므로, 오버라이드 불가하다.
- 우선 오버라이드 예제를 살펴보자.
open class View {
open fun click() = println("View Clicked!")
}
class Button : View() {
override fun click() {
println("Button Clicked!")
}
}
fun main() {
Button().click()
//출력값
//Button Clicked!
}
- 그러나 확장함수는 클래스의 일부가 아니다. 확장함수는 클래스 밖에서 선언된다.
- 실제 확장 함수를 호출 할 때, 수신객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될 지 결정된다. (View)
- 밑의 코드를 살펴보자.
- Button에 확장함수를 만들자. fun Button.showOff() = print("I'm Button!!")
- view: View = Button() 객체를 만들자.
- Button() 으로 만들었지만 실제로 출력되는 함수는 View.showOff() “I'm View!!” 가 출력된다.
- 그 변수에 저장된 객체의 동적인 타입에 의해 확장함수가 결정되지 않는다.
- 이미 확장함수는 정적으로 결정이 되어있는 상태이다. (val view: View)
- view가 가리키는 객체의 실제 타입은 Button 이지만, view의 타입이 View이기 때문에 무조건 View의 확장함수가 호출된다.
fun main() { Button().click() view.showOff() //I'm View!! } fun View.showOff() = print("I'm View!!") fun Button.showOff() = print("I'm Button!!") val view: View = Button()
- 밑의 코드를 살펴보자.
- 실제 확장 함수를 호출 할 때, 수신객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될 지 결정된다. (View)
- 어떤 클래스를 확장한 함수와 그 클래스의 멤버함수의 이름과 시그니처가 같다면, 멤버함수가 호출된다. (멤버함수 우선순위가 더 높다.)
- 어떤 클래스를 통해 확장함수를 사용하고 있다고 가정하자.
- 그 확장함수와 이름과 시그니처가 같은 함수를 클래스 내부에 추가하면 확장함수가 아닌 새로 추가된 멤버함수를 사용하게 된다.
(5) 확장 프로퍼티
- 확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가 할 수 있다.
- 프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기때문에 (기존 클래스의 인스턴스 객체에 필드를 추가할 방법은 없다.), 실제로 확장 프로퍼티는 아무상태도 가질 수 없다. 그러나 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어 편한 경우가 많다.
- 확장함수와 마찬가지로 확장 프로퍼티도 일반 프로퍼티와 같은데, 단지 수신객체 클래스가 추가되었을뿐이다.
- 뒷받침하는 필드가 없어서 기본 게터구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야한다.
- 마찬가지로 초기화코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.
- class Extension { val String.lastChar: Char get() = get(length - 1) }
- StringBuilder에 프로퍼티를 정의해보자
- var StringBuilder.lastChar: Char get() = get(length - 1) set(value: Char) { this.setCharAt(length - 1, value) } fun main() { println("Kotlin".lastChar()) //n val sb = StringBuilder("Kotlin?") sb.lastChar = '!' print(sb) //Kotiln! }
4. 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원
- varagr 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.
- 중위(infix)함수 호출 구문을 사용하면 인자가 하나뿐인 메소드를 간편하게 호출 할 수 있다.
- 구조 분해 선언(destructuring declaration)을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.
(1) 자바 컬렉션 API 확장
- 코틀린 컬렉션은 자바와 같은 클래스를 사용하지만 더 확장된 API를 제공하고 있다.
- 리스트 마지막 원소를 가져오는 기능(last()), 컬렉션의 최댓값을 찾는 기능(max()) 등… 어떻게 자바 라이브러리 클래스의 인스턴스인 컬렉션에 대해 코틀린이 새로운 기능을 추가할 수 있었을까?
- last와 max는 확장함수이다!!
- last 함수는 List<T> 클래스의 확장함수이다.
- max는 Collection<Int>의 확장함수이다.
- last와 max는 확장함수이다!!
- IDE의 자동완성 기능을 통해 코틀린 표준 라이브러리를 볼 수 있다.
- 코틀린 표준 라이브러리는 수많은 확장함수를 포함하고있다.
(2)가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의
- 컬렉션을 만들어내는 함수는 인자의 개수가 그때그때 달라 질 수 있다.
- listOf 함수의 인자는 가변길이인자 varagr로 정의 되어있다.
val list = listOf("1","2","3","4") //리스트를 생성하는 함수를 호출 할 때, 원하는 만큼 많이 원소를 전달 할 수 있다. ... fun listOf<T>(varagr values: T): List<T> {...}
- 가변 길이 인자는 메소드를 호출 할 때, 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다.
- 자바에서는 타입뒤에 … 로, 코틀린에서는 파라미터 앞에 varagr 변경자를 붙인다.
- 이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다.
- 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야한다.
- 코틀린 : 기술적으로는 스프레드 연산자가 그런 작업을 해준다. 하지만 실제로 전달하려는 배열 앞에 *을 붙이기만 하면 된다.
fun subFunction(args: Array<String>) { var a = listOf("1") val list = listOf("args : ${a}", *args) //*모든 인자를 출력해준다. println(list) } fun main() { println(subFunction(arrayOf("s","f","d"))) //출력값 [args : [1], s, f, d] kotlin.Unit }
- 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야한다.
(3) 값의 쌍 다루기: 중위 호출과 구조 분해 선언
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
- 여기서 to는 코틀린 키워드가 아닌, **중위 호출(infix call)**이라는 특별한 방식으로 to라는 일반 메소드를 호출 한 것이다.
- 중위 호출 시에는 수신객체와 유일한 메소드 인자사이에 메소드 이름을 넣는다.
- 1.to("one") //to 메소드를 일반적인 방식으로 호출함 1 to "one" // to 메소드를 중위 호출 방식으로 호출함 //두 호출은 동일하다.
- 인자가 하나뿐인 일반 메소드, 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다.
- 함수를 중위호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야한다.
- 이 to 함수는 Pair의 인스턴스를 반환한다.
- Pair는 말 그대로 두 원소로 이뤄진 순서쌍을 표현한다. Pair의 내용으로 두 변수를 즉시 초기화할 수 있다.
- val (number, name) = 1 to "one"
- 이런 기능을 **구조 분해 선언(destructuring declaration)**이라고 부른다.
- Pair 인스턴스 외 다른 객체에도 구조분해를 적용할 수 있다.
- 예를 들어, key와 value라는 두 변수를 맵의 원소를 사용해 초기화 할 수있다.
- 루프에서도 구조분해선언을 활용 할 수 있다.
- joinToString에서 본 WidthIndex를 구조분해선언과 조합하면 컬렉션 원소의 인덱스와 값을 따로 변수에 담을 수 있다.
- for ((index, element) in this.withIndex()) { if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) }
- 이 to 함수는 Pair의 인스턴스를 반환한다.
- infix fun Any.to(other: Any) = Pair(this,other)
- to 함수는 확장 함수이다. to를 사용하면 타입과 관계없이 임의의 순서쌍을 만들 수 있다.
- to의 수신객체가 제네릭하다는 뜻이다.
- mapOf 함수를 보자. 인자가 여러개가 들어갈 수 있고, 순서쌍만 가능하다.
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
5. 문자열과 정규식 다루기
- 코틀린 문자열은 자바 문자열과 같다.
- 자바와 코틀린 API의 차이를 알아보자.
(1) 문자열 나누기
- 자바 개발자라면 String의 split메소드를 잘 알것이다.
- 자바에서는 *println*("12.345-6.a".*split*(".")) 의 결과는 [12, 345-6, a]가 아니라 빈 배열이다.
- 왜냐면, split의 구분 문자열은 실제로는 정규식(regular expression)이기 때문이다
- 마침표[.]는 모든 문자를 나타내는 정규식으로 해석된다.
- 자바에서는 *println*("12.345-6.a".*split*(".")) 의 결과는 [12, 345-6, a]가 아니라 빈 배열이다.
- 코틀린에서는 자바의 split 대신에 여러가지 다른 조합의 파라미터를 받는 split 확장함수를 제공함으로써 혼동을 야기하는 메소드를 감춘다.
- 정규식을 파라미터로 받는 함수는 String이 아닌, Regex 타입의 값을 받는다.
- 따라서 코틀린에서는 split함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느것으로 문자열을 분리하는지 쉽게 알 수 있다.
println("12.345-6.a".split("\\\\.|-".toRegex())) //출력값 [12, 345, 6, a]
- split 확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.
- 이렇게 여러 문자를 받을 수 있는 코틀린 확장 함수는 자바에 있는 단 하나의 문자만 받을 수 있는 메소드를 대신한다.
- println("12.345-6.a".split(".","-")) //출력값 [12, 345, 6, a]
(2) 정규식과 3중 따옴표로 묶은 문자열
- 다른 예로 두 가지 다른 구현을 만들어보자.
- String을 확장한 함수를 사용
- 정규식 사용하기
- 파일의 전체 경로명을 디렉토리, 파일 이름, 확장자로 구분하는 것이다.
- 코틀린 표준 라이브러리에는 어떤 문자열에서 구분 문자열이 맨 나중(또는 처음)에 나타난 곳 뒤(또는 앞)의 부분 문자열을 반환하는 함수가 있다. 이런 함수를 사용해 경로 파싱을 구현한 버전은 다음과 같다.
fun main(){ parsePath("“/Users/yole/kotlin-book/chapter.adoc”") } //출력값 directory: “/Users/yole/kotlin-book fullName: chapter.adoc” Dir: “/Users/yole/kotlin-book, name: chapter, ext: adoc” fun parsePath(path: String) { val directory = path.substringBeforeLast("/") val fullName = path.substringAfterLast("/") val fileName = fullName.substringBeforeLast(".") val extension = fullName.substringAfterLast(".") println("directory: $directory") println("fullName: $fullName") println("Dir: $directory, name: $fileName, ext: $extension") }
- 코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다. 정규식은 강력하지만 나중에 알아보기 힘든 경우가 많다.
- 정규식이 필요할 때는 코틀린 라이브러리를 사용하면 더 편하다.
- 다음은 같은 작업을 정규식을 활용해 구현한 프로그램이다.
- fun parsePath2(path: String) { val regex = """(.+)/(.+)\\.(.+)""".toRegex() val matchResult = regex.matchEntire(path) if (matchResult != null) { val (directory, filename, extension) = matchResult.destructured println("Dir: $directory, name: $filename, ext: $extension") } }
(3) 여러 줄 3중 따옴표 문자열
- 3중 따옴표 문자열을 문자열 이스케이프를 피하기 위해서만 사용하지 않는다. 줄바꿈표현도 가능하다.
- 여러 줄 문자열(3중 따옴표 문자열)에는 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다.
- 보기 더 좋게하려면 들여쓰기를 하되 들여쓰기기의 끝부분을 특별한 문자열로 표시하고, trimMargin을 사용해 그 문자열과 그 직전의 공백을 제거한다.
- 줄 바꿈을 \n 과 같은 특수문자를 사용해 넣을 수 없다.
- \을 넣고싶으면 굳이 이스케이프할 필요가 없다.
- 프로그래밍 시 여러줄 문자열이 요긴한 분야로 테스트를 꼽을 수 있다. “”” “”” 안에 HTML이나 텍스트를 넣으면 된다.
- 여러 줄 문자열(3중 따옴표 문자열)에는 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다.
- fun main() { println(kotlinLogo.trimMargin(".")) } //출력값 | // | // | /\\ val kotlinLogo = """| // .| // .| /\\"""
(4) 코드 다듬기: 로컬 함수와 확장
- 많은 개발자들이 좋은 코드의 중요한 특징 중 하나가 중복이 없는것이라 믿는다.
- DRY : Don’t Repeat Yourself!
- 그러나 자바 코드를 작성 할 때는 DRY원칙을 피하기는 쉽지 않다.
- 많은 경우, 메소드 추출(ExtractMethod) 리팩토링을 적용해서 긴 메소드를 부분부분 나눠서 각 부분을 재활용 할 수 있다.
- 그러나 이렇게 되면 클래스 안에 작은 메소드가 많아지고 긱 메소드 사이의 관계를 파악하기 힘들어서 코드를 이해하기 더 어려울 수 있다.
- 리팩토링을 진행해서 추출한 메소드를 별도의 내부 클래스안에 넣으면 코드를 깔끔하게 조직할 수는 있지만, 그에 따른 불필요한 준비 코드가 늘어난다.
- 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩 시킬 수 있다.
- 이러면 문법적인 부가비용을 들이지 않고도 깔끔하게 코드를 조직할 수 있다.
- 흔히 발생하는 중복코드를 로컬함수(local)를 통해 어떻게 제거하는지 살펴보자.
- 중복 코드는 많지 않지만, 필드를 검증 할 때 여러 경우를 하나씩 처리하는건 비효율적이다. 로컬함수로 바꿔보자.
fun saveUser(user: User) { fun validate(user: User, value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id} : empty $fieldName" ) } } validate(user, user.name, "Name") validate(user, user.address, "Address") }
- User 객체를 로컬 함수에게 하나하나 전달해야한다는 것은 아쉽다.
- 로컬함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다. 불필요한 User 파라미터를 없애보자.
fun saveUser(user: User) { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id}" + "empty $fieldName" ) } } validate(user.name, "Name") validate(user.address, "Address") }
- 더 개선하고 싶으면 검증 로직을 User 클래스를 확장한 함수로 만들 수도 있다.
fun saveUser2(user: User) { user.validateBeforeSave() //유저를 데이터베이스에 저장한다. } fun User.validateBeforeSave() { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${id}" + "empty $fieldName" ) } } validate(name, "Name") validate(address, "Address") }
- 중첩이 깊어지면 코드 읽기가 어려워지므로 일반적으로는 한 단계만 함수를 중첩시키라고 권장한다.</aside>(1) 코틀린의 컬렉션
- 코틀린 자체 컬렉션은 없다. 기존 자바 컬렉션을 사용한다.
- 자체 컬렉션을 제공하지 않는 이유는 자바와의 호환성때문에
fun main() { val set = hashSetOf(1, 2, 3) val list = arrayListOf(1, 2, 3) val map = hashMapOf(1 to "one", 7 to "seven", 10 to "ten") println(set.javaClass) println(list.javaClass) println(map.javaClass) } //출력값 //class java.util.HashSet //class java.util.ArrayList //class java.util.HashMap
2. 함수를 호출하기 쉽게 만들기 : joinToString
val list = listOf(1, 2, 3) println(list) //출력값 [1, 2, 3]
- 자바 컬렉션에는 toString() 가 디폴트로 구현되어있다.
- 출력 형식은 이렇게 고정이다. [1, 2, 3]
- 출력형식을 우리에게 맞게끔 커스텀하고싶다면?
- 구아바나 아파치커먼즈 같은 서드파티 프로젝트를 추가하거나 직접 관련 로직을 구현해야한다.
- 코틀린에는 이미 이런 요구사항을 들어주는 함수가 표준 라이브러리에 들어있다.
- 이 함수는 제네릭하다. 즉 이 함수는 어떤 타입의 값을 원소로하는 컬렉션이든 처리할 수 있다.
- 함수에 전달하는 인자에 이름을 명시적으로 붙일 수 있다.
- 인자를 다 붙이거나 안붙이거나 (인자를 붙였으면 나머지 인자에도 이름을 꼭 명시해야한다.)
- 함수의 파라미터 이름을 변경 할때에는 에디터에서 직접 변경하는게아니라 refactor를 이용해야한다.
- 자바로 작성한 코드를 호출 할 때는 이름 붙인 인자를 사용할 수 없다. 자바 8 이후 추가된 선택사항
- 그러나 코틀린은 JDK 6과 호환되서 비교불가함.
- 자바에서는 일부클래스에서 오버로딩한 메소드가 너무 많아진다는 문제가 있다.
- 이런식으로 오버로딩 메소드들은 하위호환성을 유지하거나 API 사용자에게 편의를 더하는 등 이유로 만들어진다.
- 단점 : 반복되고 모든 오버로딩 함수에 대해 주석이 필요할 수 도 있다.
Constructors Constructor and Description Thread() Allocates a new Thread object. Thread(Runnable target) Allocates a new Thread object. Thread(Runnable target, String name) Allocates a new Thread object. Thread(String name) Allocates a new Thread object. Thread(ThreadGroup group, Runnable target) Allocates a new Thread object. Thread(ThreadGroup group, Runnable target, String name) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group. Thread(ThreadGroup group, Runnable target, String name, long stackSize) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size. Thread(ThreadGroup group, String name) Allocates a new Thread object.
- 디폴트 파라미터를 설정하여 오버로드 상당수를 피할 수 있다.
fun main() { val list = listOf(1, 2, 3) println(jointoString(list, "; ", "(", ")")) println(jointoString(list, prefix = "; ", separator = "(", postfix = ")")) //이름 붙인 인자면 순서 변경해도 상관없다. println(jointoString(list)) //다 생략 println(jointoString(list, "; ")) //separator만 작성 } //출력값 (1; 2; 3) ; 1(2(3) 1, 2, 3 1; 2; 3
- fun <T> jointoString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String ="" ): String {
- 디폴드값과 자바
- 자바에는 디폴트 파라미터 값이라는 개념이 없다. 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터를 제공하더라도 모든 인자를 명시해야한다.
- 자바에서 코틀린 함수를 자주 호출하는 경우 @JvmOverloads 를 추가하자.
- 추가하면 코틀린 컴파일러가 맨 마지막 파라미터부터 하나씩 생략한 오버로딩한 자바 메소드를 추가한다.
- public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit, CharSequence truncated, Function1<? super T, ? extends CharSequence> transform ) { // 함수 구현 } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit, CharSequence truncated ) { return joinToString($this$joinToString, separator, prefix, postfix, limit, truncated, (Function1)null, 64, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit ) { return joinToString($this$joinToString, separator, prefix, postfix, limit, (CharSequence)null, (Function1)null, 48, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix ) { return joinToString($this$joinToString, separator, prefix, postfix, -1, (CharSequence)null, (Function1)null, 48, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator ) { return joinToString($this$joinToString, separator, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 124, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString ) { return joinToString($this$joinToString, (CharSequence)null, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 124, (Object)null); }
- 객체지향언어 자바에서는 모든 코드를 클래스의 메소드로 작성해야한다.
- 하지만, 실전에서는 어느 클래스에 포함시키기 어려운 코드도 많다.
- 일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있다.
- 중요한 객체는 하나뿐이지만, 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지 않은 경우도 있다.
- 이 결과, 다양한 정적 메소드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메소드는 없는 클래스가 생겨난다. (ex JDK Colledtions)
- Util
- 코틀린에서는 이런 무의미한 클래스가 필요없다.
- 함수를 소스파일의 최상위 수준, 다른 클래스 밖에 위치시키면 된다.
- 이런 함수들은 그 파일의 맨앞에 정의된 패키지의 멤버함수이다. 다른 패키지에서 사용하고 싶다면 import하면 된다.
- 추가로 유틸리티 클래스 이름이 들어갈 필요는 없다.
- 이런 함수들은 그 파일의 맨앞에 정의된 패키지의 멤버함수이다. 다른 패키지에서 사용하고 싶다면 import하면 된다.
- 함수를 소스파일의 최상위 수준, 다른 클래스 밖에 위치시키면 된다.
- 내가 만든 joinToString() 함수를 join.kt를 만들어서 package strings에 넣어보자.
- 다른 파일에서 내가 만든 joinToString() 함수를 사용할 수 있다.
- 실행가능한 이유
- JVM은 클래스안에 들어있는 코드만 실행 할 수 있다. 때문에, 컴파일러는 다른 실행파일을 컴파일할때 새로운 클래스를 정의해준다.
- package strings fun <T> jointoString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String ="" ): String { val result = StringBuilder(prefix) for ((index, element) in collection.withIndex()) { //println("result start: ${result}") if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) //println("result end: ${result}") } result.append(postfix) return result.toString() }
- 어떻게 코틀린이 join.kt를 컴파일하는가?
- join.kt를 컴파일한 결과를 자바 코드로 살펴보자
- package strings; public class JoinKt { //내가 작성한 joinToString() }
- 코틀린 컴파일러가 생성하는 클래스의 이름은 최상위 함수가 틀어있던 코틀린 소스파일의 이름과 대응한다.
- 코틀린 파일의 최상위 함수는 이 클래스의 정적인 메소드가 된다.
- //호출 import strings.JoinKt; ... JoinKt.jointToString(list,", ","","");
- 코틀린 최상위함수가 포함된 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 어노테이션을 붙여라.
- //join.kt에 어노테이션을 추가하자. @file:JvmName("StringFunctions") //클래스 이름을 지정하는 어노테이션 package strings fun joinToString(...) { ... } /* 자바 */ import strings.StringFunctions; StringFunctions.joinToString(list,", ","","");
- 최상위 프로퍼티
- 함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. (흔하진않음)
3. 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
- 완전히 코틀린으로 이뤄진 프로젝트라 하더라도 JDK나 안드로이드 프레임워크, 서드파티 프레임워크 등의 자바 라이브러리를 기반으로 만들어진다.
- 기존 자바 프로젝트를 통합하는 경우, 코틀린으로 변환할 수 없거나 미쳐 변환못한 자바코드도 처리 할 수 있어야한다.
- 기존 자바 API를 재작성하지 않고도 코틀린의 확장함수를 이용하면 가능하다.
- 확장함수란?
- 어떤 클래스의 멤버 메소드인 것처럼 호출 할 수 있지만, 사실 그 클래스의 밖에 선언된 함수이다.
- strings 패키지에 마지막 문자를 돌려주는 메소드를 추가해보자.
package strings fun String.lastChar(): Char = this.get(this.length - 1)
- 함수 이름앞에 그 함수가 확장할 클래스의 이름을 덧붙인다.
- 클래스 이름을 수신 객체 타입(receiver type)이라 하고, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신객체(receiver object)라고 한다.
- 어떤 클래스의 멤버 메소드인 것처럼 호출 할 수 있지만, 사실 그 클래스의 밖에 선언된 함수이다.
- println("Kotlin".lastChar()) //수신 객체 타입은 String, 수신객체는 "Kotlin"이다.
- String이 자바나 코틀린 등의 언어 중 어떤것으로 작성된건 중요하지않다. 그루비와 같은 JVM 언어로 작성된 클래스도 확장 가능하다.
- 자바 클래스로 컴파일한 클래스 파일이 있는 한, 그 클래스에 원하는대로 확장을 추가할 수 있다.
- this 생략가능 fun String.lastChar(): Char = get( length - 1 )
- 확장 함수 내부에서는 수신객체의 메소드나 프로퍼티를 가져올 수 있다. (일반적인 인스턴스 메소드와 비슷하게)
- 캡슐화는 하지 않는다.
- 즉, private , protecte를 사용할 수 없다.
- 이제부터는 클래스의 멤버메소드, 확장함수 모두를 메소드라 부를것이다.
- 함수를 호출하는 쪽에서는 멤버 메소드, 확장 함수를 구별할 수 없다. 또한 중요하지도 않음.
- 확장함수를 사용하기 위해서는 클래스나 함수와 마찬가지로 import해야한다.
- 코틀린에서는 클래스를 임포트할 때와 동일한 구문을 사용해서 함수도 임포트 할 수 있다.
- 코틀린 문법상, 확장함수는 반드시 짧은 이름을 써야한다.
- 한 파일안에서, 여러 다른 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야하는 경우 이름을 바꿔서 import하면 충돌을 막을 수 있다.
//기본 import strings.lastChar val c = "Kotlin".lastChar() //*를 이용하기 import strings.* val c = "Kotlin".lastChar() //as를 이용해서 임포트한 클래스나 함수이름을 다른 이름으로 부르기 import strings.lastChar as last val c = "Kotlin".last()
- 내부적으로 확장함수는 수신객체를 첫번째 인자로 받는 정적 메소드이다.
- 그래서 확장함수를 호출해도 다른 아탑터 객체나 실행시점 부가비용이 들지 않는다.
- 이런 설계로 인해 자바에서 확장함수를 사용하기도 편하다.
- 다른 최상위 함수와 마찬가지로, 확장 함수가 들어있는 자바클래스 이름도 확장함수가 들어있는 파일 이름에 따라 결정된다.
/* 자바 */ char c = StringUtilKt.lastChar("Java");
- 확장함수는 단지 정적 메소드 호출에 대한 문법적인 편의일 뿐이다.
- 구체적인 타입을 수신 객체 타입으로 지정할 수 있다. T타입을 String으로 바꿔보자.
//문자열 컬렉션에 대해서만 호출 가능하다. //Int 컬렉션에서는 호출불가 fun Collection<String>.join( separator: String = " ", prefix: String = "", postfix: String = "" ) = jointoString(separator, prefix, postfix) println(listOf("a","b","c").join(" ")) //출력값 a b c
- 확장 함수가 정적 메소드와 같은 특징을 가지므로, 오버라이드 불가하다.
- 우선 오버라이드 예제를 살펴보자.
open class View { open fun click() = println("View Clicked!") } class Button : View() { override fun click() { println("Button Clicked!") } } fun main() { Button().click() //출력값 //Button Clicked! }
- 그러나 확장함수는 클래스의 일부가 아니다. 확장함수는 클래스 밖에서 선언된다.
- 실제 확장 함수를 호출 할 때, 수신객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될 지 결정된다. (View)
- 밑의 코드를 살펴보자.
- Button에 확장함수를 만들자. fun Button.showOff() = print("I'm Button!!")
- view: View = Button() 객체를 만들자.
- Button() 으로 만들었지만 실제로 출력되는 함수는 View.showOff() “I'm View!!” 가 출력된다.
- 그 변수에 저장된 객체의 동적인 타입에 의해 확장함수가 결정되지 않는다.
- 이미 확장함수는 정적으로 결정이 되어있는 상태이다. (val view: View)
- view가 가리키는 객체의 실제 타입은 Button 이지만, view의 타입이 View이기 때문에 무조건 View의 확장함수가 호출된다.
fun main() { Button().click() view.showOff() //I'm View!! } fun View.showOff() = print("I'm View!!") fun Button.showOff() = print("I'm Button!!") val view: View = Button()
- 밑의 코드를 살펴보자.
- 실제 확장 함수를 호출 할 때, 수신객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될 지 결정된다. (View)
- 어떤 클래스를 확장한 함수와 그 클래스의 멤버함수의 이름과 시그니처가 같다면, 멤버함수가 호출된다. (멤버함수 우선순위가 더 높다.)
- 어떤 클래스를 통해 확장함수를 사용하고 있다고 가정하자.
- 그 확장함수와 이름과 시그니처가 같은 함수를 클래스 내부에 추가하면 확장함수가 아닌 새로 추가된 멤버함수를 사용하게 된다.
- 확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가 할 수 있다.
- 프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기때문에 (기존 클래스의 인스턴스 객체에 필드를 추가할 방법은 없다.), 실제로 확장 프로퍼티는 아무상태도 가질 수 없다. 그러나 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어 편한 경우가 많다.
- 확장함수와 마찬가지로 확장 프로퍼티도 일반 프로퍼티와 같은데, 단지 수신객체 클래스가 추가되었을뿐이다.
- 뒷받침하는 필드가 없어서 기본 게터구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야한다.
- 마찬가지로 초기화코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.
- class Extension { val String.lastChar: Char get() = get(length - 1) }
- StringBuilder에 프로퍼티를 정의해보자
- var StringBuilder.lastChar: Char get() = get(length - 1) set(value: Char) { this.setCharAt(length - 1, value) } fun main() { println("Kotlin".lastChar()) //n val sb = StringBuilder("Kotlin?") sb.lastChar = '!' print(sb) //Kotiln! }
4. 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원
- varagr 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.
- 중위(infix)함수 호출 구문을 사용하면 인자가 하나뿐인 메소드를 간편하게 호출 할 수 있다.
- 구조 분해 선언(destructuring declaration)을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.
- 코틀린 컬렉션은 자바와 같은 클래스를 사용하지만 더 확장된 API를 제공하고 있다.
- 리스트 마지막 원소를 가져오는 기능(last()), 컬렉션의 최댓값을 찾는 기능(max()) 등… 어떻게 자바 라이브러리 클래스의 인스턴스인 컬렉션에 대해 코틀린이 새로운 기능을 추가할 수 있었을까?
- last와 max는 확장함수이다!!
- last 함수는 List<T> 클래스의 확장함수이다.
- max는 Collection<Int>의 확장함수이다.
- last와 max는 확장함수이다!!
- IDE의 자동완성 기능을 통해 코틀린 표준 라이브러리를 볼 수 있다.
- 코틀린 표준 라이브러리는 수많은 확장함수를 포함하고있다.
- 컬렉션을 만들어내는 함수는 인자의 개수가 그때그때 달라 질 수 있다.
- listOf 함수의 인자는 가변길이인자 varagr로 정의 되어있다.
val list = listOf("1","2","3","4") //리스트를 생성하는 함수를 호출 할 때, 원하는 만큼 많이 원소를 전달 할 수 있다. ... fun listOf<T>(varagr values: T): List<T> {...}
- 가변 길이 인자는 메소드를 호출 할 때, 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다.
- 자바에서는 타입뒤에 … 로, 코틀린에서는 파라미터 앞에 varagr 변경자를 붙인다.
- 이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다.
- 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야한다.
- 코틀린 : 기술적으로는 스프레드 연산자가 그런 작업을 해준다. 하지만 실제로 전달하려는 배열 앞에 *을 붙이기만 하면 된다.
fun subFunction(args: Array<String>) { var a = listOf("1") val list = listOf("args : ${a}", *args) //*모든 인자를 출력해준다. println(list) } fun main() { println(subFunction(arrayOf("s","f","d"))) //출력값 [args : [1], s, f, d] kotlin.Unit }
- 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야한다.
- 여기서 to는 코틀린 키워드가 아닌, **중위 호출(infix call)**이라는 특별한 방식으로 to라는 일반 메소드를 호출 한 것이다.
- 중위 호출 시에는 수신객체와 유일한 메소드 인자사이에 메소드 이름을 넣는다.
- 1.to("one") //to 메소드를 일반적인 방식으로 호출함 1 to "one" // to 메소드를 중위 호출 방식으로 호출함 //두 호출은 동일하다.
- 인자가 하나뿐인 일반 메소드, 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다.
- 함수를 중위호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야한다.
- 이 to 함수는 Pair의 인스턴스를 반환한다.
- Pair는 말 그대로 두 원소로 이뤄진 순서쌍을 표현한다. Pair의 내용으로 두 변수를 즉시 초기화할 수 있다.
- val (number, name) = 1 to "one"
- 이런 기능을 **구조 분해 선언(destructuring declaration)**이라고 부른다.
- Pair 인스턴스 외 다른 객체에도 구조분해를 적용할 수 있다.
- 예를 들어, key와 value라는 두 변수를 맵의 원소를 사용해 초기화 할 수있다.
- 루프에서도 구조분해선언을 활용 할 수 있다.
- joinToString에서 본 WidthIndex를 구조분해선언과 조합하면 컬렉션 원소의 인덱스와 값을 따로 변수에 담을 수 있다.
- for ((index, element) in this.withIndex()) { if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) }
- 이 to 함수는 Pair의 인스턴스를 반환한다.
- infix fun Any.to(other: Any) = Pair(this,other)
- to 함수는 확장 함수이다. to를 사용하면 타입과 관계없이 임의의 순서쌍을 만들 수 있다.
- to의 수신객체가 제네릭하다는 뜻이다.
- mapOf 함수를 보자. 인자가 여러개가 들어갈 수 있고, 순서쌍만 가능하다.
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
5. 문자열과 정규식 다루기
- 코틀린 문자열은 자바 문자열과 같다.
- 자바와 코틀린 API의 차이를 알아보자.
- 자바 개발자라면 String의 split메소드를 잘 알것이다.
- 자바에서는 *println*("12.345-6.a".*split*(".")) 의 결과는 [12, 345-6, a]가 아니라 빈 배열이다.
- 왜냐면, split의 구분 문자열은 실제로는 정규식(regular expression)이기 때문이다
- 마침표[.]는 모든 문자를 나타내는 정규식으로 해석된다.
- 자바에서는 *println*("12.345-6.a".*split*(".")) 의 결과는 [12, 345-6, a]가 아니라 빈 배열이다.
- 코틀린에서는 자바의 split 대신에 여러가지 다른 조합의 파라미터를 받는 split 확장함수를 제공함으로써 혼동을 야기하는 메소드를 감춘다.
- 정규식을 파라미터로 받는 함수는 String이 아닌, Regex 타입의 값을 받는다.
- 따라서 코틀린에서는 split함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느것으로 문자열을 분리하는지 쉽게 알 수 있다.
println("12.345-6.a".split("\\\\.|-".toRegex())) //출력값 [12, 345, 6, a]
- split 확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.
- 이렇게 여러 문자를 받을 수 있는 코틀린 확장 함수는 자바에 있는 단 하나의 문자만 받을 수 있는 메소드를 대신한다.
- println("12.345-6.a".split(".","-")) //출력값 [12, 345, 6, a]
- 다른 예로 두 가지 다른 구현을 만들어보자.
- String을 확장한 함수를 사용
- 정규식 사용하기
- 파일의 전체 경로명을 디렉토리, 파일 이름, 확장자로 구분하는 것이다.
- 코틀린 표준 라이브러리에는 어떤 문자열에서 구분 문자열이 맨 나중(또는 처음)에 나타난 곳 뒤(또는 앞)의 부분 문자열을 반환하는 함수가 있다. 이런 함수를 사용해 경로 파싱을 구현한 버전은 다음과 같다.
fun main(){ parsePath("“/Users/yole/kotlin-book/chapter.adoc”") } //출력값 directory: “/Users/yole/kotlin-book fullName: chapter.adoc” Dir: “/Users/yole/kotlin-book, name: chapter, ext: adoc” fun parsePath(path: String) { val directory = path.substringBeforeLast("/") val fullName = path.substringAfterLast("/") val fileName = fullName.substringBeforeLast(".") val extension = fullName.substringAfterLast(".") println("directory: $directory") println("fullName: $fullName") println("Dir: $directory, name: $fileName, ext: $extension") }
- 코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다. 정규식은 강력하지만 나중에 알아보기 힘든 경우가 많다.
- 정규식이 필요할 때는 코틀린 라이브러리를 사용하면 더 편하다.
- 다음은 같은 작업을 정규식을 활용해 구현한 프로그램이다.
- fun parsePath2(path: String) { val regex = """(.+)/(.+)\\.(.+)""".toRegex() val matchResult = regex.matchEntire(path) if (matchResult != null) { val (directory, filename, extension) = matchResult.destructured println("Dir: $directory, name: $filename, ext: $extension") } }
- 3중 따옴표 문자열을 문자열 이스케이프를 피하기 위해서만 사용하지 않는다. 줄바꿈표현도 가능하다.
- 여러 줄 문자열(3중 따옴표 문자열)에는 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다.
- 보기 더 좋게하려면 들여쓰기를 하되 들여쓰기기의 끝부분을 특별한 문자열로 표시하고, trimMargin을 사용해 그 문자열과 그 직전의 공백을 제거한다.
- 줄 바꿈을 \n 과 같은 특수문자를 사용해 넣을 수 없다.
- \을 넣고싶으면 굳이 이스케이프할 필요가 없다.
- 프로그래밍 시 여러줄 문자열이 요긴한 분야로 테스트를 꼽을 수 있다. “”” “”” 안에 HTML이나 텍스트를 넣으면 된다.
- 여러 줄 문자열(3중 따옴표 문자열)에는 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다.
- fun main() { println(kotlinLogo.trimMargin(".")) } //출력값 | // | // | /\\ val kotlinLogo = """| // .| // .| /\\"""
- 많은 개발자들이 좋은 코드의 중요한 특징 중 하나가 중복이 없는것이라 믿는다.
- DRY : Don’t Repeat Yourself!
- 그러나 자바 코드를 작성 할 때는 DRY원칙을 피하기는 쉽지 않다.
- 많은 경우, 메소드 추출(ExtractMethod) 리팩토링을 적용해서 긴 메소드를 부분부분 나눠서 각 부분을 재활용 할 수 있다.
- 그러나 이렇게 되면 클래스 안에 작은 메소드가 많아지고 긱 메소드 사이의 관계를 파악하기 힘들어서 코드를 이해하기 더 어려울 수 있다.
- 리팩토링을 진행해서 추출한 메소드를 별도의 내부 클래스안에 넣으면 코드를 깔끔하게 조직할 수는 있지만, 그에 따른 불필요한 준비 코드가 늘어난다.
- 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩 시킬 수 있다.
- 이러면 문법적인 부가비용을 들이지 않고도 깔끔하게 코드를 조직할 수 있다.
- 흔히 발생하는 중복코드를 로컬함수(local)를 통해 어떻게 제거하는지 살펴보자.
- 중복 코드는 많지 않지만, 필드를 검증 할 때 여러 경우를 하나씩 처리하는건 비효율적이다. 로컬함수로 바꿔보자.
fun saveUser(user: User) { fun validate(user: User, value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id} : empty $fieldName" ) } } validate(user, user.name, "Name") validate(user, user.address, "Address") }
- User 객체를 로컬 함수에게 하나하나 전달해야한다는 것은 아쉽다.
- 로컬함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다. 불필요한 User 파라미터를 없애보자.
fun saveUser(user: User) { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id}" + "empty $fieldName" ) } } validate(user.name, "Name") validate(user.address, "Address") }
- 더 개선하고 싶으면 검증 로직을 User 클래스를 확장한 함수로 만들 수도 있다.
fun saveUser2(user: User) { user.validateBeforeSave() //유저를 데이터베이스에 저장한다. } fun User.validateBeforeSave() { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${id}" + "empty $fieldName" ) } } validate(name, "Name") validate(address, "Address") }
- 중첩이 깊어지면 코드 읽기가 어려워지므로 일반적으로는 한 단계만 함수를 중첩시키라고 권장한다.</aside>(1) 코틀린의 컬렉션
- 코틀린 자체 컬렉션은 없다. 기존 자바 컬렉션을 사용한다.
- 자체 컬렉션을 제공하지 않는 이유는 자바와의 호환성때문에
fun main() { val set = hashSetOf(1, 2, 3) val list = arrayListOf(1, 2, 3) val map = hashMapOf(1 to "one", 7 to "seven", 10 to "ten") println(set.javaClass) println(list.javaClass) println(map.javaClass) } //출력값 //class java.util.HashSet //class java.util.ArrayList //class java.util.HashMap
2. 함수를 호출하기 쉽게 만들기 : joinToString
val list = listOf(1, 2, 3) println(list) //출력값 [1, 2, 3]
- 자바 컬렉션에는 toString() 가 디폴트로 구현되어있다.
- 출력 형식은 이렇게 고정이다. [1, 2, 3]
- 출력형식을 우리에게 맞게끔 커스텀하고싶다면?
- 구아바나 아파치커먼즈 같은 서드파티 프로젝트를 추가하거나 직접 관련 로직을 구현해야한다.
- 코틀린에는 이미 이런 요구사항을 들어주는 함수가 표준 라이브러리에 들어있다.
- 이 함수는 제네릭하다. 즉 이 함수는 어떤 타입의 값을 원소로하는 컬렉션이든 처리할 수 있다.
- 함수에 전달하는 인자에 이름을 명시적으로 붙일 수 있다.
- 인자를 다 붙이거나 안붙이거나 (인자를 붙였으면 나머지 인자에도 이름을 꼭 명시해야한다.)
- 함수의 파라미터 이름을 변경 할때에는 에디터에서 직접 변경하는게아니라 refactor를 이용해야한다.
- 자바로 작성한 코드를 호출 할 때는 이름 붙인 인자를 사용할 수 없다. 자바 8 이후 추가된 선택사항
- 그러나 코틀린은 JDK 6과 호환되서 비교불가함.
- 자바에서는 일부클래스에서 오버로딩한 메소드가 너무 많아진다는 문제가 있다.
- 이런식으로 오버로딩 메소드들은 하위호환성을 유지하거나 API 사용자에게 편의를 더하는 등 이유로 만들어진다.
- 단점 : 반복되고 모든 오버로딩 함수에 대해 주석이 필요할 수 도 있다.
Constructors Constructor and Description Thread() Allocates a new Thread object. Thread(Runnable target) Allocates a new Thread object. Thread(Runnable target, String name) Allocates a new Thread object. Thread(String name) Allocates a new Thread object. Thread(ThreadGroup group, Runnable target) Allocates a new Thread object. Thread(ThreadGroup group, Runnable target, String name) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group. Thread(ThreadGroup group, Runnable target, String name, long stackSize) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size. Thread(ThreadGroup group, String name) Allocates a new Thread object.
- 디폴트 파라미터를 설정하여 오버로드 상당수를 피할 수 있다.
fun main() { val list = listOf(1, 2, 3) println(jointoString(list, "; ", "(", ")")) println(jointoString(list, prefix = "; ", separator = "(", postfix = ")")) //이름 붙인 인자면 순서 변경해도 상관없다. println(jointoString(list)) //다 생략 println(jointoString(list, "; ")) //separator만 작성 } //출력값 (1; 2; 3) ; 1(2(3) 1, 2, 3 1; 2; 3
- fun <T> jointoString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String ="" ): String {
- 디폴드값과 자바
- 자바에는 디폴트 파라미터 값이라는 개념이 없다. 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터를 제공하더라도 모든 인자를 명시해야한다.
- 자바에서 코틀린 함수를 자주 호출하는 경우 @JvmOverloads 를 추가하자.
- 추가하면 코틀린 컴파일러가 맨 마지막 파라미터부터 하나씩 생략한 오버로딩한 자바 메소드를 추가한다.
- public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit, CharSequence truncated, Function1<? super T, ? extends CharSequence> transform ) { // 함수 구현 } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit, CharSequence truncated ) { return joinToString($this$joinToString, separator, prefix, postfix, limit, truncated, (Function1)null, 64, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit ) { return joinToString($this$joinToString, separator, prefix, postfix, limit, (CharSequence)null, (Function1)null, 48, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix ) { return joinToString($this$joinToString, separator, prefix, postfix, -1, (CharSequence)null, (Function1)null, 48, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator ) { return joinToString($this$joinToString, separator, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 124, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString ) { return joinToString($this$joinToString, (CharSequence)null, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 124, (Object)null); }
- 객체지향언어 자바에서는 모든 코드를 클래스의 메소드로 작성해야한다.
- 하지만, 실전에서는 어느 클래스에 포함시키기 어려운 코드도 많다.
- 일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있다.
- 중요한 객체는 하나뿐이지만, 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지 않은 경우도 있다.
- 이 결과, 다양한 정적 메소드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메소드는 없는 클래스가 생겨난다. (ex JDK Colledtions)
- Util
- 코틀린에서는 이런 무의미한 클래스가 필요없다.
- 함수를 소스파일의 최상위 수준, 다른 클래스 밖에 위치시키면 된다.
- 이런 함수들은 그 파일의 맨앞에 정의된 패키지의 멤버함수이다. 다른 패키지에서 사용하고 싶다면 import하면 된다.
- 추가로 유틸리티 클래스 이름이 들어갈 필요는 없다.
- 이런 함수들은 그 파일의 맨앞에 정의된 패키지의 멤버함수이다. 다른 패키지에서 사용하고 싶다면 import하면 된다.
- 함수를 소스파일의 최상위 수준, 다른 클래스 밖에 위치시키면 된다.
- 내가 만든 joinToString() 함수를 join.kt를 만들어서 package strings에 넣어보자.
- 다른 파일에서 내가 만든 joinToString() 함수를 사용할 수 있다.
- 실행가능한 이유
- JVM은 클래스안에 들어있는 코드만 실행 할 수 있다. 때문에, 컴파일러는 다른 실행파일을 컴파일할때 새로운 클래스를 정의해준다.
- package strings fun <T> jointoString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String ="" ): String { val result = StringBuilder(prefix) for ((index, element) in collection.withIndex()) { //println("result start: ${result}") if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) //println("result end: ${result}") } result.append(postfix) return result.toString() }
- 어떻게 코틀린이 join.kt를 컴파일하는가?
- join.kt를 컴파일한 결과를 자바 코드로 살펴보자
- package strings; public class JoinKt { //내가 작성한 joinToString() }
- 코틀린 컴파일러가 생성하는 클래스의 이름은 최상위 함수가 틀어있던 코틀린 소스파일의 이름과 대응한다.
- 코틀린 파일의 최상위 함수는 이 클래스의 정적인 메소드가 된다.
- //호출 import strings.JoinKt; ... JoinKt.jointToString(list,", ","","");
- 코틀린 최상위함수가 포함된 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 어노테이션을 붙여라.
- //join.kt에 어노테이션을 추가하자. @file:JvmName("StringFunctions") //클래스 이름을 지정하는 어노테이션 package strings fun joinToString(...) { ... } /* 자바 */ import strings.StringFunctions; StringFunctions.joinToString(list,", ","","");
- 최상위 프로퍼티
- 함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. (흔하진않음)
3. 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
- 완전히 코틀린으로 이뤄진 프로젝트라 하더라도 JDK나 안드로이드 프레임워크, 서드파티 프레임워크 등의 자바 라이브러리를 기반으로 만들어진다.
- 기존 자바 프로젝트를 통합하는 경우, 코틀린으로 변환할 수 없거나 미쳐 변환못한 자바코드도 처리 할 수 있어야한다.
- 기존 자바 API를 재작성하지 않고도 코틀린의 확장함수를 이용하면 가능하다.
- 확장함수란?
- 어떤 클래스의 멤버 메소드인 것처럼 호출 할 수 있지만, 사실 그 클래스의 밖에 선언된 함수이다.
- strings 패키지에 마지막 문자를 돌려주는 메소드를 추가해보자.
package strings fun String.lastChar(): Char = this.get(this.length - 1)
- 함수 이름앞에 그 함수가 확장할 클래스의 이름을 덧붙인다.
- 클래스 이름을 수신 객체 타입(receiver type)이라 하고, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신객체(receiver object)라고 한다.
- 어떤 클래스의 멤버 메소드인 것처럼 호출 할 수 있지만, 사실 그 클래스의 밖에 선언된 함수이다.
- println("Kotlin".lastChar()) //수신 객체 타입은 String, 수신객체는 "Kotlin"이다.
- String이 자바나 코틀린 등의 언어 중 어떤것으로 작성된건 중요하지않다. 그루비와 같은 JVM 언어로 작성된 클래스도 확장 가능하다.
- 자바 클래스로 컴파일한 클래스 파일이 있는 한, 그 클래스에 원하는대로 확장을 추가할 수 있다.
- this 생략가능 fun String.lastChar(): Char = get( length - 1 )
- 확장 함수 내부에서는 수신객체의 메소드나 프로퍼티를 가져올 수 있다. (일반적인 인스턴스 메소드와 비슷하게)
- 캡슐화는 하지 않는다.
- 즉, private , protecte를 사용할 수 없다.
- 이제부터는 클래스의 멤버메소드, 확장함수 모두를 메소드라 부를것이다.
- 함수를 호출하는 쪽에서는 멤버 메소드, 확장 함수를 구별할 수 없다. 또한 중요하지도 않음.
- 확장함수를 사용하기 위해서는 클래스나 함수와 마찬가지로 import해야한다.
- 코틀린에서는 클래스를 임포트할 때와 동일한 구문을 사용해서 함수도 임포트 할 수 있다.
- 코틀린 문법상, 확장함수는 반드시 짧은 이름을 써야한다.
- 한 파일안에서, 여러 다른 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야하는 경우 이름을 바꿔서 import하면 충돌을 막을 수 있다.
//기본 import strings.lastChar val c = "Kotlin".lastChar() //*를 이용하기 import strings.* val c = "Kotlin".lastChar() //as를 이용해서 임포트한 클래스나 함수이름을 다른 이름으로 부르기 import strings.lastChar as last val c = "Kotlin".last()
- 내부적으로 확장함수는 수신객체를 첫번째 인자로 받는 정적 메소드이다.
- 그래서 확장함수를 호출해도 다른 아탑터 객체나 실행시점 부가비용이 들지 않는다.
- 이런 설계로 인해 자바에서 확장함수를 사용하기도 편하다.
- 다른 최상위 함수와 마찬가지로, 확장 함수가 들어있는 자바클래스 이름도 확장함수가 들어있는 파일 이름에 따라 결정된다.
/* 자바 */ char c = StringUtilKt.lastChar("Java");
- 확장함수는 단지 정적 메소드 호출에 대한 문법적인 편의일 뿐이다.
- 구체적인 타입을 수신 객체 타입으로 지정할 수 있다. T타입을 String으로 바꿔보자.
//문자열 컬렉션에 대해서만 호출 가능하다. //Int 컬렉션에서는 호출불가 fun Collection<String>.join( separator: String = " ", prefix: String = "", postfix: String = "" ) = jointoString(separator, prefix, postfix) println(listOf("a","b","c").join(" ")) //출력값 a b c
- 확장 함수가 정적 메소드와 같은 특징을 가지므로, 오버라이드 불가하다.
- 우선 오버라이드 예제를 살펴보자.
open class View { open fun click() = println("View Clicked!") } class Button : View() { override fun click() { println("Button Clicked!") } } fun main() { Button().click() //출력값 //Button Clicked! }
- 그러나 확장함수는 클래스의 일부가 아니다. 확장함수는 클래스 밖에서 선언된다.
- 실제 확장 함수를 호출 할 때, 수신객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될 지 결정된다. (View)
- 밑의 코드를 살펴보자.
- Button에 확장함수를 만들자. fun Button.showOff() = print("I'm Button!!")
- view: View = Button() 객체를 만들자.
- Button() 으로 만들었지만 실제로 출력되는 함수는 View.showOff() “I'm View!!” 가 출력된다.
- 그 변수에 저장된 객체의 동적인 타입에 의해 확장함수가 결정되지 않는다.
- 이미 확장함수는 정적으로 결정이 되어있는 상태이다. (val view: View)
- view가 가리키는 객체의 실제 타입은 Button 이지만, view의 타입이 View이기 때문에 무조건 View의 확장함수가 호출된다.
fun main() { Button().click() view.showOff() //I'm View!! } fun View.showOff() = print("I'm View!!") fun Button.showOff() = print("I'm Button!!") val view: View = Button()
- 밑의 코드를 살펴보자.
- 실제 확장 함수를 호출 할 때, 수신객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될 지 결정된다. (View)
- 어떤 클래스를 확장한 함수와 그 클래스의 멤버함수의 이름과 시그니처가 같다면, 멤버함수가 호출된다. (멤버함수 우선순위가 더 높다.)
- 어떤 클래스를 통해 확장함수를 사용하고 있다고 가정하자.
- 그 확장함수와 이름과 시그니처가 같은 함수를 클래스 내부에 추가하면 확장함수가 아닌 새로 추가된 멤버함수를 사용하게 된다.
- 확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가 할 수 있다.
- 프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기때문에 (기존 클래스의 인스턴스 객체에 필드를 추가할 방법은 없다.), 실제로 확장 프로퍼티는 아무상태도 가질 수 없다. 그러나 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어 편한 경우가 많다.
- 확장함수와 마찬가지로 확장 프로퍼티도 일반 프로퍼티와 같은데, 단지 수신객체 클래스가 추가되었을뿐이다.
- 뒷받침하는 필드가 없어서 기본 게터구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야한다.
- 마찬가지로 초기화코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.
- class Extension { val String.lastChar: Char get() = get(length - 1) }
- StringBuilder에 프로퍼티를 정의해보자
- var StringBuilder.lastChar: Char get() = get(length - 1) set(value: Char) { this.setCharAt(length - 1, value) } fun main() { println("Kotlin".lastChar()) //n val sb = StringBuilder("Kotlin?") sb.lastChar = '!' print(sb) //Kotiln! }
4. 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원
- varagr 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.
- 중위(infix)함수 호출 구문을 사용하면 인자가 하나뿐인 메소드를 간편하게 호출 할 수 있다.
- 구조 분해 선언(destructuring declaration)을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.
- 코틀린 컬렉션은 자바와 같은 클래스를 사용하지만 더 확장된 API를 제공하고 있다.
- 리스트 마지막 원소를 가져오는 기능(last()), 컬렉션의 최댓값을 찾는 기능(max()) 등… 어떻게 자바 라이브러리 클래스의 인스턴스인 컬렉션에 대해 코틀린이 새로운 기능을 추가할 수 있었을까?
- last와 max는 확장함수이다!!
- last 함수는 List<T> 클래스의 확장함수이다.
- max는 Collection<Int>의 확장함수이다.
- last와 max는 확장함수이다!!
- IDE의 자동완성 기능을 통해 코틀린 표준 라이브러리를 볼 수 있다.
- 코틀린 표준 라이브러리는 수많은 확장함수를 포함하고있다.
- 컬렉션을 만들어내는 함수는 인자의 개수가 그때그때 달라 질 수 있다.
- listOf 함수의 인자는 가변길이인자 varagr로 정의 되어있다.
val list = listOf("1","2","3","4") //리스트를 생성하는 함수를 호출 할 때, 원하는 만큼 많이 원소를 전달 할 수 있다. ... fun listOf<T>(varagr values: T): List<T> {...}
- 가변 길이 인자는 메소드를 호출 할 때, 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다.
- 자바에서는 타입뒤에 … 로, 코틀린에서는 파라미터 앞에 varagr 변경자를 붙인다.
- 이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다.
- 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야한다.
- 코틀린 : 기술적으로는 스프레드 연산자가 그런 작업을 해준다. 하지만 실제로 전달하려는 배열 앞에 *을 붙이기만 하면 된다.
fun subFunction(args: Array<String>) { var a = listOf("1") val list = listOf("args : ${a}", *args) //*모든 인자를 출력해준다. println(list) } fun main() { println(subFunction(arrayOf("s","f","d"))) //출력값 [args : [1], s, f, d] kotlin.Unit }
- 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야한다.
- 여기서 to는 코틀린 키워드가 아닌, **중위 호출(infix call)**이라는 특별한 방식으로 to라는 일반 메소드를 호출 한 것이다.
- 중위 호출 시에는 수신객체와 유일한 메소드 인자사이에 메소드 이름을 넣는다.
- 1.to("one") //to 메소드를 일반적인 방식으로 호출함 1 to "one" // to 메소드를 중위 호출 방식으로 호출함 //두 호출은 동일하다.
- 인자가 하나뿐인 일반 메소드, 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다.
- 함수를 중위호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야한다.
- 이 to 함수는 Pair의 인스턴스를 반환한다.
- Pair는 말 그대로 두 원소로 이뤄진 순서쌍을 표현한다. Pair의 내용으로 두 변수를 즉시 초기화할 수 있다.
- val (number, name) = 1 to "one"
- 이런 기능을 **구조 분해 선언(destructuring declaration)**이라고 부른다.
- Pair 인스턴스 외 다른 객체에도 구조분해를 적용할 수 있다.
- 예를 들어, key와 value라는 두 변수를 맵의 원소를 사용해 초기화 할 수있다.
- 루프에서도 구조분해선언을 활용 할 수 있다.
- joinToString에서 본 WidthIndex를 구조분해선언과 조합하면 컬렉션 원소의 인덱스와 값을 따로 변수에 담을 수 있다.
- for ((index, element) in this.withIndex()) { if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) }
- 이 to 함수는 Pair의 인스턴스를 반환한다.
- infix fun Any.to(other: Any) = Pair(this,other)
- to 함수는 확장 함수이다. to를 사용하면 타입과 관계없이 임의의 순서쌍을 만들 수 있다.
- to의 수신객체가 제네릭하다는 뜻이다.
- mapOf 함수를 보자. 인자가 여러개가 들어갈 수 있고, 순서쌍만 가능하다.
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
5. 문자열과 정규식 다루기
- 코틀린 문자열은 자바 문자열과 같다.
- 자바와 코틀린 API의 차이를 알아보자.
- 자바 개발자라면 String의 split메소드를 잘 알것이다.
- 자바에서는 *println*("12.345-6.a".*split*(".")) 의 결과는 [12, 345-6, a]가 아니라 빈 배열이다.
- 왜냐면, split의 구분 문자열은 실제로는 정규식(regular expression)이기 때문이다
- 마침표[.]는 모든 문자를 나타내는 정규식으로 해석된다.
- 자바에서는 *println*("12.345-6.a".*split*(".")) 의 결과는 [12, 345-6, a]가 아니라 빈 배열이다.
- 코틀린에서는 자바의 split 대신에 여러가지 다른 조합의 파라미터를 받는 split 확장함수를 제공함으로써 혼동을 야기하는 메소드를 감춘다.
- 정규식을 파라미터로 받는 함수는 String이 아닌, Regex 타입의 값을 받는다.
- 따라서 코틀린에서는 split함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느것으로 문자열을 분리하는지 쉽게 알 수 있다.
println("12.345-6.a".split("\\\\.|-".toRegex())) //출력값 [12, 345, 6, a]
- split 확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.
- 이렇게 여러 문자를 받을 수 있는 코틀린 확장 함수는 자바에 있는 단 하나의 문자만 받을 수 있는 메소드를 대신한다.
- println("12.345-6.a".split(".","-")) //출력값 [12, 345, 6, a]
- 다른 예로 두 가지 다른 구현을 만들어보자.
- String을 확장한 함수를 사용
- 정규식 사용하기
- 파일의 전체 경로명을 디렉토리, 파일 이름, 확장자로 구분하는 것이다.
- 코틀린 표준 라이브러리에는 어떤 문자열에서 구분 문자열이 맨 나중(또는 처음)에 나타난 곳 뒤(또는 앞)의 부분 문자열을 반환하는 함수가 있다. 이런 함수를 사용해 경로 파싱을 구현한 버전은 다음과 같다.
fun main(){ parsePath("“/Users/yole/kotlin-book/chapter.adoc”") } //출력값 directory: “/Users/yole/kotlin-book fullName: chapter.adoc” Dir: “/Users/yole/kotlin-book, name: chapter, ext: adoc” fun parsePath(path: String) { val directory = path.substringBeforeLast("/") val fullName = path.substringAfterLast("/") val fileName = fullName.substringBeforeLast(".") val extension = fullName.substringAfterLast(".") println("directory: $directory") println("fullName: $fullName") println("Dir: $directory, name: $fileName, ext: $extension") }
- 코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다. 정규식은 강력하지만 나중에 알아보기 힘든 경우가 많다.
- 정규식이 필요할 때는 코틀린 라이브러리를 사용하면 더 편하다.
- 다음은 같은 작업을 정규식을 활용해 구현한 프로그램이다.
- fun parsePath2(path: String) { val regex = """(.+)/(.+)\\.(.+)""".toRegex() val matchResult = regex.matchEntire(path) if (matchResult != null) { val (directory, filename, extension) = matchResult.destructured println("Dir: $directory, name: $filename, ext: $extension") } }
- 3중 따옴표 문자열을 문자열 이스케이프를 피하기 위해서만 사용하지 않는다. 줄바꿈표현도 가능하다.
- 여러 줄 문자열(3중 따옴표 문자열)에는 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다.
- 보기 더 좋게하려면 들여쓰기를 하되 들여쓰기기의 끝부분을 특별한 문자열로 표시하고, trimMargin을 사용해 그 문자열과 그 직전의 공백을 제거한다.
- 줄 바꿈을 \n 과 같은 특수문자를 사용해 넣을 수 없다.
- \을 넣고싶으면 굳이 이스케이프할 필요가 없다.
- 프로그래밍 시 여러줄 문자열이 요긴한 분야로 테스트를 꼽을 수 있다. “”” “”” 안에 HTML이나 텍스트를 넣으면 된다.
- 여러 줄 문자열(3중 따옴표 문자열)에는 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다.
- fun main() { println(kotlinLogo.trimMargin(".")) } //출력값 | // | // | /\\ val kotlinLogo = """| // .| // .| /\\"""
- 많은 개발자들이 좋은 코드의 중요한 특징 중 하나가 중복이 없는것이라 믿는다.
- DRY : Don’t Repeat Yourself!
- 그러나 자바 코드를 작성 할 때는 DRY원칙을 피하기는 쉽지 않다.
- 많은 경우, 메소드 추출(ExtractMethod) 리팩토링을 적용해서 긴 메소드를 부분부분 나눠서 각 부분을 재활용 할 수 있다.
- 그러나 이렇게 되면 클래스 안에 작은 메소드가 많아지고 긱 메소드 사이의 관계를 파악하기 힘들어서 코드를 이해하기 더 어려울 수 있다.
- 리팩토링을 진행해서 추출한 메소드를 별도의 내부 클래스안에 넣으면 코드를 깔끔하게 조직할 수는 있지만, 그에 따른 불필요한 준비 코드가 늘어난다.
- 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩 시킬 수 있다.
- 이러면 문법적인 부가비용을 들이지 않고도 깔끔하게 코드를 조직할 수 있다.
- 흔히 발생하는 중복코드를 로컬함수(local)를 통해 어떻게 제거하는지 살펴보자.
- 중복 코드는 많지 않지만, 필드를 검증 할 때 여러 경우를 하나씩 처리하는건 비효율적이다. 로컬함수로 바꿔보자.
fun saveUser(user: User) { fun validate(user: User, value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id} : empty $fieldName" ) } } validate(user, user.name, "Name") validate(user, user.address, "Address") }
- User 객체를 로컬 함수에게 하나하나 전달해야한다는 것은 아쉽다.
- 로컬함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다. 불필요한 User 파라미터를 없애보자.
fun saveUser(user: User) { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id}" + "empty $fieldName" ) } } validate(user.name, "Name") validate(user.address, "Address") }
- 더 개선하고 싶으면 검증 로직을 User 클래스를 확장한 함수로 만들 수도 있다.
fun saveUser2(user: User) { user.validateBeforeSave() //유저를 데이터베이스에 저장한다. } fun User.validateBeforeSave() { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${id}" + "empty $fieldName" ) } } validate(name, "Name") validate(address, "Address") }
- 중첩이 깊어지면 코드 읽기가 어려워지므로 일반적으로는 한 단계만 함수를 중첩시키라고 권장한다.</aside>(1) 코틀린의 컬렉션
- 코틀린 자체 컬렉션은 없다. 기존 자바 컬렉션을 사용한다.
- 자체 컬렉션을 제공하지 않는 이유는 자바와의 호환성때문에
fun main() { val set = hashSetOf(1, 2, 3) val list = arrayListOf(1, 2, 3) val map = hashMapOf(1 to "one", 7 to "seven", 10 to "ten") println(set.javaClass) println(list.javaClass) println(map.javaClass) } //출력값 //class java.util.HashSet //class java.util.ArrayList //class java.util.HashMap
2. 함수를 호출하기 쉽게 만들기 : joinToString
val list = listOf(1, 2, 3) println(list) //출력값 [1, 2, 3]
- 자바 컬렉션에는 toString() 가 디폴트로 구현되어있다.
- 출력 형식은 이렇게 고정이다. [1, 2, 3]
- 출력형식을 우리에게 맞게끔 커스텀하고싶다면?
- 구아바나 아파치커먼즈 같은 서드파티 프로젝트를 추가하거나 직접 관련 로직을 구현해야한다.
- 코틀린에는 이미 이런 요구사항을 들어주는 함수가 표준 라이브러리에 들어있다.
- 이 함수는 제네릭하다. 즉 이 함수는 어떤 타입의 값을 원소로하는 컬렉션이든 처리할 수 있다.
- 함수에 전달하는 인자에 이름을 명시적으로 붙일 수 있다.
- 인자를 다 붙이거나 안붙이거나 (인자를 붙였으면 나머지 인자에도 이름을 꼭 명시해야한다.)
- 함수의 파라미터 이름을 변경 할때에는 에디터에서 직접 변경하는게아니라 refactor를 이용해야한다.
- 자바로 작성한 코드를 호출 할 때는 이름 붙인 인자를 사용할 수 없다. 자바 8 이후 추가된 선택사항
- 그러나 코틀린은 JDK 6과 호환되서 비교불가함.
- 자바에서는 일부클래스에서 오버로딩한 메소드가 너무 많아진다는 문제가 있다.
- 이런식으로 오버로딩 메소드들은 하위호환성을 유지하거나 API 사용자에게 편의를 더하는 등 이유로 만들어진다.
- 단점 : 반복되고 모든 오버로딩 함수에 대해 주석이 필요할 수 도 있다.
Constructors Constructor and Description Thread() Allocates a new Thread object. Thread(Runnable target) Allocates a new Thread object. Thread(Runnable target, String name) Allocates a new Thread object. Thread(String name) Allocates a new Thread object. Thread(ThreadGroup group, Runnable target) Allocates a new Thread object. Thread(ThreadGroup group, Runnable target, String name) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group. Thread(ThreadGroup group, Runnable target, String name, long stackSize) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size. Thread(ThreadGroup group, String name) Allocates a new Thread object.
- 디폴트 파라미터를 설정하여 오버로드 상당수를 피할 수 있다.
fun main() { val list = listOf(1, 2, 3) println(jointoString(list, "; ", "(", ")")) println(jointoString(list, prefix = "; ", separator = "(", postfix = ")")) //이름 붙인 인자면 순서 변경해도 상관없다. println(jointoString(list)) //다 생략 println(jointoString(list, "; ")) //separator만 작성 } //출력값 (1; 2; 3) ; 1(2(3) 1, 2, 3 1; 2; 3
- fun <T> jointoString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String ="" ): String {
- 디폴드값과 자바
- 자바에는 디폴트 파라미터 값이라는 개념이 없다. 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터를 제공하더라도 모든 인자를 명시해야한다.
- 자바에서 코틀린 함수를 자주 호출하는 경우 @JvmOverloads 를 추가하자.
- 추가하면 코틀린 컴파일러가 맨 마지막 파라미터부터 하나씩 생략한 오버로딩한 자바 메소드를 추가한다.
- public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit, CharSequence truncated, Function1<? super T, ? extends CharSequence> transform ) { // 함수 구현 } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit, CharSequence truncated ) { return joinToString($this$joinToString, separator, prefix, postfix, limit, truncated, (Function1)null, 64, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int limit ) { return joinToString($this$joinToString, separator, prefix, postfix, limit, (CharSequence)null, (Function1)null, 48, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix ) { return joinToString($this$joinToString, separator, prefix, postfix, -1, (CharSequence)null, (Function1)null, 48, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString, CharSequence separator ) { return joinToString($this$joinToString, separator, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 124, (Object)null); } public static <T> String joinToString( Collection<T> $this$joinToString ) { return joinToString($this$joinToString, (CharSequence)null, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 124, (Object)null); }
- 객체지향언어 자바에서는 모든 코드를 클래스의 메소드로 작성해야한다.
- 하지만, 실전에서는 어느 클래스에 포함시키기 어려운 코드도 많다.
- 일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있다.
- 중요한 객체는 하나뿐이지만, 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지 않은 경우도 있다.
- 이 결과, 다양한 정적 메소드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메소드는 없는 클래스가 생겨난다. (ex JDK Colledtions)
- Util
- 코틀린에서는 이런 무의미한 클래스가 필요없다.
- 함수를 소스파일의 최상위 수준, 다른 클래스 밖에 위치시키면 된다.
- 이런 함수들은 그 파일의 맨앞에 정의된 패키지의 멤버함수이다. 다른 패키지에서 사용하고 싶다면 import하면 된다.
- 추가로 유틸리티 클래스 이름이 들어갈 필요는 없다.
- 이런 함수들은 그 파일의 맨앞에 정의된 패키지의 멤버함수이다. 다른 패키지에서 사용하고 싶다면 import하면 된다.
- 함수를 소스파일의 최상위 수준, 다른 클래스 밖에 위치시키면 된다.
- 내가 만든 joinToString() 함수를 join.kt를 만들어서 package strings에 넣어보자.
- 다른 파일에서 내가 만든 joinToString() 함수를 사용할 수 있다.
- 실행가능한 이유
- JVM은 클래스안에 들어있는 코드만 실행 할 수 있다. 때문에, 컴파일러는 다른 실행파일을 컴파일할때 새로운 클래스를 정의해준다.
- package strings fun <T> jointoString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String ="" ): String { val result = StringBuilder(prefix) for ((index, element) in collection.withIndex()) { //println("result start: ${result}") if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) //println("result end: ${result}") } result.append(postfix) return result.toString() }
- 어떻게 코틀린이 join.kt를 컴파일하는가?
- join.kt를 컴파일한 결과를 자바 코드로 살펴보자
- package strings; public class JoinKt { //내가 작성한 joinToString() }
- 코틀린 컴파일러가 생성하는 클래스의 이름은 최상위 함수가 틀어있던 코틀린 소스파일의 이름과 대응한다.
- 코틀린 파일의 최상위 함수는 이 클래스의 정적인 메소드가 된다.
- //호출 import strings.JoinKt; ... JoinKt.jointToString(list,", ","","");
- 코틀린 최상위함수가 포함된 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 어노테이션을 붙여라.
- //join.kt에 어노테이션을 추가하자. @file:JvmName("StringFunctions") //클래스 이름을 지정하는 어노테이션 package strings fun joinToString(...) { ... } /* 자바 */ import strings.StringFunctions; StringFunctions.joinToString(list,", ","","");
- 최상위 프로퍼티
- 함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. (흔하진않음)
3. 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
- 완전히 코틀린으로 이뤄진 프로젝트라 하더라도 JDK나 안드로이드 프레임워크, 서드파티 프레임워크 등의 자바 라이브러리를 기반으로 만들어진다.
- 기존 자바 프로젝트를 통합하는 경우, 코틀린으로 변환할 수 없거나 미쳐 변환못한 자바코드도 처리 할 수 있어야한다.
- 기존 자바 API를 재작성하지 않고도 코틀린의 확장함수를 이용하면 가능하다.
- 확장함수란?
- 어떤 클래스의 멤버 메소드인 것처럼 호출 할 수 있지만, 사실 그 클래스의 밖에 선언된 함수이다.
- strings 패키지에 마지막 문자를 돌려주는 메소드를 추가해보자.
package strings fun String.lastChar(): Char = this.get(this.length - 1)
- 함수 이름앞에 그 함수가 확장할 클래스의 이름을 덧붙인다.
- 클래스 이름을 수신 객체 타입(receiver type)이라 하고, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신객체(receiver object)라고 한다.
- 어떤 클래스의 멤버 메소드인 것처럼 호출 할 수 있지만, 사실 그 클래스의 밖에 선언된 함수이다.
- println("Kotlin".lastChar()) //수신 객체 타입은 String, 수신객체는 "Kotlin"이다.
- String이 자바나 코틀린 등의 언어 중 어떤것으로 작성된건 중요하지않다. 그루비와 같은 JVM 언어로 작성된 클래스도 확장 가능하다.
- 자바 클래스로 컴파일한 클래스 파일이 있는 한, 그 클래스에 원하는대로 확장을 추가할 수 있다.
- this 생략가능 fun String.lastChar(): Char = get( length - 1 )
- 확장 함수 내부에서는 수신객체의 메소드나 프로퍼티를 가져올 수 있다. (일반적인 인스턴스 메소드와 비슷하게)
- 캡슐화는 하지 않는다.
- 즉, private , protecte를 사용할 수 없다.
- 이제부터는 클래스의 멤버메소드, 확장함수 모두를 메소드라 부를것이다.
- 함수를 호출하는 쪽에서는 멤버 메소드, 확장 함수를 구별할 수 없다. 또한 중요하지도 않음.
- 확장함수를 사용하기 위해서는 클래스나 함수와 마찬가지로 import해야한다.
- 코틀린에서는 클래스를 임포트할 때와 동일한 구문을 사용해서 함수도 임포트 할 수 있다.
- 코틀린 문법상, 확장함수는 반드시 짧은 이름을 써야한다.
- 한 파일안에서, 여러 다른 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야하는 경우 이름을 바꿔서 import하면 충돌을 막을 수 있다.
//기본 import strings.lastChar val c = "Kotlin".lastChar() //*를 이용하기 import strings.* val c = "Kotlin".lastChar() //as를 이용해서 임포트한 클래스나 함수이름을 다른 이름으로 부르기 import strings.lastChar as last val c = "Kotlin".last()
- 내부적으로 확장함수는 수신객체를 첫번째 인자로 받는 정적 메소드이다.
- 그래서 확장함수를 호출해도 다른 아탑터 객체나 실행시점 부가비용이 들지 않는다.
- 이런 설계로 인해 자바에서 확장함수를 사용하기도 편하다.
- 다른 최상위 함수와 마찬가지로, 확장 함수가 들어있는 자바클래스 이름도 확장함수가 들어있는 파일 이름에 따라 결정된다.
/* 자바 */ char c = StringUtilKt.lastChar("Java");
- 확장함수는 단지 정적 메소드 호출에 대한 문법적인 편의일 뿐이다.
- 구체적인 타입을 수신 객체 타입으로 지정할 수 있다. T타입을 String으로 바꿔보자.
//문자열 컬렉션에 대해서만 호출 가능하다. //Int 컬렉션에서는 호출불가 fun Collection<String>.join( separator: String = " ", prefix: String = "", postfix: String = "" ) = jointoString(separator, prefix, postfix) println(listOf("a","b","c").join(" ")) //출력값 a b c
- 확장 함수가 정적 메소드와 같은 특징을 가지므로, 오버라이드 불가하다.
- 우선 오버라이드 예제를 살펴보자.
open class View { open fun click() = println("View Clicked!") } class Button : View() { override fun click() { println("Button Clicked!") } } fun main() { Button().click() //출력값 //Button Clicked! }
- 그러나 확장함수는 클래스의 일부가 아니다. 확장함수는 클래스 밖에서 선언된다.
- 실제 확장 함수를 호출 할 때, 수신객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될 지 결정된다. (View)
- 밑의 코드를 살펴보자.
- Button에 확장함수를 만들자. fun Button.showOff() = print("I'm Button!!")
- view: View = Button() 객체를 만들자.
- Button() 으로 만들었지만 실제로 출력되는 함수는 View.showOff() “I'm View!!” 가 출력된다.
- 그 변수에 저장된 객체의 동적인 타입에 의해 확장함수가 결정되지 않는다.
- 이미 확장함수는 정적으로 결정이 되어있는 상태이다. (val view: View)
- view가 가리키는 객체의 실제 타입은 Button 이지만, view의 타입이 View이기 때문에 무조건 View의 확장함수가 호출된다.
fun main() { Button().click() view.showOff() //I'm View!! } fun View.showOff() = print("I'm View!!") fun Button.showOff() = print("I'm Button!!") val view: View = Button()
- 밑의 코드를 살펴보자.
- 실제 확장 함수를 호출 할 때, 수신객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될 지 결정된다. (View)
- 어떤 클래스를 확장한 함수와 그 클래스의 멤버함수의 이름과 시그니처가 같다면, 멤버함수가 호출된다. (멤버함수 우선순위가 더 높다.)
- 어떤 클래스를 통해 확장함수를 사용하고 있다고 가정하자.
- 그 확장함수와 이름과 시그니처가 같은 함수를 클래스 내부에 추가하면 확장함수가 아닌 새로 추가된 멤버함수를 사용하게 된다.
- 확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가 할 수 있다.
- 프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기때문에 (기존 클래스의 인스턴스 객체에 필드를 추가할 방법은 없다.), 실제로 확장 프로퍼티는 아무상태도 가질 수 없다. 그러나 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어 편한 경우가 많다.
- 확장함수와 마찬가지로 확장 프로퍼티도 일반 프로퍼티와 같은데, 단지 수신객체 클래스가 추가되었을뿐이다.
- 뒷받침하는 필드가 없어서 기본 게터구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야한다.
- 마찬가지로 초기화코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.
- class Extension { val String.lastChar: Char get() = get(length - 1) }
- StringBuilder에 프로퍼티를 정의해보자
- var StringBuilder.lastChar: Char get() = get(length - 1) set(value: Char) { this.setCharAt(length - 1, value) } fun main() { println("Kotlin".lastChar()) //n val sb = StringBuilder("Kotlin?") sb.lastChar = '!' print(sb) //Kotiln! }
4. 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원
- varagr 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.
- 중위(infix)함수 호출 구문을 사용하면 인자가 하나뿐인 메소드를 간편하게 호출 할 수 있다.
- 구조 분해 선언(destructuring declaration)을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.
- 코틀린 컬렉션은 자바와 같은 클래스를 사용하지만 더 확장된 API를 제공하고 있다.
- 리스트 마지막 원소를 가져오는 기능(last()), 컬렉션의 최댓값을 찾는 기능(max()) 등… 어떻게 자바 라이브러리 클래스의 인스턴스인 컬렉션에 대해 코틀린이 새로운 기능을 추가할 수 있었을까?
- last와 max는 확장함수이다!!
- last 함수는 List<T> 클래스의 확장함수이다.
- max는 Collection<Int>의 확장함수이다.
- last와 max는 확장함수이다!!
- IDE의 자동완성 기능을 통해 코틀린 표준 라이브러리를 볼 수 있다.
- 코틀린 표준 라이브러리는 수많은 확장함수를 포함하고있다.
- 컬렉션을 만들어내는 함수는 인자의 개수가 그때그때 달라 질 수 있다.
- listOf 함수의 인자는 가변길이인자 varagr로 정의 되어있다.
val list = listOf("1","2","3","4") //리스트를 생성하는 함수를 호출 할 때, 원하는 만큼 많이 원소를 전달 할 수 있다. ... fun listOf<T>(varagr values: T): List<T> {...}
- 가변 길이 인자는 메소드를 호출 할 때, 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다.
- 자바에서는 타입뒤에 … 로, 코틀린에서는 파라미터 앞에 varagr 변경자를 붙인다.
- 이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다.
- 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야한다.
- 코틀린 : 기술적으로는 스프레드 연산자가 그런 작업을 해준다. 하지만 실제로 전달하려는 배열 앞에 *을 붙이기만 하면 된다.
fun subFunction(args: Array<String>) { var a = listOf("1") val list = listOf("args : ${a}", *args) //*모든 인자를 출력해준다. println(list) } fun main() { println(subFunction(arrayOf("s","f","d"))) //출력값 [args : [1], s, f, d] kotlin.Unit }
- 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야한다.
- 여기서 to는 코틀린 키워드가 아닌, **중위 호출(infix call)**이라는 특별한 방식으로 to라는 일반 메소드를 호출 한 것이다.
- 중위 호출 시에는 수신객체와 유일한 메소드 인자사이에 메소드 이름을 넣는다.
- 1.to("one") //to 메소드를 일반적인 방식으로 호출함 1 to "one" // to 메소드를 중위 호출 방식으로 호출함 //두 호출은 동일하다.
- 인자가 하나뿐인 일반 메소드, 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다.
- 함수를 중위호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야한다.
- 이 to 함수는 Pair의 인스턴스를 반환한다.
- Pair는 말 그대로 두 원소로 이뤄진 순서쌍을 표현한다. Pair의 내용으로 두 변수를 즉시 초기화할 수 있다.
- val (number, name) = 1 to "one"
- 이런 기능을 **구조 분해 선언(destructuring declaration)**이라고 부른다.
- Pair 인스턴스 외 다른 객체에도 구조분해를 적용할 수 있다.
- 예를 들어, key와 value라는 두 변수를 맵의 원소를 사용해 초기화 할 수있다.
- 루프에서도 구조분해선언을 활용 할 수 있다.
- joinToString에서 본 WidthIndex를 구조분해선언과 조합하면 컬렉션 원소의 인덱스와 값을 따로 변수에 담을 수 있다.
- for ((index, element) in this.withIndex()) { if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) }
- 이 to 함수는 Pair의 인스턴스를 반환한다.
- infix fun Any.to(other: Any) = Pair(this,other)
- to 함수는 확장 함수이다. to를 사용하면 타입과 관계없이 임의의 순서쌍을 만들 수 있다.
- to의 수신객체가 제네릭하다는 뜻이다.
- mapOf 함수를 보자. 인자가 여러개가 들어갈 수 있고, 순서쌍만 가능하다.
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
5. 문자열과 정규식 다루기
- 코틀린 문자열은 자바 문자열과 같다.
- 자바와 코틀린 API의 차이를 알아보자.
- 자바 개발자라면 String의 split메소드를 잘 알것이다.
- 자바에서는 *println*("12.345-6.a".*split*(".")) 의 결과는 [12, 345-6, a]가 아니라 빈 배열이다.
- 왜냐면, split의 구분 문자열은 실제로는 정규식(regular expression)이기 때문이다
- 마침표[.]는 모든 문자를 나타내는 정규식으로 해석된다.
- 자바에서는 *println*("12.345-6.a".*split*(".")) 의 결과는 [12, 345-6, a]가 아니라 빈 배열이다.
- 코틀린에서는 자바의 split 대신에 여러가지 다른 조합의 파라미터를 받는 split 확장함수를 제공함으로써 혼동을 야기하는 메소드를 감춘다.
- 정규식을 파라미터로 받는 함수는 String이 아닌, Regex 타입의 값을 받는다.
- 따라서 코틀린에서는 split함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느것으로 문자열을 분리하는지 쉽게 알 수 있다.
println("12.345-6.a".split("\\\\.|-".toRegex())) //출력값 [12, 345, 6, a]
- split 확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.
- 이렇게 여러 문자를 받을 수 있는 코틀린 확장 함수는 자바에 있는 단 하나의 문자만 받을 수 있는 메소드를 대신한다.
- println("12.345-6.a".split(".","-")) //출력값 [12, 345, 6, a]
- 다른 예로 두 가지 다른 구현을 만들어보자.
- String을 확장한 함수를 사용
- 정규식 사용하기
- 파일의 전체 경로명을 디렉토리, 파일 이름, 확장자로 구분하는 것이다.
- 코틀린 표준 라이브러리에는 어떤 문자열에서 구분 문자열이 맨 나중(또는 처음)에 나타난 곳 뒤(또는 앞)의 부분 문자열을 반환하는 함수가 있다. 이런 함수를 사용해 경로 파싱을 구현한 버전은 다음과 같다.
fun main(){ parsePath("“/Users/yole/kotlin-book/chapter.adoc”") } //출력값 directory: “/Users/yole/kotlin-book fullName: chapter.adoc” Dir: “/Users/yole/kotlin-book, name: chapter, ext: adoc” fun parsePath(path: String) { val directory = path.substringBeforeLast("/") val fullName = path.substringAfterLast("/") val fileName = fullName.substringBeforeLast(".") val extension = fullName.substringAfterLast(".") println("directory: $directory") println("fullName: $fullName") println("Dir: $directory, name: $fileName, ext: $extension") }
- 코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다. 정규식은 강력하지만 나중에 알아보기 힘든 경우가 많다.
- 정규식이 필요할 때는 코틀린 라이브러리를 사용하면 더 편하다.
- 다음은 같은 작업을 정규식을 활용해 구현한 프로그램이다.
- fun parsePath2(path: String) { val regex = """(.+)/(.+)\\.(.+)""".toRegex() val matchResult = regex.matchEntire(path) if (matchResult != null) { val (directory, filename, extension) = matchResult.destructured println("Dir: $directory, name: $filename, ext: $extension") } }
- 3중 따옴표 문자열을 문자열 이스케이프를 피하기 위해서만 사용하지 않는다. 줄바꿈표현도 가능하다.
- 여러 줄 문자열(3중 따옴표 문자열)에는 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다.
- 보기 더 좋게하려면 들여쓰기를 하되 들여쓰기기의 끝부분을 특별한 문자열로 표시하고, trimMargin을 사용해 그 문자열과 그 직전의 공백을 제거한다.
- 줄 바꿈을 \n 과 같은 특수문자를 사용해 넣을 수 없다.
- \을 넣고싶으면 굳이 이스케이프할 필요가 없다.
- 프로그래밍 시 여러줄 문자열이 요긴한 분야로 테스트를 꼽을 수 있다. “”” “”” 안에 HTML이나 텍스트를 넣으면 된다.
- 여러 줄 문자열(3중 따옴표 문자열)에는 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다.
- fun main() { println(kotlinLogo.trimMargin(".")) } //출력값 | // | // | /\\ val kotlinLogo = """| // .| // .| /\\"""
- 많은 개발자들이 좋은 코드의 중요한 특징 중 하나가 중복이 없는것이라 믿는다.
- DRY : Don’t Repeat Yourself!
- 그러나 자바 코드를 작성 할 때는 DRY원칙을 피하기는 쉽지 않다.
- 많은 경우, 메소드 추출(ExtractMethod) 리팩토링을 적용해서 긴 메소드를 부분부분 나눠서 각 부분을 재활용 할 수 있다.
- 그러나 이렇게 되면 클래스 안에 작은 메소드가 많아지고 긱 메소드 사이의 관계를 파악하기 힘들어서 코드를 이해하기 더 어려울 수 있다.
- 리팩토링을 진행해서 추출한 메소드를 별도의 내부 클래스안에 넣으면 코드를 깔끔하게 조직할 수는 있지만, 그에 따른 불필요한 준비 코드가 늘어난다.
- 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩 시킬 수 있다.
- 이러면 문법적인 부가비용을 들이지 않고도 깔끔하게 코드를 조직할 수 있다.
- 흔히 발생하는 중복코드를 로컬함수(local)를 통해 어떻게 제거하는지 살펴보자.
- 중복 코드는 많지 않지만, 필드를 검증 할 때 여러 경우를 하나씩 처리하는건 비효율적이다. 로컬함수로 바꿔보자.
fun saveUser(user: User) { fun validate(user: User, value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id} : empty $fieldName" ) } } validate(user, user.name, "Name") validate(user, user.address, "Address") }
- User 객체를 로컬 함수에게 하나하나 전달해야한다는 것은 아쉽다.
- 로컬함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다. 불필요한 User 파라미터를 없애보자.
fun saveUser(user: User) { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id}" + "empty $fieldName" ) } } validate(user.name, "Name") validate(user.address, "Address") }
- 더 개선하고 싶으면 검증 로직을 User 클래스를 확장한 함수로 만들 수도 있다.
fun saveUser2(user: User) { user.validateBeforeSave() //유저를 데이터베이스에 저장한다. } fun User.validateBeforeSave() { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${id}" + "empty $fieldName" ) } } validate(name, "Name") validate(address, "Address") }
- 중첩이 깊어지면 코드 읽기가 어려워지므로 일반적으로는 한 단계만 함수를 중첩시키라고 권장한다.
- class User(val id: Int, val name: String, val address: String) fun saveUser(user: User){ if (user.name.isEmpty()){ throw IllegalArgumentException( "Can't save user ${user.id}: empty Name" ) } if (user.address.isEmpty()){ throw IllegalArgumentException( "Can't save user ${user.id}: empty Address" ) } }
- 코틀린 자체 컬렉션은 없다. 기존 자바 컬렉션을 사용한다.
- val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
- fun <T> Collection<T>.jointoString( //Colletion에 대한 확장함수로 변경 separator: String = ", ", prefix: String = "", postfix: String = "" ): String { val result = StringBuilder(prefix) for ((index, element) in this.withIndex()) { //수신객체를 this로 나타낸다. 여기서 this는 T타입으로 이뤄진 컬렉션이다. //println("result start: ${result}") if (index > 0) result.append(separator) result.append(element) //println("result end: ${result}") } result.append(postfix) return result.toString() }
- jointoString(list, "; ", "(", ")") //가독성이 매우 떨어진다. jointoString(list, separator = " ", prefix = "(", postfix = ")") //인자의 이름을 명시할 수 있다.
- fun main() { val list = listOf(1, 2, 3) println(jointoString(list, "; ", "(", ")")) //출력값 result start: ( result end: (1 result start: (1 result end: (1; 2 result start: (1; 2 result end: (1; 2; 3 (1; 2; 3) } //joinToString 함수를 직접 구현해보자 fun <T> jointoString( collection: Collection<T>, separator: String, prefix: String, postfix: String ): String { val result = StringBuilder(prefix) for ((index, element) in collection.withIndex()) { println("result start: ${result}") if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) println("result end: ${result}") } result.append(postfix) return result.toString() }
- 1. 코틀린에서 컬렉션 만들기
- class User(val id: Int, val name: String, val address: String) fun saveUser(user: User){ if (user.name.isEmpty()){ throw IllegalArgumentException( "Can't save user ${user.id}: empty Name" ) } if (user.address.isEmpty()){ throw IllegalArgumentException( "Can't save user ${user.id}: empty Address" ) } }
- 코틀린 자체 컬렉션은 없다. 기존 자바 컬렉션을 사용한다.
- val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
- fun <T> Collection<T>.jointoString( //Colletion에 대한 확장함수로 변경 separator: String = ", ", prefix: String = "", postfix: String = "" ): String { val result = StringBuilder(prefix) for ((index, element) in this.withIndex()) { //수신객체를 this로 나타낸다. 여기서 this는 T타입으로 이뤄진 컬렉션이다. //println("result start: ${result}") if (index > 0) result.append(separator) result.append(element) //println("result end: ${result}") } result.append(postfix) return result.toString() }
- jointoString(list, "; ", "(", ")") //가독성이 매우 떨어진다. jointoString(list, separator = " ", prefix = "(", postfix = ")") //인자의 이름을 명시할 수 있다.
- fun main() { val list = listOf(1, 2, 3) println(jointoString(list, "; ", "(", ")")) //출력값 result start: ( result end: (1 result start: (1 result end: (1; 2 result start: (1; 2 result end: (1; 2; 3 (1; 2; 3) } //joinToString 함수를 직접 구현해보자 fun <T> jointoString( collection: Collection<T>, separator: String, prefix: String, postfix: String ): String { val result = StringBuilder(prefix) for ((index, element) in collection.withIndex()) { println("result start: ${result}") if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) println("result end: ${result}") } result.append(postfix) return result.toString() }
- 1. 코틀린에서 컬렉션 만들기
- class User(val id: Int, val name: String, val address: String) fun saveUser(user: User){ if (user.name.isEmpty()){ throw IllegalArgumentException( "Can't save user ${user.id}: empty Name" ) } if (user.address.isEmpty()){ throw IllegalArgumentException( "Can't save user ${user.id}: empty Address" ) } }
- 코틀린 자체 컬렉션은 없다. 기존 자바 컬렉션을 사용한다.
- val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
- fun <T> Collection<T>.jointoString( //Colletion에 대한 확장함수로 변경 separator: String = ", ", prefix: String = "", postfix: String = "" ): String { val result = StringBuilder(prefix) for ((index, element) in this.withIndex()) { //수신객체를 this로 나타낸다. 여기서 this는 T타입으로 이뤄진 컬렉션이다. //println("result start: ${result}") if (index > 0) result.append(separator) result.append(element) //println("result end: ${result}") } result.append(postfix) return result.toString() }
- jointoString(list, "; ", "(", ")") //가독성이 매우 떨어진다. jointoString(list, separator = " ", prefix = "(", postfix = ")") //인자의 이름을 명시할 수 있다.
- fun main() { val list = listOf(1, 2, 3) println(jointoString(list, "; ", "(", ")")) //출력값 result start: ( result end: (1 result start: (1 result end: (1; 2 result start: (1; 2 result end: (1; 2; 3 (1; 2; 3) } //joinToString 함수를 직접 구현해보자 fun <T> jointoString( collection: Collection<T>, separator: String, prefix: String, postfix: String ): String { val result = StringBuilder(prefix) for ((index, element) in collection.withIndex()) { println("result start: ${result}") if (index > 0) result.append(separator) //첫 원소앞에는 구분자를 붙이면 안된다 result.append(element) println("result end: ${result}") } result.append(postfix) return result.toString() }
- 1. 코틀린에서 컬렉션 만들기
- class User(val id: Int, val name: String, val address: String) fun saveUser(user: User){ if (user.name.isEmpty()){ throw IllegalArgumentException( "Can't save user ${user.id}: empty Name" ) } if (user.address.isEmpty()){ throw IllegalArgumentException( "Can't save user ${user.id}: empty Address" ) } }
728x90
반응형
'Kotlin in Action' 카테고리의 다른 글
4. kotlin 클래스, 객체, 인터페이스 (0) | 2024.07.13 |
---|---|
2. kotlin 기본요소, 스마트 캐스트, 예외처리 등 (0) | 2024.07.13 |
1. 코틀린이란? (0) | 2024.07.13 |