References: Do it! 코틀린 프로그래밍

코틀린에서의 제네릭에 대해 알아봅니다.

 

 

1. 제네릭

 

제네릭(Generic)은 클래스 내부에서 사용할 자료형을 나중에 인스턴스를 사용할 때 확정합니다.

자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 타임에 자료형을 검사해 적당한 자료형을 선택할 수 있도록 하기 위해 제네릭이 도입되었습니다.

제네릭을 사용하면 객체의 자료형을 컴파일 타임에 체크하므로 객체 자료형의 안정성을 높이고 형 변환의 번거로움이 줄어듭니다.

 

제네릭은 앵글 브래킷(<>) 사이에 형식 매개변수를 넣어 선언합니다.

이때 형식 매개변수는 하나 이상 지정할 수 있습니다.

형식 매개변수는 자료형을 대표하는 용어로 T와 같은 특정 영문의 대문자로 사용합니다. 이는 나중에 필요한 자료형으로 대체됩니다.

 

제네릭은 다양한 자료형을 다워랴하는 컬렉션에 많이 사용됩니다.

컬렉션은 List, Set, Map등으로 다수의 데이터를 다루는 특별한 클래스로 제네릭을 사용해 정의되어 있습니다.

 

다음 예시를 통해 제네릭에 대해 알아보겠습니다.

 

class Box<T>(t: T) { var name = t }
fun main() {
    val box1: Box<Int> = Box<Int>(1) // Box(1)
    val box2: Box<String> = Box<String>("Hello") // Box("Hello")
    println(box1.name)
    println(box2.name)
}

 

  • Box<T>(t: T): 제네릭 사용. 형식 매개변수로 받은 인자를 name에 저장.

 

위 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

형식 매개변수는 T입니다. T는 보통 Type을 줄인 것을 의미하며 강제적인 것은 아닙니다.

형식 매개면수는 관습적으로 다음을 사용합니다.

  • 요소(Element) - E
  • 키(Key) - K
  • 숫자(Number) - N
  • 형식(Type) - T
  • 값(Value) - V

 

앞선 예제에서의 Box<T>를 제네릭 클래스라고 합니다.

제네릭 클래스는 형식 매개변수를 하나 이상 받는 클래스입니다.

클래스를 선언할 때 자료형을 특정하지 않고 인스턴스를 생성하는 시점에서 자료형을 정합니다.

 

 

2. 자료형 변환

 

제네릭 클래스는 가변성을 지정하지 않으면 형식 매개변수에 상, 하위 클래스가 지정되어도 서로 자료형이 변환되지 않습니다.

다음 코드를 통해 확인해 보겠습니다.

 

open class Parent
class Child: Parent()
class Cup<T>
fun main() {
    val obj1: Parent = Child()
    val obj2: Child = Parent()
    val obj3: Cup<Parent> = Cup<Child>()
    val obj4: Cup<Child> = Cup<Parent>()
    val obj5 = Cup<Child>()
    val obj6: Cup<Child> = obj5
}

 

  • obj1: Parent 형식은 Child의 자료형으로 변환될 수 있습니다.
  • obj2: 자료형이 불일치해 오류가 발생합니다.
  • obj3: 자료형이 불일치해 오류가 발생합니다. 
  • obj4: 자료형이 불일치해 오류가 발생합니다.
  • obj5: obj는 Cup<Child> 자료형이 됩니다.
  • obj6: 자료형이 일치해 오류가 발생하지 않습니다.

 

obj3, obj4에서 보듯이 제네릭 클래스에서는 형식 매개변수인 T에 상위와 하위 클래스를 지정해도 서로 관련 없는 형식이 되어 형식이 일치하지 않는 오휴가 발생합니다.

또한 제네릭 형식 매개변수는 기본적으로 Null을 허용합니다. 만약 Null을 허용하지 않게 하고 싶다면 T: Any 형식을 사용하면 됩니다.

 

 

3. 제네릭 함수 및 메서드

 

형식 매개변수를 받는 함수나 메서드를 제네릭 함수 또는 메서드라고 합니다.

제네릭 함수나 메서드는 해당 함수나 메서드 앞에 <T>와 같이 형식 매개변수를 지정합니다.

<K, V>처럼 여러 형식 매개변수를 사용할 수도 있습니다.

자료형의 결정은 함수가 호출될 때 컴파일러가 자료형을 추론할 수 있습니다.

 

다음 예시를 통해 확인해 보도록 하겠습니다.

 

fun <T> find(a: Array<T>Target: T): Int{
    for( i in a.indices ) {
        if ( a[i] == Target ) { return i }
    }
    return -1
}
fun main() {
    val arr1: Array<String> = arrayOf("Apple", "Banana", "Cherry", "Durian")
    var arr2: Array<Int> = arrayOf(1, 2, 3, 4)
    println("arr.indices ${arr1.indices}")
    println(find<String>(arr1, "Cherry"))
    println(find(arr2, 2))
}

 

  • fun <T> find: 제네릭 함수
  • Array<T>: 배열을 위한 클래스
  • a.indices: a배열의 유효 범위

 

위의 코드를 실행시키면 다음과 같은 결과를 확인할 수 있습니다.

 

 

4. 제네릭과 람다식

 

형식 매개변수로 선언된 함구의 매개 변수를 연산할 경우 자료형을 경정할 수 없어 오류가 발생합니다.

 

fun <T> add ( a: T, b: T): T { 
    return a + b // 자료형을 아직 결정할 수 없어 오류 발생. 
}

 

하지만 람다식을 매개변수로 받으면 자료형을 결정하지 않아도 실행 시 람다식 본문을 넘겨줄 때 결정되므로 이런 문제를 해결할 수 있습니다.

 

다음과 같이 코드를 작성해 봅시다.

 

fun <T> add( a: T, b: T, op: ( T, T ) -> T ): T{
    return op( a, b )
}
fun main() {
    val result = add2, 3, { a, b -> a + b } )
    println(result)
}

 

  • op: ( T, T ) -> T: 매개변수에 람다식을 받습니다.
  • { a, b -> a + b }: 매개변수에 람다식을 전달합니다.

 

