[Android] 초보자를 위한 Kotlin 200제 - 1

android

Posted by 동식이 블로그 on March 27, 2020

초보자를위한 Kotlin 200제(1)

69. 클래스를 상속하는 객체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
open class Person(val name: String, val age: Int) {
    open fun print() {
        println("이름:$name, 나이:$age")
    }
}

fun main(args: Array<String>) {
    val custom: Person = object : Person("Alan", 23) {
        override fun print() {
            println("it's a object")
        }
    }
    custom.print()
}
  • 클래스 없이 object 표현식을 사용해 상속을 할 수 있다.
  • 이때의 상속은 1회용이 된다.

70. Any 클래스

  • 모든 코틀린 클래스들은 Any 클래스를 상속한다.
1
2
3
4
5
open class Any {
		open operator fun equals(other: Any?): Boolean
		open fun hashCode(): Int
		open fun toString(): String
}
  • equals() : == 연산자를 오버로딩하는 멤버 함수
  • hashCode() : 객체 고유의 해시코드를 반환하는 멤버 함수
  • toString() : 객체의 내용을 String 타입으로 반환하는 멤버 함수
1
2
3
4
5
6
7
8
class Person(val name: String, val age: Int) {
    override fun toString() = "이름:$name 나이:$age"
}

fun main(args: Array<String>) {
    val custom = Person("a", 23)
    println(custom.toString())
}

71. 예외 (Exception)

1
2
3
4
5
6
fun main(args: Array<String>) {
    val str = "abcd"
    val num = str.toInt()

    println(num)
}
1
2
3
4
5
Exception in thread "main" java.lang.NumberFormatException: For input string: "abcd"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68)
	at java.base/java.lang.Integer.parseInt(Integer.java:658)
	at java.base/java.lang.Integer.parseInt(Integer.java:776)
	at TestKt.main(Test.kt:7)
  • str에 들어있는 문자열을 Int 타입으로 변환하려 해서 NumberFormatException이 발생한다.

72. 예외 처리

1
2
3
4
5
6
7
8
9
10
11
12
fun main(args: Array<String>) {
    try {
        val str = "abcd"
        val num = str.toInt()

        println(num)
    } catch (e: NumberFormatException) {
        println("문자열을 숫자로 변경하지 못함")
    } finally {
        println("프로그램 종료")
    }
}
  • 예외가 발생할 가능성이 있는 부분을 try 블록으로 감싸줘서 예외를 처리한다.

73. 예외 던지기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main(args: Array<String>) {
    try {
        something()
    } catch (e: Exception) {
        println(e.message)
    }
}

fun something() {
    val num1 = 10
    val num2 = 0
    div(num1, num2)
}

fun div(a: Int, b: Int): Int {
    if (b == 0) throw Exception("0으로 나눌 수 없습니다.")
    return a / b
}
  • 예외를 던져서 자신을 호출했던 메소드로 예외 처리의 책임을 전가시킨다.

74. Nothing 타입

1
2
3
4
5
6
7
fun throwing(): Nothing = throw Exception()

fun main(args: Array<String>) {
    println("start")
    val i: Int = throwing()
    println(i)
}
  • Nothing은 리턴이라는 행위 자체를 하지 않음을 뜻한다.
1
2
3
4
fun validate(num: Int): Int {
    return if (num > 0) num
    else throw Exception("num이 음수입니다.")
}
  • 위의 예제에서 if 블록은 Int 타입, else 블록이 Nothing 타입이면, if-else는 Int 타입을 따라간다.
  • Nothing 타입은 throw를 표현식으로 쓸 수 있게 하기 위한 장치다.

75. Nullable 타입과 null

  • Nullable이란, null 값을 지정할 수 있는 변수를 뜻한다.
  • 타입 이름 뒤에 ?를 붙이면 변수를 Nullable하게 만들 수 있다.
1
2
3
fun main(args: Array<String>) {
    val str : String? = if (true) "test" else null
}
  • “test”는 String, null은 Nothing? 타입이므로, if-else 표현식의 타입은 이 둘이 합쳐진 String?이 된다.

76. 안전한 호출 연산자 - ?.

  • Nullable한 참조 변수의 프로퍼티와 멤버 함수에 접근하려면 . 대신 ?. 연산자를 사용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Building(var name: String){
    fun print(){
        println("name : $name")
    }
}

