Kotlin 다시 보기 (1)

‘빅 너드 랜치의 코틀린 프로그래밍’ 을 읽고 정리

타입

코틀린 타입 특징

  • 정적 타입 시스템

    • 소스 코드에 정의된 타입을 컴파일러가 알고 있다.
  • 프로그램을 컴파일하기 전에 타입 체킹 : 정적 타입 체킹

built-in, 내장 타입

  • String

  • Char

  • Boolean

  • Int

  • Double

  • List - 컬렉션 타입

  • Set - 컬렉션 타입

  • Map - 컬렉션 타입

변수

  • val : read-only, 변경 불가능, 읽기 전용
  • var : 값이 변경 될 수 있음, writeable 쓰기 가능, 가변성

타입 추론

  • 코틀린 컴파일러가 타입을 추론 해줌
  • 따라서, 타입을 생략 가능, 컴파일러가 소스 코드의 타입을 알고 있음
  • 코틀린은 기본 타입을 포함해서, 모든 타입이 객체다.
    • 코틀린 컴파일러가 자바의 기본 타입과 가장 유사한 것과 매핑

컴파일 시점 상수(constant)

  • val 변수는 상수(constant) 가 아니다.
  • const val MAX_LIMIT = 50000
  • 상수는 프로그램 실행 전에 생성 및 초기화
  • 초기화된 값을 절대로 변경하지 않는 값
  • 프로그램 실행 전 컴파일러가 알 수 있어야 함으로, ‘built-in 타입’ 중 하나가 되어야 함
    • String, Int, Double, Float, Boolean, Char
  • 모든 함수 외부에 정의 되어야 함, 컴파일 될때 값이 지정되야 함
  • 상수와 달리, 변수들은 runtime에 생성되어 값이 지정된다.

코틀린의 자바 기본 타입

  • 자바는 참조 타입과 기본 타입 2개가 존재

    • 기본 타입: 내장, 소문자

      • int
    • 참조 타입: 별도의 소스 코드 파일로 정의, Ex) 클래스, 항상 대문자

      • Interger
    • 모든 기본 타입은 그것과 대응되는 참조 타입이 있다. (역은 불가)

    • 기본 타입이 참조 타입보다 성능이 좋음

    • 기본 타입 배열(int[], byte[], boolean[])

  • 자바와 달리 코틀린은 참조 타입만 제공 (int가 아닌 Int만 제공) → 선택지가 하나로 됨, 코드 쉽게 작성 가능

  • 기본(primitive, 원시) 타입참조 타입(reference) 으로 사용

  • 코틀린 컴파일러는 가능한 한 자바 바이트코드의 기본(primitive) 타입을 사용한다. ( 더 좋은 성능을 내기에 )

  • 코틀린은 자바와 달리 원시 타입(Primitive Type)참조 타입(Reference Type)엄격하게 구분하지 않는다.

  • Nullable type(ex. Int?)은 자바의 Wrapper Class(Interger)로 컴파일된다.

  • 원시, 기본 타입, primitive
    • 값을 직접 저장하는 데이터 타입입니다.
    • 종류
      • 정수 타입: Byte, Short, Int, Long
      • 부동 소수점 타입: Float, Double
      • 문자 타입: Char
      • 불리언 타입: Boolean
    • 특징
      • 컴파일 시 자바의 기본 타입으로 변환됩니다. (예: Int -> int)
      • 숫자 타입은 객체처럼 사용할 수 있습니다. (박싱/언박싱 자동 처리)
        • 기본형을 참조형으로 변환: 이 기능을 박싱(boxing)
        • 오토박싱(Autoboxing)은 Java 컴파일러가 원시 타입해당 객체 래퍼 클래스 간에 수행하는 자동 변환을 말한다. (예시: int => Integer)
  • 참조 타입, reference
    • 객체의 메모리 주소를 저장하는 데이터 타입입니다.
    • 종류
      • 클래스 (Class)
      • 인터페이스 (Interface)
      • 배열 (Arrays)
      • 문자열 (String)
      • 컬렉션 (Collection)
      • 기타 객체 타입
특징기본 타입 (Primitive Type)참조 타입 (Reference Type)
메모리 할당JVM의 스택(Stack) 영역에 직접 값 할당JVM의 힙(Heap) 영역에 객체 생성 후 스택에 참조 값 저장
값 표현실제 값 (정수, 실수, 문자, 논리 값 등)객체의 메모리 주소 (참조 값)
크기고정된 크기 (타입별로 정의됨)가변적인 크기 (객체의 상태에 따라 달라짐)
연산값 자체를 이용한 연산참조 값을 이용한 연산 (객체의 메서드 호출 등)
비교== 연산자로 값 비교== 연산자로 참조 값 비교, .equals() 메서드로 객체 내용 비교
제네릭사용 불가사용 가능
종류byte, short, int, long, float, double, boolean, char클래스, 인터페이스, 배열, 열거형(enum), 문자열 등
메모리 사용량적음많음
속도스택에서 값을 직접 사용, 빠름스택에 저장된 참조 값을 통해 힙에 있는 객체에 접근, 느림
GC없음있음
OOP불가가능

조건문과 조건식

문과 식

  • 문(Statement): 프로그램의 실행 흐름을 제어하는 명령어 또는 명령어들의 집합입니다.

  • 식(Expression): 값을 생성하는 코드 조각, 값을 반환

  • 의 일부가 될 수 있다.

특징문 (Statement)식 (Expression)
값 생성XO
실행 결과 반환XO
상태 변경OX
세미콜론(;)O (일반적으로)X
예시int x = 10x + 5

조건 표현식 (conditional expression)

  • if/else 문(Statement) 대신 조건 표현식 을 사용할 수 있다.

  • 식(Expression)값을 반환한다.

  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
      val tmp1 = if () {
       	"yes"
      } else {
          "no"
      }
    
      val tmp2 = when (para) {
          100 -> "1"
          in 1..90 -> "2"
          else -> "3"
      }
    
  • when 문

    • else 없어도 상관 없음
  • when 표현식

    • else 가 꼭 있는지 컴파일러가 검증
    • 결과를 반환하기때문
    • 간결하고 다양하게 로직을 나타냄
  • when

    • 임의의 조건들을 검사할 수 있다
    • 풀 스루가 아님
  • Java switch 문

    • 값만 검사
    • 풀 스루 (break 없으면, 아래의 조건들도 검사)

함수

  • 기본적으로 가시성 제한자가 public

  • 함수의 매개변수(===인자)var이 아닌 val 이다.

  • 함수 내에 있는 지역 범수는 함수 범위에만 존재, 함수가 return 되면 소멸된다. (매개변수도 마찬가지)

    • 매개변수 또한 함수 종료시 소멸됨
  • 파일 수준 변수는 프로젝트 어디서든 사용 가능

    • 자바 클래스의 static 메서드가 된다, ~.kt 파일Java byteCode(.class)를 decompile(역컴파일)로 확인 가능
  • 파일 수준 변수는 초깃값이 지정되어야 한다. 아니면 컴파일 에러

  • 기본 인자 ( default argument ) 지원

    • 함수 인자가 기본값을 가짐
  • 단일 표현식, 함수 대입 연산자 = 으로 정의

  • overload 함수들은 컴파일러가 함수 호출시에 전달되는 인자의 타입갯수로 무슨 함수인지 알 수 있다.

Unit 함수

  • 아무 값도 return 하지 않는 함수 타입

  • void 로는 제네릭 함수 를 구현할 방법이 없음, Unit 으로 해결

    • void는 타입이 아니며, 타입 정보가 의미가 없으므로, 생략해라 라는 의미
  • Unit으로 함수의 반환 타입을 나타냄 => 제네릭 함수에도 사용 가능

Nothing 타입

  • 함수의 실행이 끝나더라도, 호출 코드로 제어가 복귀되지 않는다
  • 의도적으로 exception 예외를 발생시킬 때 사용
    • public inline fun TODO(): Nothing = throw NotImplementedError()
    • 컴파일러는 이 TODO 코드를 에러로 처리하지 않는다.
    • 제어가 복귀되지 않아서, 컴파일러가 TODO 코드의 다음 코드는 절대 실행되지 않는 것을 알게됨