람다식은 add 함수가 실행될 때 넘겨지는 인자이므로 연산식을 함수 선언부에 직접 구현하지 않고 전달하는 방법을 사용합니다. 따라서 함수의 형식 매개변수의 자료형을 특정하지 않아도 실행 가능합니다.

 

함루의 람다식 매개변수를 좀 더 읽기 좋게 단순화할 수 있습니다.

다음 예제 코드는  typealias를 사용해 람다식 매개변수에 다른 이름을 사용한 예시입니다.

 

typealias arithmetic<T> = ( T, T ) -> T
fun <T> add( a: T, b: T, op: arithmetic<T> ): T{
    return op( a, b )
}
fun main() {
    val sumInt1: (Int, Int) -> Int = { a, b -> a + b }
    val sumInt2 = { a: Int, b: Int -> a + b
}

    val result = add(2, 3, { a, b -> a + b } )
    val result1 = add( 2, 3, sumInt1 )
    val result2 = add( 2, 3, sumInt2 )

    println(result)
    println(result1)
    println(result2)
}

 

  • typealias: typealias를 사용해 람다식 매개변수를 단순화하였습니다.
  • sumInt1, sumInt2: 람다식을 간소화 후 변수에 저장해 사용하였습니다.

 

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 37. 제네릭  (0) 2019.09.17
[Kotlin] 36. 연산자 오버로딩  (0) 2019.09.16
[Kotlin] 35. 어노테이션 클래스  (0) 2019.09.16
[Kotlin] 34. 봉인 클래스와 열거형 클래스  (0) 2019.09.16
[Kotlin] 33. 내부 클래스  (0) 2019.09.16
[Kotlin] 32. 데이터 클래스  (0) 2019.09.12

References: Do it! 코틀린 프로그래밍

코틀린에서 연산자를 오버 로딩하는 법에 대해 알아봅니다.

오버 로딩에 대한 내용은 여기서 확인해 주세요.

 

 

1. 연산자 오버 로딩

 

말 그대로 오버 로딩을 연산자에 적용하는 것을 말합니다.

사실 코틀린에선 이미 여러 연산자가 많이 오버 로딩되어 있습니다.

 

'+'를 예시로 살펴보겠습니다.

지금까지 +연산을 직접 구현한 적이 있나요?

사실 A + B는 A.plus(B)와 같습니다. A와 B엔 다양한 자료형이 올 수 있습니다.

코틀린은 편의를 위해 이미 여러 자료형에 대해 오버 로딩을 구현해 두었습니다.

Byte, Short, Int, Long, Float, Double, Any 등 이미 오버 로딩된 '+'를 바로 사용할 수 있습니다.

 

만약 사용자가 정의한 객체에 대해 '+'연산을 수행하고 싶으면 어떻게 해야 할까요?

이런 경우에 연산장 오버 로딩이 필요합니다.

다음 예시를 통해 직접 정의한 클래스에 대해 연산자 오버 로딩을 해 봅시다.

 

class Point(var x: Int = 0, var y: Int = 0) {
    operator fun plus(p: Point): Point { return Point( x + p.x, y + p.y) }
}
fun main() {
    val p1 = Point(3, -8)
    val p2 = Point(2, 9)
    var point = Point()
    point = p1 + p2
    println("point = (${point.x}, ${point.y})")
}

 

Point 클래스에 대해서 plus 연산자를 오버 로딩하였습니다.

위의 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

 

* 연산자의 종류

  • a + b = a.plus(b)
  • a - b = a.minus(b)
  • a * b = a.times(b)
  • a / b = a.div(b)
  • a % b = a.rem(b)
  • a .. b = a.rangeTo(b)
  • ...

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 37. 제네릭  (0) 2019.09.17
[Kotlin] 36. 연산자 오버로딩  (0) 2019.09.16
[Kotlin] 35. 어노테이션 클래스  (0) 2019.09.16
[Kotlin] 34. 봉인 클래스와 열거형 클래스  (0) 2019.09.16
[Kotlin] 33. 내부 클래스  (0) 2019.09.16
[Kotlin] 32. 데이터 클래스  (0) 2019.09.12

References: Do it! 코틀린 프로그래밍

코틀린에서의 어노테이션 클래스에 대해 알아봅니다.

 

 

1. 어노테이션(Annotation) 클래스

 

어노테이션은 코드에 부가 정보를 추가하는 역할을 합니다.

@기호와 함께 나타내는 표기법입니다.

주로 컴파일러나 프로그램 런타임에서 사전 처리를 위해 사용합니다.

 

 

2. 어노테이션의 사용

 

사용자 정의 어노테이션을 만들기 위해서는 annotation 키워드를 사용해 클래스를 선언합니다.

선언한 어노테이션 클래스는 @기호와 함께 사용할 수 있습니다.

 

annotation class AnnoTest
...
@AnnoTest class MyClass{ ... }

 

어노테이션은 다음과 같은 속성을 사용해 정의될 수 있습니다.

  • @Target : 어노테이션이 지정되어 사용할 종류를 정의
  • @Retention : 어노테이션을 컴파일된 클래스 파일에 저장할 것인지 런타임에 반영할 것인지 정의
  • @Repeatable : 어노테이션을 같은 요소에 여러번 사용 가능하게 할지를 정의
  • @MustBeDocumented : API의 일부분으로 문서화하기 위해 사용.

위와 같은 속성과 함께 정의된 어노테이션 클래스의 예는 다음과 같습니다.

 

@Target( AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
             AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION )
@Retention( AnnotationRetention.SOURCE ) // or RUNTIME
@MustBeDocumented
annotation class AnnoTest

 

 

3. 어노테이션의 위치

 

어노테이션이 들어갈 수 있는 위치는 다음과 같습니다.

 

@AnnoTest class MyClass {
    @AnnoTest fun myMethod( @AnnoTest myProperty: Int): Int {
        return( @AnnoTest 1 )
    }
}

 