fun main(args: Array<String>) {
    var obj: Building? = null
    obj?.print() // print() 함수가 호출되지 않는다.

    obj = Building("백화점")
    obj?.print() // print() 함수 호출된다.
}

77. Not-null 단정 연산자 - !!

  • !! 연산자는 Nullable 타입을 Not-null 타입으로 강제로 캐스팅한다.
1
2
3
4
5
6
class Building(var name: String)

fun main(args: Array<String>) {
    val obj: Building? = null
    obj!!.name = "백화점"
}
  • obj는 null이기 때문에 obj!!.name에서 KotlinNullPointerException이 발생한다.

78. 엘비스 연산자 - ?:

  • 엘비스 연산자는 왼쪽의 피연산자가 null이 아니면 그 값을 그대로 쓰고, null이면 우측의 피연산자로 대체하는 연산자이다.
1
2
3
4
5
6
7
fun main(args: Array<String>) {
    val num1: Int? = null
    println(num1 ?: 0) // 0

    val num2: Int? = 15
    println(num2 ?: 0) // 15
}

79. 스마트 캐스팅

  • 특정 조건을 만족하는 경우, 컴파일러는 변수의 타입을 다른 타입으로 자동 캐스팅하는 것을 스마트 캐스팅이라 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main(args: Array<String>) {
    val num1: Int? = null
    val num2 = 1225

    checkNull(num1)
    checkNull(num2)
}

fun checkNull(any: Any?) {
    if (any == null) {
        println("null이 들어왔습니다.")
        return
    }

    println(any.toString())
}
  • if (any == null) 에서 any가 null이면 return 하므로, 아래에서 any?.toString()이 아닌 any.toString()로 호출이 가능하다.

80. is 연산자

  • is 연산자로 참조 변수가 실제로 가리키고 있는 객체의 타입을 알아낼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
open class Person(name: String, age: Int)
class Student(name: String, age: Int): Person(name, age)
class Professor(name: String, age: Int): Person(name, age)

fun main(args: Array<String>) {
    val person1: Person = Student("a", 1)

    println(person1 is Person) // true
    println(person1 is Student) // true
    println(person1 is Professor) // false
}
  • 코틀린의 is 연산자는 자바의 instanceof에 해당한다.

81. as 연산자와 다운캐스팅

  • 다운캐스팅은 업캐스팅과는 반대로 슈퍼클래스의 타입을 서브클래스 타입으로 받는 것을 뜻한다.
1
2
3
4
5
6
7
8
9
10
open class Person(name: String, age: Int)
class Student(name: String, age: Int): Person(name, age)

fun main(args: Array<String>) {
    val person: Person = Student("John", 32)
    val person2: Person = Person("Jack", 27)

    var person3: Student = person as Student
    person3 = person2 as Student // 예외 발생
}
  • person2 참조 변수는 Person의 인스턴스를 가리키고 있으므로, ClassCastException 예외가 발생한다.

  • 캐스팅에 실패했을 때 예외가 발생하는 것을 막고 싶으면 as? 연산자를 대신 사용하면 된다.

1
2
3
4
fun main(args: Array<String>) {
    val person: Person = Person("Jack", 27)
    var person2: Student? = person as? Student
}

82. 접근 지정자 (Access Modifier)

  • public : 모든 곳에서 접근 가능, 접근 지정자를 생략하면 기본적으로 public이 된다.
  • internal : 같은 모듈 안에서 접근 가능. (IntelliJ IDEA 모듈, Maven / Gradle 프로젝트 모듈)
  • protected : 클래스 내부와, 서브클래스 안에서만 접근 가능.
  • private : 프로퍼티와 멤버 함수일 경우, 해당 클래스 안에서만 접근 가능하고, 그 외의 경우, 같은 파일 내에서만 접근 가능하다.

83. 접근 지정자 : private

1
2
3
4
5
6
class Person(private var name: String)

fun main(args: Array<String>){
    val person = Person("jack")
    println(person.name) // 오류
}

84. 접근 지정자 : protected

1
2
3
4
5
6
7
8
9
10
11
12
13
open class Person(protected val name: String)

class Student(name: String) : Person(name) {
    fun printName() {
        println(name)
    }
}

fun main(args: Array<String>) {
    val person = Student("jack")
    println(person.name) // 에러
    person.printName()
}
  • protected는 해당 클래스 안에서만 접근 가능하므로 person.name은 에러가 생긴다.
  • 반면에 person.printName() 메소드는 클래스 내부에서의 접근이기 때문에 가능하다.

