본문 바로가기
Kotlin

Kotlin #11 : 제네릭, 배열 230223

by haheehee 2023. 2. 24.

제네릭(generic)

- 자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 시간에 자료형을 검사해 적당한 자료형을 선택할 수 있도록 하기 위해 사용

- 앵글 브래킷(<>) 사이에 형식 매개변수를 사용해 선언 

- 형식 매개변수는 자료형을 대표하는 용어로 T와 같이 특정 영문의 대문자로 사용

 

형식 매개변수의 이름

- 강제사항은 없으나 일종의 규칙

  • E        : 요소(element)
  • K         : 키(key)
  • N         : 숫자(number)
  • T          : 형식(type)
  • V          : 값(value)
  • S, U, V etc.   : 두번째, 세번째, 네 번째 형식(2nd, 3rd, 4th types)

 

- 자료형이 추론 가능한 경우, 앵글 브라켓을 생략할 수 있다.

val box3 = Box(1) // 1은 Int형이므로 Box<Int>로 유추함
val box4 = Box("Hello") // "Hello"은 String형이므로 Box<String>로 유추함

 

형식 매개변수를 한 개 이상 받는 클래스

- 인스턴스를 생성하는 시점에서 클래스의 자료형을 정함. 

-  제네릭 클래스 내에 메서드에도 다음과 같이 형식 매개변수를 사용

class MyClass<T> { // 한 개의 형식 매개변수를 가지는 클래스
	fun myMethod(a: T) { // 메서드의 매개변수 자료형에 사용됨
		...
	}
}

 -  프로퍼티에 지정하는 경우

     주 생성자나 부 생성자에 형식 매개 변수를 지정해 사용

 

주 생성자의 형식 매개변수

class MyClass<T>(val myProp: T) {   }	// 주 생성자의 프로퍼티

 

부 생성자의 형식 매개변수

class MyClass<T> {
	val myProp: T	// 프로퍼티
    constructor(myProp: T) {	// 부 생성자 이용
    	this.myProp = myProp
    }
}

package com.example.pp243

class Box<T>(t: T) {    // 제네릭을 사용해 형식 매개변수를 받아 name에 저장
    var name = t
}
fun main() {
    val box1: Box<Int> = Box<Int>(1)
    val box2: Box<String> = Box<String>("Hello")
    
    println(box1.name)
    println(box2.name)
}

제네릭 예제

 

package com.example.pp248

open class Parent
class Child: Parent()
class Cup<T>
fun main() {
    val obj1: Parent = Child()  // Parent형식의 obj1은 Child로 형 변환될 수 있음

    //val obj2: Child = Parent() // 에러 하위 형식인 Child의 객체 obj2는 Parent로 변환되지 않음
    //val obj3: Cup<Parent> = Cup<Child>()    // 에러 자료형 형식이 일치하지 않음
    //val obj4: Cup<Child> = Cup<Parent>()    // 에러 자료형 형식이 일치하지 않음
    
    val obj5 = Cup<Child>() // obj5는 Cup<Child>형식이 된다.
    val obj6: Cup<Child> = obj5 // 형식이 일치하므로 괜춘
}

제네릭 클래스의 자료형 변환하기 예제

open되어 있어 Parent클래스를 상속받을 수 있음

Child클래스가 Parent()클래스를 상속받음.

가변성을 주기위해 in, out등을 설정해야 한다.

 

package com.example.pp249

class GenericNull<T> {  // 기본적으로 null이 허용되는 형식 매개변수
    fun EqualityFunc(arg1: T, arg2: T) {
        println(arg1?.equals(arg2))
    }
}

fun main(args: Array<String>) {
    val obj = GenericNull<String>() // non-null로 선언됨
    obj.EqualityFunc("Hello", "World")  // null이 허용되지 않음
    
    val obj2 = GenericNull<Int?>()  // null이 가능한 형식으로 선언
    obj2.EqualityFunc(null, 10) // null 사용
}

형식 매개변수의 null 제어 예제

제네릭의 형식 매개변수는 기본적으로 null 가능한 형태로 선언

null을 허용하지 않으려면? ==> 특정 자료형으로 제한한다. <T: Any>

 


제네릭 함수 혹은 메서드

- 해당 함수나 메서드 앞쪽에 <T>와 같이 지정

- 자료형의 결정은 함수가 호출될 때 컴파일러가 자료형 추론

- 이 자료형은 반환 자료형과 매개변수 자료형에 사용 가능

