변수, 타입, 연산자
Index
변수 다루기
변수 선언 키워드 - var, val 차이점
Java에서의 int와 final int의 차이 = 가변이냐? 불변이냐?
var n = 10 // int n = 10
n = 20
val x = 20 // final int n = 10
x = 20 // Error
코틀린은 타입을 자동으로 컴파일러가 추론해준다.
초기값을 지정하지 않는 경우?
var n
val x // 이렇게만 작성할 경우 컴파일러가 타입을 추론할 수 없어 컴파일 에러 발생
x = 10
x = 20 // val이라 컴파일 에러
val 컬렉션에는 element를 추가할 수 있다.
자바에서도 컬렉션 자체를 바꿀 수 없지만 컬렉션 내 조작은 가능하다. 코틀린도 마찬가지
Kotlin에서의 Primitive Type
var n1 = 20L
var n3 = 1_000L
자바에서는 Primitive Type과 Reference Type이 존재한다. 코틀린에서는 이런 구분이 따로 없다. 그렇다면 성능 문제는 없는가?
- 코틀린에서는 기본적으로 Reference Type을 사용하지만 디컴파일했을 때의 자바 코드는 primitive type으로 작성되어있다.
- 즉, 개발자가 Boxing / Unboxing을 신경쓰지 않도록 해준다.
Kotlin에서의 nullable 변수
코틀린은 null이 들어갈 수 있는 경우를 간주한다. 타입 옆에 ‘?’를 붙여준다.
var n = 10L
n = null // 컴파일에러 발생
var n2? = 10L
n2 = null
Kotlin에서의 객체 인스턴스화
var person = Person("고범석")
자바는 new 키워드를 사용하지만, 코틀린은 new를 붙이지 않는다.
null 다루기
코틀린은 null이 들어가는 변수를 다르게 취급한다. 그렇다면 코틀린은 어떻게 이를 다룰까?
Kotlin에서의 null 체크
public boolean startsWithA(String str) {
return str.startsWith("A");
}
위 자바 코드는 안전한 코드가 아니다. str에 null이 올 경우 NPE가 발생한다. 이를 안전하게 고쳐보면
public boolean startsWithA1(String str) {
if (str == null) {
throw new IllegalArgumentException();
}
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");
}
위 방법들을 코틀린으로 작성해보면
fun startsWithA1(str: String?): Boolean {
if (str == null) {
throw IllegalArgumentException()
}
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
}
// 앞에서 null 체크를 하지 않으면 코틀린에서는 컴파일 에러가 발생.
return str.startsWith("A")
}
즉, 코틀린에서는 null이 가능한 타입을 완전히 다르게 취급한다.
Safe Call과 Elvis 연산자
Safe Call
val str: String? = "ABC"
str.length // 불가능, null인 경우도 있기 떄문
str?.length // 가능, null이 아닌 경우만 호출, null일 경우는 애초에 null로 취급
Elvis 연산자
val str: String? = "ABC"
str?.length ?: 0 // ?: 뒤 쪽의 '0'은 앞의 호출 결과가 null일 경우 리턴된다.
Safe Call, Elvis를 위 함수에서 활용해본다면?
fun startsWithA1(str: String?): Boolean {
return str?.startsWith("A") ?: throw IllegalArgumentException()
}
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가 null일 경우 로직을 수행하지 않고 바로 리턴
number ?: return 0
// 계산 로직
}
널 아님 단언!!
nullable type이지만 아무리 생각해도 null이 될 수 없을 경우에 사용한다.
fun startsWith(str: String?): Boolean {
return str!!.startWith("A")
}
만약 파라미터에 null이 들어올 경우 NPE가 발생한다.
플랫폼 타입
한 프로젝트에서 코틀린과 자바를 같이 사용할 때가 있다.
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
@Nullable // 코틀린에서 인식 가능
public String getName() {
return name;
}
}
fun main() {
val person = Person("고범석") // 자바 코드의 Person 클래스
startWithA(person.name) // 컴파일 에러 발생
}
// 이 함수의 매개변수는 null이 올 수 없다. Person 객체는 nullable
fun startsWithA(str: String): Boolean {
return str.startsWith("A")
}
코틀린에서는 다음과 같은 패키지에서 null을 다루는 어노테이션을 활용할 수 있다.
- javax.annotation
- android.support.annotation
- org.jetbrains.annotation
만약 Person
자바 클래스에서 @Nullable
이 없다면?
- 컴파일 에러는 발생하지 않는다.
- 하지만 런타임에서 예외가 발생한다.
- null이 들어갈 수 있는 가능성을 꼼꼼히 체크해야한다.
- 코틀린에서 null인지 파악하지 못하는 타입을 플랫폼 타입이라 한다.
Type 다루기
기본 타입
코틀린은 선언된 기본값을 보고 타입을 추론한다.
val n1 = 3 // Int
val n2 = 3L // Long
val n3 = 3.0f // Float
val n4 = 3.0 // Double
자바는 기본 타입간의 변환은 암시적으로 이뤄질 수 있으나, 코틀린의 기본 타입간의 변환은 명시적으로 이루어져야 한다.
int n1 = 4;
long n2 = n1; // 암시적으로 큰 타입으로 형변환
val n1 = 4
val n2: Long = n1 // Type Mismatch, 컴파일 에러
val n1 = 4
val n2: Long = n1.toLong() // toXXX() 다양한 타입 변환 메서드가 있다.
변수가 nullable이라면 적절한 처리를 해줘야한다.
val n1: Int? = 3
val n2: Long = n1.toLong() // 컴파일 에러
val n2: Long = n1?.toLong() ?: 0L
타입 캐스팅
자바에서는 instanceof로 타입을 체크하고 (타입)을 통해 캐스팅한다.
fun printAgeIfPerson(obj: Any) {
if (obj is Person) { // is == instanceof
val person = obj as Person // (Person) == as Person
println(person.age)
}
}
스마트 캐스트
만약 is
를 통해 특정 타입이 맞다고 판단되면 as
를 쓰지 않아도 된다. 이를 스마트 캐스트라고 한다.
fun printAgeIfPerson(obj: Any) {
if (obj is Person) {
println(obj.age)
}
}
instanceof의 반대는? 즉, 자바에서 if (!(obj instanceof Person)) {…}
를 코틀린에서는 어떻게 표현할까?
fun printAgeIfPerson1(obj: Any) {
if (!(obj is Person)) {
println(obj.age)
}
}
// 이 방법이 좀 더 간결
fun printAgeIfPerson2(obj: Any) {
if (obj !is Person) {
println(obj.age)
}
}
그럼 obj에 null
이 들어온다면?
fun printAgeIfPerson2(obj: Any?) {
val person = obj as? Person
println(person?.age)
}
정리
Kotlin의 3가지 특이한 타입
코틀린에서는 Any
, Unit
, Nothing
이라는 세 가지 타입이 존재한다.
Any
-
자바에서의
Object
와 같은 역할을 한다. -
자바는 Primitive Type은
Object
로 치환할 수 없지만 코틀린에서는 애초에Int
,Long
으로 취급하기 때문에 코틀린의 Primitive Type의 최상위도Any
다. -
Any
자체로는 null을 표현할 수 없어Any?
로 표현한다. -
Any
에equals
/hashCode
/toString
존재
Unit
- 자바의
void
와 동일한 역할 void
와 다르게Unit
은 그 자체로 타입 인자로 사용 가능- 자바에서는
Void
클래스를 따로 썼다. 코틀린에서는Unit
으로 사용 가능
- 자바에서는
- 함수형 프로그래밍에서
Unit
은 단 하나의 인스턴스만 갖는 타입을 의미- 코틀린의
Unit
은 실제 존재하는 타입이라는 것을 표현
- 코틀린의
Nothing
-
함수가 정상적으로 끝나지 않았다는 사실을 표현하는 역할
-
무조건 예외를 반환하는 함수, 무한 루프 함수 등등..
fun fail(message: String): Nothing { throw IllegalArgumentException() }
String Interpolation, String indexing
코틀린에서는 문자열을 출력할 때 “” 안에 ${변수}를 사용하면 변수에 해당하는 값이 문자열로 들어간다. 또한 중괄호는 생략이 가능하다.
val person = Person("고범석", 28)
val log = "내 이름은 ${person.name}이고, 나이는 ${person.age} 이다."
val log = "이름 : $person.name"
코틀린에서 여러 줄의 문자열을 “”” “””
안에 쉽게 작성할 수 있다.
fun main() {
val str = """
ABC
DEF
""".trimIndent()
println(str)
}
자바에서 문자열 안에 특정 문자를 가져올 때는 charAt()
으로 가져와야 한다. 코틀린에서는 문자열[index]
로 가져온다.
val str = "ABCDE"
println(str[1])
연산자 다루기
단항 연산자 / 산술 연산자
자바와 완전 동일하다.
비교 연산자와 동등성, 동일성
비교 연산자
비교 연산자도 자바와 동일하다.
차이점이 있다면 객체를 비교할 때 비교 연산자를 사용하면 자동으로 compareTo()
를 호출한다.
// 자바에서의 대소 비교
Money m1 = new Money(200L);
Money m2 = new Money(100L);
if (m1.compareTo(m2) > 0) {
System.out.println("m1이 m2 보다 크다.")
}
// 코틀린에서의 대소 비교
val m1 = Money(100L);
val m2 = Money(200L);
// 객체 비교 시 compareTo() 호출, 더 직관적
if (m1 > m2) {
println("m1이 m2보다 크다.");
}
동등성, 동일성
- 동등성 : 두 객체의 값이 같은가?
- 동일성 : 동일한 객체의 주소인가?
자바에서는 동일성을 판단할 때는 ‘==’
, 동등성을 판단할 때는 equals()
로 판단한다.
코틀린에서는 동일성은 ‘===’
, 동등성은 ‘==’
로 판단한다. 또한 ‘==’
를 사용하면 간접적으로
equals()
를 호출한다.
논리 연산자 / 코틀린에 있는 특이한 연산자
논리 연산자는 자바와 완전히 동일하다. 자바처럼 lazy 연산을 수행한다.
// Lazy 연산?
fun main() {
// fun2()를 호출하지 않는다. 이게 lazy 연산
// 어차피 fun1()이 true를 반환하면 뒤에 함수는 실행할 이유가 없으니 호출하지 않음
if (fun1() || fun2()) {
println("lazy 연산 수행")
}
}
fun fun1() {
println("fun1()")
return true
}
fun fun2() {
println("fun2()")
return true
}
in / !in
- 컬렉션이나 범위에 포함되어 있다, 포함되어 있지 않음을 판단할 때 쓰는 연산자
a..b
- a 부터 b까지 range 연산자를 의미
a[i]
- a 컬렉션에서 i 번째 원소를 가져오는 연산자
a[i] = b
- a 컬렉션에서 i 번째 원소 자리에 b를 넣는다.
연산자 오버로딩
코틀린에서는 연산자를 직접 오버로딩할 수 있다. Money 클래스에서 operator 키워드와 지정된 예약어를 통해 오버로딩 할 수 있고, 메서드가 아닌 연산자를 통해 메서드를 수행하여 가독성을 높여준다.
fun main() {
val m1 = Money(1_000L)
val m2 = Money(2_000L)
println("(m1 + m2) == (m1.plus(m2)) => ${(m1 + m2) == (m1.plus(m2))}")
}
data class Money(
val amount: Long,
) {
operator fun plus(other: Money) = Money(this.amount + other.amount)
}