85. 접근 지정자 오버라이딩

  • 오버라이딩을 통해 protected인 프로퍼티나 멤버 함수의 접근 지정자를 public으로 변경할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open class AAA(protected open val number: Int)

class BBB(number: Int) : AAA(number) {
    public override val number: Int
        get() = super.number
}


fun main(args: Array<String>) {
    val b = BBB(26)
    val a: AAA = b

    println(a.number) // 에러
    println(b.number)
}

86. 확장 함수 (Extension Function)

  • String은 코틀린에 내장된 클래스이기 때문에 원하는대로 멤버 함수를 추가할 수 없다. 이럴 때는 확장 함수를 사용하여, 상속 없이 클래스 외부에서 멤버 함수를 추가할 수 있다.
  • 여기서 함수를 주입할 클래스를 리시버 타입이라고 부른다.
1
2
3
4
5
6
7
8
9
10
11
fun String.containNumber(): Boolean { // 문자열에 숫자가 포함되어 있으면 true
    this.forEach {
        if (it in '0'..'9') return true
    }
    return false
}

fun main(args: Array<String>) {
    println("123abc".containNumber()) // true
    println("abc".containNumber()) // false
}
  • this를 사용하면 리시버 타입의 프로퍼티나 멤버 함수에 접근할 수 있다.

87. 확장 프로퍼티 (Extension Property)

1
2
3
4
5
6
7
val String.isLarge: Boolean
    get() = this.length >= 10

fun main(args: Array<String>) {
    println("1234567890".isLarge) // true
    println("abcd".isLarge) // false
}

88. 객체 선언

1
2
3
4
5
6
7
8
9
10
11
12
13
14
object Person{
    var name: String = ""
    var age: Int = 0
    
    fun print(){
        println("$name $age")
    }
}

fun main(args: Array<String>) {
    Person.name = "정상현"
    Person.age = 27
    Person.print()
}
  • object 키워드를 사용하면 싱글톤 패턴 코드를 더 이상 사용하지 않아도 된다.
  • 프로그램 전체에서 단 하나만 존재하는 객체를 만들 수 있다.

89. 동반자 객체 (Companion Object)

  • 어떤 클래스의 모든 인스턴스가 공유하는 객체를 만들고 싶을 때 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person private constructor() {
    companion object {
        fun create(): Person {
            countCreated += 1
            return Person()
        }

        var countCreated = 0
    }
}

fun main(args: Array<String>) {
    val a = Person.create()
    val b = Person.create()
    println(Person.countCreated)
}
  • 코틀린에는 static 키워드가 존재하지 않는다. static의 효과를 얻고 싶으면 동반자 객체를 사용해야 한다.

90. Inline 함수

  • Inline 함수는 함수 호출문을 함수의 몸체로 대체하기 때문에 성능을 조금이나마 개선할 수 있다.
1
2
3
4
5
6
7
8
9
10
inline fun hello(){
    println("hello")
    println("kotlin")
}

fun main(args: Array<String>) {
    hello()
    hello()
    hello()
}
  • Inline 함수를 호출하면 컴파일 되는 순간 아래와 같은 코드로 대체된다.
1
2
3
4
5
6
7
8
fun main(args: Array<String>) {
    println("hello")
    println("kotlin")
    println("hello")
    println("kotlin")
    println("hello")
    println("kotlin")
}

91. const

  • 런타임에 할당되는 val와 달리 const가 붙은 변수는 컴파일 시간동안 할당이 되어야 한다.

  • const는 함수나 어떤 클래스의 생성자에게도 할당 될 수 없고 오직 문자열이나 기본 자료형으로 할당되어야 한다.

1
2
3
4
5
const val hello = "hello" + " world"

fun main(args: Array<String>) {
    println(hello)
}
1
2
3
4
5
fun abc(): String {
    return "yes"
}

const val abc = abc() // 오류

92. lateinit

  • lateinit 키워드가 붙은 프로퍼티는 클래스 안에서 바로 초기화하지 않아도 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
    lateinit var name: String

    fun print() {
        println(name)
    }
}

fun main(args: Array<String>) {
    val person = Person()

    person.name = "Jack"
    person.print()
}
  • lateinit은 var 프로퍼티에만 붙일 수 있다.
  • lateinit 프로퍼티가 초기화되었는지 알려면 isInitialized 함수를 사용하면 된다.
  • 만약 name 프로퍼티에 값을 지정하지 않은 채 프로퍼티에 접근하면 UninitializedPropertyAccessException 예외가 발생한다.