자바의 파일 수준 함수

  • 코틀린의 파일 수준 함수자바 클래스의 static 메서드가 된다.
    • 코틀린 파일 이름의 Kt를 붙임
    • Game.kt => public final class GameKt

함수 Overloading

  • 이름은 같지만 매개변수의 개수가 타입이 다른 여러 개의 함수로 구현하는 것
1
2
3
4
5
6
7
8
9
fun performCombat() { 
    //...
}
fun performcombat (eneryName: String) {
    //...
}
fun performcombat (enemyName : string, isBlessed: Boolean) {
    // ...
}
  • 코틀린 컴파일러는 함수 호출시에 전달되는 인자의 개수타입의 일치되는 것을 알기 때문에 어떤 오버로딩 함수를 실행할지 알게됨

익명 함수와 함수 타입

익명 함수

  • 말그대로, 이름이 없는 함수

  • 익명 함수 == 람다(lambda)

  • 다른 함수의 인자와 반환 가능

  • 익명 함수도 타입을 가질 수 있고, 이것을 함수 타입 이라고 한다

    • 함수의 타입을 컴파일러에게 알려 준다.
  • 익명 함수를 변수화 할 수 있다.

  • 익명 함수는 익명 클래스 인스턴스로 생성된다.

  • 암시적 반환

    • 암시적으로 or 자동으로 함수 정의의 마지막 코드 결과를 반환한다.
    • 람다에서 return 키워드는 사용이 금지되어 있음
    • 어디로 복귀 되어야 하는지, 컴파일러는 알 수 없기 때문, 어디에서든 람다가 호출 될 수 있기 때문
  • 하나의 인자만 받는 익명 함수의 매개변수 이름을 지정하는 대신 it 키워드를 사용할 수 있다.

  • 함수 타입 역시 타입 추론(type inference) 지원

    • 익명 함수가 값으로 지정되면 타입을 필수로 지정하지 않아된다. (당연히 해도 됨)

람다 (lambda)

  • 익명 함수를 람다(lambda)라 부름
  • 람다 표현식, 람다식: 익명 함수 정의
  • 익명 함수의 반환 결과 : 람다 결과(lambda result)
  • 어떤 함수에서 마지막 매개변수로 함수 타입을 받을때는 ()를 생략할 수 있다.
    • "Mississippi".count ({ it == 's' })
    • "Mississippi".count { it == 's' }
    • { it == 's' }람다, 익명 함수 이다.
    • 이것은 람다가 마지막 인자로 함수에 전달될 때만 가능, 따라서 함수 타입 매개변수를 마지막 매개변수로 선언하는 것이 좋다.

inline 함수로 만들기

  • 람다(익명 함수)JVM에서 객체로 생성

    • 메모리 부담
  • 인라인을 사용하면 람다의 객체 사용과 변수의 메모리 할당을 JVM이 하지 않아도 된다.

  • inline 키워드를 추가하면, 람다가 객체로 전달 되지 않는다.

  • 컴파일러가 바이트 코드를 생성할 때, 람다 코드가 포함된 함수 몸체의 전체 코드를 복사한후, 함수를 호출하는 곧에 붙여 넣기 하여 교체 하기 때문

  • 하지만 재귀 함수는 무수히 많이 복사 됨으로, 코틀린 컴파일러는 재귀함수를 단순히 인라인 처리하지 않고, 루프 형태로 변경한다.

함수 참조

  • 람다를 사용해서 다른 함수의 인자로 함수를 전달하는 방법도 있지만,
  • 다른 방법으로 함수 참조를 인자로 전달
  • 함수 참조를 얻을 때는 함수 이름 앞에 :: 연산자를 사용한다.
  • 매개변수에 적합한 이름 있는 함수가 있다면 람다 대신 함수 참조를 사용할 수 있다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fun runsimulation(playerName: String, greetingFunction: (String, Int) -> string) {
	val numBuildings = (1.3).shuffled().last() 
	println (greetingFunction(playerName, numButlatngs))
}

fun printConstructionCost(numBuildings: Int) {
	vaL cost = 500
	println("건축 비용: sfcost * numBuildings}")
}

fun main(args: Array<string>) {
	runsimulation("김선달", ::printConstructionCost) { playerName, numBuildings ->
	val currentYear = 2019
	"simvitlage 방문을 환영합니다, SplayerName! (copyright scurrentyear)"
	}
}

반환 타입으로 함수 타입 사용하기

  • 코틀린의 람다클로저(closure, close over) 이다.

    • 다른 함수에 포함한 함수(예를 들어, 람다) 에서 자신을 포함하는 함수의 매개변수와 변수를 사용할 수 있는 것을 말한다.

    • 람다(익명 함수)가 외부 변수의 참조를 갖는다.

  • 따라서 람다식은 자신이 포함된 외부 함수에 선언된 매개변수와 변수를 그냥 사용할 수 있다.

    • 외부 함수에 val 변수는 람다식에서 그 값이 바로 저장

    • var은 별도의 객체로 저장되며, 그 객체의 참조값이 람다식 코드에 저장되어 값을 변경할 때 사용

  • 다른 함수를 인자로 받거나 반환하는 함수를 고차 함수(higher-order function) 이라고 한다.

    • filter, map
  • 코틀린에서는 익명 함수가 자신의 범위 밖에 정의된 변수를 변경하고 참조할 수 있다.

    • 람다가 외부의 변수를 포획한다는 의미

람다 vs 익명 내부 클래스

  • 함수 타입을 사용하면 진부한 코드가 줄어들고 유연성이 증가한다.

    • 예를 들어, 자바 8과 같이 함수 타입을 제공하지 않는 언어
  • 자바 8은 객체지향 프로그래밍과 람다 표현식을 모두 지원한다.

    • 그러나 함수의 매개변수변수 에 함수를 정의할 수 있는 기능이 없다. (함수의 변수화가 불가)

    • 대신에 자바는 익명 내부 클래스(anonymous inner cass) 를 제공

    • 이것은 단일 메서드(method)를 구현하기 위해 다른 클래스에 정의된 이름이 없는 클래스다.

    • 그리고 람다처럼 익명 내부 클래스를 인스턴스로 생성하여 전달할 수 있다.

  • 자바에서는 람다를 정의하는 함수를 나타내기 위해(변수화) 이름이 있는 타입(인터페이스나 클래스)의 정의가 추가로 필요하다

null 안전과 예외

코틀린의 null 처리 개요

  • nullable / non-nullable

  • 자바는 어떤 타입의 변수도 null 값을 가질 수 있다.

  • 코틀린은 non-nullable 변수는 null을 가질 수 없다. 따라서 런타임이 아닌 컴파일 시점에 방지 할 수 있다.

  • null이 필요 없다면 non-nullable 타입을 사용하는 것이 가장 안전하다.

    • 가능한 한, non-nullable 타입을 사용하자

에러 검출 시점(컴파일 vs 런타임)

  • 코틀린은 컴파일러라는 특별한 프로그램에 의해, 기계어로 변환되는 컴파일 언어 이다.
  • 런타임 에러는 프로그램이 실행된 이후에 발생된 에러임으로, 사전에 알려 줄 수 없다.

null 안전 처리

  • 안전 호출 연산자?.

    • ?.는 객체가 null일 경우에는 null을 반환하고, 그렇지 않으면 해당 객체의 속성이나 메서드를 호출
  • non-null 단언(assertion) 연산자 → !!

    • null이면 런타임에 NPE 예외 발생
    • 컴파일러가 null 발생을 미리 알 수 없는 상황이 생길 수 있을 때 사용된다.
    • 프로그램의 오류가능성을 runtime에 예측하는 것은 거의 불가능하니, 조심해서 사용해야 함
  • 값이 null인지 if 문 검사

  • 엘비스 연산자(null 복합 연산자) → ?:

    • val beverageServed: String = beverage ?: "맥주"

    • 엘비스 연산자는 null이 될 수 있는 값을 바로잡는 데 사용될 수 있다.

  • 자바에서 @NotNull 어노테이션 → null 일 수 없다.

    • 코틀린 소스 코드가 컴파일되어 JVM의 자바 코드로 생성될 때, checkParameterIsNotNull 이라는 메서드가 사용됨.
    • 자바로 역컴파일된 바이트코드를 보면 알 수 있다.