어노테이션은 클래스앞, 메서드 앞, 프로퍼티 앞에서 사용 가능하며 반환 시 반환 값 앞에 표기하고 소괄호로 감싸줍니다.

 

만약 생성자 앞에 어노테이션을 사용하면 constructor 키워드를 반드시 명시해야 합니다.

 

class MyClass @AnnoTest constructor( dependency: MyDependency ) { ... }

 

어노테이션은 게터/세터에서도 사용할 수 있습니다.

 

class MyClass {
    var x: MyDependendcy? = null
        @AnnoTest set
}

 

 

4. 어노테이션의 매개변수와 생성자.

 

어노테이션에 매개변수를 지정하려면 다음과 같이 생성자를 통해 지정할 수 있습니다.

 

annotation class Special( val why: String )
@Special( "example" ) class MyClass{ ... }

 

매개변수로 사용될 수 있는 자료형은 다음과 같습니다.

  • 기본 자료형
  • 열거형
  • 문자형
  • 기타 어노테이션
  • 클래스
  • 위의 목록을 갖는 배열

 

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 37. 제네릭  (0) 2019.09.17
[Kotlin] 36. 연산자 오버로딩  (0) 2019.09.16
[Kotlin] 35. 어노테이션 클래스  (0) 2019.09.16
[Kotlin] 34. 봉인 클래스와 열거형 클래스  (0) 2019.09.16
[Kotlin] 33. 내부 클래스  (0) 2019.09.16
[Kotlin] 32. 데이터 클래스  (0) 2019.09.12

References: Do it! 코틀린 프로그래밍

코틀린에서의 봉인 클래스와 열거형 클래스에 대해 알아봅니다.

 

 

1. 봉인(Sealed) 클래스

 

실드(Sealed) 클래스는 미리 만들어 놓은 자료형들을 묶어서 제공합니다.

따라서 어떤 의미에서는 열거형(Enum) 클래스의 확장으로 볼 수도 있습니다.

 

실드 클래스는 sealed 키워드를 통해 선언할 수 있습니다.

실드 클래스는 추상 클래스와 같기 때문에 객체를 만들 수 없습니다.

실드 클래스는 private 이 아닌 생성자는 허용하지 않습니다.

실드 클래스는 같은 파일안에서만 상속을 허용합니다.

 

다음 예제를 통해 실드 클래스에 대해 알아보도록 하겠습니다.

 

sealed class Result{
    open class Success(val message: String): Result()
    class Error(val code: Int, val message: String): Result()
}
class Status: Result()
class Inside: Result.Success("Status")
fun eval(result: Result): String = when(result) {
    is Status -> "in progress"
    is Result.Success -> result.message
    is Result.Error -> result.message
}
fun main() {
    val result = Result.Success("Good!")
    val msg = eval(result)
    println(msg)
}

 

  • sealed class Result: 실드 클래스를 선언합니다.
  • class Stauts: Result(): 실드 클래스를 상속합니다. 같은 파일에서만 가능합니다.
  • class Inside: Result.Success("Status"): 내부 클래스 상속

위의 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

실드 클래스는 특정 객체 자료형에 따라 when문과 is에 의해 선택적으로 실행할 수 있습니다.

 

 

2. 열거형 클래스

 

열거형 클래스는 여러 상수를 선언하고 열거된 값을 조건에 따라 선택할 수 있는 특수한 클래스입니다.

 

열거형 클래스는 실드 클래스처럼 다양한 자료형을 다룰 순 없습니다.

열거형 클래스는 enum 키워드와 함께 선언됩니다.

열거형 클래스의 상수값은 매개변수를 통해 초기화될 수 있습니다.

열거형 클래스의 상수값의 끝은 세미콜론을 통해 알립니다.

열거형 클래스는 필요한 경우 메서드를 포함할 수 있습니다.

 

다음 코드를 통해 열거형 클래스에 대해 알아보도록 합시다.

 

enum class Color(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);
    fun rgb() = (r * 256 + g) * 256 + b
}
fun getColor(color:Color) = when(color) {
    Color.RED -> color.name
    Color.ORANGE -> color.ordinal
    Color.YELLOW -> color.toString()
    Color.GREEN -> color
    Color.BLUE -> color.r
    Color.INDIGO -> color.g
    Color.VIOLET -> color.rgb()
}
fun main() {
    println(Color.BLUE.rgb())
    println(getColor(Color.BLUE))
}

 

  • enum class Color: enum 키워드를 이용해 열거형 클래스를 선언합니다.
  • VIOLET(238, 130, 238); : 세미콜론을 통해 끝을 알려줍니다.

위의 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

열거형 클래스에서 인터페이스의 메서드를 구현할 수도 있습니다.

다음 예제를 통해 인터페이스의 메서드를 구현하는 방법을 알아봅시다.

 

interface ScoreScore { fun getScore(): Int}
enum class MemberType(var prio: String): Score {
    NORMAL("Third"){ override fun getScore(): Int = 100},
    SILVER("Second"){ override fun getScore(): Int = 500},
    GOLD("First"){ override fun getScore(): Int = 1500}
}
fun main() {
    println(MemberType.NORMAL.getScore())
    println(MemberType.GOLD)
    println(MemberType.valueOf("SILVER"))
    println(MemberType.SILVER.prio)
    for( grade in MemberType.values() ){ println("grade.name = ${grade.name}, prio = ${grade.prio}") }
}

 

  • interface Score: 인터페이스를 선언합니다.
  • enum class MemberType: 인터페이스를 구현하는 열거형 클래스입니다.
  • for( grade in MemberType.values() ): 열거형 클래스의 모든 값을 가져오는 반복문입니다.

위의 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

 

 

 

 

 

References: Do it! 코틀린 프로그래밍

코틀린에서의 내부 클래스에 대해 알아봅니다.

 

 

1. 내부 클래스

 

내부 클래스는 클래스 내부에 클래스를 다시 정의하는 것을 말합니다.

내부 클래스는 독립적인 클래스로 정의하기 모호한 경우나 한 클래스의 내부에서만 사용하고 외부에선 접근할 필요가 없는 클래스를 정의하기 위해 사용합니다.