93. Nullable 리시버

  • 확장 함수를 응용하면, 참조 변수에 null이 지정되어 있어도 함수 호출이 가능하게 할 수 있다.
1
2
3
4
5
6
7
8
9
10
fun String?.isNumber() {
    if (this == null) {
        println("문자열이 null입니다.")
    }
}

fun main(args: Array<String>) {
    val empty: String? = null
    empty.isNumber()
}
  • empty는 String? 타입이고, null이 지정되어 있다.
  • isNumber 확장 함수는 리시버 타입이 Nullable이기 때문에, 표현식의 값이 null이어도 isNumber 확장 함수를 호출할 수 있다.

94. 동반자 객체의 확장 함수

  • 확장 함수를 이용해 동반자 객체에도 확장 함수를 달 수 있다.
1
2
3
4
5
6
7
8
9
class Person {
    companion object
}

fun Person.Companion.create() = Person()

fun main(args: Array<String>) {
    Person.create()    
}

95. 확장 함수의 리시버 타입이 상속 관계에 있을 때

1
2
3
4
5
6
7
8
9
10
11
open class AAA

class BBB : AAA()

fun AAA.hello() = println("AAA")
fun BBB.hello() = println("BBB")

fun main(args: Array<String>) {
    val test: AAA = BBB()
    test.hello() // AAA
}
  • 타입은 AAA이나, 실제로는 BBB 객체를 가리키고 있다.
  • 확장 함수는 멤버 함수와는 다르게 참조 변수가 실제로 가리키는 객체의 타입을 따르지 않고, 참조 변수의 타입을 그대로 따른다.

96. 추상 클래스

1
2
3
4
5
6
7
8
9
10
11
abstract class Person {
    abstract fun getSalary(): Int
}

class Student(private val tuition: Int) : Person() {
    override fun getSalary() = -tuition
}

class Professor(private val classCount: Int) : Person() {
    override fun getSalary() = classCount * 120
}
  • abstract 키워드는 그 자체로 open을 포함하고 있기 때문에 open 키워드는 따로 적지 않아도 된다.

97. 인터페이스

  • 인터페이스는 클래스에 어떤 멤버 함수와 프로퍼티가 반드시 존재한다는 것을 보장하기 위한 장치이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
interface Printable {
    fun print(): Unit
}

class AAA : Printable {
    override fun print() {
        println("hello")
    }
}

fun main(args: Array<String>) {
    AAA().print()
}

98. 다이아몬드 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Parent {
    fun follow(): Unit
}

interface Mother : Parent {
    override fun follow() = println("follow his mother")
}

interface Father : Parent {
    override fun follow() = println("follow his father")
}

class Child: Mother, Father{
    override fun follow() {
        println("A child decided to ")
        super.follow()
    }
}
  • Child 클래스는 Mother, Father 인터페이스를 모두 구현하고 있을 때, super.hello()를 하면 Mother의 follow()가 호출될지 Father의 follow()가 호출될지 애매하다.
  • 이런 경우엔 super<Mother>.follow() 이런 식으로 호출하면 된다.

99. 중첩 클래스 (Nested Class)

  • 클래스 안에 또 다른 클래스를 선언할 수 있다.
1
2
3
4
5
6
7
8
9
10
class Outer {
    class Nested {
        fun hello() = println("hello")
    }
}
fun main(args: Array<String>){
    val instance: Outer.Nested = Outer.Nested()

    instance.hello()
}
  • Nested 클래스의 멤버 함수는 Outer 클래스의 프로퍼티나 멤버 함수에 접근할 수 없다.
1
2
3
4
5
6
7
class Outer {
    private val name = "Jack"

    class Nested {
        fun printName() = println(name) // 오류
    }
}

100. 내부 클래스 (Inner Class)

  • 중첩 클래스가 단순히 식별자만 바깥 클래스에 속해있는 것이었다면, 내부 클래스는 인스턴스가 바깥 클래스의 인스턴스에 완전히 소속된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Outer(private val value: Int) {
    fun print() {
        println(this.value)
    }

    inner class Inner(private val innerValue: Int) {
        fun print() {
            this@Outer.print()
            println(this.innerValue + this@Outer.value)
        }
    }
}

