본문 바로가기
Program Language/Kotlin

[Do it! 코틀린 프로그래밍] 8. 제네릭과 배열

by SungJe 2021. 4. 25.

8. 제네릭과 배열


1. 제네릭 다루기

제네릭(Generic) 은 클래스 내부에서 사용할 자료형을 나중에 인스턴스를 생성할 때 확정한다. 객체의 자료형을 컴파일할 때 체크하기 때문에 객체 자료형의 안정성을 높이고 형 변환의 번거로움을 줄인다.

/* 간단한 제네릭 예시 */
// 앵글 브래킷(<>) 사이에 형식 매개변수 이름을 넣어 자료형을 대체한다.
class Box<T>(var name: T)

fun main() {
    val box1 = Box<Int>(1) // 1은 Int 타입이므로 Box<Int>으로 추론
    val box2 = Box<String>("Hello") // "Hello"는 String 타입이므로 Box<String>으로 추론
    println(box1.name) // 1
    println(box2.name) // Hello
}

일반적으로 제네릭에서 사용하는 형식 매개변수 이름

형식 매개변수 이름 의미
E 요소(Element)
K 키(Key)
N 숫자(Number)
T 형식(Type)
V 값(Value)
S, U, V, etc. 두 번째, 세 번째, 네번째 형식(2nd, 3nd, 4th types)
  • 제네릭 클래스: 형식 매개변수를 1개 이상 받는 클래스다. 프로퍼티를 사용하는 경우 생성자를 통해 초기값을 지정한다. 형식 매개변수는 기본적으로 null을 허용한 형태로 선언된다.
/* 제네릭 클래스 생성자 */
class MyClass<T>(val myProp: T) { ... } // 주 생성자 이용

class MyClass<T> {
    var myProp: T // 프로퍼티
    constructor(myProp: T) { // 부 생성자 이용
        this.myProp = myProp
    }
}
/* 형식 매개변수의 null 제어 */
class GenericNullable<T> { // 기본적으로 null이 허용되는 형식 매개변수
    fun equalityFunc(arg1: T, arg2: T) { // 제네릭 메서드
        println(arg1?.equals(arg2))
    }
}

class GenericNonNull<T: Any> { // 자료형 Any가 지정되어 null을 제한함
    fun equalityFunc(arg1: T, arg2: T) { // 제네릭 메서드
        println(arg1.equals(arg2))
    }
}

fun main() {
    val nullableObj = GenericNullable<Int?>()
    // val nonNullObj = GenericNonNull<Int?>() // 오류!
    val nonNullObj = GenericNonNull<Int>()

    nullableObj.equalityFunc(null, 10) // null
    nonNullObj.equalityFunc(10, 10) // true
}
  • 제네릭 함수 혹은 메서드: 형식 매개변수를 받는 함수나 메서드를 말한다.
/* 구조 */
fun <형식 매개변수[, ...]> 함수 이름(매개변수: <매개변수 자료형>[, ...]): <반환 자료형>

/* 배열의 인덱스 찾아내기 */
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")
    val arr2: Array<Int> = arrayOf(1, 2, 3, 4)

    println("arr.indices ${arr1.indices}")
    println(find<String>(arr1, "Cherry"))
    println(find(arr2, 2)) // Int형으로 추론
}
  • 제네릭과 람다식: 람다식을 매개변수로 받으면 자료형을 결정하지 않아도 실행 시 람다식 본문을 넘겨 줄 때 결정되어 오류를 해결할 수 있다.
/* 람다식에서 제네릭 사용 */
// fun <T> add(a: T, b: 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) // 5
}

자료형 제한하기: 제네릭 클래스나 메서드가 받는 형식 매개변수를 특정한 자료형으로 제한할 수 있다. 코틀린에서는 콜론(:)을 사용하여 제한한다.

// 제네릭 클래스의 자료형을 Number로 제한
class Calc<T: Number> {
    fun plus(arg1: T, arg2: T): Double {
        return arg1.toDouble() + arg2.toDouble()
    }
}

// 제네릭 함수의 자료형을 Int로 제한
fun <T: Number> addNumber(a: T, b: T, op: (T, T) -> T): T {
    return op(a, b)
}

