- 3주만에 다시 “kotlin in action”을 읽으려고 열었더니 어떨떨.. 이것부터 해보자.
- 이 내용에 대한 저작권은 심재철님에게 있으며 상업적 목적이 아니리면 누가나 공유하고 배포할 수 있습니다. 라고 써있음!! 무료다!! ㅋㅋ 아래는 링크!
- https://github.com/Jpub/AndroidStudio3
개요와 실습환경 구축
개요
-
2011년부터 오픈소스로 JetBrains에서 개발 시작, 2016년 1.0 정식 버전 발표. 현재도 진화중.
- 코틀린 특징
- JVM에서 실행되므로 자바와 완전히 호환, 크로스 플랫폼 지원
- 정적 타입 언어. type inference(타입 추론) 기능이 있음
- 객체지향 프로그래밍을 지원. extension 함수와 확장 속성을 사용할 수 있음.(objective-c의 그 기능..)
- 함수형 프로그래밍 지원.
- 문법과 코드가 간결
- NPE(NullPointerException) 예외가 생기지 않도록 언어 자체에서 배려하고 있어 자바보다 안전
- OSS이므로 무상 사용 가능
- 자바 코드를 kotlin코드로 변환해주는 변환기 제공(Intellij, Android Studio, Eclipse)
- kotlin compile
kotlin hello.kt -include-runtime -d hello.jar
- -include-runtime : 코틀린 런타임 라이브러리를 애플리케이션에 포함시키라는 의미, 다른 코드에서 라이브러리로 사용하기 위해 컴파일할 때는 필요 없음.
- 코틀린 컴파일러는 소스 파일 이름의 첫 자를 대문자로 바꾸고 그 뒤에 Kt를 붙인 .class파일을 생성한다.
- 기본 함수는 public static final로 생성된다.
- 클래스는 public final 로 생성된다. 즉 상속이 안된다. 상속이 가능하게 하려면 class 키워드 앞에 open 키워드를 추가해야 한다.
구성 요소와 문법
// 함수는 fun 으로 시작한다.
// 파라메터는 (파라메터명: 타입) 으로 쓴다
fun main(args: Array<String>) {
// ;은 사용하지 않는다.
// println은 System.out.println을 래핑한 함수이다.
println(makeMessage1(1))
println(makeMessage1(2))
println(makeMessage2(1))
println(makeMessage2(2))
}
// 리턴 타입은 : Type 형태로 쓴다
fun makeMessage1(msgType: Int) : String {
return if (msgType == 1) "안녕하세요?" else "또 만났군요!"
}
// 아래와 같이 함수 선언과 동시에 반환값을 생략하고 대입문에 정의할 수 있다.
fun makeMessage2(msgType: Int) = if (msgType == 1) "날씨 좋죠?" else "참 맑군요!"
B.3 코틀린 타입
B.3.1 기본타입
- 기본(primitive)타입은 클래스로 정의되어 있다.
- Byte(1), Short(2), Int(4), Long(8), Float(4), Double(8), Char(2). Boolean
val a = 100
// 자동으로 변환되지 않는다.
val b: Long = a.toLong()
// toByte(), toShort(), toInt(), toLong(), toFloat(), toDouble(), toChar()
B.3.2 문자열 타입
- String 사용, 자바와 동일
B.3.3 기본 타입의 리터럴
- Long : L
- Float : F,f
- Double : 실수는 기본적으로 double 타입으로 간주됨. 별도의 표기 없음
- 16진수 : 0x, 0X
- 문자 : ‘1’, ‘ㅁ’ 홀 따옴표 사용. ‘\t’ ‘\u009’
B.3.4 문자열 리터럴
- val s = “삶이 그대를 속일지라도\n슬퍼하거나 노하지 말라\n”
- 겹따옴표 사용
- 아래와 같이 형태로 줄바꿈을 나타낼 수도 있다.
val s = """
삶이 그대를 속일지라도
슬퍼하거나 노하지 말라
슬픈 날엔 참고 견디라
즐거운 날이 오고야 말리니
"""
// trimMargin() 여백 제거 문자를 전달하면 해당 문자까지 여백을 제거, default 문자는 "|"
val s = """
>삶이 그대를 속일지라도
>슬퍼하거나 노하지 말라
>슬픈 날엔 참고 견디라
>즐거운 날이 오고야 말리니
""".trimMargin(">")
- 정수, 실수 변환
-
// 정수, 실수 타입 변환 val c = "77".toInt() val d = "123.4".toDouble() // 문자열 템플릿을 만들어 사용 println("$c, $d")
- 문자열 템플릿
val count = 77
val s1 = "Count = $count"
// 배열과 표현식의 경우는 중괄호({})를 앞뒤로 붙인다.
val s2 = "$s1 의 길이는 ${s1.length}"
println(s1)
println(s2)
- 리터럴 값을 알아보기 쉽게 값의 중간에 _를 사용할 수 있다.
val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456
val socialSecurityNumber = 999_99_9999
val hexBytes = 0xff_fe_ad_de
val bytes = 0b00000000_11111111_10101010_01011100
println("$oneMillion, $creditCardNumber, $socialSecurityNumber, $hexBytes, $bytes")
B.3.5 배열
- 배열은 Array로 클래스로 정의되어 있음
- Array
과 같이 배열에 저장되는 요소의 타입을 제네릭 타입으로 나타냄 - [] 연산자는 배열을 선언할때는 사용하지 않음. 읽거나 쓸 때만 사용
// arrayOf()를 사용하여 배열 생성
val item = arrayOf("사과", "바나나", "키위")
for (fruit in item) {
println(fruit)
}
// Array클래스 생성자를 사용
val num = Array<String>(5, { i -> (i * i).toString() })
// 위와 동일한 코드
val num2 = Array<String>(5) { i -> (i * i).toString() }
// <String>은 생략 가능. 컴파일러가 추론할 수 있음
for (item in num) {
println(item)
}
// 기본 타입의 요소를 저장하는 별도의 클래스
// IntArray, ByteArray, ShortArray, LongArray, FloatArray, DoubleArray, CharArray, BooleanArray
// 위 클래스들은 코틀린 컴파일러가 컴파일할 때 자바의 기본 타입 배열로 변환한다.
val intItem: IntArray = intArrayOf(1, 2, 3, 4, 5)
val intItem2 = IntArray(5, { i -> (i * i) })
val intItem3 = IntArray(5) { i -> (i * i) }
// index를 사용하여 읽거나 쓸수 있다
intItem[0] = intItem[1] + intItem[2]
B.4 연산자와 연산자 오버로딩
B.4.1 산술 연산자
- +, -, *, /, % 연산자는 알고 있는 그것. 우선순위도 알고 있는 대로.(* / % »> + -)
- 단, 코틀린에서는 해당 연산자를 오버로딩한 함수를 사용해서 연산을 처리함
- plus, minus, times, div, rem/mod
- 오버로딩한 함수를 사용하므로 객체간 연산도 가능함
fun main(args: Array<String>) {
val m1 = Score(100, 200)
val m2 = Score(300, 400)
// 객체간 + 연산을 사용하면 plus 함수를 사용한다. plus함수를 오버로딩 하지 않으면 컴파일에러가 발생.
println(m1 + m2)
println(m1 * m2)
}
// data 키워드는 나중에 자세히 알아보자
data class Score(val a: Int, val b: Int) {
// 연산자를 오버로딩 하는 함수는 앞에 operator 키워드를 붙여준다.
operator fun plus(other: Score): Score {
return Score(a + other.a, b + other.b)
}
}
// 확장 함수를 사용하여 아래와 같이 정의할 수도 있다.
operator fun Score.times(other: Score): Score {
return Score(a * other.a, b * other.b)
}
B.4.2 단항연산자
- +a, -a, !a, ++a, a++, –a, a–
- unaryPlus, unaryMinus, not, inc, dec
operator fun Score.unaryMinus(): Score {
return Score(-a, -b)
}
operator fun Score.inc(): Score {
return Score(a + 1, b + 1)
}
B.4.3 복합 대입 연산자
- +=, -=, *=, /=, %=
- plusAssign, minusAssign, timesAssign, divAssign, modAssign
B.4.4 비트 연산자
- <<, >>, >>>, &, |, ^, ~ 가 없다. -_-;
- shl, shr, ushr, and, or, xor, inv
B.4.5 논리 연산자
- &&, ||, ! 아직은 사용가능하다.
- and, or, not
B.4.6 동등 비교 연산자
- a == b »» a?.equals(b) ?: (b==null)
- null 체크 안해도 된다. 야호!
- a === b 변수의 참조 값(같은 객체를 참조하는지)을 비교. 오버로딩 안된다.
- a !=b »» !(a?.equals(b) ?: (b == null))
- a !== b 오버로딩 안됨.
B.4.7 그 밖의 비교 연산자
- >, <, >=, <=
- a.compareTo(b) > 0, a.compareTo(b) < 0, a.compareTo(b) >=0, a.compareTo(b) <= 0
- compareTo함수는 a가 b보다 크면 양수, 작으면 음수값이 반환됨.
fun main(args: Array<String>) {
val p1 = Customer("홍길동", "010-1111-2222")
val p2 = Customer("김선달", "010-1111-3333")
println(p1 < p2)
println(p1 > p2)
}
class Customer(val name: String, val phone: String) : Comparable<Customer> {
override fun compareTo(other: Customer): Int {
return compareValuesBy(this, other, Customer::name, Customer::phone)
}
}
B.4.8 In 연산자
- 컬렉션 객체에 사용
- a in b »» b.contains(a)
- a !in b »» !b.contains(a)
B.4.9 범위(..) 연산자
- 범위 값을 가지며 a..b로 표기하면 컴파일러가 a.rangeTo(b)코드로 생성
val start = LocalDate.now()
val end = start..start.plusDays(15)
println(start.plusWeeks(1) in end)
println(end)
// 결과
// true
// 2017-09-14..2017-09-29
B.4.10 인덱스 연산자
- 배열과 컬렉션 클래스에서 사용
- a[i] => a.get(i)
- a[i, j] => a.get(i, j) : 왼쪽과 같이 쓰고 싶으면 오른쪽과 같은 operator 함수를 만들자.
- a[i_1, …, i_n] => a.get(i_1, …, i_n)
- a[i] = b => a.set(i, b)
- a[i, j] = b => a.set(i, j, b)
- a[i_1, …, i_n] = b => a.set(i_1, …, i_n, b)
B.4.11 Invoke 연산자
- 코틀린에서는 클래스 인스턴스에 괄호()를 붙여서 호출할 수 있다.
- operator 키워드를 지정하여 연산자를 오버로딩한 invoke() 함수를 정의하면 된다.
- a() ==> a.invoke()
- a(i) ==> a.invoke(i)
- a(i, j) ==> a.invoke(i, j)
- a(i_1, …, i_n) ==> a.invoke(i_1, …, i_n)
fun main(args: Array<String>) {
val instance = InvokeOperator("코틀린을")
instance("배우자")
}
class InvokeOperator(val message1: String) {
operator fun invoke(message2: String) {
println("$message1 $message2")
}
}
B.4.12 타입 확인 연산자: is, !is
val b: String = "코틀린을 배우자"
if(b is String) {
println("String 타입")
} else {
println("String 타입아님")
}
open class A{}
class B : A() {}
val x = B()
if(x is A) println("A type") else println("not A type")
if(x is B) println("B type") else println("not B type")
// 결과
// String 타입
// A type
// B type
B.5 Null 타입 처리 메커니즘
- java에서 런타임에 흔히 발생하는 NullPointerException을 컴파일 시점에 미리 방지할 수 있게함
B.5.1 Null 가능 타입
- 코틀린에서는 기본적으로 모든 타입에 null 값을 허용하지 않는다.
- null을 사용하려면 타입 이름 끝에 물음표(?)를 붙여야 한다.
val s1: String? = null
val s2: String = null // Error: Kotlin: Null can not be a value of a non-null type String
val s3: String = s1 // Error: Kotlin: Type mismatch: inferred type is String? but String was expected
B.5.2 Null 처리 연산자 #1: “?.”
- ?. 연산자는 객체의 속성이나 함수를 안전하게 참조/호출할 수 있게 해준다.
var a: String? = "코틀린을 배우자"
// a가 널이 아닌 경우 substring을 호출하고 호출한 결과가 널이 아닌 경우 length를 호출
println(a?.substring(5)?.length)
B.5.3 Null 처리 연산자 #2 “?:”
- 엘비스 프레슬리 이모티콘과 유사하다고 하여 엘비스 연산자 라고함.
- 왼쪽 피연산자 값이 null이 아니면 그 연산자의 결과를 반환하고 null이면 오른ㅉ/ㅗㄱ 피연산자의 결과 값을 반환
var a: String? = "코틀린을 배우자"
val b = a?.length ?: 0 // val b: Int = if (a != null) a.length else 0
println(b)
B.5.4 Null 처리 연산자 #3: “!!”
- !! 연산자는 참조 변수의 값이 null이 아니면 정상적으로 코드를 수행하고, null인 경우 런타임에 NPE를 발생 시킨다.
- NPE를 컴파일 시점에 방지하고자 하는 코틀린에서 이녀석은 왜 쓰는가?
- 우리가 작성하는 클래스와 함수에서는 왠만하면 쓰지 말자
- 다른 라이브러리의 변수나 함수를 참조하거나 호출할 때 명시적으로 NPE를 파악하고자 하는 경우 사용하자.
B.5.5 Null 처리 연산자 #4: as, as?
- 객체의 타입을 캐스팅할 때는 as 연산자를 사용한다. (premitive 타입은 변환 함수를 써야한다. toLong(), toDouble()…)
// s2를 nullable string으로 캐스팅. s2가 String타입에 부적합한경우 ClassCastException이 발생
val s1: String? = s2 as String?
// s2가 String타입에 부적합하더라도 ClassCastException이 발생하지 않고 null 값이 반환
val s1: String? = s2 as? String?
B.6 코드 실행 제어
- if, when, for, while, do
B.6.1 if
- 자바의 if와 동일하게 사용가능하다.
- 코틀린에서는 if문을 하나의 표현식으로 간주하므로, 변수의 대입문에 if를 사용할 수 있다.
val result = if (param == 1) {
"one"
} else if (param == 2) {
"two"
} else {
"three"
}
B.6.2 when
- 자바의 switch~case와 유사하지만, 더 간결하고 기능도 뛰어나다.
fun main(args: Array<String>) {
whenUsage(2, 50, "서울시")
}
fun whenUsage(inputType: Int, score: Int, city: String) {
// -------------- #1
when (inputType) {
// 값 -> 실행 코드 블록 형태로 작성
1 -> println("type-1")
// , 를 사용하여 여러 조건을 검사할 수 있다
2, 3 -> println("type-2,3")
// 모두 일치하지 않는 경우 else의 -> 코드를 실행한다.
// 자바의 break는 필요 없다
else -> {
println("미확인")
}
}
// --------------- #2
when (inputType) {
// 함수를 실행하여 그 결과를 검사할 수도 있다
checkInputType(inputType) -> {
println("타입정상")
}
else -> println("타입 비정상")
}
// --------------- #3
val start = 0
val end = 100
when (score) {
// in연산자와 범위 연산자 ..을 사용하여 검사할 수 있다.
in start..(end / 4) -> println("우수함")
50 -> println("평균임")
in start..end -> println("범위에 있음")
else -> println("범위를 벗어남")
}
// ---------------- #4
// 결과를 변수에 대입할 수 있다.
val isSeoul = when (city) {
// is 연산자를 사용하여 타입을 확인할 수 있다.
is String -> city.startsWith("서울")
// 값을 대입하는 경우 else가 없으면 컴파일 에러가 발생한다
else -> false
}
println(isSeoul)
// ----------------- #5
// if-else 문처럼도 사용가능하다.
when {
city.length == 0 -> println("도시명을 입력하세요")
city.substring(0, 2).equals("서울") -> println("서울이군요")
else -> println(city)
}
}
fun checkInputType(inputType: Int): Int {
// 아래 식은 inputType in 1..2 로 변경할 수 있다.
if (inputType >= 1 && inputType < 3) {
return inputType
}
return -1
}
B.6.3 for 루프
// in 연산자를 사용하여 반복을 처리
val item1 = listOf("사과", "바나나", "키위")
for (item in item1) {
println(item)
}
// index를 사용하여 반복
val item2 = listOf("사과", "바나나", "키위")
for (index in item2.indices) {
println("item at $index is ${item2[index]}")
}
// array도 동일하게 사용 가능
val item3 = arrayOf("사과", "바나나", "키위")
for (index in item3.indices) {
println("item at $index is ${item3[index]}")
}
// 1부터 100까지 반복
for (i in 1..100) {
print(i)
}
// 1부터 100까지 반복. 100은 제외
for (i in 1 until 100) {
print(i)
}
// 2부터 10까지 반복. i는 2씩 증가
for (i in 2..10 step 2) {
print(i)
}
// 10부터 1까지 1씩 감소
for (i in 10 downTo 1) {
print(i)
}
B.6.4 while과 do-while 루프
- while과 do-while 루프는 다른 언어와 동일
val items = listOf("사과", "바나나", "키위")
var index = 0
while (index < items.size) {
println("item at $index is ${items[index]}")
index++
}
index = 0
do {
println("item at $index is ${items[index]}")
index++
} while (index < items.size)
B.7 함수
B.7.1 함수 선언과 호출
- 함수를 선언할 때 fun 키워드를 함수 이름 앞에 지정
- 함수는 소스 코드 파일의 어디든 바로 정의 할 수 있음
- 함수의 매개변수는 변수이름:타입의 형태로 지정
- 리턴 값의 타입은 함수 정의 문장 끝에 콜론을 추가한 후 지정
fun min(valueLeft: Int, valueRight: Int): Int {
return if(valueLeft < valueRight) valueLeft else valueRight
}
// 코틀린에서 if는 표현식으로 간주하므로 대입문에 바로 추가 가능
// min 함수의 리턴 타입이 생략되어 있지만 컴파일러가 추론하므로, 에러가 생기지 않는다.
fun min(valueLeft: Int, valueRight: Int) = if(valueLeft < valueRight) valueLeft else valueRight
// 매개변수의 기본값을 정의할 수도 있다.
fun min(valueLeft: Int = 0, valueRight: Int = 0) = if(valueLeft < valueRight) valueLeft else valueRight
// 사용 예시
min(100,50)
min(100) // valueRight는 기본값인 0이됨
min() // valueLeft, valueRight 모두 0
min(valueLeft=50, valueRight=100) // 지명인자(named argument)를 사용하여 값을 전달
min(valueRight=100, valueLeft=50) // 지명인자를 사용하야 정의된 순서와 무관하게 전달
min(valueLeft=50) // 지명인자와 기본값을 사용하여 호출
B.7.2 가변 인자
- 함수를 호출할 때 인자의 개수를 가변적으로 전달할 수 있다.
- vararg 키워드를 사용한다.
- 가변 인자들은 배열로 전달 된다.
// 가변인자를 배열로 받아 arrayList를 생성하여 저장한 후 반환
fun <T> newList(vararg ts: T): List<T> {
val result = ArrayList<T>()
// 아래 for문은 아래 연산자 오버로딩 함수로 변경 가능하다
// result += ts
for (t in ts) {
result.add(t)
}
return result
}
val list = newList(1, 2, 3)
val e = arrayOf(7, 8, 9) // Int 타입의 요소 3개를 저장한 배열 생성
val list2 = newList(1, 2, 3, *e, 5) // 1,2,3,7,8,9,5 를 가진 ArrayList 생성
// *e는 배열의 요소를 하나씩 가져와서 인자로 전달하라는 의미. *를 확산(spread)연산자라고함
B.7.3 함수의 종류
- 최상위 함수
- 함수를 클래스 외부에 정의. 소스 코드 파일(.kt)에 바로 정의 하여 사용
- 애플리케이션에서 공통으로 사용하는 기능을 처리. (자바의 경우 모든 것이 클래스에 정의되어야 하므로, 특정 클래스에 static 메서드로 모아두고 사용)
- 위 함수는 public final static으로 컴파일됨.
- 최상위 수준의 속성(property)
- (.kt)파일에 선언
- private final static으로 컴파일됨. getter는 public으로 생성됨
- const 키워드를 사용하는 경우 public final static으로 컴파일됨
-
ex) const val ERROR_CODE = 1 ==> public final static Int ERROR_CODE = 1;
- 클래스 내부에 정의하여 사용
- 멤버 함수(자바의 인스턴스 메서드)
class Customer() {
fun checkId() {..}
}
// 인스턴스를 생성(Customer())하여 호출
Customer().checkId()
- 지역(local) 함수를 선언하여 사용
- 다른 함수 내부에 포함된 함수
fun main(args: Array<String>) {
println(calcCombination(45, 6)) // 로또 복권이 모든 조합 가능한 번호 개수 출력
}
fun calcCombination(whole:Int, selected: Int): Double {
if (selected > whole || selected <= 0 || whole <= 0) {
return -1.0
} else if (selected == whole) {
return 1.0
}
// 함수 내부에 factorial을 계산하는 지역함수를 정의하여 사용
// 지역 함수에서는 자신을 포함하는 외부 함수의 인자나 변수를 그냥 사용할 수 있다.(여기서는 안썻지만...)
fun calcFactorial(num: Int): Double {
var total: Double = 1.0
for (i in num downTo 1) {
total *= i
}
return total
}
return calcFactorial(whole) / (calcFactorial(whole - selected) * calcFactorial(selected))
}
- 제네릭(generic) 함수
- fun
newList(vararg ts: T): List T -
위 예에서
가 generic 타입이다. 즉, list에 저장할 요소의 타입을 미리 정하지 않고 함수 호출 코드에서 타입을 지정할 수 있다는 의미이다. - 확장(extension) 함수
- 특정 클래스로부터 상속받지 않고 해당 클래스의 기능을 확장할 때 사용
// 코틀린 MutableList 클래스를 상속받지 않고 추가로 swap 기능을 하는 함수를 확장
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // this는 MutableList의 객체를 나타냄
this[index1] = this[index2]
this[index2] = tmp
}
// 사용예시
val l = mutableListOf(1, 2, 3)
l.swap(0, 2)
- 중위(infix) 함수
- 두 개의 피연산자 사이에 함수 호출을 넣어서 사용
- 새로운 함수라기 보다는 사용을 편리하게 하기 위해 코틀린에 추가된 기능
- 대표적인 예시로는 비트 연산자가 있다. ==> 8 shr 2
// Int 클래스에 정의되어 있는 shl 함수 예시
infix fun shl(bitCount: Int): Int {
...
}
val a = 8
// a+8은 (a+8)로 괄호를 씌우지 않아도 됨. shr()함수 호출보다 +연산자의 우선순위가 더 높음.
println(a+8 shr 2)
- 8 shl 2, 8.shl(2) 두 가지 방법으로 호출하여 사용 가능
- 중위 함수 정의 3가지 조건
- 클래스의 멤버 함수이거나 확장 함수이어야함
- 매개변수가 한 개여야 함
- infix 키워드로 함수가 정의되어야함
- 재귀(tail recursive)함수
- 일련의 코드를 반복해서 실행해야 하는 알고리즘의 경우 루프나, 재귀함수를 사용할 수 있다.
- 재귀 함수를 사용하는 경우 스택 오버플로의 위험이 따르고, 시스템의 부담도 커진다.
- 코틀린에서는 이런 단점을 줄이고 실행 속도도 빠르며, 효율적인 루프를 사용하는 재귀 함수를 제공한다.
- fun 키워드 앞에 tailrec 키워드를 붙인다.
fun main(args: Array<String>) {
println(calcFactorial(10000))
println(getFibonacciNumber(10000, BigInteger("1"), BigInteger("0")))
}
// 아래 함수는 tailrec와 무관하게 컴파일 후 재귀함수 형태로 보여진다.
tailrec fun calcFactorial(num: Int): Double {
if (num == 1) {
return 1.0
}
else {
return (num * calcFactorial(num -1)).toDouble()
}
}
// 아래 함수는 tailrec 키워드 여부에 따라 컴파일 후 재귀함수형태 or while루프(tailrec키워드가 있는 경우) 형태로 변경된다.
tailrec fun getFibonacciNumber(n: Int, a: BigInteger, b:BigInteger): BigInteger {
if(n==0) {
return b
} else {
return getFibonacciNumber(n - 1, a + b, a)
}
}
B.8 클래스와 인터페이스
B.8.1 클래스 선언과 생성자
// 클래스 이름의 첫 자는 대문자
class Friend {}
// 본체가 없는 경우 {}도 생략이 가능하나.. 이렇게 쓸일이 없을듯.
class Friend
fun main(args: Array<String>) {
val f1 = Friend1()
f1.name = "박문수"
f1.tel = "010-123-4567"
f1.type = 5
println(f1.name + "," + f1.tel + "," + f1.type)
val f2 = Friend2()
f2.name = "홍길동"
f2.tel = "010-456-4567"
f2.type = 5 // set() 함수가 실행되므로 4값이 들어간다.
println(f2.name + "," + f2.tel + "," + f2.type)
// named argument를 사용하여 생성자의 인자를 전달
val f3 = Friend3(tel = "010-678-1234", name = "김선달", type = 5)
println(f3.name + "," + f3.tel + "," + f3.type)
val f4 = Friend4("전신주", "010-1111-1234", 3)
println(f4.name + "," + f4.tel + "," + f4.type)
// 실행 결과
박문수,010-123-4567,5
홍길동,010-456-4567,4
김선달,010-678-1234,4
전신주,010-1111-1234,3
}
class Friend1 {
// 생성자 정의가 없어 기본 생성자가 자동으로 생성된다
// 아래의 속성들은 자동으로 getter/setter가 생성된다.
var name: String = ""
var tel: String = ""
var type: Int = 4
}
class Friend2 {
var name: String = ""
var tel: String = ""
// 아래와 같이 커스텀 세터를 정의할 수 있다
var type: Int = 4
set(value: Int) {
// set 함수에서 해당 속성을 액세스 하려면 field 키워드를 사용한다
if(value < 4) field = value else field = 4
}
}
// 생성자를 아래와 같의 정의 할 수 있다. 아래 코드와 동일하다
// class Friend3 constructor(var name: String, var tel: String, var type: Int)
// 기본 생성자에서는 constructor 키워드를 생략할 수 있다
// private 와 같은 접근 제한자를 사용하는 경우 constructor 키워드를 생략할 수 없다.
class Friend3 (var name: String, var tel: String, var type: Int)
{
// 초기화 블록을 사용하여 속성의 값을 초기화 할 수 있다.
init {
type = if(type < 4) type else 4
}
}
class Friend4 {
var name: String
var tel: String
var type: Int
// java 처럼 보조 생성자를 정의할 수 있다.
constructor(name: String, tel: String, type: Int) {
this.name = name
this.tel = tel
this.type = if(type < 4) type else 4
}
}
B.8.2 멤버 함수
- 클래스 내부의 멤버로 가질 수 있는 속성, 생성자, 초기화 블록, 멤버 함수, 중첩/내부 클래스 다른 객체 중에서 멤버함수에 대해 알아본다.
- 클래스 내부에 정의된 함수는 멤버 함수로 간주된다.
- 멤버 함수는 해당 클래스의 인스턴스를 생성하여 호출할 수 있다.
- 접근 제한자를 추가로 지정할 수 있다.
- 기본 접근 제한자는 public 이다.
class Friend2 {
...
// 멤버 함수 정의
fun printName(): Unit = println(this.name)
}
fun main(args: Array<String>) {
val f2 = Friend2()
// 인스턴스.함수명() 으로 호출
f2.printName()
}
B.8.3 접근 제한자
- 코틀린에서틑 클래스, 클래스 멤버, 최상위 수준 변수/함수에 다음의 접근 제한자를 사용할 수 있다.
접근 제한자 | 클래스 멤버일 때 | 최상위 수준으로 선언되었을 때 |
---|---|---|
public(기본값) | 어디서든 사용 가능 | 어디서든 사용 가능 |
internal | 같은 모듈에서만 사용 가능 | 같은 모듈에서만 사용 가능 |
protected | 서브 클래스에서만 사용 가능 | 해당 없음 |
private | 클래스 내무에서만 사용 가능 | 코틀린 파일 내부에서만 사용 가능 |
-
internal의 모듈 : Intellij의 모듈, Gradle의 Project
-
클래스와 클래스 멤버에만 사용 가능한 접근 제한자
접근 제한자 | 클래스 | 클래스 멤버 |
---|---|---|
final(기본값) | 서브 클래스를 만들 수 없음 | 슈퍼 클래스의 멤버를 오버라이딩 할 수 없음 |
open | 서브 클래스를 만들 수 있음 | 슈퍼 클래스의 멤버를 오버라이딩할 수 있음 |
abstract | 추상 클래스를 의미 | 함수에만 해당되며 몸체(실행코드)가 없음 |
override | 해당 없음 | 슈퍼 클래스의 멤버를 오버라이딩함 |
- 코틀린에서는 멤버 함수는 물론 속성도 오버라이딩할 수 있다.
- 속성을 오버라이딩 하는 경우 슈퍼 클래스의 속성과 타입이 같아야 한다.
B.8.4 클래스 상속과 멤버 오버라이딩
- 코틀린은 다중상속을 지원하지 않는다.
- 코틀린의 모든 클래스는 기본적으로 Any라는 이름의 슈퍼 클래스로부터 상속 받게 되어 있다.
open class Father(nameF: String, ageF: Int)
// 상속관계는 콜론(:)을 사용한다
// 서브 클래스에서는 슈퍼클래스의 속성도 초기화해주어야 한다.
class Child(nameC: String, ageC: Int) : Father(nameC, ageC)
// 서브 클래스에 기본 생성자가 정의되지 않고 보조 생성자가 정의된 경우에는 : 과 super키워드를
// 사용하여 슈퍼 클래스의 속성을 초기화 한다.
class Child : Father {
constructor(nameC: String, ageC: Int) : super(nameC, ageC)
}
open class Father(nameF: String, ageF: Int) {
// 속성의 경우도 상속이 가능하다. open 키워드를 사용해야 한다.
open val height: Int = 170
// 속성의 경우도 상속이 가능하다. open 키워드를 사용해야 한다.
open var weight: Int = 70
// 서브 클래스에서 오버라이딩을 하게 하려면 open 키워드를 사용해야 한다
open fun sleep() {}
fun looseWeight() {}
}
class Child : Father {
constructor(nameC: String, ageC: Int) : super(nameC, ageC)
// 서브 클래스에서는 override 키워드를 사용해야 한다.
// val로 정의된 속성은 var로 override 가능하다.
override var height: Int = 180
// var로 정의된 속성은 val로 override 할 수 없다.
// 슈퍼 클래스에 var 속성으로 정의되면 setter가 추가로 생성되어 있게된다. 서브 클래스에서 val로 override하게 되면 슈퍼클래스의
// setter가 무의미해진다.
// override val weight: Int = 65
// 서브 클래스에서는 override 키워드를 사용해야한다
override fun sleep() {}
}
B.8.5 인터페이스 구현과 어버라이딩
- 자바처럼 interface 키워드로 인터페이스를 정의한다.
- 인터페이스는 주로 추상 함수를 갖지만 구현 코드가 있는 함수도 가질 수 있다.
interface PlayMusic {
var musicalInstrument: String
fun play()
fun stop() {
println("stop")
}
}
// 클래스를 상속할때처럼 : 을 사용한다. 클래스 상속처럼 ()를 붙이지는 않는다.
class Professional(override var musicalInstrument: String) : PlayMusic {
override fun play() {
}
}
자바 코드로 변환해보면, interface의 속성은 getter, setter(var)로 표현됨.
java8의 default method 와 비슷한 구현이 있는 메서드는 DefaultImpl static class가 생성되어 표현된다.
// 변환된 java 코드.
public interface PlayMusic {
@NotNull
String getMusicalInstrument();
void setMusicalInstrument(@NotNull String var1);
void play();
void stop();
public static final class DefaultImpls {
public static void stop(PlayMusic $this) {
String var1 = "stop";
System.out.println(var1);
}
}
}
public final class Professional implements PlayMusic {
@NotNull
private String musicalInstrument;
public void play() {
}
@NotNull
public String getMusicalInstrument() {
return this.musicalInstrument;
}
public void setMusicalInstrument(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.musicalInstrument = var1;
}
public Professional(@NotNull String musicalInstrument) {
Intrinsics.checkParameterIsNotNull(musicalInstrument, "musicalInstrument");
super();
this.musicalInstrument = musicalInstrument;
}
public void stop() {
PlayMusic.DefaultImpls.stop(this);
}
}
- 상속과 인터페이스 구현을 함께 하는 경우
open class MusicType {
open fun sing() {}
open fun play() {
println("play MusicType")
}
}
interface PlayMusic2 {
var musicalInstrument: String
fun play() {
println("play PlayMusic2")
}
}
// ,을 사용하여 상속과 구현을 나열하면 된다.
class Professional2(override var musicalInstrument: String) : MusicType(), PlayMusic2 {
override fun sing() {}
override fun play() {
// 인터페이스와 슈퍼클래스에 동일 이름의 메서드가 정의되어 있는 경우 super<타입> 키워드를 사용해서 참조하면 된다.
super<MusicType>.play()
}
}
B.8.6 추상 클래스와 오버라이딩
- 추상 클래스는 인터페이스처럼 추상 함수를 가진다.
- 서브 클래스에서는 추상 함수를 반드시 오버라이딩해야 한다
- 일반 함수와 속성도 가질 수 있다
이쯤되면 속성과 함수 구현을 가질 수 있는 interface와 뭐가 다른지 생각하게 된다.
- 추상 클래스는 abstract 키워드로 지정한다.
- 자바처럼 인스턴스를 생성할 수 없다.
abstract class PlayMusic3 {
val musicalInstrument: String = "피아노"
abstract fun play()
fun sing() {}
}
class Professional3() : PlayMusic3() {
// abstract 멤버 함수를 반드시 오버라이딩 해야 한다.
override fun play(){}
}
B.8.7 “object” 키워드
객체 선언(object declaration), 동반 객체(companion object), 객체 표현식(object expression)
- 객체 선언(object declaration) : singleton을 구현할 때 좋은 방법
object StateManager {
var msgNumber: Int = 0
var msgContent: String = ""
fun storeMessage() {
// 데이터를 저장하는 코드
println("메시지 번호 = $msgNumber, 내용 = $msgContent")
}
}
fun main(args: Array<String>) {
for (i in 1..10){
StateManager.msgNumber += 1
StateManager.msgContent = StateManager.msgNumber.toString() + "번째 메시지"
StateManager.storeMessage()
}
}
// java code
public final class StateManager {
private static int msgNumber;
@NotNull
private static String msgContent;
public static final StateManager INSTANCE;
public final int getMsgNumber() {
return msgNumber;
}
public final void setMsgNumber(int var1) {
msgNumber = var1;
}
@NotNull
public final String getMsgContent() {
return msgContent;
}
public final void setMsgContent(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
msgContent = var1;
}
public final void storeMessage() {
String var1 = "메시지 번호 = " + msgNumber + ", 내용 = " + msgContent;
System.out.println(var1);
}
private StateManager() {
INSTANCE = (StateManager)this;
msgContent = "";
}
static {
new StateManager();
}
}
- 인터페이스를 구현하거나 다른 클래스로부터 상속 받을 수 있다.
- 클래스 내부에서도 선언 할 수 있다.
class Outer() {
object InnerObject {
var count: Int = 0
fun printCount() {
println(count)
}
}
}
// 아래와 같이 사용 가능하다.
Outer.InnerObject.printCount()
- 동반 객체(companion object)
- 코틀린에는 자바의 static과 같은 키워드가 없다.
- companion 키워드를 추가하여 object 객체 선언과 같이 사용할 수 있다.
- 이러한 동반 객체는 이 객체를 포함하는 클래스에서는 자신의 멤버인 것처럼 인식한다.
fun main(args: Array<String>) {
OuterClass.printMsg()
val m = OuterClass.create()
m.printMyClass()
}
class OuterClass {
private constructor()
companion object ComObj { // ComObj는 companion object의 이름이다. 생략할 수 있다.
fun printMsg() {
println("동반 객체의 함수가 호출됨")
}
// 동반 객체에서는 private 멤버도 액세스 할 수 있다.
fun create(): OuterClass = OuterClass()
}
fun printMyClass() {
println("팩토리 객체의 함수가 호출됨")
}
}
// java code..
public final class OuterClass {
public static final OuterClass.ComObj ComObj = new OuterClass.ComObj((DefaultConstructorMarker)null);
public final void printMyClass() {
String var1 = "팩토리 객체의 함수가 호출됨";
System.out.println(var1);
}
private OuterClass() {
}
// $FF: synthetic method
public OuterClass(DefaultConstructorMarker $constructor_marker) {
this();
}
@Metadata(
mv = {1, 1, 7},
bv = {1, 0, 2},
k = 1,
d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004J\u0006\u0010\u0005\u001a\u00020\u0006¨\u0006\u0007"},
d2 = {"LOuterClass$ComObj;", "", "()V", "create", "LOuterClass;", "printMsg", "", "production sources for module Kotlin01"}
)
public static final class ComObj {
public final void printMsg() {
String var1 = "동반 객체의 함수가 호출됨";
System.out.println(var1);
}
@NotNull
public final OuterClass create() {
return new OuterClass((DefaultConstructorMarker)null);
}
private ComObj() {
}
// $FF: synthetic method
public ComObj(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
-
객체 표현식 : 익명 객체를 생성하는 방법
코틀린에서 좀 아쉬운 부분이다. 더 간결하게 할 수 있지 않았을까..?
fun countClicks(window: Window) {
var count = 0
window.addMouseListener(
// 매번 새로운 객체가 생성된다. 싱글톤이 아니다.
object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
count++
}
override fun mouseEntered(e: MouseEvent) {
..
}
}
}
}
B.8.8 중첩 클래스와 내부 클래스
- 중첩(nested)클래스와 내부(inner)클래스는 모두 다른 클래스 내부에 선언된 클래스를 말한다.
- 중첩/내부 클래스의 멤버는 이들을 포함하는 외부 클래스에서 엑세스 할 수 있다.
- 중첩 클래스에서는 외부 클래스의 멤버를 참조할 수 없다. (자바의 static 중첩 클래스와 동일하다.)
- 내부 클래스에서는 외부 클래스의 멤버를 참조할 수 있다.
- 중첩 클래스는 별도의 키워드를 사용하지 않고, 내부 클래스는 inner 키워드를 사용한다.
class ClassOuter {
private val bar: Int = 1
class Nested {
fun funcNested() = 2 // fun funcNested() = bar 는 에러이다.
}
inner class Inner {
fun funcInner() = bar
}
}
fun main(args: Array<String>) {
val result1 = ClassOuter.Nested().funcNested()
println(result1) // 2가 출력
val result2 = ClassOuter().Inner().funcInner()
println(result2) // 1이 출력
}
// java code
public final class ClassOuter {
private final int bar = 1;
public static final class Nested {
public final int funcNested() {
return 2;
}
}
public final class Inner {
public final int funcInner() {
return ClassOuter.this.bar;
}
}
}
B.8.9 데이터 클래스
- 클래스를 정의할 때 data 키워드를 사용하면 데이터 클래스로 간주된다.
- equals(), hashCode(), toString()이 자동 생성된다.
- 자동 생성되는 함수들이 데이터 클래스의 속성을 처리하게 하려면 반드시 기본 생성자에 속성을 지정해야 한다.
- 해체 선언(destructuring declaration) 이라는 신기한 기능이 있다.
- val (fname, fage, ftel) = f1
data class Friend7(val name: String, val age: Int, val tel: String)
fun main(args: Array<String>) {
val test = Friend7("이름", 10, "010-1111-2222")
println(test)
// 아래 java로 변환된 코드로 이해하자.
// test객체의 속성을 추출하여 좌측의 변수에 대입한다.
val (fname, fage, ftel) = test
println("$fname, $fage, $ftel")
}
public final class Friend7 {
@NotNull
private final String name;
private final int age;
@NotNull
private final String tel;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
@NotNull
public final String getTel() {
return this.tel;
}
public Friend7(@NotNull String name, int age, @NotNull String tel) {
Intrinsics.checkParameterIsNotNull(name, "name");
Intrinsics.checkParameterIsNotNull(tel, "tel");
super();
this.name = name;
this.age = age;
this.tel = tel;
}
// 각각의 속성에 대해 componentN 형태의 메소드를 생성한다.
@NotNull
public final String component1() {
return this.name;
}
public final int component2() {
return this.age;
}
@NotNull
public final String component3() {
return this.tel;
}
@NotNull
public final Friend7 copy(@NotNull String name, int age, @NotNull String tel) {
Intrinsics.checkParameterIsNotNull(name, "name");
Intrinsics.checkParameterIsNotNull(tel, "tel");
return new Friend7(name, age, tel);
}
// $FF: synthetic method
// $FF: bridge method
@NotNull
public static Friend7 copy$default(Friend7 var0, String var1, int var2, String var3, int var4, Object var5) {
if ((var4 & 1) != 0) {
var1 = var0.name;
}
if ((var4 & 2) != 0) {
var2 = var0.age;
}
if ((var4 & 4) != 0) {
var3 = var0.tel;
}
return var0.copy(var1, var2, var3);
}
public String toString() {
return "Friend7(name=" + this.name + ", age=" + this.age + ", tel=" + this.tel + ")";
}
public int hashCode() {
return ((this.name != null ? this.name.hashCode() : 0) * 31 + this.age) * 31 + (this.tel != null ? this.tel.hashCode() : 0);
}
public boolean equals(Object var1) {
if (this != var1) {
if (var1 instanceof Friend7) {
Friend7 var2 = (Friend7)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age && Intrinsics.areEqual(this.tel, var2.tel)) {
return true;
}
}
return false;
} else {
return true;
}
}
}
public static final void main(@NotNull String[] args) {
Friend7 test = new Friend7("이름", 10, "010-1111-2222");
System.out.println(test);
// val (fname, fage, ftel) = test 가 아래와 같이 동작한다.
String fname = test.component1();
int fage = test.component2();
String ftel = test.component3();
String var7 = null;
var7 = "" + fname + ", " + fage + ", " + ftel;
System.out.println(var7);
}
B.8.10 클래스 위임
- 코드를 재사용하는 클래스 상속은 슈퍼 클래스가 변경되면 서브 클래스에 영향을 줄 수 있다. 또한 슈퍼 클래스의 구현 부분이 서브 클래스에 노출 된다.
- 상속의 단점을 보완하면서 코드를 재사용하는 방법이 위임(delegation)이다.
- 해당 일을 처리할 수 있는 객체의 코드를 재사용
- 상속처럼 클래스 간의 계층 관계를 구성하지 않아도 된다
- 위 내용을 직접 구현할 수도 있으나, 코틀린의 by 키워드를 사용하면 적은 양의 코드로 쉽게 구현할 수 있다.
- 도형 클래스로 알아보자.
open class Figure() {
open fun draw() {}
open fun fill() {}
}
class Rectangle() : Figure() {
override fun draw() {
println("Draw rectangle")
}
override fun fill() = println("Fill rectangle")
}
class Circle() : Figure() {
override fun draw() = println("Draw circle")
override fun fill() = println("Fill circle")
}
class Window(val figure: Figure) {
fun drawFigure() {
figure.draw()
}
fun fillFigure() {
figure.fill()
}
}
fun main(args: Array<String>) {
val r = Rectangle()
val c = Circle()
// 클래스의 상속과 다형성으로 위임 구현
Window(r).drawFigure()
Window(r).fillFigure()
Window(c).drawFigure()
Window(c).fillFigure()
}
- by 키워드를 사용하여 구현한 위임 예시
interface Figure1 {
fun draw() {}
fun fill() {}
}
class Rectangle1() : Figure1 {
override fun draw() {
println("Draw rectangle")
}
override fun fill() = println("Fill rectangle")
}
class Circle1() : Figure1 {
override fun draw() = println("Draw circle")
override fun fill() = println("Fill circle")
}
// Window클래스(delegate)에서는 위임을 받아 실행되는 인스턴스의 함수를 호출하는 코드가 필요 없다.
// 함수를 오버라이딩 할 수도 있다.
class Window1(val figure: Figure1) : Figure1 by figure {}
fun main(args: Array<String>) {
val r = Rectangle1()
val c = Circle1()
Window1(r).draw()
Window1(r).fill()
Window1(c).draw()
Window1(c).fill()
}
// 위 Windows1 클래스의 변환 코드
// 아래 코드가 by 키워드 하나로 정리됐다.
public final class Window1 implements Figure1 {
@NotNull
private final Figure1 figure;
@NotNull
public final Figure1 getFigure() {
return this.figure;
}
public Window1(@NotNull Figure1 figure) {
Intrinsics.checkParameterIsNotNull(figure, "figure");
super();
this.figure = figure;
}
public void draw() {
this.figure.draw();
}
public void fill() {
this.figure.fill();
}
}
B.8.11 enum 클래스
// enum 키워드를 사용한다
enum class Friendtype {
SCHOOL, COMPANY, SNS, OTHERS
}
fun getFriendTypeName(friend: Friendtype) =
when (friend) {
Friendtype.SCHOOL -> "학교 친구"
Friendtype.COMPANY -> "회사 친구"
Friendtype.SNS -> "SNS 친구"
Friendtype.OTHERS -> "기타 친구"
}
fun main(args: Array<String>) {
println(Friendtype.SCHOOL)
// 0부터 시작한다.
println(Friendtype.SCHOOL.ordinal)
println(Friendtype.COMPANY.ordinal)
// name이라는 기본 속성이 있다.
println(Friendtype.COMPANY.name)
println(Friendtype.valueOf("COMPANY"))
// values() : 모든 항목의 이름을 배열로 반환한다.
val friends = Friendtype.values()
for (item in friends) {
println(item)
}
println(getFriendTypeName(Friendtype.OTHERS))
}
// 아래와 같이 enum 클래스 항목의 속성을 우리가 정의할 수도 있다.
enum class RGBColor(val r: Int, val g: Int, val b: Int) {
RED(255, 0, 0), ORANGE(255, 165, 0), YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255),
INDIGO(75, 0, 130), VIOLET(238, 130, 238); // enum클래스의 멤버 함수를 정의하는 경우 세미콜론(;)을 써야한다.
fun rgbValue() = (r * 256 + g) * 256 + b
}
B.8.12 sealed 클래스
- enum 클래스의 확장
- enum과는 다르게 여러 인스턴스를 가질 수 있다.
- sealed(실드??) 키워드를 사용한다.
언제, 어떻게 써먹지..?
// 코틀린 1.1 이전에는 하위 클래스가 중첩되어야 했으나, 현재는 아래와 같이 사용할 수 있다.
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
// when에 else가 필요 없다.
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// the `else` clause is not required because we've covered all the cases
}
B.9 람다식
- 함수를 따로 정의하지 않고 간결하게 나타날 수 있으며, 이름을 지정하지 않아도 된다.
- 람다식은 값으로 처리되므로 변수에 저장하고 실행시킬 수 있다.
- 람다식은 다른 함수의 안자로 전달되어 실행될 수 있다.
B.9.1 람다식 작성 방법
// 기존 형태의 덧셈 함수
fun sum1(a: Int, b: Int): Int{
return a + b
}
// 코틀린의 대입문 형태로 정의된 뎃셈 함수
fun sum2(a: Int, b: Int): Int = a + b
fun main(args: Array<String>) {
println("합계는 ${sum1(10, 20)} 입니다.")
println("합계는 ${sum2(10, 20)} 입니다.")
// 람다식으로 작성된 덧셈 함수 #1
val sum3: (Int, Int) -> Int = {x, y -> x + y}
println("합계는 ${sum3(10, 20)} 입니다.")
// 람다식으로 작성된 뎃셈함수 #2
val sum4 = {a:Int, b:Int -> a + b}
println("합계는 ${sum3(10, 20)} 입니다.")
}
B.9.2 람다식 활용
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
/* 버튼 클랙시 실행될 코드 */
}
}
button.setOnClickListener { view -> /* 버튼 클랙시 실행될 코드 */ }
- 위와 같이 사용하려면 람다식에서 구현하는 인터페이스에 정의된 추상 함수가 하나만 있어야 한다.
val list = listOf(1, 2, 3, 4, 5)
// 하나의 매개변수만 갖는 람다식의 경우 기본으로 생성되는 it으로 매개 변수를 대체할 수 있다.
println(list.filter { it % 2 == 0})
// 각각의 요소에 10을 더한 값을 갖는 새로운 list를 생성하고 출력
println(list.map {it + 10})
data class Friend (val name: String, val age: Int, val tel: String)
val fr = listOf(Friend("김선달", 30, "010-1111-2222"), Friend("홍길동", 25, "010-2222-3333"))
// list에서 30세 이상인 친구의 이름만 출력
// 객체의 멤버(속성이나 함수)는 클래스이름::멤버이름의 형태로 참조가 가능하다.
println(fr.filter {it.age >= 30}.map(Friend::name))
// friend 필터링 및 출력 관련 변환된 java code
fr = CollectionsKt.listOf(new Friend[]{new Friend("김선달", 30, "010-1111-2222"), new Friend("홍길동", 25, "010-2222-3333")});
Iterable $receiver$iv = (Iterable)fr;
Collection destination$iv$iv = (Collection)(new ArrayList());
Iterator var22 = $receiver$iv.iterator();
Object item$iv$iv;
while(var22.hasNext()) {
item$iv$iv = var22.next();
Friend it = (Friend)item$iv$iv;
if (it.getAge() >= 30) {
destination$iv$iv.add(item$iv$iv);
}
}
$receiver$iv = (Iterable)((List)destination$iv$iv);
destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
var22 = $receiver$iv.iterator();
while(var22.hasNext()) {
item$iv$iv = var22.next();
String var24 = ((Friend)item$iv$iv).getName();
destination$iv$iv.add(var24);
}
List var20 = (List)destination$iv$iv;
System.out.println(var20);
B.9.3 익명 함수
- 실행 가능한 코드 블록을 다른 함수의 인자로 전달하는 또 다른 방법
- 일반 함수와 유사하지만 함수 이름과 매개변수 타입을 갖지 않는다
println(fr.filter { it.age >= 30})
// 위 코드를 익명함수를 사용하면 다음과 같이 작성할 수 있다.
println(fr.filter(fun (friend) = friend.age >= 30))
println(
fr.filter(
fun (friend): Boolean {
return friend.age >= 30
)
)
}
- 람다식과 익명 함수 모두 사용 가능 범위의 외부 변수를 액세스할 수 있다.
val value: Int = 100
val list2 = listOf(1, 2, 3, 4, 5)
println(list2.map {it + value})
println(list2.map(fun (it): Int { return it + value}))
B.9.4 상위차 함수
- 상위차(higher-order) 함수는 다른 함수나 람다식을 인자로 받거나 반환할 수 있는 함수를 말한다.
- 위의 filter()나 map()함수는 람다식이나 익명 함수를 인자로 받아서 실행시킨다.
- 인자로 받는 함수의 타입을 매개변수로 지정해야 한다.
val sum3: (Int, Int) -> Int = {x, y -> x + y}
- 위 코드에서는 두 개의 Int 값을 인자로 받아 더한 값을 반환하여 sum3 변수에 저장한다.
- 여기서 (Int, Int) -> Int를 함수 타입이라고 한다.
- 매개 변수가 없는 경우는 () -> Int처럼 쓸수 있다.
- 아무것도 반환하지 않는 경우 () -> Unit처럼 반드시 Unit를 지정해야 한다.
fun calc(value1: Int, value2: Int, execCode: (Int, Int) -> Int): Int {
return execCode(value1, value2)
}
fun main(args: Array<String>) {
val v1 = calc(2, 7, {a, b -> a * b})
val v2 = calc(2, 7, {a, b -> a + b})
val v3 = calc(2, 7, {a, b -> a - b})
println(v1)
println(v2)
println(v3)
}
B.9.5 인라인 함수
- 상위차 함수를 사용할 때는 런타임 시에 단점이 생길 수 있다.
- 각 함수가 객체로 동작하므로 메모리 할당과 사용 및 호출에 따른 시스템의 부담이 따른다.
- 함수 외부의 변수를 사용가능하므로 변수 액세서에 따른 부담도 추가된다.
- 이럴 때 인라인(inline)함수를 사용하면 그런 단점을 해소할 수 있다.
inline fun calc(value1: Int, value2: Int, execCode: (Int, Int) -> Int): Int {
return execCode(value1, value2)
}
- inline 함수를 호출하는 모든 코드는 컴파일러에 의해 해당 함수의 바이트 코드로 복사 및 대체된다.(인라인 처리된다.)
- 따라서 컴파일된 코드의 크가가 증가한다.
- 그러나 런타임 시의 성능은 향상된다.
- 반복 실행되는 루프에 람다식을 포함하는 함수는 인라인 함수로 지정하는 것이 좋다.
- 람다식이 인라인 함수의 인자로 전달될 때는 해당 함수와 람다식 모두 인라인 처리된다.
- 인자로 전달되는 람다식이 다른 객체에 저장되는 경우 해당 람다식은 인라인 처리 될 수 없다.
- 람다식을 인라인 처리하고 싶지 않거나, 또는 인라인 처리가 불가능한 경우 noinlin키워드를 지정하면 된다.
inline fun makeMoney(lambda1: () -> Int, noinline lambda2: (Int) -> Unit) {
// 실행될 코드
}
- 인라인 처리된 람다식은 해당 인라인 함수 내부에서만 호출될 수 있으며, 인라인 인자로만 전달 될 수 있다.
- noinline 지정된 람다식은 일반 람다식과 동일하게 변수에 저장하거나 함수의 인자로 전달할 수 있다.
B.10 예외 처리
- 코틀린 프로그램에서 발생하는 모든 에러는 클래스로 정의된 **예외(Exception)로 처리된다.
- 예외 발생과 처리 매커니즘이 언어 자체에 배려되어 있다.(자바와 거의 동일, 실제로 대부분 자바의 예외 클래스를 사용)
- 코틀린의 모든 에러 및 예외 클래스는 Throwable을 상속 받았다.
- 예외 객체와 메시지 및 스택 기록을 속성으로 가지고 있다.
B.10.1 try~catch~finally
- 자바와 마찬가지로, throw문을 사용해서 해당 예외 객체를 던지면 된다.
throw MyException("예외 발생!")
try {
// 실행중 예외가 발생할 수 있는 코드
} catch (e: SomeException) {
// SomeException이 발생한 경우 처리하는 코드
} finally {
// finally 블록은 생략 가능하다.
}
B.10.2 Checked와 UnChecked 예외
- Checked 예외
- 자바의 경우 메서드에서 throw 문으로 예외를 발생 시키는 경우 해당 메서드 헤더에 throws 키워드로 명시해야 한다.
- 위 메서드를 사용하는 다른 메서드에서 예외를 처리하지 않는 경우 컴파일 에러가 발생
- 코틀린에서는 발생하는 예외 처리를 강제하지 않는다.
- 자바와의 호환을 위해 @Throw(SomeExceptin::class) annotation을 사용할 수 있다.
B.10.3 try~catch는 표현식이다.
- 아래 처럼 대입문에 사용 가능하다.
val err: Int? = try { parseInt(vale)}
catch (e: NumberFormatException) { null }
- throw도 표현식으로 간주된다.
val s = fr.name ?: throw IllegalArgumentException("이름을 입력하세요")
- fr.name의 값이 null이 아니면 그 값을 s에 넣고 null이면 IllegalArgumentException을 발생 시킨다.
- 이때 throw의 표현식의 타입은 Nothing이라는 키워드로 나타내는 특별한 타입이다.
val err = fr.name ?: fail("이름을 입력하세요)
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
B.11 package와 import
- 코틀린에서는 하나의 소스 코드(.kt) 파일에 하나 이상의 클래스, 함수, 속성을 포함시킬 수 있다.
- 이것들을 최상위 수준의 클래스, 함수, 속성이라고 한다.
- 자바와 마찬가지로 패키지 단위로 클래스, 함수, 속성들을 모아 두고 사용할 수 있다.
- 이때는 소스 코드 파일의 맨 앞에 package 키워드를 지정하면 된다.
- 클래스, 함수, 속성들이 다른 소스코드 파일에 있더라도 패키지가 같으면 바로 사용할 수 있다.
- 자바와 다르케 패키지 구조와 디렉토로 구조가 일치하지 않아도 된다.(가급적 일치시켜 사용하는게 좋다.)
- 다른 패키지에 있는 클래스, 함수, 속성들을 사용하려면 import문을 사용해서 그것들의 위치를 알려주어야 한다.
끝.