예외 던지기, Exception Throw

  • 예외는 프로그램이 잘못되었다는 것을 나타낼때 사용
  • 예외가 발생할 때는 그것을 처리해야 하며, 그렇지 않으면 실행이 중단(crash) 된다.
    • 미처리 예외(unhandled exception): 처리되지 않은 예외
1
2
3
fun proficiencyCheck(swordsJuggling: Int?) {
	swordsJuggling ?: throw IllegalStateException("플레이어가 저글링을 할 수 없음!)
}
  • throw : 예외를 발생 시킨다. === 예외를 던진다

    • 실행전에 처리되어야 하는 문제를 알려 주기 위해 예외를 던진다.

    • Crash 되는 이유를 명확히 알 수 있음

      • stackTrace, message
    • 예시에서, swordsJuggling 변수가 null 이되는 경우가 생긴다면, 개발하는 시점에 알 수 있는 가능성이 커진다.

  • 커스텀 예외 : 예외를 상속 받은 클래스, 코드 유연성 증가, 재사용성 증가, 에러 메서지 출력 가능

예외 처리 가이드

  • 논리 오류: 프로그램이 강제 종료되지도 않고 의도대로 동작하지도 않는 상황

    • 예외가 던져지지 않으므로 런타임에 발생하는 RuntimeException과는 다른 상황

      • 기존에는 예외가 던져지지 않았기에, 이런 경우에 예외를 던져보자
    • 기본적으로 일단, 최대한 논리 오류가 태초에 발생 하지 않도록 만들어야 함

    • 그럼에도 불구하고, 논리 오류가 발생하는 경우에 예외를 던져야 함

    • 논리 오류는 에러 원인을 예측하기 어렵기 때문에 디버깅이 굉장히 어렵다.

    • Ex) 양수여야 하는 값이 음수인 경우

  • 논리 오류(‘예외적인 상황’) 일 때만 예외를 던지세요.

  • 논리 오류가 아니면 예외를 던지지 말고, null을 반환하세요.

  • 실패하는 경우가 복잡해서 null로 처리할 수 없으면 sealed class를 반환하세요.

    • 성공, 실패들을 모두 sealed class로 선언해보세요.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    class IdentityNumber(private val numbers: List<Int>) {
        companion object {
            fun from(front: List<Int>, back: List<Int>): IdentityNumberResult = when {
                front.size != 6 -> IdentityNumberResult.InvalidFrontSize
                back.size != 7 -> IdentityNumberResult.InvalidBackSize
                else -> IdentityNumberResult.Success(IdentityNumber(front + back))
            }
        }
    }
    
    sealed class IdentityNumberResult {
        data class Success(val identityNumber: IdentityNumber) : IdentityNumberResult()
        object InvalidFrontSize : IdentityNumberResult()
        object InvalidBackSize : IdentityNumberResult()
    }
    
  • 일반적인 코틀린 코드에서 try-catch, runCatching()를 사용하지 마세요.

    • 예외는 반드시 예외적인 상황에서만 던져야 합니다.

    • 논리 오류일 때만 예외를 던지세요.’

    • 논리 오류일 때 던지는 예외를 포함해서, 프로그램 전체적으로 발생하는 예외들전역적으로 처리해주는 예외 처리기를 통해 보고해야 합니다.

      • 예시) 예상 가능한 네트워크 예외 처리에 대한, CoroutineExceptionHandler
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class PositiveNumber private constructor(val value: Int) {
    companion object {
        fun from(value: Int): PositiveNumber? {
            if (value <= 0) return null
            return PositiveNumber(value)
        }    }}

class PositiveNumberTest {
    @Test
    fun `음수로는 만들 수 없다`() {
        // when
        val actual = PositiveNumber.from(-1)
        // then
        assertNull(actual)
    }}

예외 처리

  • try / catch

  • try문에서는 예외가 발생될 수 있는 코드를 넣는다.

  • catch문은 예외가 생길 때만 실행

  • catch 문에서는 처리할 예외의 특정 타입을 인자로 받는다

  • 코틀린에서의 예외는 unchecked 예외,

    • 예외를 생길 수 있는 모든 코드를 컴파일러가 try / catch문으로 처리하도록 강제하지 않는다. (자바는 checked, unchecked가 구분되어 있다.)

전제 조건

  • 전제 조건 함수: 일부 코드가 실행되기 전에 충족되어야 하는 전제 조건을 정의한 함수
    • checkNotNull, require, requireNotNull, error, assert

    • 안전하게 코드 작성 가능

null 무엇이 좋을까?

  • 값을 지정하지 않고 변수를 초기화 할 수 있다.

    • null으로 상태(State) 를 표시함
    • 변수의 초깃값에 많이 사용
  • NPE를 피하는 방법

    • empty 값을 통해 더 좋고 안전한 코드 초기화를 할 수 있다.

      • null 불가 타입으로 지정하고, 빈 문자열("")로 초기화
    • null이 될 수 있다는 것을 인정하고, null 가능 타입의 처리 방법들 을 사용

      • var personName: String? = ""

checked 예외와 unchecked 예외

  • 코틀린에서는 모든 예외가 unchecked 예외다.

    • 즉, 예외가 생길 수 있는 모든 코드를 우리가 try / catch로 반드시 처리하도록 강요하지 않음
  • 자바는 checked와 unchecked 예외 타입이 구분되어 있다.

    • checked 예외의 경우, 예외 처리를 했는지를 컴파일러가 검증함
  • 대부분의 checked 예외(예를 들어, 파일을 저장, IOException) 는 발생했더라도 우리가 특별히 할 것이 없다.

    • 따라서 개발자들이 해당 예외의 catch 블록 안에 처리 코드를 작성하지 않는 경우가 있다

      • 예를 들어, catch (e: IOException) { }
    • 해당 checked 예외가 무시되고(경보가 울렸는데 무시하는 것과 같다) 프로그램은 정상적으로 수행된다. 원인을 찾기 어려워짐

  • checked 예외는 문제를 해결하기 보다는 오히려 더 많은 문제를 야기할 수 있다.

    • 코드 중복, 어려운 에러 복구 로직, 예외 무시

문자열

해체 선언 (destructing declaration)

val (type, name, price) = menuData.split(',')

문자열은 불변이다.

  • 코틀린의 모든 문자열(String 타입)은 val or var 중 어느 것으로 정의되든 자바처럼 불변이다.
  • replace 처럼 값을 변경하는 것 처럼 보이는 어떤 함수도 실제로는 새로운 문자열로 변경값을 생성한다.

문자열 비교

  • 참조 동등 : 특정 타입 인스턴스의 참조를 똑같이 갖는지 검사한다.
    • heap 메모리 영역에 같은 객체를 참조하는지 검사한다. (=== 연산자)
  • 자바에서는 == 연산자가 두 문자열의 참조를 비교하므로 코틀린과 다르다.
    • 자바에서 문자열의 값을 비교할때는 equals 메서드를 사용해야 한다.

비교 연산

KotlinJava
동등성(Equality, Structural equality)==equals
동일성(Identify, Referential equality)=====

image-20240517141930928.png

  • 자바, 코틀린 모두 원시 타입인 경우 동등성 비교== 으로 같은 연산자를 사용

  • 참조 타입일 때만, 달라짐

  • 구조적 동등성, Equality 비교 (==, !=)

    • 기본 타입인 경우: 비교, 값의 동등성 비교

    • 참조 타입인 경우: equals() 로 객체의 내용 비교

      • ==는 내부적으로 equals를 호출한다. (연산자 오버라이딩 기능)
  • 참조 동등성, Identify 비교 (===, !==))

    • 객체의 메모리 주소를 비교

    • 코틀린은 자바에는 없는 ===연산자를 지원한다.

    • 즉, 자바의 주소 값 비교인 ==와 코틀린의 ===가 동일한 역할을 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int a = 1 // 원시 타입
int b = 2
int c = 1
System.out.println(a == b) // false
System.out.println(a == c) // true
    
String a = "hi" // 주소값 : 1번지, 참조 타입
String b = "hi" // 주소값 : 2번지
System.out.println(a == b) // false, Idenrify 비교
System.out.println(a.equals(b)) // true, Equality 비교
1
2
3
4
5
val a: String = "hi"
val b: String = "hi"

