본문 바로가기

Kotlin

자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide) (1)

1. 코틀린에서 변수를 다루는 방법

1. 변수 선언 키워드 - var과 val의 차이점

// Java
long number1 = 10L;
final long number2 = 10L;
// Kotlin
var number1 = 10L // 가변
val number2: Long = 10L // 불변 - Java의 final 변수와 같다.

 

Kotlin은 number1처럼 타입을 명시해주지 않아도 컴파일러가 추론해주기 때문에 의무적으로 타입을 작성해주지 않아도 된다. 단, 값을 할당하지 않았을 때는(아직 초기화하지 않았을 때) 타입을 선언해주어야 컴파일 에러가 나지 않는다.

타입을 작성하고 싶다면 number2처럼 변수명 뒤에 콜론(:) 뒤에 타입을 작성해주면 된다. 

var과 val 중 어떤 것을 사용해야할지 헷갈리때는 모든 변수는 우선 val로 만들고 필요한 경우에만 var로 변경하자

2. Kotlin에서의 Primitive Type

// Java
long number1 = 10L;
Long number3 = 1_000L;
// Kotlin
var number1 = 10L
var number3 = 1_000L

 

Kotlin에서 타입은 Reference 타입으로 표시되지만 연산이 수행될때는 Kotlin이 알아서 내부적으로 Primitive 타입으로 바꿔서 불필요한 객체 생성이 일어나지 않도록 한다.

즉, 프로그래머가 boxing과 unboxing을 고려하지 않도록 Kotlin이 자동으로 처리해준다.

3. Kotlin에서의 nullable 변수

var number3: Long? = 1_000L
number3 = null

 

Java는 Reference 타입의 경우 null을 허용하지만 Kotlin은 기본적으로 null을 허용하지 않는다. 만약 null을 허용하고 싶으면 위와 같이 타입 뒤에 물음표(?)를 작성하여 null을 허용한다고 선언해야한다.

4. Kotlin에서의 객체 인스턴스화

// Java
Person person = new Person("key");
// Kotlin
var person = Person("key")

 

Kotlin에서는 객체를 인스턴스화 할때 new를 붙이지 않는다.

2. 코틀린에서 null을 다루는 방법

1. Kotlin에서의 null 체크

// Java에서의 null 체크
public boolean startsWithA1(String str) {
    if (str == null) {
        throw new IllegalArgumentException("null이 들어왔습니다");
    }
    
    return str.startsWith("A");
}

public Boolean startsWithA2(String str) {
    if (str == null) {
        return null;
    }
    
    return str.startsWith("A");
}

public boolean startsWithA3(String str) {
    if (str == null) {
        return false;
    }
    
    return str.startsWith("A");
}
// Kotlin에서의 null 체크
fun startsWithA1(str: String?): Boolean {
    if (str == null) {
        throw IllegalArgumentException("null이 들어왔습니다")
    }
    return str.startsWIth("A")
}

fun startsWithA2(str: String?): Boolean? {
    if (str == null) {
        return null
    }
    return str.startsWIth("A")
}

fun startsWithA3(str: String?): Boolean {
    if (str == null) {
        return false
    }
    return str.startsWIth("A")
}

 

null이 들어올 수 있는 파라미터에는 타입 뒤에 물음표(?)를 붙여주고 startsWithA2 메서드처럼 return 타입으로 명시된 Boolean 값이 아닌 null 값이 반환될수도 있는 경우 return 타입 뒤에 물음표(?)를 붙여준다.

코틀린에서 null이 들어갈 수 있는 타입은 완전히 다르게 간주되며 한번 null 검사를 하면 이후부터는 컴파일러가 non-null임을 알 수 있다.

2. Safe Call과 Elvis 연산자

fun main() {
    // Safe Call(?)
    val str1: String? = "ABC"
    str1.length // 불가능
    str1?.length // 가능
    
    // Elvis 연산자(?:)
    val str2: String? = "ABC"
    str2?.length ?: 0
}

 

  • Safe Call(번수명?) : null이 아니면 실행하고 null이면 실행하지 않는다. (null을 리턴)
  • Elvis 연산자(?:) : 앞의 연산 결과가 null이면 뒤의 값을 사용
// Safe Call과 Elvis 연산자가 적용된 코드
fun startsWithA1(str: String?): Boolean {
    return str?.startsWith("A") 
        ?: throw IllegalArgumentException("null이 들어왔습니다")
}

fun startsWithA2(str: String?): Boolean? {
    return str?.startsWith("A")
}

fun startsWithA3(str: String?): Boolean {
    return str?.startsWith("A") ?: false
}

 

Elvis 연산은 early return에서도 사용할 수 있다.

 

public long calculate(Long number) {
    if (number == null) {
        return 0;
    }
    
    ...
}
fun calculate(number: Long?): Long {
    number ?: return 0
    
    ...
}

 

number 변수가 null인 경우 0을 return, null이 아니면 아래 비즈니스 로직을 수행

3. 널 아님 단언!!

fun startsWith(str: String?): Boolean {
    return str!!.startsWith("A")
}

 

nullable 타입이지만 절대 null이 될 수 없는 경우 "!!"로 단언할 수 있다.

"!!" 선언했음에도 불구하고 null이 들어오게 되면 런타임에서 NullPointerException 에러가 발생한다.