fun <형식 매개변수[,...]> 함수명(매개변수: <매개변수 자료형>[, ...]): <반환 자료형>

// 예시
fun <T> genericFunc(arg: T): T? { ... } // 매개변수와 반환 자료형에 형식 매개변수 T가 사용됨
fun <K, V> put(key: K, value: V): Unit { ... } // 형식 매개변수가 여러 개인 경우

 

제네릭과 람다식

- 형식 매개변수로 선언된 함수의 매개변수를 연산

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})
	// val result = add(2, 3) {a, b -> a + b}와 같이 표현 가능
	println(result)
}

** 람다식은 매개변수와 처리부(바디)로만 이루어진 것.

    (람다식은 함수명과 반환형이 없다. ==> 중문으로 람다식임을 알려줘야한다.)

    *** 함수에는 1. 함수명 2. 매개변수 3. 바디(처리부) 4. 반환형 이 필요하다.


package com.example.pp251

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}")  // indices는 배열의 유효 범위 반환
    println(find<String>(arr1, "Cherry"))   // 요소 C의 인덱스 찾아내기
    println(find(arr2, 2))  // 요소 2의 인덱스 찾아내기
}

배열의 인덱스 찾아내기

 

package com.example.pp252

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})
    // val result = add(2, 3) {a, b -> a + b}와 같이 표현 가능
    println(result)
}

람다식으로 연산식 작성 예제

 


자료형 제한하기

형식 매개변수를 특정한 자료형으로 제한

 

- 자료형의 사용범위를 좁히기 위해 자료형을 제한

- 코틀린은 콜론(:)과 자료형을 기입하면 형식 매개변수 T의 자료형이 기입한 자료형으로 제한

    (자바에서는 extends나 super를 사용해 자료형을 제한)


package com.example.pp254

class Calc4<T: Number> {    // 클래스의 형식 매개변수 제한
    fun plus(arg1: T, arg2: T): Double {
        return arg1.toDouble() + arg2.toDouble()
    }
}

fun main(args: Array<String>) {
    val calc = Calc4<Int>()
    println(calc.plus(10,20))

    val calc2 = Calc4<Double>()
    val calc3 = Calc4<Long>()
    //val calc4 = Calc4<String>()     // 제한된 자료형으로 오류

    println(calc2.plus(2.5, 3.5))
    println(calc3.plus(5L, 10L))
}

자료형을 숫자로 제한하는 예제

** String은 Number의 자식이 아니다! 그래서 오류

 

package com.example.pp255

fun <T: Number> addLimit(a: T, b: T, op: (T, T) -> T): T {
    return op(a, b)
}

fun main() {
    //val result = addLimit("abc", "def", {a, b -> a + b}) // 제한된 자료형으로 오류
}

함수의 자료형 제한 예제

 


상, 하위 형식의 가변성

- 가변성(variance) : 형식 매개변수가 클래스 계층에 어떤 영향을 미치는지 정의

    형식 A의 값이 필요한 모든 장소에 형식 B의 값을 넣어도 아무 문제가 없다면 B는 A의 하위 형식(subtype)

    (Int는 Number의 하위 클래스)

  • String : 클래스O, 자료형O
  • String? : 클래스X, 자료형O
  • List : 클래스O, 자료형O
  • List<String> : 클래스X, 자료형O

** null을 허용하는 자료형은 자료형x , 객체형으로 본다. 따라서 힙영역에 저장됨.

 

- 하위클래스는 상위클래스가 수용

    하위 자료형은 상위 자료형으로 자연스럽게 형 변환이 이루어짐

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



val integer: Int = 1;
val nullableInteger: Int? = integer;
println($integer)
println(nullableInteger)

 

가변성의 3가지 유형

  • 공변성(covariance)   :  T'가 T의 하위 자료형이면, C<T'>는 C<T>의 하위 자료형이다. 생산자 입장의 out 성질
  • 반공변성(contravariance)   :  T'가 T의 하위 자료형이면, C<T'>는 C<T>의 하위 자료형이다. 소비자 입장의 in 성질
  • 무변성(invariance)   :  C<T>와 C<T'>는 아무 관계가 없다. 생산자 + 소비자

무변성

- 자료형 사이의 하위 자료형 관계가 성립하지 않음

- 코틀린에서는 따로 지정해주지 않으면 기본적으로 무변성

package com.example.pp259

// 무변성(Invariance) 선언
class Box<T>(val size: Int)

fun main(args: Array<String>) {
    val anys: Box<Any> = Box<Int>(10)   // 자료형 불일치 오류
    val nothings: Box<Nothing> = Box<Int>(20)   // 자료형 불일치 오류
}