fun main(args: Array<String>) {
    val innerInstance: Outer.Inner = Outer(610).Inner(40)

    innerInstance.print()
}
  • 내부 클래스는 this@Outer 키워드를 이용하여 자신이 속한 바깥 클래스의 인스턴스에 접근할 수 있다.

101. 데이터 클래스 (Data Class)

  • 코틀린은 데이터에 특화된 클래스를 선언할 수 있는 문법을 제공한다.
  • 클래스를 데이터 클래스로 선언하면, 다음과 같은 이점이 생긴다.
    • Any 클래스에 들어있는 equals, hashCode, toString 멤버 함수가 자동으로 오버라이딩 된다.
    • equals 멤버 함수는 각 프로퍼티의 값이 서로 모두 같으면 true를 반환한다.
    • 객체를 복사하는 copy 함수가 자동으로 선언된다.
  • 클래스를 데이터 클래스로 선언하기 위해선 다음의 규칙들을 지켜야 한다.
    • 적어도 하나의 프로퍼티를 가져야 한다.
    • 프로퍼티에 대응하지 않는 생성자 매개변수를 가질 수 없다.
    • abstract, open, sealed, inner 키워드를 붙일 수 없다.
    • 인터페이스만 구현할 수 있다. 코틀린 1.1 버전부터는 sealed 클래스도 상속 가능하다.
    • component1, component2, … 와 같은 이름으로 멤버 함수를 선언할 수 없다. 컴파일러가 내부적으로 사용하는 이름이기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
data class Employee(val name: String, val age: Int, val salary: Int)

fun main(args: Array<String>) {
    val first = Employee("a", 30, 2000)
    val second = Employee("b", 24, 5000)
    val third = first.copy()

    println(first.toString())
    println(third.toString())
    println(first == second) // false
    println(first == third) // true
 	  println(first === third) // false
}

102. 객체 분해하기

  • 데이터 클래스의 인스턴스에 한해, 객체를 여러 개의 변수로 쪼개는 것이 가능하다.
1
2
3
4
5
6
data class Employee(val name: String, val age: Int, val salary: Int)

fun main(args: Array<String>) {
    val(name, _, salary) = Employee("a", 20, 2000)
    println("$name $salary")
}

103. 함수 리터럴과 람다식

1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
    val instantFunc: (Int) -> Unit
    instantFunc = { number: Int ->
        println("hello $number")
    }

    instantFunc(33)
    instantFunc.invoke(33)
}
  • 위와 같이 함수를 저장할 수 있는 타입을 함수 타입이라고 한다.
  • 함수 타입의 변수는 invoke 멤버 함수를 통해서도 호출할 수 있다. instanceFunc?.invoke(33)와 같이 쓸 수 있으므로 null 처리를 하기 편해진다.

104. 익명 함수 (Anonymous Function)

1
2
3
4
5
6
7
8
fun main(args: Array<String>) {
    val instantFunc: (Int) -> Unit = fun(number: Int): Unit {
        println("hello $number")
    }

    instantFunc(33)
    instantFunc.invoke(33)
}
  • 람다식으로 된 함수 리터럴을 익명 함수의 형태로도 표현할 수 있다.

105. it 식별자

1
2
3
4
5
6
7
fun main(args: Array<String>) {
    val instantFunc: (Int) -> Unit = {
        println("hello $it")
    }

    instantFunc(33)
}
  • 매개변수를 생략하면 it이라는 특별한 식별자가 만들어진다. 여기서 it은 생략한 Int 타입의 매개변수를 대체한다.

106. 함수 참조 (Function Reference)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun plus(a: Int, b: Int) = println("plus 호출됨 ${a + b}")

object Object {
    fun minus(a: Int, b: Int) = println("object minus 호출됨 ${a - b}")
}

class Class {
    fun average(a: Int, b: Int) = println("class average 호출됨 ${(a + b) / 2}")
}

fun main(args: Array<String>) {
    var instantFunc: (Int, Int) -> Unit
    instantFunc = ::plus
    instantFunc(60, 28) // 88

    instantFunc = Object::minus
    instantFunc(60, 28) // 32

    instantFunc = Class()::average
    instantFunc(60,28) // 44
}
  • 함수 타입의 변수는 이미 선언되어 있는 함수나 객체의 멤버 함수를 가리킬 수도 있다.