단, 이러한 내부 클래스는 남용하면 클래스의 의존성이 커지고 가독성이 저하되므로 주의가 필요합니다.

 

 

2. 중첩(Nested) 클래스

 

코틀린에서 내부 클래스를 정의하기 위한 방법 중 하나입니다.

코틀린에서 중첩 클래스는 기본적으로 적적 클래스처럼 다뤄집니다. 즉 객체 생성 없이 접근이 가능합니다.

 

다음 예제를 통해 확인해 보도록 하겠습니다.

 

class Outer{
    val ov = 5
    class NestedNested {
        val nv = 10
        fun greeting() = "[Nested] Hello! $nv"
    }
    fun outside() {
        val msg = Nested().greeting()
        println("[Outer] $msg, ${Nested().nv}")
    }
}
fun main() {
    val output = Outer.Nested().greeting()
    println(output)
    val outer = Outer()
    outer.outside();
}

 

  • class Nested: 중첩 클래스를 정의합니다.
  • $nv: 내부의 nv는 접근 가능하나 외부의 ov는 접근할 수 없습니다.
  • Nested().greetring: 정적 클래스 취급으로 객체 생성 없이 사용 가능합니다.
  • val outer = Outer(): 외부 클래스는 객체를 생성해야 접근 가능합니다.

위 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

 

만약 외부 내부 클래스에서 외부 클래스에 접근하고 싶으면 외부 클래스에 컴패니언 객체를 정의하면 됩니다.

컴패니언 객체는 static처럼 접근이 가능하므로 중첩 클래스에서 접근이 가능합니다.

 

예를 들어 다음과 같은 경우엔 중첩 클래스에서 외부 클래스의 프로퍼티에 접근할 수 있습니다.

 

class Outer{
    val ov = 5
    class Nested{
        val nv = 10
        fun gretting() = "[Nested] Hello! $nv"
        fun accessOuter() = println("Country: $country")
    }
    fun outside() {
        val msg = Nested().gretting()
        println("[Outer] $msg, ${Nested().nv}")
    }
    companion object{
        const val country = "Korea"
    }
}

 

  • const val country: 외부에 존재하는 클래스에 컴패니언 객체를 선언하였습니다.

위와 같이 컴패니언 객체 내부의 프로퍼티는 중첩 클래스에서 접근할 수 있습니다.

   

 

3. 이너(Inner) 클래스

 

이너 클래스는 inner라는 선언자를 통해 정의할 수 있습니다.

inner를 통해 선언된 이너 클래스는 외부 클래스의 멤버에 접근할 수 있습니다.

심지어 private멤버까지도 사용할 수 있습니다.

 

다음 예시를 통해 확인해 보도록 하겠습니다.

 

class SmartPhone(val model: String){
    private val cpu = "Snapdragon"
    inner class ExternalStorage(val size: Int){
        fun getInfo() = "${model}: Installed on $cpu with ${size}${size} GB"
    }
}
fun main() {
    val mySdcard = SmartPhone("Note9").ExternalStorage(256)
    println(mySdcard.getInfo())
}

 

  • inner class ExternalStorage: 이너 클래스에서는 외부 클래스의 멤버에 접근 가능합니다.

위의 코드를 실행시키면 다음과 같은 결과를 확인할 수 있습니다.

이너 클래스에선 외부 클래스의 private멤버를 사용할 수 있는 것을 확인할 수 있습니다.

 

 

4. 익명 객체

 

자바에서는 익명 이너 클래스를 통해 일회용 객체를 생성하였습니다.

코틀린에서는 object 키워드를 사용하는 익명 객체로 같은 기능을 수행할 수 있습니다.

자바와의 차이점은 다중 인터페이스를 통한 구현이 가능하는 것입니다.

 

다음 예제를 통해 확인해 보도록 하겠습니다.

 

interface SwitcherSwitcher { fun on(): String }
class SmartPhone(val model: String){
    private val cpu = "Snapdragon"
    inner class ExternalStorage(val size: Int){ fun getInfo() = "${model}: Installed on $cpu with ${size}GB" }
    fun powerOn(): String {
        class Led(val color: String){ fun blink(): String = "Blinking $color on $model" }
        val powerStatus = Led("Red")
        val powerSwitch = object: Switcher {
            override fun on(): String { return powerStatus.blink() }
        }
        return powerSwitch.on()
    }
}
fun main() {
    val myPhone = SmartPhone("Note9")
    myPhone.ExternalStorage(256)
    println(myPhone.powerOn())
}

 

  • interface switcher: 인터페이스 선언
  • object: Switcher: 익명 객체 사용
  • override fun on: 인터페이스의 메서드 구현
  • powerSwitch.on(): 익명 객체의 메서드 사용

위의 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

 

 

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 35. 어노테이션 클래스  (0) 2019.09.16
[Kotlin] 34. 봉인 클래스와 열거형 클래스  (0) 2019.09.16
[Kotlin] 33. 내부 클래스  (0) 2019.09.16
[Kotlin] 32. 데이터 클래스  (0) 2019.09.12
[Kotlin] 31. 인터페이스  (0) 2019.09.12
[Kotlin] 30. 추상 클래스  (0) 2019.09.05

References: Do it! 코틀린 프로그래밍

코틀린에서의 데이터 클래스에 대해 알아봅니다.

 

 

1. 데이터 클래스

 

데이터 클래스는 데이터 전달을 위한 클래스라고 말합니다. 

보통 이러한 객체를 DTO(Date Transfer Object)라고 부릅니다.

DTO는 구현 로직을 갖지 않고 순수한 데이터를 표현하기 때문에 보통 속성과 게터/세터를 갖습니다.

 

