자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)
강의를 들으며 내용 정리
코틀린에서 변수를 다루는 방법
변수 선언 키워드 - var과 val의 차이점
<java>
long number1 = 10L; // 가변
final long number2 = 10L; // 불변
<kotlin>
var number1 = 10L // 가변
val number2 = 10L // 불변
코틀린에서는 모든 변수에 수정 가능 여부(var/val)를 명시해주어야 한다.
또한, 타입을 명시해주지 않아도 할당된 값에 따라 컴파일러가 알아서 타입 추론을 해준다.
물론 타입을 명시적으로 적어줘도 된다.
var number1: Long = 10L
val number2: Long = 10L
주의할 점은 컴파일러가 타입을 추론할 수 있어야 한다는 점이다.
var number1 // 타입 추론 불가능 -> 컴파일 오류
var number2 = 10L // 타입 추론 가능
var number3: Long // 타입 추론 가능 -> but 컴파일 오류 (기본적으로 변수 선언과 동시에 초기화가 되야 한다)
fun main() {
var number4: Long // 함수 안에서는 초기화 없이 변수를 선언할 수 있다
number4 = 10L
val number5: Long // val은 불변이지만 선언과 동시에 초기화하지 않아도 된다
number5 = 10L // 이렇게 나중에 최초 한번 값을 초기화할 수 있다
}
자바에서 final 키워드가 붙은 List의 경우 List 자체를 변경하는 것은 불가능하지만, List에 값을 add 할 수는 있다.
코틀린도 마찬가지이다.
<java>
final List<Long> list = new ArrayList<>();
list = new ArrayList<>(); // 컴파일 오류
list.add(10L); // 값 추가는 가능
<kotlin>
val list = ArrayList<Long>()
list = ArrayList<Long>() // 컴파일 오류
list.add(10L) // 값 추가는 가능
+) tip
코틀린에서 모든 변수는 우선 안전하게 val로 만들고 꼭 필요한 경우 var로 변경하자.
Kotlin에서의 Primitive Type
<java>
long number1 = 10L; // primitive type
Long number2 = 1_000L; // reference type
<kotlin>
var number1: Long = 10L
val number2: Long = 10L
코틀린은 primitive type, reference type이 따로 구분되어 있지 않다.
Q: 그럼 무조건 reference type을 써야하는 건가? 성능상 문제는 없는 것인가?
A: 코틀린 공식 문서에는 숫자, 문자, 불리언과 같은 몇몇 타입은 실행시에 primitive type으로 표현되지만, 코드에서는 평범한 클래스처럼 보인다. 즉, 프로그래머가 reference type에 대해 boxing/unboxing을 고려하지 않아도 되도록 코틀린이 알아서 처리 해준다.
실제로 코틀린 코드를 자바로 확인해보자.
intellij -> Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile -> 자바 코드 확인
@Metadata(
mv = {1, 5, 1},
k = 1,
d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
d2 = {"Lcom/lannstark/lec01/Lec01;", "", "()V", "main", "", "kotlin-with-java"}
)
public final class Lec01 {
public final void main() {
long number1 = 10L; // long 타입을 사용
long number2 = 10L;
}
}
Kotlin에서의 nullable 변수
<java>
long number1 = 10L; // null 불가
Long number2 = null; // nullable
<kotlin>
var number1: Long = 10L // null 불가
var number2: Long? = null // nullable
타입 뒤에 '?'을 붙이면 된다.
Kotlin에서의 객체 인스턴스화
<java>
Person person = new Person("한영규");
<kotlin>
var person:Person = Person("한영규")
new를 붙이지 않는다.
코틀린에서 null을 다루는 방법
Kotlin에서의 null 체크
<java>
public boolean startsWithA1(String str) {
return str.startsWith("A");
}
이 코드는 안전한 코드일까? -> 아니다.
만약 str에 null이 들어오면 위 코드는 바로 NPE가 발생하게 된다.
따라서 위 코드를 null에 대해 안전하게 3가지 버전으로 바꿔보자.
// str이 null일 경우 Exception을 낸다
public boolean startsWithA1(String str) {
if (str == null) {
throw new IllegalArgumentException("null이 들어왔습니다");
}
return str.startsWith("A");
}
// str이 null일 경우 null을 반환한다
public Boolean startsWithA2(String str) {
if (str == null) {
return null;
}
return str.startsWith("A");
}
// str이 null일 경우 false를 반환한다
public boolean startsWithA3(String str) {
if (str == null) {
return false;
}
return str.startsWith("A");
}
이제 자바 코드를 코틀린으로 바꿔보자.
<kotlin>
// str이 null일 경우 Exception을 낸다
fun startsWithA1(str: String?): Boolean { // null 불가 boolean 리턴
if (str == null) {
throw IllegalArgumentException("null이 들어왔습니다")
}
return str.startsWith("A")
}
// str이 null일 경우 null을 반환한다
fun startsWithA2(str: String?): Boolean? { // nullable Boolean 리턴
if (str == null) {
return null
}
return str.startsWith("A")
}
// str이 null일 경우 false를 반환한다
fun startsWithA3(str: String?): Boolean { // null 불가 boolean 리턴
if (str == null) {
return false
}
return str.startsWith("A")
}
만약 if 구문으로 null처리를 하지 않고 nullable한 str의 메서드를 호출한다면 컴파일 오류가 발생한다.
fun startsWithA1(str: String?): Boolean {
return str.startsWith("A") // 컴파일 오류
}
Safe Call과 Elvis 연산자
Safe Call
var str: String? = "ABC"
str.length // 컴파일 오류
str?.length // safe call
"?."을 사용한 Safe Call: str이 null이 아니면 실행하고 null이면 실행하지 않고 그대로 null을 반환한다.
Elvis 연산자
var str: String? = "ABC"
str?.length ?: 0 // elvis
"?:"를 사용한 Elvis 연산자: 앞의 연산 결과가 null이면 뒤의 값을 사용 (즉, null이 아닌 0을 리턴)
+) 왜 Elvis 연산자인가?
"?:"를 90도 회전하면 엘비스 프레슬리와 닮았다고 한다...ㅋㅅㅋ
이제 아까 작성했던 3가지 함수에 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에도 사용할 수 있다.
<java>
public long calculate(Long number) {
if (number == null) {
return 0;
}
}
<kotlin>
fun calculate(number: Long?): Long {
number ?: return 0
}
널 아님 단언!!
nullable type이지만, 절대 null될 수 없는 경우라면 non-null asserted call("!!.")을 사용할 수 있다.
fun startsWithA1(str: String?): Boolean {
return str!!.startsWith("A")
}
혹시나 null이 들어오면 NPE가 발생하기 때문에 정말 null이 아닌게 확실한 경우에만 사용해야 한다.
ex) nullable한 필드를 갖는 엔티티를 설계했다가 이후에 null이 아닌 필드로 업데이트하는 경우
플랫폼 타입
코틀린에서 자바 코드를 쓰는 경우를 보자.
<java>
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
@Nullable
public String getName() {
return name;
}
}
<kotlin>
fun main() {
val person: Person = Person("한영규")
startsWithA1(person.name)
}
fun startsWithA1(str: String?): Boolean {
return str?.startsWith("A") ?: throw IllegalArgumentException("null이 들어왔습니다")
}
startsWithA1() 메서드의 인자는 nullable한 타입이고, person.name에 해당하는 자바의 getter코드도 @Nullable 애노테이션이 붙어 있기 때문에 위 코드는 문제가 되지 않는다.
그렇다면 만약 startsWithA1()의 인자가 String?이 아닌 String이라면?
person.name은 nullable한 값이기 때문에 컴파일 오류가 발생한다.
-> 이런식으로 코틀린에서 자바 코드를 가져다 쓸 때는 @Nullable과 같은 애노테이션 정보를 코틀린이 이해한다.
만약 자바 코드에 @Nullable과 같은 애노테이션 정보가 없다면?
코틀린에서는 person.name이 nullable인지 non-nullable인지 알 수가 없다.
이렇게 코틀린이 null 관련 정보를 알 수 없는 타입을 플랫폼 타입이라고 한다.
플랫폼 타입이 있어도 코틀린이 실행은 되지만 런타임에 NPE가 발생할 수 있으니 플랫폼 타입은 가급적 사용하지 않는 것이 좋다.
따라서 코틀린에서 자바 코드를 사용할 때는,
1. 자바 코드를 읽으며 null 가능성을 확인 (@Nullable 등 애노테이션 확인)
2. 만약 라이브러리를 가져온다면, 필요한 값의 null 가능성을 확인
3. 라이브러리를 가져다 쓰는 지점을 코틀린으로 wrapping해서 자바 코드를 가져오는 지점을 최소화한다.
코틀린에서 Type을 다루는 방법
기본 타입
코틀린에서는 선언된 기본값을 보고 타입을 추론한다.
val number1 = 3 // Int
val number2 = 3L // Long
val number3 = 3.0f // Float
val number4 = 3.0 // Double
자바에서 기본 타입간의 변환은 암시적으로 이루어질 수 있다.
int number1 = 4;
long number2 = number1; // 더 큰 타입으로 암시적 변경
코틀린에서 기본 타입간의 변환은 명시적으로 이루어져야 한다.
val number1 = 3
val number2: Long = number1 // 컴파일 오류
val number2: Long = number1.toLong() // 명시적 변환
변수가 nullable이라면 적절한 처리가 필요하다.
val number1: Int? = 3
val number2: Long = number1?.toLong() ?: 0L
타입 캐스팅
<java>
public static void printAgeIfPerson(Object obj) {
if (obj instanceof Person) {
Person person = (Person) obj;
System.out.println(person.getAge());
}
}
<kotlin>
fun printAgeIfPerson(obj: Any) {
if (obj is Person) {
val person = obj as Person
println(person.age)
}
}
자바: "instanceof" -> 코틀린: "is"
자바: "(Person) obj" -> 코틀린: "obj as Person"
코틀린에서는 if 문에서 타입 체크가 됐다면 따로 타입 캐스팅할 필요 없이 스마트 캐스트를 지원한다.
fun printAgeIfPerson(obj: Any) {
if (obj is Person) {
println(obj.age)
}
}
is의 반대는 !is로 표현할 수 있다.
fun printAgeIfPerson(obj: Any) {
if (obj !is Person) { // obj가 Person의 인스턴스가 아니라면
}
}
만약 obj에 null이 들어올 수 있다면 다음과 같이 코드를 바꿀 수 있다.
fun printAgeIfPerson(obj: Any?) {
val person = obj as? Person
println(person?.age)
}
as?는 obj가 null이라면 null을 반환하고, null이 아니라면 Person으로 타입 캐스팅을 한다.
as를 정리하면 다음과 같다.
value as Type
value가 Type이면 -> Type으로 타입 캐스팅
value가 Type이 아니면 -> 예외 발생
value as? Type
value가 Type이면 -> Type으로 타입 캐스팅
value가 Type이 아니면 -> null
value가 null이면 -> null
Kotlin의 3가지 특이한 타입
Any
자바의 Object 역할. (모든 객체의 최상위 타입)
모든 Primitive 타입의 최상위 타입도 Any이다.
Any 자체로는 null을 포함할 수 없어 null을 포함하고 싶다면, Any?로 표현.
Any에 equals/hashCode/toString 존재.
Unit
자바의 void 역할.
자바의 void와 다르게 Unit은 타입 인자로 사용 가능하다. (제네릭에서 void를 쓰려면 Void 타입을 사용해야 한다)
Nothing
함수가 정상적으로 끝나지 않았다는 사실을 표현하는 역할.
ex) 무조건 예외를 반환하는 함수/무한 루프 함수 등
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
+) 사실 저런 함수는 잘 작성하지 않기 때문에 Nothing도 거의 사용하지 않게 된다.
String Interpolation, String indexing
String Interpolation
<java>
Person person = new Person("한영규", 100);
String log = String.format("사람의 이름은 %s이고 나이는 %s세 입니다", person.getName(), person.getAge());
StringBuilder builder = new StringBuilder();
builder.append("사람의 이름은 ");
builder.append(person.getName());
builder.append("이고 나이는 ");
builder.append(person.getAge());
builder.append("세 입니다");
<kotlin>
val person = Person("한영규", 100)
val log = "사람의 이름은 ${person.name}이고 나이는 ${person.age}세 입니다"
$변수를 사용할 수도 있다.
val name = "한영규"
val age = 100
val log = "사람의 이름: $name 나이: $age
물론 ${변수}를 사용하는 것이 가독성, 일관 변환, 정규식 활용 측면에서 좋다.
줄바꿈이 있는 문자열의 경우 """ 형식을 사용하면 깔끔하게 코딩할 수 있다.
val withoutIndent = """
ABC
123
456
""".trimIndent() // intellij에서 자동으로 trimIndent()를 추가해준다.
String indexing
<java>
String str = "ABCDE";
char ch = str.charAt(1);
<kotlin>
val str = "ABCDE"
val ch = str[1]
코틀린에서 연산자를 다루는 방법
단항 연산자 / 산술 연산자
단항 연산자 (++, --)
산술 연산자(+, -, *, /, %)
산술대입연산자(+=, -=, *=, /=, %=)
-> 자바, 코틀린 완전 동일하다.
비교 연산자와 동등성, 동일성
비교 연산자
비교 연산자(>, <, >=, <=)
-> 자바, 코틀린 완전 동일하다.
단, 자바와 다르게 코틀린은 객체를 비교할 때 비교 연산자를 사용하면 자동으로 compareTo를 호출해준다.
<java>
실습을 위한 클래스 하나를 정의한다.
public class JavaMoney implements Comparable<JavaMoney> {
private final long amount;
public JavaMoney(long amount) {
this.amount = amount;
}
public JavaMoney plus(JavaMoney other) {
return new JavaMoney(this.amount + other.amount);
}
@Override
public int compareTo(@NotNull JavaMoney o) {
return Long.compare(this.amount, o.amount);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JavaMoney javaMoney = (JavaMoney) o;
return amount == javaMoney.amount;
}
@Override
public int hashCode() {
return Objects.hash(amount);
}
@Override
public String toString() {
return "JavaMoney{" +
"amount=" + amount +
'}';
}
}
JavaMoney money1 = new JavaMoney(1_000L);
JavaMoney money2 = new JavaMoney(2_000L);
if (money1.compareTo(money2) > 0) {
System.out.println("Money1이 Money2보다 금액이 큽니다");
}
<kotlin>
val money1 = JavaMoney(2_000L)
val money2 = JavaMoney(1_000L)
if (money1 > money2) {
println("Money1이 Money2보다 금액이 큽니다")
}
자동으로 compareTo() 호출한다.
사실 compareTo()가 0보다 큰게 뭐지? 하고 헷갈릴 때가 많은데, 코틀린의 방식이 자바보다 더 직관적으로 코드를 짤 수 있는 것 같다.
동등성, 동일성
동등성: 두 객체의 값이 같은가?
동일성: 완전히 동일한 객체인가?, 즉 주소가 같은가?
<java>
JavaMoney money1 = new JavaMoney(1_000L);
JavaMoney money2 = money1;
JavaMoney money3 = new JavaMoney(1_000L);
System.out.println(money1 == money2); // 동일성
System.out.println(money1.equals(money3)); // 동등성
<kotlin>
val money1 = JavaMoney(2_000L)
val money2 = money1
val money3 = JavaMoney(2_000L)
println(money1 === money2) // 동일성
println(money1 == money3) // 동등성
논리 연산자 / 코틀린에 있는 특이한 연산자
논리 연산자
논리 연산자(&&, ||, !)
-> 자바, 코틀린 완전 동일하다.
+) 코틀린도 자바처럼 Lazy 연산을 수행한다.
<Lazy 연산>
fun main() {
if (fun1() || fun2()) {
println("본문")
}
}
fun fun1(): Boolean {
println("fun1")
return true
}
fun fun2(): Boolean {
println("fun2")
return false
}
// 결과
fun1
본문
fun1()이 이미 true기 때문에, fun2()를 굳이 수행하지 않고, 바로 조건문을 수행한다.
in / !in
컬렉션이나 범위에 포함되어 있다 / 포함되어 있지 않다
println(1 in numbers)
연산자 오버로딩
코틀린에서는 객체마다 연산자를 직접 정의할 수 있다.
data class Money (
val amount: Long
) {
operator fun plus(other: Money): Money {
return Money(this.amount + other.amount)
}
}
operator fun plus()로 '+' 연산자를 정의할 수 있다.
val money1 = Money(2_000L)
val money2 = Money(1_000L)
println(money1 + money2)
// 결과
Money(amount=3000)
'kotlin' 카테고리의 다른 글
[Kotlin] JPA 플러그인 정리 (0) | 2024.07.19 |
---|---|
[Kotlin] 추가적으로 알아두어야 할 코틀린 특성 (0) | 2023.06.20 |
[Kotlin] 코틀린에서의 FP (0) | 2023.06.04 |
[Kotlin] 코틀린에서의 OOP (0) | 2023.05.07 |
[Kotlin] 코틀린에서 코드를 제어하는 방법 (4) | 2023.05.07 |