107. 고차 함수

  • 고차 함수란, 인수로 함수를 받거나, 함수를 반환하는 함수를 뜻한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun decorate(task: () -> Unit) {
    println("=== 작업 시작 ===")
    task()
    println("=== 작업 완료 ===")
}

fun main(args: Array<String>) {
    decorate {
        val a = 10
        val b = 5

        println("$a + $b = ${a + b}")
    }
}

108. 클로저 (Closure)

  • 클로저는 내부 scope를 뛰어 넘어서 하위 함수가 상위 함수의 접근할 수 있는 것을 말한다.
1
2
3
4
5
6
7
fun main(args: Array<String>) {
    var name = "kim"
    fun closureTest() {
        println(name)   //close over 접근  
    }
    closureTest()   //kim deokhwa 출력  
}
  • 리터럴이 만들어지는 순간, 함수 리터럴은 자기 주변의 상황을 함께 저장한다. 즉, 함수가 만들어질 때 name 매개변수의 값을 복사해 갖고 있는다.

109. 리시버가 붙은 함수 리터럴

  • 함수 리터럴에 리시버를 적용하여 확장 함수처럼 만들 수 있다.
1
2
3
4
5
6
7
8
fun main(args: Array<String>) {
    val min: Int.(Int) -> Int = { value ->
        if (this < value) this
        else value
    }
    
    println(25.min(20))
}

110. 제네릭 (Generic)

1
2
3
4
5
6
fun <T> toFunction(value: T): () -> T = { value }

fun main(args: Array<String>) {
    val func: () -> Int = toFunction<Int>(1170)
    println(func())
}

111. 여러 타입을 인수로 받기

1
2
3
4
5
6
fun <T, R> toFunction(value1: T, value2: R): () -> T = { value1 }

fun main(args: Array<String>) {
    val func: () -> Int = toFunction<Int, String>(1170, "test")
    println(func())
}

112. 구체화된(Reified) 타입 매개변수

  • 타입 매개변수는 is 연산자의 피연산자로 사용할 수 없다.
1
2
3
4
5
fun <T> check() {
    val number = 0
    if (number is T) // 오류
        println("T는 Int 타입입니다.")
}
  • 타입 매개변수를 is 연산자의 피연산자로 사용하고 싶으면 함수는 inline으로 선언 한 뒤, 타입 매개변수 앞에 reified를 붙여주면 된다.
1
2
3
4
5
inline fun <reified T> check() {
    val number = 0
    if (number is T)
        println("T는 Int 타입입니다.")
}

113. 클래스와 인터페이스에서 제네릭 사용하기

1
2
3
class Pair<A, B> (val first: A, val second: B){
    override fun toString() = "$first $second"
}

114. 제네릭이 적용된 클래스 / 인터페이스 상속, 구현하기

1
2
3
4
5
6
7
8
9
10
interface Plusable<T> {
    operator fun plus(other: T): T
}

class Rectangle(val width: Int, val height: Int) : Plusable<Rectangle> {
    
    override fun plus(other: Rectangle): Rectangle {
        return Rectangle(width + other.width, height + other.height)
    }
}

115. 특정 타입을 상속, 구현하는 타입만 인수로 받기

  • 특정 타입을 구현하는 타입만 인수로 받으려면, 상속을 할 때처럼 타입 매개변수 뒤에 :타입이름 을 적어준다.
  • : 타입1, 타입2 와 같이 적으면 여러 개를 지정할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface ValueContainer {
    fun getValue(): Int
}

class AAA : ValueContainer {
    override fun getValue(): Int {
        return 1102
    }
}

fun <T : ValueContainer> T.printValue() {
    println(this.getValue())
}

fun main(args: Array<String>) {
    AAA().printValue()
}

116. in / out 키워드

  • out T는 자바의 ? extends T와 같고, in T는 자바의 ? Super T와 같다.
  • 타입 인수를 *로 지정하면, 타입 인수가 무엇이든 상관없이 AAA 타입을 대입할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
class AAA<out T>
class BBB<in T>

fun main(args: Array<String>) {
    val aaaSub = AAA<Int>()
    val aaaSup: AAA<Any> = aaaSub

    val bbbSub = BBB<Any>()
    val bbbSup: BBB<Int> = bbbSub
  
  	val star: AAA<*> = aaaSub
}

117. .. 연산자와 범위 표현식

  • .. 연산자는 범위를 표현하는 연산자이다.