println(a === b) // false, Idenrify 비교
println(a == b) // true, Equality 비교

숫자

  • Byte (8비트)

  • Short (16비트)

  • Int (32비트)

  • Long (64비트)

  • Float (32비트)

  • Double (64비트)

  • 숫자에는 정수와 소수가 있다.

문자열을 숫자 타입으로 변환하기

  • toFloat, toDouble, toLong, toIntOrNull
  • val gold: Int = "5.91".toIntOrNull ?: 0

소수

  • 소수점값을 구하려면 코틀린이 부동 소수점 연산을 해야 한다.

  • 위치가 달라질 수 있는 소수점을 의미하는 부동 소수점은 실수의 근사치이며, 정밀도와 성능 모두를 지원하기 위해 근사치를 사용한다.

  • %.2f.format(~~~) : 소수점 이하 두 자리 형식

    • Double 타입에 toInt 를 호출하면, 소수점 이하 값이 절삭되어 정수로 변환된다. (4.91.toInt → 4)

표준 함수 (범위 지정 함수)

  • 코틀린의 표준 함수(범위 지정 함수) 는 내부적으로 확장 함수이며,

  • 확장 함수를 실행하는 주체수신자 또는 수신자 객체, 수신 객체, receiver 라고 한다.

  • 표준 함수(범위 지정 함수)람다를 인자로 받아 동작한다.

    • 인자로 받은 block 변수(함수)를 실행
  • 수신 객체라는 용어는 Kotlin의 확장 함수에서 등장한다.

image-20240519163737211.png

  • 확장 함수에서 this는 확장된 클래스의 객체, 즉 확장 함수를 사용하는 그 객체가 된다.

    • 그 객체가 바로 수신 객체(Receiver object)이고,

    • 확장할 클래스가 수신 객체 타입(Receiver Type)

    • 수신: ‘(수신) 객체가 코드를 받는다.’

수신 객체 지정 람다 (Lambdas with Receivers)

  • 수신 객체 지정 람다는 람다에 확장 함수 처럼 수신 객체를 사용한 것이다. T.() -> R

    • 확장 함수에서 수신 객체를 사용하여 블록 내에 객체를 전달했듯이,

      • 수신 객체 지정 람다(Lambdas with Receivers) 또한 수신 객체를 이용하여 객체를 전달한다.
    • 따라서 수신 객체 지정 람다에서는 수신 객체를 this로 대신할 수 있다.

    • 또한 this를 생략하고 해당 객체의 멤버에 바로 접근이 가능하다.

  • 반면 일반 람다는 객체를 인자(파라미터) 로 전달한다. (T) -> R

    • 람다의 매개변수가 하나뿐이고, 컴파일러가 타입을 추론할 수 있을 때 객체는 기본 매개변수인 it으로 받을 수 있다.

image-20240519164131680.png

image-20240519165948160.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}
inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}
inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}


inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}
inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

apply

  • 수신자 객체의 참조 가능

    • this로 참조, this 생략 가능
  • 람다의 실행이 끝나면 현재의 수신자 객체가 반환된다.

run

  • apply와 다르게 run은 수신자 객체를 반환하지 않는다.
  • 람다의 결과, 마지막 코드 줄의 실행 결과를 반환한다.
  • 함수 호출이 여러 개 있을때 함수 중첩 보다 run을 사용하면 편리하다.
  • "~~~".run(::myFun) = myFun("~~~")

with

  • run 과 동일하게 동작하지만 호출 방식이 다르다.

  • 수신자 객체를 첫 번째 매개변수의 인자로 받는다.

  • with 대신 run을 사용할 것을 권한다.

    • 잘 사용 안됨

also

  • let 처럼 자신을 호출한 수신자 객체를 람다의 인자로 전달한다.
  • let과 달리, 람다의 결과를 반환하지 않고, 수신자 객체를 반환한다.

let

  • 수신 객체를 람다의 인자(파라미터) 로 전달

  • 전달된 람다를 실행한 후, 마지막 코드 줄의 실행 결과를 반환해준다.

  • null check 후 코드를 실행해야 하는 경우

    • 안전 호출 연산자 → ?.
    • ‘?.let’을 사용 하게 되면 let의 block은 수신객체가 null이 아닐 때만 수행된다.
    • 따라서 let block에서의 it의 타입은 nullable하지 않은 타입이 된다. (스마트 캐스팅)
  • nullable한 수신객체를 다른 타입의 변수로 변환해야 하는경우

1
2
3
4
var person: Person? = null
val isReserved = person?.let { it: Person ->
    reserveMovie(it)
}

List, Set, Map

  • 컬렉션연관된 값들을 모아서 저장하고 사용하는 자료구조
    • List, Map, Set이 있다.
  • 컬렉션에 저장된 각각의 데이터를 element (요소) 라고 한다.
  • 코틀린에서는 mutable(변경 가능한) 타입과 read-only(읽기 전용) 타입이 있다.
  • 코틀린 컬렉션은 가변성 개념을 제공하면서, 강력한 기능을 제공

image-20240517130419031.png

image-20240517125819629.png

image-20240517130702187.png

List

  • List<myType> 에서

    • myType: 매개변수화 타입, 요소의 타입이 myType이라는 것을 알려줌.
  • List은 제네릭 타입 이다. ⇒ List는 어떤 타입의 데이터도 저장할 수 있다

  • listOf 함수: 컬렉션 자체를 생성 하고, 데이터 추가 하는 일을 둘 다 하는 함수

  • 안전한 인덱스 사용 : getOrElse, 예외 대신 기본값을 반환 / getOrNull 와 :? 사용

  • containsAll : 요소가 존재하는 지 한 번에 확인할때 사용

  • List의 변경 가능 여부는 List의 타입에 의해 결정

    • ‘변경 가능(mutable)’ 은 저장된 요소를 변경할 수 있다는 것을 의미한다.
    • List 타입은 read-only 이다.
  • 변경 가능 ⇒ MutableList 타입을 사용해야 함, add, remove 사용 가능

  • toList <=> toMutableList 으로 상호 변경 가능

반복 처리

  • in 키워드 : for 루프에서 반복 처리되는 객체를 나타낸다.

  • itertation(반복 처리) 가능

    • for 루프로 요소의 이름을 지정하면, 코틀린 컴파일러가 타입을 알아서 처리
  • Iterable 타입은 반복 처리를 지원한다.

    • List, Set, Map, IntRange

해체 선언

  • List는 또한, 맨 앞의 다섯 개 요소까지 변수로 해체 할 수 있는 기능을 제공한다.
  • 해체를 원하는 않는 요소에 _(밑줄)을 사용해서 선택적으로 해체할 수도 있다.

Set

  • 수학의 집합처럼 요소가 고유한 것을 보장해 주는 컬렉션

  • 인덱스와 인덱스 연산자([])를 사용해서 요소를 처리할 수 없다.

    • elementAt 함수를 사용해서 특정 인덱스의 요소를 요청 할 수 있다.

    • ‘논리적’으로 인덱스처럼 접근

    • 한 번에 하나의 요소를 반복해서 읽는다. (반복 처리 가능, itertation)

    • 따라서, 인덱스 연산자 보다 느리다

  • 순서를 갖지 않는다.

  • ‘값’ 이 고유하다.

  • 인덱스로 접근하는 자료구조는 아님

Array, 배열 타입

  • 코틀린은 참조 타입밖에 없다. (컬렉션도 참조 타입)

  • 자바에서는 Array기본(원시) 타입으로 지원

  • 코틀린에서 기본 타입이 아닌 Arrays 라는 참조 타입으로 배열을 지원한다.

    • val playerAges: IntArray = IntArrayOf(34, 23, 12)
    • Kotlin에서 Arrays참조 타입이지만, 특별한 배열 타입을 통해 기본 타입 배열과 유사한 기능을 제공
    • 내부적으로 원시 타입 값을 저장하여 메모리 사용량을 줄이고 성능을 향상
    • 코틀린의 (특별한) 배열 타입(IntArray, ByteArray, BooleanArray)
  • IntArray 타입은 자바의 기본 타입(배열 타입) 으로 컴파일 된다.

  • 코틀린 컬렉션을 자바의 기본 배열 타입으로 변환 가능

    • toIntArray
  • val array1 = arrayOf(1, 2, 3) , val array2 = Array(5) { it }

    • 자바로 디컴파일 후 확인: @NotNull final Interger[] array1;
  • val array1 = intarray(1, 2, 3) ,val array2 = IntArray(5) { it }

    • 자바로 디컴파일 후 확인: @NotNull final int[] array1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 일반 객체 배열
