
interface DiInterfaceA {
fun print() {
println("A: 기본적으로 구현되어있는 내용입니다!")
}
}
interface DiInterfaceB {
fun print() {
println("B: 기본적으로 구현되어있는 내용입니다!")
}
}
class DiImpl2 : DiInterfaceA, DiInterfaceB {
override fun print() {
println("D: 직접 구현했습니다!")
}
}
class DiImpl3 : DiInterfaceA, DiInterfaceB {
override fun print() {
super<DiInterfaceA>.print()
super<DiInterfaceB>.print()
println("D: 직접 구현했습니다!")
}
}
print() 메서드를 재정의해준다. super 키워드를 사용할 때 어떤 인터페이스의 메서드를 사용하는 것인지 <>로 명시해준다.companion object는 한 쌍처럼 작동하며, 클래스 내부에 단 하나만 선언할 수 있습니다.class HelloRobot {
companion object {
fun hello() {
println("Hello! Hello! Hello!")
}
}
}
fun main() {
HelloRobot.hello()
}
public static void main(String[] args) {
HelloRobot.Companion.hello();
Companion을 붙여줘야 가능하다. class ByeRobot {
companion object {
@JvmStatic
fun bye() {
println("Bye! Bye! Bye!")
}
}
}
@JvmStatic을 붙여주면, Kotlin에서 정의된 정적(static) 멤버를 Java에서 자연스럽게 사용할 수 있다.public static void main(String[] args) {
ByeRobot.bye();
}
equals(), hashCode(), toString(), copy(), componentN() 등을 자동으로 생성해준다.⚠️ data class 제약사항
primary constructor(주 생성자)에는 최소 하나 이상의 val 또는 var 프로퍼티가 있어야 함abstract, open, sealed, inner는 사용할 수 없음final class)data class MemberLabel(var name: String, val code : String)
구조 분해
val (member1Name, member1Code) = member1
println("member1Name = ${member1Name}")
println("member1Name = ${member1Code}")
val (member2Name, _) = member2
val (_, member2Code) = member2
println("member2Name = ${member2Name}")
println("member2Code = ${member2Code}")
val member3Name = member3.component1()
println("member3Name = ${member3Name}")
data class의 값들을 얻기 위해서는 위와 같이 접근해야 한다. var member1Name = member1.name은 사용할 수 없다.object 는 단 하나의 인스턴스만 존재하는 싱글통 객체가 만들어진다.
object Logger {
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
}
object Config {
val appName: String
val version : String
init {
println("Config initialized")
appName = "MyApp"
version = "1.0"
}
}
fun main() {
Logger.log("Hello")
Config.appName
}
object 키워드를 사용하면 기본적으로 lazy(지연) 초기화된다.runCatching {
10/0
}.onSuccess {
println("나눗셈한 결과 $it")
}.onFailure {
println("오류가 발생했습니다!")
println("${it.message}")
}
Result.success / Result.failure로 나눠 받는다. onSuccess, onFailure) 사용 가능클래스나 함수, 인터페이스를 정의할 때 데이터 타입을 일반화(Generalization)하는 것이다. 컴파일 타임에 타입을 지정하게 하기 때문에 타입 안정성을 확보할 수 있다.
클래스 정의 시 타입 매개변수를 사용하여 다양한 데이터 타입을 처리할 수 있는 클래스를 의미한다.
fun <문자> 함수이름(매개변수:문자,[..]) {
}
class 클래스명<문자> {
}
fun <T> func(param:T) {
}
fun main() {
func<String>("100")
func(100)
}
<>을 생략할 수 있다. 제네릭 함수 (Generic Function)는 함수 정의 시 데이터 타입을 일반화하여 여러 타입에 대해 동일한 로직을 적용할 수 있도록 하는 기능이다. 제네릭 함수는 함수나 메서드 앞쪽에 형식 매개변수를 지정하는 방식으로 사용한다.
fun <타입문자열[, ...]> 함수 이름(매개변수 : [<타입문자열[,...]>]): [<타입문자열>]
fun <T> printList(target: List<T>) : Unit {
for (i: T in target) {
print("$i ")
}
}
Kotlin의 제네릭 타입 제한 (Type Bounds)은 제네릭 타입에 사용할 수 있는 타입을 제한하는 기능이다. 이 개념은 타입의 안전성(Type Safety)을 유지하면서, 특정 메서드나 클래스가 사용할 수 있는 기능(메서드)을 보장하기 위해 사용된다.
class Container<T: Number>
fun main() {
val intContainer: Container<Int> = Container()
val doubleContainer: Container<Double> = Container()
val stringContainer: Container<String> = Container()
// Number로 타입이 제한되어있기 때문에 String을 매개변수로 가질 수 없다.
}
: 를 이용해서 제네릭 타입의 상한을 지정할 수 있다.class GeDog: GeAnimal("Dog")
class GeCat: GeAnimal("Cat")
class GeCar
fun <T : GeAnimal> feedToAnimal(animals: List<T>, sth: String): Unit {
for (animal in animals) {
animal.eat(sth)
}
}
fun main() {
val animals = listOf(GeDog(), GeCat())
val cars = listOf(GeCar(), GeCar())
feedToAnimal(animals, "간식")
// feedToAnimal(cars, "휘발유") // 타입 제한으로 사용할 수 없음
}
:를 사용하면 된다.interface A
interface B
interface C
class AbImpl : A, B
class AbcImpl : A, B, C
class AImpl : A
class BImpl : B
class Container<T> where T : A, T : B
fun main() {
Container<AbImpl>() // 생성 가능
Container<AbcImpl>() // 생성 가능
// Container<AImpl>() // 생성 불가능
// Container<BImpl>() // 생성 불가능
}
where 키워드를 통해 제한할 수 있다. 가변성은 형식 매개변수가 클래스 계층에 영향을 주는 것을 의미한다. 가변성은 제네릭 타입 A와 B가 있을 때, A가 B의 하위 타입이라면
Class<A>도Class<B>의 하위타입이어야 할지 아닐지를 결정하기 위한 규칙이라고 볼 수 있다.
open class Animal
class Cat : Animal()
class Dog : Animal()
val cats : List<Cat> = listOf(Cat(), Cat())
val animals: List<Animal> = cats // 불가능
Cat은 Animal의 하위 타입이지만, List<Cat>은 List<Animal>의 하위 타입이 아니기 때문에 animals를 cats에 다시 할당하는 것이 불가능하다.제네릭 타입 A, B가 있는데 A가 B의 하위 타입일 때, 제네릭 타입이나 함수 등의 특정 형식에서 A를 B처럼 안전하게 사용할 수 있는 상황을 공변성이라고 한다. 즉, 공변성은 타입간의 상속관계가 어떤 컨텍스트에서도 그대로 보존되는 성질을 의미한다.
하지만, 공변성을 아무 조건없이 허용하게 되면 오류가 발생할 수 있다.
open class Animal
class Cat : Animal()
class Dog : Animal()
class Box<T>(private var item: T) {
fun get(): T = item
fun set(value: T) { item = value }
}
fun feedAnimal(box: Box<Animal>) {
// ...
}
val catBox: Box<Cat> = Box(Cat())
// feedAnimal(catBox) // ❌ 오류! 타입 불일치
Box<Cat>는 Box<Animal>의 하위 타입이 아니다.그렇기 때문에, 명시적으로 읽기 전용으로만 쓸 것이라고 표기하고 값을 받을 수 있도록 한다면 A를 B처럼 안전하게 사용할 수 있는 상황이 된다.
class ReadOnlyBox<out T>(private val item: T) {
fun get(): T = item
// fun set(value: T) { item = value } // ❌ 컴파일 오류: out 사용 시 set 불가
}
fun readAnimal(box: ReadOnlyBox<Animal>) {
println(box.get())
}
val catBox = ReadOnlyBox(Cat()) // 생성자에서 값 넣는 건 OK
val animalBox: ReadOnlyBox<Animal> = catBox // 공변성 덕분에 업캐스팅 OK
readAnimal(catBox) // ✅ OK! ReadOnlyBox<Cat> is a subtype of
val animal: Animal = animalBox.get() // 값 꺼내는 건 OK
out을 사용할 수 있다.반공변성은 공변성과 반대의 개념으로, 타입 상속관계가 특정 컨텍스트에서 반대방향으로 적용되는 성질을 의미한다. 반공변성은 쓰기만 하는 객체를 다룰 때 안전하게 타입을 일반화할 수 있다.
open class Animal {
fun feed() = println("Feeding animal")
}
class Cat : Animal()
class Dog : Animal()
class AnimalFeeder<in T> {
fun feed(animal: T) {
println("Feeding one ${animal::class.simpleName}")
}
}
val catFeeder: AnimalFeeder<Cat> = AnimalFeeder<Animal>() // ✅ 가능!
catFeeder.feed(Cat()) // ✅ Cat을 받아서 먹일 수 있음
val animalFeeder: AnimalFeeder<Animal> = AnimalFeeder<Cat>() // ❌ 불가!
in를 사용한다.특정 자료형에
in또는out을 지정하여 제한하는 것을 자료형 프로젝션(Type Projection)이라고 한다.
선언 지점 변성
Kotlin은 가변성을 설정할 때 클래스를 정의하는 시점, 즉, 타입 파라미터를 선언하는 그 순간에 변성 방향성을 명시할 수 있는데 이를 선언 지점 변성(Declaration-Site Variance)이라 한다.
// T를 out으로 선언하여 공변성 부여
class ReadOnlyBox<out T>(private val value: T) {
fun get(): T = value
// fun set(newValue: T) { value = newValue } // out은 값을 넣을 수 없음
}
open class Animal
class Dog : Animal()
class Cat : Animal()
fun main() {
val dogBox: ReadOnlyBox<Dog> = ReadOnlyBox(Dog())
// 공변성 덕분에 Dog는 Animal의 하위 타입이므로 대입 가능
val animalBox: ReadOnlyBox<Animal> = dogBox
val animal = animalBox.get() // 리턴은 Animal로 추론
println(animal is Dog) // true
}
in 또는 out을 지정하는 것이다.사용자 지점 변성(Use-Site Variance)
이미 정의된 제네릭 타입을 사용하는 입장에서, 즉, 함수나 변수에 전달할 때 가변성을 지정할 때 사용하는 방식을 말한다. 사용자 지점 변성은 제네릭 타입을 사용하는 그 순간에 해당 타입의 방향성(입력만 가능한지, 출력만 가능한지)를 지정해주는 방식을 의미한다.
open class Animal {
fun eat() = println("밥을 먹습니다!")
}
class Dog : Animal() {
fun bark() = println("멍!")
}
fun copyAnimals(from: List<out Animal>) {
for (animal in from) {
animal.eat()
// animal.bark() // Animal로 인식되므로 bark 사용 불가
}
}
fun main() {
val dogs: List<Dog> = listOf(Dog(), Dog())
copyAnimals(dogs) // Dog 리스트를 Animal 리스트처럼 사용
val animals: List<Animal> = listOf(Animal(), Dog())
copyAnimals(animals) // Animal 리스트도 가능
}
dogs 리스트가 copyAnimals 함수의 인자로 전달된다 하더라도 Animal 타입을 받고 있기 때문에 animal.bark() 함수를 사용할 수 없다.open class Animal {
fun eat() = println("밥을 먹습니다!")
}
class Dog : Animal() {
fun bark() = println("멍멍!")
}
class Feeder<T> {
fun feed(animal: T) {
println("먹이를 줍니다.")
animal.eat()
}
}
fun feedDogs(feeder: Feeder<in Dog>) {
feeder.feed(Dog()) // Dog 넣기 가능
}
fun main() {
val animalFeeder = Feeder<Animal>()
feedDogs(animalFeeder) // Feeder<Animal>을 Feeder<Dog> 대신 사용 가능 (in 덕분)
}
스타 프로젝션
*는 제네릭 타입 매개변수의 구체적인 타입을 알 수 없거나, 여러 가지 타입 중 하나를 허용할 필요가 있을 때 사용된다.
fun printEl(el: List<*>) {
for ( item: Any? in el ) {
print("$el ")
}
}
*는 타입 매개변수에 대한 제한이 없는 경우에 사용된다. 이 경우, 어떤 타입이든 상관없이 사용할 수 있다. fun printBox(box: Box<*>) {
println(box.value)
}
box.value는 타입이 뭐든 알 수 없으니까 Any?로 간주된다.1. 컴파일 시점의 *
Box<>를 쓰면 컴파일러는 "타입 파라미터가 뭔지 모른다"라고 인식한다. 그래서 Box<> 내부 값은 Any? 타입으로 처리되고, 쓰기(값 넣기)는 제한된다.
즉, 컴파일러 입장에서는 "이건 어떤 타입인지 알 수 없지만, 타입 안전을 위해 제한적으로 다룰게"라는 뜻입니다.
2. 런타임에는?
JVM에서 제네릭은 타입 소거(Type Erasure)되어서 런타임에는 제네릭 타입 정보가 사라진다.
따라서 Box<String>이나 Box<Int> 모두 그냥 Box로 실행됩니다.
즉, 런타임에 *인지 아닌지, 구체적인 타입인지 알 수 없어요.
정리하자면, (스타 프로젝션)은 컴파일러에게 "타입 정보를 알 수 없어요"라고 표시하는 것이고,
런타임에서는 제네릭 타입 정보가 아예 없기 때문에, 결국 는 완전히 타입 정보를 모르는 상태이다.
아... 코틀린 어렵다.. 근데 어려운 게 당연하다고 생각해.. 지금까지 안 어려웠던 것도 대단하다고 생각해 .. 그래 어려워야지.. 가변성부터 진~짜 너무 헷갈린다. 공변성, 불공변성, 스타 프로젝션, 자료형 프로젝션 진짜 너무 헷갈린다. 실습 해보면 또 다를 것 같긴 한데 오늘 복습을 잘 해놔야 내일 잘 이해한 건지 확인하면서 실습 들을 수 있을 것 같다.
어렵군..! 정말 !!
그리고, 오늘 강사님이 자습 시간에 들어오셔서 면접 질문같이 CS 질문을 하셨는데,, 대답하지 못한 나.. ㅠㅠ 참.. 제대로 알지도 못하는 게 1차 문제지만, 이걸 말로 설명하는 것도 문제다 ! 안주하지 말고, 더 더 열심히 하려고 노력해야겠다.. 파이팅하자 !