1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
    val intRange: IntRange = 1..10
    intRange.forEach { print("$it ") }

    val charRange: CharRange = 'A'..'Z'
		if ('B' in charRange) {
        println("대문자입니다.")
    }  
}
  • CharRange는 유니코드를 기반으로 범위를 표시한다.

118. 반복자 (Iterator)

1
2
3
4
5
6
7
8
fun main(args: Array<String>) {
    val range = 1..10
    val iter = range.iterator()

    while(iter.hasNext()){
        println(iter.next())
    }
}

119. 흐름 제어 - 반복문 for

  • 코틀린의 for문은 for-each 스타일만 지원한다. C 스타일의 전통적인 for문은 지원하지 않는다.
1
2
3
4
5
fun main(args: Array<String>) {
    for(i in 1..10){
        print("$i ")
    }
}

120. 배열 (Array)

1
2
3
4
5
6
7
8
9
10
fun main(args: Array<String>) {
    val integers: Array<Int> = arrayOf(10, 20, 30) // 배열 초기화

    println(integers.size)
    println(integers[2])

    for (i in integers) {
        print("$i ")
    }
}
1
2
val size = 10
var array = Array(size) { 0 } // 0으로 초기화
  1. 배열을 가변 인수로 활용하기
  • vararg 키워드를 사용하여 배열 속에 들어있는 원소들을 가변 인수로 활용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
fun printAll(vararg tokens: String) {
    for (token in tokens) {
        print("$token ")
    }
}

fun main(args: Array<String>) {
    val numbers = arrayOf("what's", "your", "name?")
    printAll(*numbers)

    printAll("hello", "kotlin")
}
  • vararg 키워드는 java의 … 키워드와 동일한 역할을 한다.
1
2
void printAll(String... tokens){
}
  1. 열거 클래스 (Enum)
  • 열거 클래스는 정해진 집합 내의 값을 표현하기 위해 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum class Mode {
    SELECTION, PEN, SHAPE, ERASER
}

fun main(args: Array<String>) {
    val mode: Mode = Mode.PEN

    when (mode) {
        Mode.SELECTION -> println("선택 모드")
        Mode.PEN -> println("펜 모드")
        Mode.SHAPE -> println("도형 모드")
        Mode.ERASER -> println("지우개 모드")
    }
}
  • 열거 클래스에 들어가는 식별자를 열거 상수(Enum Constant)라고 한다.
  1. 열거 클래스에 프로퍼티와 멤버 함수 선언하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum class Mode(val number: Int) {
    SELECTION(0),
    PEN(1),
    SHAPE(2),
    ERASER(3);

    fun printNumber() {
        println("모드 $number")
    }
}

fun main(args: Array<String>){
    val mode: Mode = Mode.ERASER

    println(mode.number) // 3
    mode.printNumber() // 모드 3
}
  • 마지막 열거 상수 끝에 세미콜론을 반드시 붙여야 한다.
  1. 열거 클래스 활용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum class Mode {
    SELECTION, PEN, SHAPE, ERASER
}

fun main(args: Array<String>) {
    val shapeMode: Mode = Mode.SHAPE
    println(shapeMode.name) // 열거 상수의 이름
    println(shapeMode.ordinal) // 열거 상수의 순서

    val modes: Array<Mode> = Mode.values() // 모든 열거 상수들을 배열로 반환
    for(mode in modes){
        println(mode)
    }

    println(Mode.valueOf("PEN").ordinal)
}
  1. sealed 클래스
  • sealed 클래스는 자신의 중첩 클래스에만 상속을 허용하는 클래스이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sealed class Outer {
    class One : Outer()
    class Two : Outer()
}

class Three : Outer() // 1.1 버전 이후로는 같은 파일 내에서 가능

fun main(args: Array<String>) {
    val instance: Outer = Outer.One()

    val text: String = when (instance) {
        is Outer.One -> "첫 번째"
        is Outer.Two -> "두 번째"
        is Three -> "세 번째"
    }
}
  1. 위임된 프로퍼티 (Delegated Property)
  • 코틀린에서는 Getter / Setter 구현을 다른 객체에 맡길 수 있는 문법을 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Sample {
    var number: Int by OnlyPositive()
}

class OnlyPositive {
    private var realValue = 0

    operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return realValue
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        realValue = if (value > 0) value else 0
    }
}

fun main(args: Array<String>){
    val sample = Sample()

    sample.number = -50
    println(sample.number)

    sample.number = 100
    println(sample.number)
}
  • 프로퍼티를 대리하는 객체는 아래의 두 함수를 멤버 함수로 갖고 있어야 한다.