val numbers: Array<Int> = arrayOf(1, 2, 3)
val strings: Array<String> = arrayOf("hello", "world")

// 기본 타입 배열
val intNumbers: IntArray = intArrayOf(1, 2, 3)
val booleanValues: BooleanArray = booleanArrayOf(true, false, true)

// 가변 배열
val mutableArray = Array(5) { 0 } // 0으로 초기화된 크기 5의 배열, Array<Int> 타입
mutableArray[0] = 1

읽기 전용 vs 변경 불가능

  • 불변은 ‘변경 불가능’ 을 의미
  • 따라서 코틀린 컬렉션은 ‘읽기 전용’이 더 어울림.
1
2
3
4
5
6
7
8
val x = listOf( mutableListOf(1, 2, 3) )
x[0].add(4)
// x = list( mutableListOf(1, 2, 3, 4) )


var myList: List<Int> = listOf(1,2,3)
(myList as MutableList)[2] = 1000
println(myList) // [1, 2, 1000]

Map

  • 키와 값의 쌍의 데이터 (entry 라고 한다.)
  • iteration 반복 처리 지원
  • 정수 인덱스 기반 처리 대신 키를 기반으로 데이터 처리
  • to 함수로 키와 값을 정의한다.
  • getValue, [ ], getOrElse, getOrDefault
  • ‘키’ 가 고유하다.
  • Map이 Iterable을 직접 구현하지 않음
    • 하지만, keys, values, entries 가 Iterable을 구현함

클래스 정의하기

  • 클래스는 사물이나 개념을 추상화 한 프로그래밍 요소이다.

  • 속성(property)과 기능(function) 을 갖는다.

  • 행동(역할)과 데이터를 정의한다.

    • OOP의 핵심 요소
  • default가 public

인스턴스 생성하기

  • 생성자(constructor) 를 호출하여 인스턴스를 생성한다.
  • 함수 호출과 비슷

가시성 제한자

  • 클래스 함수나 속성의 가시성을 제한 하는 개념을 OOP에서 정보은닉, 캡슐화라 한다.

  • public : 클래스 외부에서 클래스 요소 사용가능

  • private : 클래스 내부에서만 요소 사용 가능

  • protected : 클래스 내부 or 클래스의 서브 클래스에서만 사용될 수 있다

  • internal : 클래스가 포함된 ‘모듈(module) ’에서 사용될 수 있다

속성(프로퍼티, property)

  • 프로퍼티는 클래스의 데이터 즉, 상태나 특성을 나타낸다.

  • 변수와 다르게 클래스 속성반드시 초깃값이 지정되어야 한다.

    • 인스턴스가 생성될 때 모든 속성이 값을 가져야 한다.
    • 다른 함수나 프로그램에서 이 클래스의 속성에 접근할 수 도 있기 때문

속성(프로퍼티)의 getter와 setter

  • 프로퍼티를 외부에서 사용할때, 코틀린은 자동으로 getter를 통해 가져오고, setter를 통해 값을 지정한다.

  • 정의한 각 속성에 대해 field와 getter or setter 가 생성된다.

  • getter 에서는 속성값을 읽는 방법이 명시된다.

  • 커스텀 getter, setter를 정의할 수 있다. ⇒ getter, setteroverriding

    • 커스텀(override) 하지 않으면, 기본으로 생성되는 속성값을 있는 그대로 반환 및 지정
  • field 키워드는 프로퍼티에 대해 코틀린이 자동으로 관리해주는 backing field 를 참조한다.

    • field 키워드는 getter, setter에서만 사용할 수 있다.

    • backing field 는 getter, setter가 사용하는 프로퍼티의 데이터다.

  • getter는 backing field를 변경하지 않는다. setter는 backing field를 변경한다.

  • 프로퍼티는 외부에 노출시키되(public), setter는 노출시키지 않으려면 private set 으로 따로 정의도 가능하다.

    • 기본적으로 getter, setter 의 가시성은 속성 자체의 가시성과 일치
  • getter는 프로퍼티를 참조할 때 자동 호출

  • setter는 대입 연산자(ex. =)를 사용해서 속성에 값을 지정할 때 자동 호출

1
2
3
4
5
var name = "tae"
	get() = field.capitialize()
	private set(value) {
        field = value.trim()
    }

산출 속성(computed property)

  • 다른 속성이나 변수 등의 값을 사용해서 자신의 값을 산출하는 속성, backing field 생성하지 않음.
  • 초깃값이나 기본값이 없다
1
2
3
4
class Dice() {
    val rolledValue
    	get() = (1..6).shuffled().first()
}
  • 이런 경우에서도 볼 수 있듯, ‘변경 불가능’ 보다 ‘읽기 전용’ 이라는 표현이 더 적합

패키지 사용하기

  • Ex) com.myProject.presentation.music

  • 패키지(package) 는 폴더 처럼 비슷한 요소들을 분류하고 모아 놓은 것이다.

  • 프로그램에서 직접 패키지를 지정할 때는 package 키워드를 사용한다.

  • 지정된 .kt 파일이 컴파일되면, 생성된 바이트코드 파일(.clsas) 는 정의한 패키지 경로에 위치하게 된다.

  • 같은 패키지에 있는 클래스들은 기본적으로 같이 사용할 수 있다.

    • 단, 코틀린 표준 라이브러리의 모든 클래스나 함수 등은 import를 지정하지 않아도 바로 사용 가능
  • 다른 패키지에 있는 클래스나 함수 등을 사용하려면 import 문을 사용해서 그것들의 위치를 컴파일러에게 알려주어야 한다.

경합 상태(race condition)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Weapon(val name: String)
class Player {
	var weapon: Weapon? = Weapon("Ebony Kris")
	fun printweaponName() {
		if (weapon != null) {
			println(weapon.name) // Weapon 타입으로 스마트 캐스팅 할 수 없는 컴파일 에러
		}
	}
}
fun main(args: Array<string>) {
	player().printWeaponName()
}
  • 컴파일러는 변수가 null이 아님이 if문 등으로 확인되더라도,

    • 경합 상태가 생길 수 있기 때문에(여러 개의 스레드로 실행될 가능성 존재)
    • 스마트 캐스팅이 할 수 없고, 컴파일 에러를 알려준다.
  • weapon의 값이 if로 확인된 시점println으로 weapon의 name을 출력하는 시점 사이에 weapon 이 null로 변경될 가능성이 여전히 있다. => 스마트 캐스팅 불가

1
2
3
4
5
6
class Player { // 해결법
	var weapon: Weapon? = Weapon("Ebony Kris")
	fun printweaponName() {
		weapon?.also { print(it.name) }
	}
}
  • also를 사용하면 해결 가능
  • also 익명 함수 내에서만 존재하는 지역변수 it 으로 weapon 인스턴의 name 속성이 참조되기 때문
  • ?.로 null이 아님이 확인 되었고,
  • it의 값은 프로그램의 다른 코드에서 변경불가능
  • 컴파일러가 Weapon 타입으로 스마트 캐스팅 가능

패키지 가시성

  • 자바는 기본적으로 패키지 가시성을 사용한다.

    • 같은 패키지에 있는 클래스에서만 사용 가능하다.
  • 코틀린에서는 패키지 가시성이 없다.

    • 같은 패키지에 있는 클래스, 함수, 속성 등은 기본적으로 상호 사용가능하기 때문에 굳이 별도의 패키지 가시성을 가질 필요가 없기 때문이다.
  • 모듈은 독자적으로 실행 및 테스트될 수 있는 프로그래밍 구성 단위

    • 코틀린은 internal 가시성을 지원한다. 자바는 지원하지 않음

    • 바이트코드 파일에서 internal은 public이 된다.