fun main() {
    val calc = Calc<Long>()
    println(calc.plus(10L, 20L)) // 30.0

    // 제한된 자료형으로 인해 오류 발생!
    // val calc2 = Calc<String>()

    val resultInt = addNumber(2, 3) { a, b -> a + b }
    println(resultInt) // 5

    // 제한된 자료형으로 인해 오류 발생!
    // val resultString = addNumber("Hello ", "Kotlin!") { a, b -> a + b }
}
/* 다수 조건의 형식 매개변수 제한하기 */
interface InterfaceA
interface InterfaceB

class HandlerA : InterfaceA, InterfaceB
class HandlerB : InterfaceA

// where 키워드 사용으로 2개의 인터페이스를 구현하는 클래스로 제한
class ClassA<T> where T: InterfaceA, T: InterfaceB

fun main() {
    val obj1 = ClassA<HandlerA>() // 객체 생성 가능
    val obj2 = ClassA<HandlerB>() // 범위에 없으므로 오류 발생!
}

상·하위 형식의 가변성

가변성(Variance)란 형식 매개 변수가 클래스 계층에 영향을 주는 것을 말한다. 예를 들어 형식 A의 값이 필요한 모든 클래스에 형식 B의 값을 넣어도 아무 문제가 없다면 B는 A의 하위 형식(Subtype)이 된다.
가변성에 대해서 더 자세한 내용은 공식 사이트를 참고한다. Link

val integer: Int = 1
val number: Number = integer // 하위 자료형 Int를 Number가 수용

가변성의 3가지 유형

Variance 3 Type
  • 무변성(Invariance): 자료형 사이의 하위 관계가 성립하지 않는다. 코틀린에서는 따로 지정해 주지 않으면 기본적으로 무변성이다.
/* 무변성(Invariance) 선언 */
class Box<T>(val size: Int)

fun main() {
    val anys: Box<Any> = Box<Int>(10) // 오류! 자료형 불일치
    val nothings: Box<Nothing> = Box<Int>(20) // 오류! 자료형 불일치
}
  • 공변성(Covariance): 형식 매개변수 사이의 하위 자료형 관계가 성립하고, 그 관계가 인스턴스 자료형 관계로 이어진다. out 키워드를 사용해 정의한다.
/* 공변성(Covariance) 선언 */
class Box<out T>(val size: Int)

fun main() {
    val anys: Box<Any> = Box<Int>(10) // 관계 성립으로 객체 생성 가능관계 성립으로 객체 생성 가능
    val nothings: Box<Nothing> = Box<Int>(20) // 오류! 자료형 불일치
}
  • 반공변성(Contravariance): 공변성의 반대 개념으로 자료형의 상하 관계가 반대로 된다. in 키워드를 사용해 정의한다.
/* 반공변성(Contravariance) 선언 */
class Box<in T>(val size: Int)

fun main() {
    val anys: Box<Any> = Box<Int>(10) // 오류! 자료형 불일치
    val nothings: Box<Nothing> = Box<Int>(20) // 관계 성립으로 객체 생성 가능관계 성립으로 객체 생성 가능
}

가변성의 두 가지 방법

  1. 선언 지점 변성(Declaration-Site Variance): 클래스 자체에 가변성을 지정하는 방식이다.
class Box<in T: Animal>(var item: T) // 선언 지점에서 가변성을 지정
  1. 사용 지점 변성(Use-Site Variance): 메서드의 매개변수나 제네릭 클래스를 생성할 때와 같이 사용 위치에서 가변성을 지정하는 방식이다.
class Box<T>(var item: T) // 무변성

