제네릭(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>와 같이 형식 매개변수를 지정합니다.
형식 매개변수로 선언된 함구의 매개 변수를 연산할 경우 자료형을 경정할 수 없어 오류가 발생합니다.
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 = add( 2, 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 )
사실 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})") }
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() ): 열거형 클래스의 모든 값을 가져오는 반복문입니다.
내부 클래스는 독립적인 클래스로 정의하기 모호한 경우나 한 클래스의 내부에서만 사용하고 외부에선 접근할 필요가 없는 클래스를 정의하기 위해 사용합니다.
단, 이러한 내부 클래스는 남용하면 클래스의 의존성이 커지고 가독성이 저하되므로 주의가 필요합니다.
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()) }