공변성

- 형식 매개변수 사이의 하위 자료형 관계가 성립

- 하위 자료형 관계가 그대로 인스턴스 자료형 사이의 관계로 이루어지는 경우

- out 키워드를 사용해 정의

package com.example.pp260

// 공변성(Covariance) 선언
class Box<out T>(val size: Int)

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


반공변성

- 자료형의 상하 관계가 반대

- 하위 클래스의 자료형을 상위 클래스의 자료형이 허용

package com.example.pp261

// 반공변성(Contravariance) 선언
class Box<in T>(val size: Int) 

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


package com.example.pp262

open class Animal(val size: Int) {
    fun feed() = println("Feeding...")
}

class Cat(val jump: Int): Animal(50)

class Spider(val poison: Boolean): Animal(1)

// 형식 매개변수를 Animal로 제한
class Box<out T: Animal>(val element: T) {  // 주 생성자에서 val만 허용
    fun getAnimal(): T = element // out은 반환 자료형에만 사용 가능
    /*fun set(new: T) {   // 오류 T는 in 위치에 사용할 수 없다.
        element = new
    }*/
}

fun main() {
    // 일반적인 객체 선언
    val c1: Cat = Cat(10)
    val s1: Spider = Spider(true)

    // 클래스의 상위 자료형 반환은 아무런 문제가 없다.
    var a1: Animal = c1
    a1 = s1     // a1은 Spider의 객체가 된다.
    println("a1.size = ${a1.size}")

    // 공변성 - Cat은 Animal의 하위 자료형
    val c2: Box<Animal> = Box<Cat>(Cat(10))
    println("c2.element.size = ${c2.element.size}")

    // 반대의 경우는 가능하지 않다.
    // val c3: Box<Cat> = Box<Animal>(10)   // 오류

    // 자료형이 제한되어 Animal과 하위 클래스 이외에는 사용할 수 없다.
    // val c4: Box<Any> = Box<Int>(10)      // 오류류
}

자료형을 제한하는 제네릭 클래스

 


배열

- arrayOf()나 Array() 생성자를 사용해 배열 생성

- arrayOfNulls()은 빈배열

val numbers = arrayOf(2, 5, 2, 3) // 정수형으로 초기화된 배열
val animals = arrayOf("abc", "def", "ghi") // 문자열형으로 초기화된 배열

for (element in numbers) { // 정수형으로 초기화된 배열 출력하기
	println(element)
}

 

다양한 자료형의 혼합 가능

val mixArr = arrayOf(4, 5, 7, 3, "Click", false) // 정수, 문자열, Boolean 혼합

 

특정 자료형을 제한할 경우

- arrayOf<자료형>()                혹은                자료형이름 + ArrayOf()

- charArrayOf(), booleanArrayOf(), longArrayOf(), shortArrayOf(), byteArrayOf(), intArrayOf()

- ubyteArrayOf(), ushortArrayOf(), uintArrayOf(), ulongArray()

** intArrayOf()는 내부적으로 int[] 형으로 변환

 

연산자를 통한 접근

  • arr.get(index)       ->       value = arr[index]
  • arr.set(index)       ->       arr[index] = value

package com.example.test

import java.util.Arrays

fun main(args: Array<String>) {
    val arr = intArrayOf(1, 2, 3, 4, 5, 8)
    println("arr: ${Arrays.toString(arr)}")
    println("size: ${arr.size}")
    println("sum(): ${arr.sum()}")

    // 게터에 의한 접근과 대괄호 연산자 표기법
    println(arr.get(2))
    println(arr[2])

    // 세터에 의한 값의 설정
    arr.set(1, 5)
    arr[0] = 3
    println("size: ${arr.size} arr[0]: ${arr[0]}, arr[2], ${arr[2]}")

    // 루프를 통한 배열 요소의 접근
    for(i in 0..arr.size -1) {
        println("arr[$i] = ${arr[i]}")
    }
}

Array Test 예제

 

package com.example.pp270

fun main() {
    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 arr2d2 = arrayOf(arrayOf(1, 2, 3), arrayOf(4, 5, 6), arrayOf(7, 8, 9))

    for(e1 in arr2d) {
        for(e2 in e1) {
            print(e2)
        }
        println()
    }
    println()
    for(e1 in arr2d2) {
        for(e2 in e1) {
            print(e2)
        }
        println()
    }
}

다차원 배열 예제

 

 

댓글