fun <T> printObj(box: Box<out Animal>) { // Box<>의 사용 지점에서 가변성을 지정
    val obj: Animal = box.item
    println(obj
}

자료형 프로젝션(Type Projection): 사용하고자 하는 요소의 특정 자료형에 in 또는 out을 지정해 제한하는 것을 말한다. 자료형 프로젝션을 통해 자료의 안정성을 보장한다.

  • 스타 프로젝션(Star Projection): 어떤 자료형이라도 들어올 수 있으나 구체적으로 자료형이 결정되면 그 자료형과 하위 자료형의 요소만 허용한다.

    • in으로 정의된 형식 매개변수를 *로 받으면 in Nothing으로 간주
    • out으로 정의된 형식 매개변수를 *로 받으면 out Any?으로 간주
/* 스타 프로젝션 */
class InOutTest<in T, out U>(t: T, u: U) {
    // T: in 프로젝션, U: out 프로젝션
    val propertyT: T = t // 오류! T는 in 프로젝션이기 때문에 out 위치에 사용 불가
    val propertyU: U = u

    fun functionU(u: U) = println(u) // 오류! U는 out 프로젝션이기 때문에 in 위치에 사용 불가
    fun functionT(t: T) = println(t)
}

fun starTestFunc(v: InOutTest<*, *>) {
    v.functionU(1) // 오류! Nothing으로 인자를 처리함
    println(v.propertyU)
}
  • 프로젝션 정리
종류 가변성 제한
out 프로젝션 Box<out Int> 공변성 형식 매개변수는 세터를 통해 값을 설정하는 것이 제한된다.
in 프로젝션 Box<in Int> 반공변성 형식 매개변수는 게터를 통해 값을 읽거나 반환할 수 있다.
스타 프로젝션 Box<*> 모든 인스턴스는
하위 형식이 될 수 있다.
in과 out은 사용 방법에 따라 결정된다.

reified 자료형

  • 형식 매개변수를 실행 시간에 직접 접근하기 위해 사용한다.
  • inline 함수에서만 사용할 수 있다.
/* reified를 이용한 제네릭 자료형의 처리 */
fun main() {
    val result = getType<Float>(10)
    println("result = $result")
}

inline fun <reified T> getType(value: Int): T {
    // 형식 매개변수 T를 실행 시간에 사용 가능
    println(T::class)
    println(T::class.java)

    return when (T::class) { // 제네릭 자료형에 따라 반환
        Float::class -> value.toFloat() as T
        Int::class -> value as T
        else -> throw IllegalStateException("${T::class} is not supported!")
    }
}

NOTE✏️

  • Nothing 클래스: 코틀린의 최하위 자료형으로 아무것도 가지고 있지 않은 클래스다. Link

2. 배열 다루기

배열(Array) 이란 데이터를 연속적으로 나열한 형태로 순서 번호에 해당하는 인덱스(Index)와 값이 들어 있는 요소(Element)로 구성된다. 코틀린에서 배열은 여러 가지 자료형을 혼합하여 구성할 수 있다.

val numbers = arrayOf(4, 5, 7, 3) // 정수형으로 초기화된 배열
val animals = arrayOf("Cat", "Dog", "Lion") // 문자열형으로 초기화된 배열
val mixArr = arrayOf(6, 8, "Kim", "Lee", false, true) // 정수, 문자열, Boolean 혼합
val limitArr = arrayOf<Long>(3L, 6L, 9L) // Long형 요소만으로 제한

for (element in numbers) { // 배열의 요소를 출력
    println(element)
}
println(animals[0]) // Cat(인덱스는 0부터 시작)

다차원 배열: 기본적인 배열을 묶어서 2차원 이상의 배열로 표현하는 형태를 말한다. 너무 많은 차원의 배열을 구성하면 접근하기 복잡하여 버그가 발생할 확률이 높아지므로 되도록 사용하지 않은 것이 좋다.

/* 첫 번째 방법 */
val array1 = arrayOf(1, 2, 3)
val array2 = arrayOf(4, 5, 6)
val array3 = arrayOf(7, 8, 9)

val arr2d = arrayOf(array1, array2, array3)
/* 두 번째 방법 */
val arr2d = arrayOf(arrayOf(1, 2, 3), arrayOf(4, 5, 6), arrayOf(7, 8, 9))
val arr3d = arrayOf(arrayOf(arrayOf(...), ...), ...) // 3차원 배열

배열 요소에 접근

// 코틀린 표준 라이브러리의 Array.kt
public class Array<T> {
    public inline constructor(size: Int, init: (Int) -> T)
    public operator fun get(index: Int): T
    public operator fun set(index: Int, value: T) Unit
    public val size: Int
    public operator fun iterator(): Iterator<T>
}

// 게터, 세터를 통한 접근
array.get(index)
array.set(index, value)

// 인덱스 연산자를 통한 접근
array[index]
array[index] = value

표현식을 통해 배열 생성하기

/* 구조 */
val | var 변수 이름 = Array(요소 개수, 초기값)

// 짝수 배열 생성하기
val evenArray = Array<Int>(5) { it * 2 } // [ 0, 2, 4, 6, 8 ]

// 많은 양의 배열 생성
var arr1 = arrayOfNulls<Int>(1000) // 1000개의 null로 채워진 정수 배열
var arr2 = Array<Int>(1000) { 0 } // 0으로 채워진 정수 배열

다양한 방법의 배열 다루기

  • 배열에 요소 추가하고 잘라내기
val arr1 = arrayOf<Int>(1, 2, 3, 4, 5) // 다섯개로 고정된 배열
val arr2 = arr1.plus(6) // 하나의 요소를 추가한 새 배열 생성
val arr3 = arr1.sliceArray(1..2) // 필요한 범위를 잘라내 새 배열 생성

println(arr2.contentToString()) // [1, 2, 3, 4, 5, 6]
println(arr3.contentToString()) // [2, 3]
  • 배열의 순환
val array = arrayOf<Int>(1, 2, 3, 4, 5)

// 순환 메서드 사용
array.forEach { element -> print("$element ") }
array.forEachIndexed({ i, e -> println("array[$i] = $e") })

// Iterator 사용
val iter: Iterator<Int> = array.iterator()
while (iter.hasNext()) {
    val e = iter.next()
    print("$e ")
}
  • 배열의 정렬
val array = arrayOf<Int>(1, 4, 5, 3, 2)

// 오름차순, 내림차순으로 정렬된 일반 배열로 반환
val sortedArray = array.sortedArray()
val sortedArrayDesc = array.sortedArrayDescending()

println(sortedArray.contentToString()) // [1, 2, 3, 4, 5]
println(sortedArrayDesc.contentToString()) // [5, 4, 3, 2, 1]

// 원본 배열에 대한 정렬
array.sort()
println(array.contentToString()) // [1, 2, 3, 4, 5]

array.sortDescending()
println(array.contentToString()) // [5, 4, 3, 2, 1]

// 오름차순, 내림차순으로 정렬된 List로 반환
val sortedList: List<Int> = array.sorted()
val sortedListDesc: List<Int> = array.sortedDescending()

println(sortedList) // [1, 2, 3, 4, 5]
println(sortedListDesc) // [5, 4, 3, 2, 1]

// sortBy를 이용한 특정 표현식에 따른 정렬
val items = arrayOf<String>("Dog", "Cat", "Lion", "Kangaroo", "Po")
items.sortBy { item -> item.length }
println(items.contentToString()) // [Po, Dog, Cat, Lion, Kangaroo]
/* sortWith() 비교자로 정렬하기 */
data class Product(val name: String, val price: Double)

fun main() {
    val products = arrayOf<Product>(
        Product("Snow Ball", 870.00),
        Product("Smart Phone A", 999.00),
        Product("Drone", 240.00),
        Product("Mouse", 633.55),
        Product("Ketboard", 125.99),
        Product("Smart Phone B", 1500.99),
        Product("Mouse", 512.99)
    )

    // Comparator로 가격 정렬하기
    println("----* Comparator *----")
    products.sortWith(
        Comparator<Product> { p1, p2 ->
            when {
                p1.price > p2.price -> 1
                p1.price == p2.price -> 0
                else -> -1
            }
        }
    )
    products.forEach { println(it) }

    // compareBy()로 이름 정렬후 가격 정렬하기
    println("----* compareBy() *----")
    products.sortWith(compareBy({ it.name }, { it.price })) // 가변형 인자
    products.forEach { println(it) }
}
  • 배열 필터링 하기
val array = arrayOf<Int>(1, -2, 3, 4, -5, 0)
array // 체이닝
.filter { it > 0 }
.sortedBy { it }
.forEach { print("$it ") } // 1 3 4
  • 배열 평탄화하기
val numbers = arrayOf<Int>(1, 2, 3)
val strings = arrayOf<String>("one", "two", "three")
val simpleArray = arrayOf(numbers, strings) // 2차원 배열

val flattenSimpleArray = simpleArray.flatten() // 1차원 배열로 변환
println(flattenSimpleArray) // [1, 2, 3, one, two, three]

NOTE✏️

  • iterator: 컬렉션에 저장되어 있는 요소들을 읽어오는 방법을 표준화한 인터페이스다. Link

3. 문자열 다루기

문자열은 불변(immutable) 값으로 생성되기 때문에 참조되고 있는 메모리가 변경될 수 없다. 더 이상 참조되지 않는 문자열은 GC(Garbage Collector)에 의해 제거된다.

val hello: String = "Hello World!"
println(hello[0]) // H
hello[0] = 'K' // 오류

var s = "abcdef"
s = "xyz" // 새로운 메모리 공간이 생성

문자열 추출하고 병합하기

/* 문자열 추출 메서드 */
String.substring(인덱스 범위 지정): String
CharSequence.subSequence(인덱스 범위 지정): CharSequence

/* 문자열 추출 */
var str = "abcdef"
println(str.substring(1..3)) // bcd

/* 문자열 병합 */
str = str.substring(0..1) + "x" + str.substring(3..str.lastIndex)
println(str) // abxdef

문자열 비교하기

/* 문자열 비교 메서드 */
stringA.compareTo(stringB) // A == B: 0, A < B: 양수, A > B: 음수

/* 문자열 비교하기 */
var stringA = "Hello Kotlin"
var stringB = "Hello KOTLIN"

println(stringA.compareTo(stringB))
println(stringA.compareTo(stringB, true)) // 대소문자 무시

StringBuilder 사용하기

-   간단한 요소 변경이 있을 경우 용이하다.
-   기존의 문자열보다 처리가 조금 느리다.
-   단어를 변경하지 않는 경우 불필요한 메모리 낭비가 발생한다.
var str = StringBuilder("Hello")
str[2] = 'x' // Hexlo - 요소의 변경 가능

str.append(" World") // Hexlo World - 문자열 끝에 문자열 추가
str.insert(11, " Added") // Hexlo World Added - 해당 인덱스에 문자열 추가
str.delete(5, 11) // Hexlo Added - 해당 인덱스에 문자열 제거

기타 문자열 처리

/* 소문자/대분자 변경 */
val str = "HELLO kotlin"
println(str.toLowerCase()) // hello kotlin - 소문자 변경
println(str.toUpperCase()) // HELLO KOTLIN - 대분자 변경

/* 특정 문자 단위로 잘라내기 */
var deli = "Welcome to Kotlin"
val sp = deli.split(" ") // 분리된 내용을 List로 반환
println(sp) // [Welcome, to, Kotlin]

/* 앞, 뒤 공백 제거 */
println(" Hello ".trim()) // Hello

리터럴 문자

  • 이스케이프(Escape) 문자
종류
\t 탭(Tab) \r 캐리지 리턴(Carriage Return) \\ 백슬래시(Backslash)
\b 백스페이스(Backspace) \' 작은따옴표(Single Quote) \$ 달러 기호(Dollar)
\n 개행(Newline) \" 큰따옴표(Double Quote) \uHHHH 유니코드(Unicode) 문자
val str = "\tYou're just too \"good\" to be true\n\tI can't take my eyes off you."
val uni = "\uAC00"

println(str)
println(uni)
  • 3중 따옴표 부호(""")
val text = """
    |Tell me and I forget.
    |Teach me and I remember.
    |Involve me and I learn.
    |(Benjamin Franklin)
    """.trimMargin() // trim default는 |
println(text)
  • 형식 문자
종류
%b 참과 거짓의 Boolean 유형 %o 8진 정수 %g 10진 혹은 E 표기법의 실수
%d 부호 있는 정수 %t 날짜나 시간 %n 줄 구분
%f 10진 실수 %c 문자 %s 문자열
%h 해시코드 %e E 표기법의 실수 %x 16진 정수
/* format() 메서드 구조 */
fun String.format(vararg args: Any?): String

/* 형식 문자 사용하기 */
val pi = 3.1415926
val dec = 10
val s = "Hello"

println("pi = %.2f, %3d, %s".format(pi, dec, s)) // pi = 3.14,  10, Hello