코틀린에서는 DTO를 위해 데이터 클래스를 정의할 때 게터/세터/toString/equal 등을 직접 구현할 필요 없이 자동으로 구현해 줍니다. 코틀린의 데이터 클래스 내부에선 다음과 같은 메서드를 자동으로 생성해 줍니다.

 

  • 프로퍼티를 위한 게터/세터
  • 비교를 위한 equals()
  • 키 사용을 위한 hashCode()
  • 프로퍼티를 문자열로 변환해 순서대로 보여주는 toString()
  • 객체 복사를 위한 copy()
  • 프로퍼티에 상응하는 component1, component2 등

DTO를 통해 일종의 표준과 같은 약속을 정하면 송신측과 수신 측 모두 데이터를 쉽게 다룰 수 있어 DTO를 사용합니다.

 

 

2. 데이터 클래스 선언 방법

 

데이터 클래스는 data라는 선언자를 이용해서 선언합니다.

데이터 클래스는 다음과 같은 조건을 만족해야 합니다.

  • 주 생성자는 하나 이상의 매개변수를 가져야 합니다.
  • 주 생성자의 모든 매개변수는 var 혹은 val로 지정된 프로퍼티여야 한다.
  • 데이터 클래스는 abstract, open, sealed, inner 키워드를 사용할 수 없다.

단, 필요하다면 부 생성자나 init을 사용해 간단한 로직을 포함할 수 있습니다.

 

다음 예시를 통해 DTO에 대해 알아보겠습니다.

 

data class Customer(var name: String, var email: String){
    var job: String = "Unknown"
    constructor(name: String, email: String, _job: String): this(name, email){ job = _job }
    init { /** Simple logic here. */ }
}
fun main() {
    val cus1 = Customer("smoh", "smoh@mail.com")
    val cus2 = Customer("smoh", "smoh@mail.com")
    println(cus1 == cus2)
    println(cus1.equals(cus2))
    println("${cus1.hashCode()}, ${cus2.hashCode()}")
    println(cus1.toString())
}

 

  • var name: Stirng, var email: String: 주 생성자를 통해 두 프로퍼티를 갖습니다.
  • constructor: 부 생성자를 통해 job 프로퍼티를 하나 더 갖습니다.
  • init: init에 간단한 로직을 포함하게 할 수 있습니다.
  • 이 외에도 코틀린은 자동으로 여러 메서드를 생성해 둡니다.

위의 코드를 수행하면 다음과 같은 결과를 확인할 수 있습니다.

위 처럼 우리가 직접 정의하지 않은 여러 메서드를 코틀린에서 자동으로 추가해주며 바로 사용할 수 있습니다

 

 

 

3. 객체 디스트럭처링.

 

디스트럭처링(Destructuring)은 객체가 갖고 있는 프로퍼티를 개별 변수로 분해하여 할당하는 것을 말합니다.

js의...(Spread syntax)의 구조 분해 할당과 같은 개념으로 보입니다.

 

다음 예시를 통해 디스트럭처링에 대해 알아보겠습니다.

 

data class Customer(var name: String, var email: String){
var job: String = "Unknown"
constructor(name: String, email: String, _job: String): this(name, email){ job = _job }
init { /** Simple logic here. */ }
}
fun myFunc(): Customer { return Customer("smoh92", "smoh92@mail.com") }
fun main() {
    val cus1 = Customer("smoh1", "smoh1@mail.com")
    val (name, email) = cus1
    println("name = $name, email = $email")

    val (_, email2) = cus1

    val name3 = cus1.component1()
    val email3 = cus1.component2()
    println("name = $name3, email = $email3")

    val cus2 = Customer("smoh2", "smoh2@mail.com")
    val customers = listOf(cus1, cus2)
    for((name, email) in customers){ println("name = $name, email = $email") }

    val(myName, myEmail) = myFunc()

    val myLamda = {
        (nameLa, emailLa): Customer ->
        println(nameLa)
        println(emailLa)
    }
    myLamda(cus1)
}

 

  • val (name, email) = cus1: 기본적인 디스트럭처링
  • val (_, email2) = cus1: 디스트럭처링 하지 않을 프로퍼티는 언더바로 표시합니다.
  • val name3 = cus1.component1(): 컴포넌트 순서대로 골라서 디스트럭처링 하는 방법입니다.
  • for((name, email) in customers): 디스트럭처링 해야할 대상이 많은 경우 루프 문을 이용할 수 있습니다.
  • val(myName, myEmail) = myFunc(): 리턴 값이 객체인 함수에 대해서도 디스트럭처링을 적용할 수 있습니다.
  • val myLamda: 람다식을 사용해 디스트럭처링 하는 방법입니다.

해당 예시코드를 수행하면 다음과 같은 결과를 확인할 수 있습니다.

 

 

 

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 34. 봉인 클래스와 열거형 클래스  (0) 2019.09.16
[Kotlin] 33. 내부 클래스  (0) 2019.09.16
[Kotlin] 32. 데이터 클래스  (0) 2019.09.12
[Kotlin] 31. 인터페이스  (0) 2019.09.12
[Kotlin] 30. 추상 클래스  (0) 2019.09.05
[Kotlin] 29. 컴패니언 객체  (0) 2019.09.01

References: Do it! 코틀린 프로그래밍

코틀린에서의 인터페이스에 대해 알아봅니다.

 

 

1. 인터페이스(Interface)

 

코틀린의 인터페이스에는 추상 메서드나 일반 메서드가 포함될 수 있습니다. 

다른 객체지향 언어와는 달리 메서드에 구현 내용이 포함될 수 있습니다. 단, 추상 클래스처럼 프로퍼티를 통해 상태를 지정할 수는 없습니다.

 

추상 클래스는 기본적으로 클래스이기 때문에 상속을 통해 하위로 확장이 가능합니다. 

그런데 하위 클래스는 상속을 하나만 허용합니다. 또한 상위 클래스와 강한 관계가 생기며 상위 클래스의 영향을 받게 됩니다.

 

반면에 인터페이스는 기본적으로 클래스가 아닙니다. 따라서 상속으로 하위 클래스에 프로퍼티와 메서드를 전달하지 않습니다. 상속받은 하위 클래스라고 하기보단 구현 클래스라고 합니다.

구현 클래스는 인터페이스와 강한 관계를 갖지 않습니다. 