초기화

  • 클래스의 인스턴스를 생성하는 것은, 클래스에 정의된 속성을 구조로 갖는 객체를 메모리에 할당하는 것

기본 생성자

  • 커스텀하지 않고, 자동으로 생성되는 기본 getter와 setter를 사용하는 속성의 경우에는,

    • 클래스 내부에 속성을 따로 정의하지 않고 기본 생성자에만 정의해도 된다.
  • 기본 생성자에 정의된 변수는 클래스 속성과 생성자 매개변수 두 가지 역할을 하게 된다.

  • 기본 생성자속성을 정의할 때는 var, val을 추가해야 한다.

1
2
3
4
5
6
7
class Player(_name: String, _health: Int) {
    val name = _name
}

class Player(private val name: String, var health: Int = 100) {
    
}

보조 생성자

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Player(_name: string, val health: Int) {
	val race = "DWARF"
    val alignment: String

	init {
        require(health > 0, {"health는 양수여야 한다"})
		println("initializing player")
		alignment ="GOOD"
	}
	constructor (_name: String) : this(_name, 100) {
		race = "The Shire"'
    }
}
  • 말 그대로, 기본 생성자에 정의된 속성을 다양하게 초기화하는 보조 역할을 수행

  • 보조 생성자에서는 클래스 속성(프로퍼티) 를 정의할 수 없다.

    • 속성은 기본 생성자 or 클래스 몸체에서 정의되어야 한다.
  • this 키워드는 보조 생성자를 사용해서 생성되는 클래스 인스턴스의 기본 생성자를 뜻한다.

초기화 블록 (init)

  • 전제 조건 검사는 초기화 블록에서 주로 한다.

    • 생성자에 전달되는 인자가 적합한지 확인
    • require 문을 주로 사용
  • 초기화 블록은 어떤 생성자가 호출되든 클래스 인스턴스가 생성될 때 마다 자동으로 호출되어 실행된다.

  • 속성을 초기화하는 코드가 복잡하다면, init 에 초기화 코드를 넣는 것도 가능하다.

속성(프로퍼티) 초기화

  • 속성(프로퍼티)은 정의된 타입으로 반드시 초기화되어야 한다.

초기화 순서

image-20240517102821177.png

  1. 기본 생성자에 정의된 속성에 인자값 지정

  2. 클래스 내부에 지정된 속성에 초깃값 저장

  3. 초기화 블록(init) 실행

  4. 보조 생성자 실행

초기화 지연시키기

  • 클래스 속성은 non-nullable 변수가, null이 아닌 값으로 초기화된 다는 것을 보장하기 때문에, 초기화는 중요하다.

  • 기본 타입(ex. Int) 이 아니고, 다른 객체를 참조하는 속성의 경우 ‘지연 초기화(lateinit)‘가 가능하다.

    • 다른 객체를 참조하는 속성의 경우, 생성자가 호출되는 방법과 시점은 우리가 제어할 수 없는 경우가 있기 때문에
    • Ex) 외부 프레임워크에서 초기화되는 경우, 안드로이드의 뷰 속성
  • lateinit 키워드는 개발자가 해당 속성을 사용하기 전에 초기화해야 된다는 것을 의미한다.

    • 개발자가 스스로 책임지고, 해당 속성을 사용하기 전에 초기화해야 된다는 것을 뜻함

    • ‘지연 초기화’

  • isInitialized 함수는 속성이 초기화 되었는지 검사해준다.

    • if (::alignment.isInitialized)
    • 속성의 값이 아닌 참조를 전달해야 함으로 :: 를 붙임
  • 클래스 인스턴스의 생성 시점에서 속성을 초기화 할 수 없다면, 지연 초기화를 사용해야 하고, lateinit 키워드를 사용해서 이를 나타낸다.

  • lateinit

    • 기본 타입이 아니여야 함
      • 다른 타입의 객체 참조 때문에
    • var
    • non-nullable이어야 한다.
    • 커스텀 getter, setter 정의 불가하고, 기본으로 생성되는 getter, setter 사용해야 함
  • lateinit 대신 nullable 타입으로 변수 선언하고, 변수에 null으로 초기화도 가능하긴 한데, null 체크를 개발자가 계속 해야 한다.

    • var tmp: String? = null
  • lateinit은 클래스 속성 뿐만 아니라 최상위 수준 속성과 함수의 지역 변수에서도 사용 가능하다.

늦 초기화 (lazy initalization)

  • ‘지연 초기화’ 만이 초기화를 지연시킬 수 있는 유일한 방법은 아니다.

  • 변수나 속성이 ‘최초’ 사용될 때까지 초기화를 ‘연기’ 할 수도 있다.

  • 속성을 즉시로 사용할 필요가 없다면 ‘늦 초기화’가 좋은 선택이 된다.

    • 늦 초기화는 코틀린에서 delegation(위임) 패턴을 사용해서 구현한다.
    • lazy 함수대리자로 위임 처리 한다.
    • lazy 함수람다를 인자로 받아 실행 시켜준다.
    • lazy 함수와 람다로 초기화 후에 이후에는 다시 초기화되지 않고, 캐시에 저장되 결과가 사용된다.
  • val homtTown by lazy { myFun() }

  • by 키워드를 사용해서, 위임받은 일을 처리하는 대리자(delegate) 를 지정한다

    • 대리자로 커스텀 함수 또는 코틀린 표준 라이브러리의 함수를 사용할 수 있다.
  • 컴파일러는 소스 코드상의 초기화 순서를 검사하지 않는다.

    • 속성을 사용하는 함수의 순서를 비교하지는 않는다.

상속

  • 서브 클래스(자식 클래스) 는 상속해 주는 클래스의 모든 속성과 함수를 공유한다.

    • 상속해 주는 클래스를 부모 클래스 또는 슈퍼 클래스라고 한다.
  • open 키워드를 붙혀서 서브 클래스를 가질 수 있게 해야 한다.

  • 상속 받은 속성과 함수를 그대로 사용하지 않고, overrideing 할 수도 있다.

    • 이때 override 하는 함수에도 open 키워드 사용해야 한다.
  • super 키워드를 사용해서, 부모 클래스 함수 or 속성을, 자식(서브) 클래스에서 호출 or 접근 할 수 있다.

    • 슈퍼 클래스의 public, protected 속성과 함수를 사용할 수 있다.
  • 슈퍼 클래스로 타입을 선언하면, 어떤 서브 클래스 인스턴스도 참조 할 수 있다. ⇒ 다형성

  • 서브 클래스는 기본적으로 open이 되므로, 서브 클래스의 서브 클래스는 언제든 override 할 수 있다.

  • 코틀린에서는 클래스가 정의될 때 기본적으로 서브 클래스를 만들지(상속 되게) 못하게 되어 있다.

  • final 키워드: 키워드를 붙인 함수 or 속성만, override 될 수 없게 함.

타입 검사

  • is 연산자로 객체가 특정 타입인지 검사할 수 있다.

  • 자식 클래스의 인스턴스는 해당 자식 클래스의 타입이면서 동시에 부모 클래스의 타입도 된다.

    • 사자 인스턴스는 사자 타입이면서 동시에 동물 타입이다.

코틀린 타입의 상속 계층

image-20240517124609914.png

image-20240517124559835.png

image-20240517125535503.png

  • 코틀린의 모든 non-nullable 클래스는 자동으로 Any라는 최상위 슈퍼 클래스로 부터 상속 받는다.

    • Any는 자바의 모든 클래스가 java.lang.Object의 서브 클래스인 것과 비슷하다.
  • as 연산자 : 상속 관계가 있을 때, 타입 변환에 사용

    • 변환된 타입의 속성 참조나 함수 호출을 할 수 있는 것이지 해당 객체가 갖는 값을 변환하는 것은 아니다.
  • 두 타입 간에 상속 관계가 없으면 타입 변환은 불가능

