제네릭 다루기
제네릭이 나오게 된 이유는 다료형의 객체들을 다루는 메서드나 클래스에서 컴파일 시간에 자료형을 검사해 적당한 자료형을 선택할 수 있도록 하기 위해서 나오게 되었다.
클래스 내부에서 사용할 자료형을 나중에 인스턴스를 생성할 때 확정한다.
아래는 예시이다.
package chap08.section1
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)
}
Box<T>에서 T는 형식 매개변수이다.
제네릭에서 사용하는 형식 매개변수는 아래와 같다.
형식 매개변수 이름 | 의미 |
E | 요소(Element) |
K | 키(Key) |
N | 숫자(Number) |
T | 형식(Type) |
V | 값(Value) |
S, U, V etc. | 두번째, 세번째, 네번째 형식 |
자료형 변환
제네릭 클래스의 자료형을 변환할 수 있다.
이것은 코드블럭보다 아래 스크린샷으로 보는게 좋을것 같다.
null 또한 가능하다
package chap08.section1
class GenericNull<T> { // 기본적으로 null이 허용되는 형식 매개변수
fun EqualityFunc(arg1: T, arg2: T){
println(arg1?.equals(arg2))
}
}
fun main() {
val obj = GenericNull<String>() // non-null로 선언됨
obj.EqualityFunc("Hello", "World") // null이 허용되지 않음
val obj2 = GenericNull<Int?>() // null이 가능한 형식으로 선언됨
obj2.EqualityFunc(null, 10) // null 사용
}
형식 매개변수를 받는 함수나 메서드를 제네릭 함수 또는 메서드라고 한다.
제네릭 함수를 통해 배열의 인덱스를 찾아내는 코드이다.
package chap08.section1
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 chap08.section1
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}와 같이 표현 가능
print(result)
}
자바에서는 extends나 super를 사용해서 자료형을 제안했는데, 코틀린에서는 콜론(:)을 사용해서 제한할 수 있다.
아래의 예시이다.
package chap08.section1
class Calc<T: Number>{ // 클래스의 형식 매개변수 제한
fun plus(arg1: T, arg2: T) : Double{
return arg1.toDouble() + arg2.toDouble()
}
}
fun main(){
val calc = Calc<Int>()
println(calc.plus(10, 20))
val calc2 = Calc<Double>()
val calc3 = Calc<Long>()
// val calc4 = Calc<String>() // 제한된 자료형으로 인해 오류 발생!
println(calc2.plus(2.5, 3.5))
println(calc3.plus(5L, 10L))
}
상 - 하위 형식의 가변성
가변성이란 형식 배개변수가 클래스 계층에 영향을 주는것을 말한다.
예를 들어 형식 A의 값이 필요한 모든 클래스에 형식 B의 값을 넣어도 아무 문제가 없다던 B는 A의 하위 형식이 된다.
클래스와 자료형
String은 클래스이고 자료형이다.
하지만 null도 가질 수 있는데 String? 는 클래스가 아닌 자료형이다.
왜 이런식으로 구분을 할까?
보통 클래스는 파생된 하위 클래스와 상위 클래스가 있다. 예를 들어 Int는 Number의 하위 클래스이다.
하위 클래스는 상위 클래스가 수용할 수 있다.
가변성에는 유형이 3가지가 있다.
용어 | 의미 |
공변성(Covariance) | T`가 T의 하위 자료형이면, C<T`>는 C<T>의 하위 자료형이다. 생산자 입장의 out 성질 |
반공변성(Contravariance) | T`가 T의 하위 자료형이면, C<T>는 C<T`>의 하위 자료형이다. 소비자 입장의 in 성질 |
무변성(Invariance) | C<T>와 C<T`>는 아무 관계가 없다. 생산자 + 소비자 |
무변성
제네릭 클래스를 인스턴스화 할 때 서로 다른 자료향을 인자로 사용하려면 자료형 사이의 상, 하위 관계를 잘 따져야 한다.
Any, Int형 자료형과 하상관계를 가지고 있는 Nothing, Int형 자료형을 형식 매개변수의 인자로 사용하고 있다면,
상하 관계를 잘 따졌어도 Box<T>가 무변성이므로 자료형 불일치 오류가 발생한다.
// 무변성 선언
class Box<T>(val size: Int)
fun main() {
val anys: Box<Any> = Box<Int>(10) // 자료형 불일치
val nothings: Box<Nothing> = Box<Int>(20) // 자료형 불일치
}
공변성
형식 매개변수의 상하 자료형 관계가 성립하고, 그 관계가 그대로 인스턴스 자료형 관계로 이어지는 경우를 공변성이라고 한다.
예를 들어 Int가 Any의 하위 자료형일 때 형식 매개변수 T에 대해 공변적이라고 한다. 이때는 out 키워드를 사용한다.
// 공변성 선언
class Box<out T>(val size: Int)
fun main() {
val anys: Box<Any> = Box<Int>(10) // 관계 성립으로 객체 생성 가능
val nothings: Box<Nothing> = Box<Int>(20) // 자료형 불일치
}
여기서는 Out이라는 키워드에 의해 형식 매개변수가 공변적으로 선언되어 상하 자료형 관계가 성립되었다.
즉, Any의 하위 클래스인 Int는 공변성을 가지므로 Box<Any>에 Box<Int>자료형을 할당할 수 있게 되었다.
하지만 <Nothing>은 <Int>의 하위 자료형이 아니므로 오류가 난다.
반공변성
자료형의 상하 관계가 반대가 되어 인스턴스의 자료형이 상위자료형이 되는것.
키워드를 in으로 사용한다.
// 반공변성 선언
class Box<in T>(val size: Int)
fun main() {
val anys: Box<Any> = Box<Int>(10) // 자료형 불일치
val nothings: Box<Nothing> = Box<Int>(20) // 관계 성립으로 객체 생성 가능
}
위 3가지 종류를보니 각 클래스의 관계별로 사용하는 키워드가 다르다는 것을 확인했다.
공변성에 따른 자료형을 제한할 수 있다.
package chap08.section1.limit
open class Animal(val size: Int){
fun feed() = println("Feeding...")
}
class Cat(val jump: Int): Animal(50)
class Spider(val posion: 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}")
val c2: Box<Animal> = Box<Cat>(Cat(10)) // 공변성 - Cat은 Animal의 하위 자료형
println("c2.element.size = ${c2.element.size}")
// val c3: Box<Cat> = Box<Animal>(10) // 반대의 경우에는 인스턴스화 되지 않음
// val c4: Box<Any> = Box<Int>(10) // 자료형을 제한하여 Animal과 하위 클래스 이외에는 사용할 수 없음
}
class VarianceLimitTest {
}
자료형 프로젝션
가변성을 지정하는 2가지 방법이 있다.
1. 선언 지점 변성(declaration-site variance)
클래스를 선언하면서 클래스 자체에 가변성을 지정하는 방식으로 클래스에 in/out을 지정할 수 있다.
클래스를 선언하면서 가변성을 지정하면 클래스의 공변성을 전체적으로 지정하는 것이 되기 때문에 클래스를 사용하는 장소에서는 따로 자료형을 지정해 줄 필요가 없어 편리하다.
2. 사용 지점 변성(use-site variance)
메서드 매개변수에서 또는 제네릭 클래스를 생성할 때와 같이 사용 위치에서 가변성을 지정하는 방식이다.
형식 매개변수가 있는 자료형을 사용할 때마다 해당 형식 매개변수를 하위 자료형이나 상위 자료형 중 어떤 자료형으로 대체 할 수 있는지를 명시해야한다.
아래는 사용 지점 변성에 대한 예시이다.
fun <T> printObj(box: Box<out Animal>){
box.item = Animal() // 설정(set)하려고 할때는 in이 지정되어야함.
println(obj)
}
val animal: Box<Animal> = Box(Animal())
val cat: Box<Cat> = Box(Cat())
printObj(animal) // 가능
printObj(cat) // Box<>는 무변성으로 지정되었기 때문에 오류
스타 프로젝션
in과 out을 정하지 않고 스타(*)를 통해 지정하는 방법을 스타 프로젝션이라고 한다.
class InOutTest<in T, out U>(t: T, u: U) {
val propT: T = t // T는 in 위치이기 때문에, out위치에 사용 불가
val propU: U = u // U는 out 위치로 가능
fun func1(u: U) // U는 out위치이기 때문에 in 위치에 사용불가
fun func2(t: T){ // T는 in위치에 사용됨
print(t)
}
}
fun starTestFunc(v: InOutTest<*.*>) {
v.func2(1) // Nothing으로 인자를 처리함
println(v.propU)
}
in으로 정의되어 있는 형식 매개변수를 *로 받으면 in Nothing인 것으로 간주하고,
Out으로 정의되어 있는 형식 매개변수를 *로 받으면 out Any?인것으로 간주한다.
따라서 *를 사용할 때 그 위치에 따라 메서드 호출이 제한될 수 있다.
(Nothing과 Any는 정반대이다.)
자료형 프로젝션을 정리하자면 아래와 같다.
종류 | 예 | 가변성 | 제한 |
out 프로젝션 | Box<out Cat> | 공변성 | 형식 매개변수는 세터를 통해 값을 설정하는 것이 제한된다. |
in 프로젝션 | Box<in Cat> | 반공변성 | 형식 매개변수는 게터를 통해 값을 읽거나 반환할 수 있다. |
스타 프로젝션 | Box<*> | 모든 인스턴스는 하위 형식이 될 수 있다. | in과 out은 사용 방법에 따라 결정된다. |
reified 자료형
reified는 우리말로 '구체화된'이라는 뜻을 가진다.
인라인함수에서만 사용할 수 있다.
호출되면 본문 코드 내용은 호출되는 곳 어디든 복사되어 들어가게 된다.
이때 reified T 자료형은 컴파일러가 복사해 넣을 때 실제 자료형을 알 수 있기 때문에 실행 시간에도 사용할 수 있게 된다.
사용 예시를 보자.
package chap08.section1
fun main() {
val result = getType<Float>(10)
println("result = $result")
}
inline fun <reified T> getType(value: Int): 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!")
}
}
배열 다루기
C언어와 비슷하다.
선언은 val numbers = arrayOf(4, 5, 7, 3) 이런식으로 선언을 한다.
C언어와 다른점은 원소가 어떤 type을 쓸지 적지 않아도 된다는것이다.
2차원 배열은 아래와 같이 선언한다. 3차원배열도 저런 방식일 것이다.
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 mixArr = arrayOf(4, 5, 7, 3, "Chike", false)
이런 경우를 방지하고자 할때는 자료형을 제한할 수도 있다.
아래가 그 예시이다.
val intOnlyArr1 = arrayOf<Int>(4, 5, 7, 3)
val intOnlyArr2 = intArrayOf(4, 5, 7, 3) // 자료형 이름과 ArrayOf의 조합
배열을 선언하고 접근을 연습해보자.
package chap08.section2
import java.util.Arrays
fun main() {
val arr = intArrayOf(1, 2, 3, 4, 5)
println("arr: ${Arrays.toString(arr)}") // Arrays.toString()은 베열의 내용을 문자열로 변환
println("size: ${arr.size}") // size는 배열의 크기를 나타냄
println("sum(): ${arr.sum()}") // sum() 메서드는 배열의 합을 계산
// 게터에 의한 접근과 대괄호 연산자 표기법
println(arr.get(2))
println(arr[2])
// 세터에 의한 값의 설정
arr.set(2, 7)
arr[0] = 8
println("size: ${arr.size} arr[0]: ${arr[0]}, arr[2]: ${arr[2]}")
for (i in 0..arr.size -1){
println("arr[$i] = ${arr[i]}")
}
}
배열을 제한하고 처리하는 방법이 있다.
python에서 append pop index그런 내용의 느낌이다.
append -> plus
[a:b] -> sliceArray(a..b)
index(a) -> indexOf(a)
average()
count()
Any로도 배열을 선언할 수 있다.
package chap08.section2
fun main(){
val b = Array<Any>(10, {0})
b[0] = "Hello World"
b[1] = 1.1
println(b[0])
println(b[1])
println(b[2])
}
배열 정렬하는 메서드도 있다.
package chap08.section2
import java.util.*
fun main() {
val arr = arrayOf(8, 4, 3, 2, 5, 9, 1)
// 오름차순, 내림차순으로 정렬된 일반 배열로 반환
val sortedNums = arr.sortedArray()
println("ASC: " + Arrays.toString(sortedNums))
val sortedNumsDesc = arr.sortedArrayDescending()
println("DEC: " + Arrays.toString(sortedNumsDesc))
// 원본 배열에 대한 정렬
arr.sort(1, 3)
println("ORI: " + Arrays.toString(arr))
arr.sortDescending()
println("ORI: " + Arrays.toString(arr))
// List로 반환
val listSorted: List<Int> = arr.sorted()
val listDesc: List<Int> = arr.sortedDescending()
println("LST: " + listSorted)
println("LST: " + listDesc)
// SortBy를 이용한 특정 표현식에 따른 정렬
val items = arrayOf<String>("Dog", "Cat", "Lion", "Kangaroo", "Po")
items.sortBy{item -> item.length}
println(Arrays.toString(items))
}
sortBy()로 데이터 클래스를 정렬했는데, 편리하다.
좀 더 사용해보자.
package chap08.section2
data class Product(val name: String, val price: Double)
fun main() {
val products = arrayOf(
Product("Snow Ball", 870.00),
Product("Smart Phone", 999.00),
Product("Drone", 240.00),
Product("Mouse", 333.55),
Product("Keyboard", 125.99),
Product("Monitor", 1500.99),
Product("Tablet", 512.99)
)
products.sortBy{it.price} // 값에 따라 정렬
products.forEach {println(it)}
}
iterator 너무 좋은데?
필요한 정보만 골라서 변경하는 메소드도 있다.
package chap08.section2
fun main() {
val fruits = arrayOf("banana", "avocado", "apple", "kiwi")
fruits
.filter {it.startsWith("a")}
.sortedBy { it }
.map {it.toUpperCase()}
.forEach{ println(it) }
}
파이썬에서는 zip을 활용해서 2차원 list를 1차원으로 변경했다고 하면,
코틀린에서는 flatten이 있다.
이것을 배열을 평탄화(flatten) 한다고 한다.
package chap08.section2
fun main() {
val numbers = arrayOf(1, 2, 3)
val strs = arrayOf("one", "two", "three")
val simpleArray = arrayOf(numbers, strs) // 2차원 배열
simpleArray.forEach { println(it) }
val flattenSimpleArray = simpleArray.flatten() // 단일 배열로 반환하기
println(flattenSimpleArray)
}
문자열 다루기
눈에 띄는 내용이 없다.
예제 하나로 정리해보자.
package chap08.section3
import java.util.*
fun main(){
// books
val booksName = arrayOf("java", "C", "C++", "Kotlin", "C#", "html")
val price = 200.00f
val studentDiscount = .25f
val teacherDiscount = .15f
// Scanner 객체에 System.on이라는 InputStream을 넣어서 scanner에 저장
// 'in'으로 감싼 이유는 in이 예약어 이기 때문
val scanner = Scanner(System.`in`)
println("** 오리의 서점에 오신 걸 환영합니다. **")
do{
println(Arrays.toString(booksName))
println("어떤 책을 원하시나요?")
println("답번: ")
// \n을 포함하는 한 줄을 읽고 \n을 버리고 남은 문자열 반환 및 할당
val book = scanner.nextLine()
if (booksName.contains(book.toLowerCase())){
println("선택하신 책은 $book 입니다. 가격은 \$200")
println("학생과 선생님인 경우에 할인을 적용하고 있습니다." +
"\n당신의 직업은 무엇인가요(student, teacher, etc)?")
print("답번: ")
val occupation = scanner.nextLine()
when (occupation.toLowerCase()){
"student" -> calculatePrice(price, studentDiscount)
"teacher" -> calculatePrice(price, teacherDiscount)
else -> println("할인을 적용할 수 없습니다." +
"\n최종 가격은 \$200 입니다.")
}
} else if(book == "exit" || book == "q"){
break
} else{
println("죄송합니다. $book 의 재고가 없습니다.")
}
} while(true)
}
private fun calculatePrice(orig: Float, x: Float): Unit{
val result = orig - (orig * x)
println("최종 가격은 \$$result 입니다.")
}
10주차 - 표준 함수와 파일 입출력 (0) | 2024.01.12 |
---|---|
9주차 - 컬렉션 (1) | 2024.01.11 |
7주차 - 다양한 클래스와 인터페이스 (0) | 2024.01.09 |
6주차 - 프로퍼티와 초기화 (3) | 2024.01.09 |
5주차 - 클래스와 객체의 정의 (1) | 2024.01.06 |
댓글 영역