또한 원하는 인터페이스 만큼 구현 클래스에 붙여서 필요한 메서드를 구현할 수 있습니다.

 

 

2. 인터페이스 선언 및 구현 방법.

 

인터페이스는 interface라는 선언자를 통해 선언합니다.

구현 클래스에서는 override 라는 선언자를 통해 메서드들을 구현합니다.

 

다음 예시를 통해 확인하겠습니다.

 

interface Pet{
    var category: String
    fun feeding()
    fun patting(){ println("Keep patting") }
}
class Cat(override var category: String): Pet{
    override fun feeding() { println("Feed the cat a tuna can!") }
}
fun main() {
    val obj = Cat("small")
    println("Pet category: ${obj.category}")
    obj.feeding()
    obj.patting()
}

 

  • var category / fun feeding: 인터페이스에서는 별도의 선언자 없어도 기본적으로 abstract로 선언됩니다.
  • fun patting: 단, 이와 같이 메서드를 구현하면 일반 메서드가 됩니다.
  • override var category / override fun feeding: 추상 프로퍼티와 클래스를 구현합니다.
  • : Pet: Pet 인터페이스를 구현하는 클래스임을 알려줍니다.

 

위의 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

 

 

 

3. 인터페이스의 필요성.

 

다음 예시를 통해 인터페이스의 필요성에 대해 알아보겠습니다.

우선 애완동물과 주인의 관계를 표현하는 코드를 작성해 보겠습니다.

 

open class Animal(val name: String)
interface Pet{
    var category: String
    fun feeding()
    fun patting(){ println("Keep patting") }
}
class Dog(name: String, override var category: String): Animal(name), Pet{
    override fun feeding() { println("Feed the dog a bone.") }
}
class Cat(name: String, override var category: String): Animal(name), Pet{
    override fun feeding() { println("Feed the cat a tuna can.") }
}
class Master {
    fun playWithPet(dog: Dog){ println("Enjoy with my dog.") }
    fun playWithPet(cat: Cat){ println("Enjoy with my cat") }
}
fun main(){
    val master = Master()
    val dog = Dog("Toto", "small")
    val cat = Cat("Coco", "BigFat")
    master.playWithPet(dog)
    master.playWithPet(cat)
}

 

  • Dog / Cat: Animal클래스를 상속받고 Pet 인터페이스를 구현하는 클래스
  • playWithPet: 함수 인자에 따른 함수 오버로딩

위의 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

여기서 만약 애완동물 종류가 늘어나면 어떻게 될까요? 애완동물마다 playWithPet함수를 새로 오버로딩해야 하는 문제가 생깁니다. 

 

이러한 문제를 피하기 위해 코드를 다음과 같이 수정해 봅시다.

 

open class Animal(val name: String)
interface Pet{
    var category: String
    var species: String
    fun feeding()
    fun patting(){ println("Keep patting") }
}
class Dog(name: String, override var category: String): Animal(name), Pet{
    override var species: String = "dog"
    override fun feeding() { println("Feed the dog a bone.") }
}
class Cat(name: String, override var category: String): Animal(name), Pet{
    override var species: String = "cat"
    override fun feeding() { println("Feed the cat a tuna can.") }
}
class Master {
    fun playWithPet(pet: Pet){ println("Enjoy with my ${pet.species}.") }
}
fun main(){
    val master = Master()
    val dog = Dog("Toto", "small")
    val cat = Cat("Coco", "BigFat")
    master.playWithPet(dog)
    master.playWithPet(cat)
}

 

위와 같이 수정하면 더 이상 애완동물을 추가 할 때 마다 새로운 함수를 오버로딩할 필요가 없어집니다.

기존의 Master 클래스는 각각의 애완동물 클래스에 의존적이었으나 이젠 더이상 의존적인 클래스가 아니게 됩니다.

이런 식으로 클래스의 독립성을 유지하는데 인터페이스가 큰 역할을 합니다.

 

 

 

4. 여러 인터페이스를 구현하는 방법

 

클래스는 하나의 클래스만 상속이 가능합니다.

하지만 인터페이스를 이용하면 다중 상속과 같은 형태로 여러 인터페이스를 구현할 수 있습니다.

 

다음 예시를 통해 확인해 보겠습니다.

 

interface Bird{
    val wings: Int
    fun fly()
    fun jump(){ println("Bird jump!") }
}
interface Horse{
    val maxSpeed: Int
    fun run()
    fun jump(){ println("Jump!, max speed: $maxSpeed") }
}
class Pegasus: Bird, Horse{
    override val wings: Int = 2
    override val maxSpeed: Int = 100
    override fun fly(){ println("Fly") }
    override fun run(){ println("Run!") }
    override fun jump() {
        super<Horse>.jump()
        println("Pegasus jump!")
    }
}
fun main(){
    val pegasus = Pegasus()
    pegasus.fly()
    pegasus.run()
    pegasus.jump()
}

 

  • class Pegasus: Bird, Horse: 페가수스 클래스를 만들고 Bird, Horse 인터페이스를 구현하였습니다.
  • super<Horse>.jump(): 부모 인터페이스 중 Horse 인터페이스의 jump 메서드를 호출합니다.

 

해당 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

위와 같은 방법을 통해 한 클래스에 여러 인터페이스를 구현하게 할 수 있습니다.

 

 

5.  인터페이스 위임

 

인터페이스에서 by위임을 사용하는 방법을 예시를 통해 알아보겠습니다.

 

interface Nameable { var name: String }
class StaffName: Nameable { override var name: String = "Sean" }
class Work: Runnable { override fun run() { println("Work...") } }
class Person(name: Nameable, work: Runnable): Nameable by name, Runnable by work
fun main() {
    val person = Person(StaffName(), Work())
    println(person.name)
    person.run()
}

 

  • class Person: 각 매개변수에 인터페이스들을 위임합니다.
  • Person(StaffName(), Work()): 클래스의 생성자를 통해 객체를 전달합니다.

위의 코드를 실행한 결과는 다음과 같습니다.