스마트 캐스팅

  • 코틀린 컴파일러가 특정 조건에서 명시적 캐스팅이 아닌, 자동으로 타입 변환을 수행하는 것
    • 간결성, 가독성, 안전성
  • 직접 변환하지 않아도 스마트 캐스팅이 일어나면, 컴파일러는 해당 타입으로 간주한다. (컴파일 에러 안남)
  • non-nullable 타입은 nullable 타입의 자식(서브) 타입이다.
  • JVM 애플리케이션으로 컴파일하면 Any 클래스는 java.lang.Object로 바이트 코드에 구현되지만, 다른 플랫폼을 대상으로 컴파일하면 해당 플랫폼에 맞게 다른 형태로 구현된다.

Any 클래스

1
2
3
4
5
public open class Any {
    public open operator fun equals(other: Any?): Boolean
    public open fun hashCode() : Int
    public open fun tostring(): String
}
  • 모든 클래스의 부모 클래스

  • 기본 메서드 제공: Any 클래스는 다음과 같은 기본 메서드를 제공합니다.

    • equals(other: Any?): Boolean: 객체의 동등성을 비교합니다.
    • hashCode(): Int: 객체의 해시 코드를 반환합니다.
    • toString(): String: 객체의 문자열 표현을 반환합니다.
  • 해당 타입에 맞게 오버라이딩해서 구현하라는 의미

  • 확장 함수 활용: Any 클래스는 확장 함수를 통해 다양한 기능을 추가할 수 있습니다. 예를 들어, to() 함수를 사용하여 Pair 객체를 생성할 수 있습니다.

  • 다양한 플랫폼에 독립적인 애플리케이션을 생성할 수 있게 해주는 방법 중 하나다.

  • 즉, 각 플랫폼에 공통적으로 사용할 수 있는 최상위 슈퍼 클래스인 것이다.

    • 코틀린 프로그램을 JVM 애플리케이션으로 컴파일하면 Any 클래스가 java, Lang.object로 바이트 코드에 구현됨

      • PC 운영체제의 JVM에서 실행되는 애플리케이션
    • 다른 플랫폼을 대상으로 컴파일하면 해당 플랫폼에 맞게 다른 형태로 구현된다.

      • JVM 없이 실행되는 네이티브 App, 웹 브라우저에서 실행되는 자바스크립트, 안드로이드 App
    • 따라서 우리 코드에서는 최상위 슈퍼 클래스가 Any라고 생각하고 사용하면 된다.

    • 코드가 실행된 각 플랫폼에서 Any가 어떻게 다르게 구현되는지 자세히 알 필요 없기 때문이다.

객체

object 키워드

  • 싱글톤은 하나의 인스턴스(객체)만 메모리에 생성되는 것을 말한다.

  • 코드의 정의된 곳에 생성되어 동작하며, 다른 클래스 내부에 포함시켜 사용할 수 있다.

  • 멀티 스레드로 실행될때는 반드시 하나의 객체만 생성되도록 동기화 처리를 해야 한다.

  • 최초 사용 시점에 하나만 생성되어, 계속 유지됨

  • 일반 클래스처럼 속성과 함수가 포함될 수 있고, 최초로 사용될 때 초기화 된다.

  • object 키워드를 사용하여 정의된 객체는 JVM에서 로드될 때 즉시 초기화되며, 이 때 쓰레드 안전(thread-safe) 하게 초기화됩니다. => 동기화 문제 해결

  • 객체 선언(object 키워드) 에도 일반 클래스처럼 속성과 함수가 포함될 수 있다. 그리고 이런 속성이나 함수가 최초로 사용될 때 비로소 해당 객체가 생성되고 초기화된다.

객체 표현식

  • 기존 클래스의 서브 클래스를 원하는 코드 안에 ‘이름 없이’ 정의 하고 바로 인스턴스를 생성해서 사용하는 경우 편하게 사용된다.
  • annonymous 클래스 (익명 클래스) 라고 한다.
1
2
3
val anyClassInstance = object : ParentClass() {
		override fun load() = "~~~"
}
  • 익명 클래스ParentClass 의 자식 클래스임으로, 속성과 함수를 상속 받는다.

    • override 및 새로운 속성 및 함수 추가 가능하다.
  • 'anyClassInstance'인스턴스는 싱글톤 객체가 됨으로,

    • 함수 내부에서 사용될 때는, 매번 인스턴스가 생성될 수 있기 때문에 사용시 유의해야 한다.

동반 객체 (companion object)

  • 최상위 수준에서는 사용할 수 없고, 클래스 내부에서 정의하여 사용한다.
  • 클래스 내부에 정의된 객체 선언(object)
  • 하나의 클래스에서는 하나의 동반 객체만 포함될 수 있다.
  • 클래스의 인스턴스가 얼마나 많이 생성되던, 동반 객체의 인스턴스는 하나만 생긴다.

중첩 클래스

  • 다른 클래스 내부 안에 정의된 클래스, nested class
  • 중첩된 클래스의 인스턴스는 외곽 클래스의 인스턴스가 생성되어야 사용할 수 있다.
  • 외곽 클래스는 중첩 클래스의 속성과 함수를 사용할 수 있다.
  • Nested Class는 외부 클래스의 인스턴스에 대한 참조를 가지지 않으며, 외부 클래스의 멤버에 직접 접근할 수 없습니다.
1
2
3
4
5
6
class OuterClass {
    private val outerValue = "외부 클래스 값"
    class NestedClass {
        // outerValue에 접근 불가
    }
}

내부 클래스 (inner class)

  • Kotlin의 inner 키워드를 사용하여 선언된 클래스를 Inner Class(내부 클래스)라고 합니다.
  • 자신을 감싸고 있는 외부 클래스(Outer Class)의 인스턴스에 대한 참조를 암시적으로 가지고 있습니다.
    • nested class와 다름
    • 암시적으로 참조하므로, 메모리 누수에 주의해야 합니다.
  • Inner Class는 외부 클래스의 인스턴스 없이 생성할 수 없습니다.
1
2
3
4
5
6
7
8
class OuterClass {
    private val outerValue = "외부 클래스 값"
    inner class InnerClass {
        fun accessOuter() {
            println(outerValue) // 외부 클래스의 private 멤버 접근 가능
        }
    }
}

data class

  • 주로 데이터를 표현하는 객체를 간편하게 생성, 저장, 표현하기 위해 사용됩니다.

  • 일반 클래스와 달리 데이터 클래스는 컴파일러가 자동으로 몇 가지 유용한 메서드를 생성해줌

  • JVM은 객체를 고유하기 관리하기 위해 해시 코드 값을 생성함

  • 인스턴스끼리 각 속성(프로퍼티) 의 값을 비교 (equals 함수)

    • Equality, 동등성 연산에서 사용
  • 인스턴스를 컬렉션(Ex. Map)에 저장할 때 사용할 키 값인 해시 코드를 생성 (hashCode 함수)

    • hash와 관련된 연산을 할 때 사용
  • 객체를 문자열로 나타내는 기능 (toString 함수)

  • 해체 선언 함수 (componentN 함수)

  • 기존 인스턴스(객체)의 속성값을 변경하여 새로운 인스턴스를 생성하는 (copy 함수)

    • 얕은 복사; Shallow Copy: copy() 메서드는 기본적으로 얕은 복사를 수행합니다. 원본 객체와 같은 참조를 공유합니다. 따라서 참조 타입 프로퍼티를 변경하면 원본 객체에도 영향을 미칠 수 있습니다.

      • copy() 를 사용하면 원시 타입 프로퍼티는 값 복사(Value Copy) 를 통해 새로운 객체에 복사됩니다.

      • copy() 를 사용하면 참조 타입 프로퍼티는 참조 복사(Reference Copy) 를 통해 새로운 객체에 복사됩니다.

    • 깊은 복사; Deep Copy: 새로운 객체 생성, 이는 코틀린에서 개발자가 직접 구현해야 합니다.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      
      data class Address(var city: String)
      data class Person(val name: String, val address: Address)
      
      // 참조 타입 복사
      val address = Address("Seoul")
      val person1 = Person("Alice", address)
      val person2 = person1.copy()
      person2.address.city = "Busan" // person2의 address 변경
      println(person1.address.city) // Busan (person1의 address도 변경됨)
      
      // 원시 타입 복사
      val person1 = Person("Alice", 25)
      val person2 = person1.copy()
      person2.age = 26 // person2의 age 값만 변경
      println(person1.age) // 25 (person1의 age는 변경되지 않음)
      
  • 위 5개의 함수를 코틀린 컴파일러가 자동으로 생성한다.

    • Any 클래스의 (equals, toString, hashCode) 함수들을 오버라이딩 해준다.
  • 반드시 ‘기본 생성자’ 에 속성들을 지정해야 한다.

  • open 으로 피상속 불가, 슈퍼 클래스 불가

    • 상속을 허용한다면, 부모 클래스에서 자동 생성된 메서드들이 자식 클래스의 모든 프로퍼티를 고려하여 정확하게 동작하도록 보장하기가 어렵기 때문
    • data class의 목적과 부합하지 않음
  • Data Class는 다른 클래스를 상속할 수 없으며, 인터페이스만 구현할 수 있습니다.

  • 해시 코드 값인스턴스를 고유하기 식별하기 위해 생성된 값이다.