3. 코틀린에서 Type을 다루는 방법

1. 기본 타입

val number1 = 3    // Int
val number2 = 3L   // Long
val number3 = 3.0f // Float
val number3 = 3.0  // Double

 

코틀린에서는 선언된 기본값을 보고 타입을 추론한다.

 

val number1 = 4
val number2: Long = number1 // Type mismatch 컴파일 에러
val number2: Long = number1.toLong() // 명시적으로 타입 변환해줌

 

Java에서 기본 타입간의 변환은 암시적으로 이루어 지지만 Kotlin에서는 명시적으로 이루어져야 한다.

2. 타입 캐스팅

// Java 타입 캐스팅
public static void printAgeIfPerson(Object obj) {
    if (obj instanceof Person) {
        Person person = (Person) obj;
        ...
    }
}
fun printAgeIfPerson(obj: Any) {
    if (obj is Person) {
        val person = obj as Person
        ...
    }
}

 

Java에서는 괄호()안에 타입을 명시하여 타입 캐스팅을 해줘야 했지만 Kotlin에서는 as라는 키워드를 사용하여 타입 캐스팅을 한다.

Kotlin은 또한 스마트 캐스트라는 기능을 제공하는데 if문으로 타입체크를 해주었기 때문에 굳이 Person 객체로 타입 캐스팅을 하지 않아도 Kotlin이 Person 객체로 인식한다.

 

만약 null 객체가 넘어오는 경우에는 as 뒤에 물음표(?)를 붙여서 null일 수도 있다는 것을 명시해주어야 하고 Person 객체의 메서드를 호출할때는 person 변수명 뒤에 ?를 붙여서 Safe Call로 호출해야 한다.

3. Kotlin의 3가지 특이한 타입(Any, Unit, Nothing)

Any

  • Java의 Object 역할 -> 모든 객체의 최상위 타입
  • 모든 Primitive Type의 최상의 타입도 Any
  • Any 자체로는 null을 포함할 수 없다. 만약 null을 포함하고 싶다면 Any?로 표현
  • Any에 equals / hashCode / toString 존재

Unit

  • Unit은 Java의 void와 동일한 역할
  • void와 다르게 Unit은 그 자체로 타입 인자로 사용 가능 (추가 학습 필요)
  • 함수형 프로그래밍에서 Unit은 단 하나의 인스턴스만 갖는 타입을 의미한다. 즉, 코틀린의 Unit은 실제 존재하는 타입이라는 것을 표현

Nothing

  • Nothing은 함수가 정상적으로 끝나지 않았다는 사실을 표현
  • 무조건 예외를 반하는 함수 / 무한 루프 함수 등등
// Nothing 예시
fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

4. String Interpolation, String indexing

// Java
Person person = new Person("key", 30);
String log = String.format("사람의 이름은 %s이고 나이는 %s세 입니다", person.getName(), person.getAge());

String str = "ABCDE";
char ch = str.charAt(0); // A
// Kotlin
val person = new Person("key", 30);
val log = "사람의 이름은 ${person.name}이고 나이는 ${person.age}세 입니다"

val str = "ABCDE"
val ch = str[0] // A

 

문자열에 인덱스로 접근할 때 Java는 charAt() 메서드를 사용하지만 Kotlin은 배열처럼 대괄호[]를 사용해 접근할 수 있다.

4. 코틀린에서 연산자를 다루는 방법

1. 단항 연산자 / 산술 연산자

  • 단항 연산자(++, --), 산술 연산자(+, -, *, /, %), 산술대입 연산자(+=, -=, *=, /=, %=) 모두 Java와 동일하다.

2. 비교 연산자와 동등성, 동일성

  • 비교 연산자(>, <, >=, <=) 모두 Java와 동일하지만 객체 간의 관계를 비교할 때는 자동으로 compareTo를 호출해준다.
  • Java에서는 동일성을 비교에는 == 를 사용하고 동등성을 비교할 때는 equals()를 호출했지만 Kotlin에서는 동일성에 ===을 사용하고 동등성에 ==를 사용한다.

3. 논리 연산자 / 코틀린에 있는 특이한 연산자

  • 논리 연산자(&&, ||, !) 모두 Java와 동일하며 Java와 마찬가지로 Lazy 연산을 수행한다.
  • in / !in : 컬렉션이나 범위에 포함되어 있다, 포함되어 있지 않다.
  • a..b : a부터 b까지의 범위 객체를 생성한다.
  • a[i] : a에서 특정 Index i로 값을 가져온다.
  • a[i] = b : a의 특정 Index i에 b를 넣는다.

4. 연산자 오버로딩

  • Kotlin에서는 객체마다 연산자를 직접 정의할 수 있다.
data class Money (
    val amount: Long
) {
    operator fun plus(other: Money): Money {
        return Money(this.amount + other.amount)
    }
}

 

즉, 위와 같은 Kotlin 클래스가 정의되어 있을 때 plus 메서드를 호출해도 되지만 +연산자로 대체할 수 있다.

 

fun main() {
    val money1 = Money(1_000L)
    val money2 = Money(2_000L)
    println(money1.plus(money2)) // Money(amount=3000)
    println(money1 + money2) // Money(amount=3000)
}