1
2
operator fun getValue(thisRef: Any?, property: KProperty<*>): T
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
  1. 클래스 위임 (Class Delegation)
  • 코틀린에서는 인터페이스의 구현을 다른 클래스에 맡길 수 있는 문법도 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Interface {
    fun abc()
}

class ClassDelegator : Interface {
    override fun abc() {
        println("기본 구현")
    }
}

class Sample : Interface by ClassDelegator()

fun main(args: Array<String>) {
    Sample().abc()
}
  1. Pair 클래스 : 두 변수를 하나로 묶기
  • Pair 클래스를 이용하면 두 변수를 하나로 묶을 수 있다.
1
2
3
4
5
6
fun divide(a: Int, b: Int): Pair<Int, Int> = Pair(a / b, a % b)

fun main(args: Array<String>) {
    val (q, r) = divide(10,3)
    println("몫: $q, 나머지: $r")
}
  1. to 확장 함수 : 두 값을 간단히 Pair로 묶기
  • to는 모든 타입에 적용되는 확장 함수이다.
  • to 확장 함수를 이용하여 Pair 객체를 간단히 생성할 수 있다.
1
2
3
4
5
fun main(args: Array<String>) {
    val test: Pair<Int, Double> = 10 to 3.14
  
  	println(test) // (10, 3.14)
}
  1. Triple 클래스 : 세 변수를 하나로 묶기
  • Triple 클래스는 제네릭을 이용하여 세 가지 타입의 값을 보관한다.
1
2
3
4
5
6
7
8
9
fun calculateCircle(r: Int): Triple<Int, Double, Double> =
    Triple(2 * r, 2 * r * 3.14, r * r * 3.14)

fun main(args: Array<String>) {
    val (diameter, _, area) = calculateCircle(5)

    println("지름 : $diameter")
    println("넓이 : $area")
}
  1. Comparable 인터페이스 : 클래스를 비교 가능하게 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Node(var value: Int) : Comparable<Node> {
    override fun compareTo(other: Node): Int =
        when {
            this.value < other.value -> -1
            this.value > other.value -> 1
            else -> 0
        }
}

fun main(args: Array<String>) {
    val node1 = Node(10)
    val node2 = Node(20)

    println(node1 < node2) // true
}
  1. ClosedRange 인터페이스 : 닫힌 구간을 표현하는 인터페이스
1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
    val intRange = 1..10
    val longRange = 1L..100L
    val floatRange = 1.0..2.0
    val charRange = 'A'..'Z'

    println(intRange.start) // 1
    println(longRange.endInclusive) // 100
    println(floatRange.contains(1.1)) // true
    println(charRange.isEmpty()) // false
}
  • ClosedFloatRange와 ClosedDoubleRange 클래스는 in 연산자를 사용할 수 없다.
  1. Iterable 인터페이스 : 클래스가 반복자를 지원하도록 하기
1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
    val prog: IntProgression = 3..7

    println(prog.first) // 3
    println(prog.last) // 7
    println(prog.step) // 1

  	prog.forEach { print("$it ") } // 3 4 5 6 7
}
  • step은 반복자의 next를 호출할 때, 몇 칸씩 건너뛸 것인지를 나타내는 프로퍼티이다.
  1. Progression과 관련된 함수
1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
    val prog1 = 7 downTo 3 // 7 6 5 4 3
    val prog2 = (3..7).reversed() // 7 6 5 4 3
    println(prog1 == prog2) // true
  
    val prog3 = (1..10) step 3 // 1 4 7 10
    val prog4 = 10 downTo 2 step 3 // 10 7 4
    val prog5 = 2 until 5 // 2 3 4
}
  1. 코틀린의 컬렉션 (Collection)
  • 컬렉션이란 프로그램을 개발하는데 필요한 기본적인 자료구조들을 통칭하는 말이다.
    • List : 순서가 있는 목록을 표현하는 자료구조
    • Set : 집합을 표현하는 자료구조. 원소의 중복을 허용하지 않으며, 각 원소는 순서를 갖지 않는다.
    • Map : 연관 배열을 표현하는 자료구조. 키와 값을 1:1로 대응시켜 저장한다.
  • List와 Set은 Collection 인터페이스를 상속하고, Map은 상속하지 않는다.

  • 참고 도서

초보자를 위한 Kotlin 200제