이처럼 각 클래스의 위임된 멤버에 접근해 사용할 수 있습니다.

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 33. 내부 클래스  (0) 2019.09.16
[Kotlin] 32. 데이터 클래스  (0) 2019.09.12
[Kotlin] 31. 인터페이스  (0) 2019.09.12
[Kotlin] 30. 추상 클래스  (0) 2019.09.05
[Kotlin] 29. 컴패니언 객체  (0) 2019.09.01
[Kotlin] 28. 위임(Delegation)  (0) 2019.09.01

References: Do it! 코틀린 프로그래밍

코틀린에서의 추상 클래스에 대해 알아봅니다.

 

 

1. 추상 클래스

 

추상 클래스는 abstract 키워드와 함께 전업됩니다.

추상 클래스는 일반적인 방법으로 인스턴스화 될 수 없습니다.

추상 클래스를 상속하는 하위 클래스가 어떻게 만들어져야 하는지 나타내는 용도로 사용됩니다.

 

추상 클래스의 멤버인 프로퍼티나 메서드 또한 abstract로 선언될 수 있으며 이를 추상 프로퍼티 혹은 추상 메서드라고 합니다. 추상 프로퍼티나 추상 메서드가 하나라도 존재한다면 그 클래스는 추상 클래스가 되어야 합니다.

추상 프로퍼티와 추상 메서드는 abstract로 선언 함으로써 미완성의 의미를 줄 수 있습니다.

 

다음 예시를 통해 추상 클래스에 대해 알아보겠습니다.

 

abstract class Vehicle(val name: String, val color: String, val weight: Double){
    abstract var maxSpeed: Double
    var year = "2019"
    abstract fun start()
    abstract fun stop()
    fun displaySpecs(){
        println("Name: $name, Color: $color, Weight: $weight, Year: $year, MaxSpeed: $maxSpeed")
    }
}

 

  • abstract class Vehicle : 추상 클래스 정의
  • abstract var maxSpeed : 추상 프로퍼티 정의
  • var year : 일반 프로퍼티 정의
  • abstract fun start / stop : 추상 메서드 정의
  • fun displaySpecs : 일반 메서드 정의

해당 클래스는 객체를 생성할 수 없고 설계의 역할을 합니다.

abstract선언자가 있는 모든 프로퍼티와 메서드는 반드시 자식 클래스에서 정의되어야 합니다.

이 추상 클래스를 상속하는 자식 클래스를 만들어 보겠습니다.

 

class Car (name: String, color: String, weight: Double, override var maxSpeed: Double): Vehicle(name, color, weight){
    override fun start() = println("Car started.")
    override fun stop() = println("Car stopped.")
}
class Motorcycle(name: String, color: String, weight: Double, override var maxSpeed: Double): Vehicle(name, color, weight){
    override fun start() = println("Motorcycle started.")
    override fun stop() = println("Motorcycle stopped.")
}

 

  • override : 추상 클래스에서 선언된 모든 추상 프로퍼티와 추상 메서드를 구현.

모든 추상 프로퍼티와 추상 메서드를 구현하지 않으면 컴파일러에서 오류를 반환하는 것을 확인할 수 있습니다.

모든 추상 클래스와 프로퍼티를 구현 후 다음 메인 함수를 실행시켜 봅시다.

 

fun main() {
    val car = Car("SuperMatiz", "Yellow", 1100.0, 270.0)
    var motor = Motorcycle("DreamBike", "Red", 173.0, 100.0)
    car.year = "2013"
    car.displaySpecs()
    car.start()
    motor.displaySpecs()
    motor.start()
}

 

실행결과는 다음과 같습니다.

추상 클래스는 이처럼 하위 클래스에서 반드시 가져야 하는 프로퍼티나 메서드를 제시할 수 있습니다. 또한 일반 프로퍼티나 메서드를 만들어 공통적으로 필요한 기능을 정의할 수도 있습니다.

 

만약 하위 클래스를 생성하지 않고 단일 인스턴스로 객체를 생성하려면 다음과 같이 수행하면 됩니다.

 

abstract class Printer{
    abstract fun print()
}
var myPrinter = object: Printer(){
    override fun print() = println("Print!")
}
fun main() {
    myPrinter.print()
}

 

  • abstract class Printer : 추상 클래스를 정의합니다.
  • abstract fun print() : 추상 메서드를 정의합니다.
  • var myPrinter = object: Printer() : 익명 객체를 생성하고 Printer() 추상 클래스를 상속받습니다.
  • override fun print() : 추상 메서드를 구현합니다.

실행 결과는 다음과 같습니다.

익명 객체를 지정하는 object 키워드를 사용해 추상 메서드를 인스턴스화 시킬 수 있습니다.

 

 

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 32. 데이터 클래스  (0) 2019.09.12
[Kotlin] 31. 인터페이스  (0) 2019.09.12
[Kotlin] 30. 추상 클래스  (0) 2019.09.05
[Kotlin] 29. 컴패니언 객체  (0) 2019.09.01
[Kotlin] 28. 위임(Delegation)  (0) 2019.09.01
[Kotlin] 27. 지연 초기화: lazy  (0) 2019.08.27

References: Do it! 코틀린 프로그래밍

코틀린에서의 컴패니언 객체에 대해 알아봅니다.

 

 

1. 컴패니언(companion)

 

코틀린은 static 키워드가 없는 대신 companion객체를 제공합니다.

 

컴패니언 객체는 프로그램 실행 시 독립적인 메모리 공간을 할당받습니다.

따로 인스턴스화 할 필요가 없으며 어떤 객체라도 동일한 참조 값을 갖고 있어 해당 클래스의 상태에 관계없이 접근할 수 있습니다.

따라서 모든 객체에 의해 공유되는 효과를 갖습니다.

 

다음 예시를 통해 컴패니언 객체를 사용하는 방법에 대해 알아봅시다

 