enum 클래스

  • ‘상수값’을 정의하는 열거형 클래스를 정의할때 사용

    • 상수보다 더 서술적이라서 무엇을 의미하기 알기 쉽다.

    • enum 의 항목은 단순한 상수가 아니라, 서브 타입이다.

      • 각 상수(항목들) 는 자체적으로 프로퍼티와 메서드를 가질 수 있는 객체
    • 항목들은 enum class 내부에서만 생성되며, 외부에서 임의로 생성할 수 없습니다.

    • enum 는 모든 상수가 컴파일 타임에 결정되므로, 각 상수는 JVM 내부적으로 단일 인스턴스로 표현됩니다. (메모리 사용 측면에서 효율적)

  • enum 클래스 내부적으로 ‘name’(항목 이름, String 타입), ‘ordinal’(항목 위치, Int 타입, 0부터 시작)

  • EnumClass.values : 모든 항목 이름을 ‘배열’로 생성

  • enum 클래스의 함수 호출은 ‘enum 클래스명.항목명.함수명’ 의 형태로 호출해야 한다.

  • 컴파일러가 모든 타입을 처리했는지 검사할 수 있다는 장점

  • 싱글턴 패턴과 유사한 특징을 가지고 있어, 싱글턴처럼 사용할 수 있습니다.

    • 하지만 상속 불가 및 값 표현 등의 차이점이 있으므로, 엄밀히 말하면 싱글턴은 아닙니다.

연산자 오버로딩

  • 연산자 오버로딩: 피연산자의 타입이 무엇이든 같은 연산자를 사용해서 동일한 기능을 구현할 수 있게 해주는 것

  • 코틀린에서는 각 연산자를 사전에 약속된 함수로 구현하여 연산자 오버로딩을 지원한다.

    • 코틀린 컴파일러는 a+b 를 컴파일하여, a.plus(b) 를 실행하도록 바이트코드로 생성
    • + 연산자 를 사전에 약속된 plus 함수로 싱행하게 되면, 피연산자의 타입이 다르더라도 덧셈은 항상 + 로 표기가능
    • 피연산자의 타입마다 서로 다른 덧셈 연산자를 사용하면, 불편하고 연산자 수가 매우 많아짐
  • 코틀린에서== 비교 연산자 가 내부적으로 equals 함수로 호출되는 이유가 연산자 오버로딩 때문이다. (ex. +, plus() )

image-20240517140637006.png

객체의 값 비교하기

  • equals 함수를 override 할 때는 haseCode 함수도 같이 override 해야 한다.
  • 동등성, Equality 연산

image-20240517141823571.png

  • hash와 관련된 자료구조(hashMap, hashTable)는 동등성 연산(equals) 전에 먼저, Hash Value비교를 수행한다. 즉, hashCode()의 값이 같은 경우에만 **동등성 연산(equals)**이 수행된다.

  • 두 개의 다른 인스턴스에 대해 같은 Hash Value가 나오는 경우를 Hash 충돌(Hash Collision) 이라 한다.

    • Hash Value가 같으므로 동등성 연산(equals) 가 수행된다.

    • 같은 해시 값을 갖는 인스턴스들이 LinkedList 형태로 이어져있어 하나하나씩 Iteration이 돌아가면서 동등성 연산이 수행

  • 따라서 N개의 값 객체가 있고 해당 값 객체들이 모두 같은 Hash Value를 갖는다면 동등성 연산 수행에 O(N)의 시간 복잡도가 필요하다.

    • 하지만 만약 모든 값 객체들이 다른 Hash Value를 갖는다면 동등성 연산 수행에 O(1)의 시간 복잡도
    • 따라서 hashCode()값을 Hash 충돌을 최대한 피할 수 있도록 짜야한다.

image-20240517151033275.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class GalaxyTab(val modelName: String, val size: Int) {
    override fun hashCode(): Int {
        return modelName[1].toString().toInt()
    }
}

val tabS6 = GalaxyTab("S6", 11)
val tabS7 = GalaxyTab("S7", 11)

val tabStock = mutableMapOf<GalaxyTab,Int>()
tabStock[tabS6] = 0
tabStock[tabS7] = 2

JVM에서 Hash와 메모리 주소 값 비교

특징Hash (해시 코드)메모리 주소 값
의미객체의 데이터를 기반으로 생성된 정수 값객체가 저장된 메모리 상의 실제 위치
목적객체 비교, 해시 기반 컬렉션 활용객체 식별, 참조 비교
고유성동일 객체는 항상 같은 값, 다른 객체는 다른 값을 가질 수 있음 (해시 충돌 가능)각 객체마다 고유한 값
가변성객체의 상태가 변하지 않으면 불변가비지 컬렉션 등에 의해 변경될 수 있음
메모리 주소와의 관계일반적으로 무관직접적인 관계
  • JVM에서 hash메모리 주소 값은 둘 다 객체를 식별하는 데 사용되지만, (공통)

    • Hash는 주로 객체의 논리적 동등성 비교와 해시 기반 컬렉션에서 활용되며,
    • 메모리 주소 값은 객체의 고유성 판별과 참조 비교에 사용됩니다.
  • Hash

    • 객체의 데이터를 기반으로 생성된 정수 값입니다.
    • Object 클래스의 hashCode() 메서드를 통해 얻을 수 있습니다.
    • 해시 기반 컬렉션: 해시 기반 컬렉션에서 객체를 효율적으로 저장하고 검색하는 데 사용됩니다.
      • 해시 코드를 통해 객체를 버킷에 분류하고, 충돌 발생 시 equals() 로 최종 비교
  • 메모리 주소 값

    • 객체가 저장된 메모리 상의 실제 위치를 나타내는 값입니다.
    • 자바는 == 연산자, 코틀린은 === 연산자

sealed class

  • enum 항목이 복잡한 로직을 가질 경우, 각 항목을 클래스로 정의하고, 이를 sealed class로 묶어서 사용 가능
    • 유연한 데이터 저장, 복잡한 구조 가능
  • 자신의 서브(자식) 클래스 종류를 제한하기 위해 사용된다.
  • enum 클래스의 각 항목은 하나의 인스턴스만 생성되지만, sealed class에 속하는 서브 클래스들은 일반 클래스 이므로, 인스턴스 개수에 제한이 없다.
  • 컴파일러는 해당 수퍼 클래스의 서브 클래스들을 컴파일 타임에 파악할 수 있게됨
    • when 표현식과 함께 사용될 때 모든 하위 클래스를 검사하므로, else 절이 필요하지 않습니다.
      • enum도 마찬가지
1
2
3
4
5
sealed interface ApiResult<out T> {
    data class Success<out T>(val data: T) : ApiResult<T>
    data class Error(val exception: Exception) : ApiResult<Nothing>
    object Loading : ApiResult<Nothing>
}
  • 말 그대로 인터페이스이기 때문에 다중 상속을 통한 타입의 다형성을 부여할 수 있습니다.
  • 다시 말해, 특정 타입의 성질을 하위 타입에게 전달하기가 쉽습니다.
  • 또한, when 문을 사용하여 타입을 참조하여 분기해야할 상황에서 장점이 명확해짐

enum의 한계

  • 각 요소당 하나의 single instance를 사용하기 때문에 서로 다른 형태를 가질 수 없습니다.
1
2
3
4
enum class Result {
    SUCCESS,
    FAILED(val exception: Exception) // 이런 형태는 불가함.
}