class Person{
    var id: Int = 0
    var name: String = "smoh"
    companion object {
        var language: String = "Korean"
        fun work(){ println("working ...") }
    }
}
fun main() {
    println(Person.language)
    Person.language = "English"
    println(Person.language)
    Person.work()
    //println(Person.name)
}

 

  • companion object : 컴패니언 객체를 선언합니다. 해당 객체의 내부에 선언된 내용은 프로그램 시작 시 초기화되어 바로 사용할 수 있습니다.
  • //println(Person.name) : 컴패니언 객체 외부의 프로퍼티를 사용하기 위해선 초기화가 먼저 필요합니다.

위 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다

 

컴패니언 객체는 주로 실제 객체의 싱글톤(Singleton)으로 정의됩니다.

이는 전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며 생성된 객체를 어디서든 참조하도록 하기 위함입니다.

이와 같이 하면 객체가 동일한 정보를 가질 때 하나의 메모리만 유지해 메모리를 절약할 수 있는 장점이 있습니다.

 

 

 

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 31. 인터페이스  (0) 2019.09.12
[Kotlin] 30. 추상 클래스  (0) 2019.09.05
[Kotlin] 29. 컴패니언 객체  (0) 2019.09.01
[Kotlin] 28. 위임(Delegation)  (0) 2019.09.01
[Kotlin] 27. 지연 초기화: lazy  (0) 2019.08.27
[Kotlin] 26. 지연초기화: lateinit  (0) 2019.08.27

References: Do it! 코틀린 프로그래밍

코틀린에서 by를 이용한 위임에 대해 알아봅니다.

 

1. 위임(Delegation)

 

현실에서의 위임(Delegation)이란 특정한 일을 대신하는 중간자의 역할을 말합니다.

코틀린에서도 by를 이용한 위임을 사용해 특정 클래스를 확장하거나 이용할 수 있습니다.

 

기본적으로 코틀린의 라이브러리는 open 되지 않은 채 제공됩니다. 따라서 상속이나 직접 클래스에 기능 확장이 어렵습니다.

따라서 필요한 경우에만 상속과 비슷하게 위임을 사용하여 해당 클래스의 모든 기능을 사용하면서 기능을 추가 확장 구현을 가능하게 합니다.

 

 

2. by에 의한 클래스 위임

 

by를 사용하면 하나의 클래스가 다른 클래스에 위임 하도록 선언할 수 있습니다.

위임이 선언되면 위임된 클래스가 가지는 멤버를 참조없이 호출할 수 있게 됩니다.

 

다음 예시를 통해 by를 이용한 클래스 위임에 대해 알아봅시다.

 

interface Car { fun go(): String }
class VanImpl(val power: String): Car{
    override fun go() = "은 짐을 적재하며 $power 을 가집니다."
}
class SportImpl(var power: String): Car{
    override fun go() = "은 경주용에 사용되며 $power 을 가집니다."
}
class CarModel(var model: String, impl: Car): Car by impl{
    fun carInfo(){ println("$model ${go()}") }
}
fun main(){
    var myDamas = CarModel("Damas", VanImpl("100마력"))
    var my350z = CarModel("350z", SportImpl("350마력"))
    myDamas.carInfo()    
    my350z.carInfo()
}

 

  • VanImpl / SportImpl : Car 인터페이스를 상속받아 구현합니다.
  • Car by impl인자 impl은 CarModel에 의해 위임됩니다. 타입이 Car이므로 특정 참조 없이 Car의 멤버를 사용할 수 있게 됩니다.

위 예시의 실행결과는 다음과 같습니다.

 

 

 

3. observable() 함수에 의한 위임

 

observable() 함수는 특정 프로퍼티를 감시하고 있다가 변화가 감지되면 호출되어 처리됩니다.

특정 변경 이벤트에 따라 호출되므로 콜백이라고 부르기도 합니다.

 

다음 예시를 통해 observable() 함수의 사용법에 대해 알아보겠습니다.

 

import kotlin.properties.Delegates
class User{
    var name: String by Delegates.observable("NONAME"){
        prop, old, new -> println("$old -> $new")
    }
}
fun main(){
    var user = User()
    user.name="smoh"
    user.name="Anonymous"
}

 

  • by Delegates.observable("NONAME"해당 프로퍼티에 obervable 함수로 위임합니다. 초깃값은 "NONAME"으로 설정합니다.
  • new -> println("$old -> $new") : 프로퍼티의 변경이 감지되면 new에 할당되어 있는 람다식이 실행됩니다.

 

위의 함수를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

 

 

User클래스의 name이 변경될 때마다 지정해둔 람다식이 실행된 것을 알 수 있습니다.

 

 

4. vetoable() 함수에 의한 위임

 

obervable() 함수와 유사한 함수입니다. 프로퍼티 값이 변경되면 호출되며 콜백이 true값을 반환할 때 만 프로퍼티를 업데이트합니다.

 

다음 예시를 통해 알아보겠습니다.

 

import kotlin.properties.Delegates
fun main() {
    var max: Int by Delegates.vetoable(0){
        prop, old, new -> new > old
    }
    println(max)
    max = 10
    println(max)
    max = 5
    println(max)
}

 

  • by Delegates.vetoable(0) : 해당 프로퍼티에 vetoable() 함수로 위임합니다. 초깃값은 0으로 설정합니다.
  • new -> new > old : 콜백 함수입니다. 해당 조건이 true로 리턴되면 new값을 prop에 새로 저장합니다.

 

위 함수를 실행한 결과는 다음과 같습니다.

 

 

 

초깃값은 0으로 초기화됩니다. 이후 10은 0보다 크니까 프로퍼티 값에 저장합니다. 그다음 5는 10보다 작으므로 프로퍼티 값을 경신하지 않습니다.

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] 30. 추상 클래스  (0) 2019.09.05
[Kotlin] 29. 컴패니언 객체  (0) 2019.09.01
[Kotlin] 28. 위임(Delegation)  (0) 2019.09.01
[Kotlin] 27. 지연 초기화: lazy  (0) 2019.08.27
[Kotlin] 26. 지연초기화: lateinit  (0) 2019.08.27
[Kotlin] 25. Custom Getter & Setter  (0) 2019.08.27

+ Recent posts