var / val을 붙여주어야 한다.var : 변경 가능 / val : 변경 불가능 (read-only)null이 들어갈 수 있는 변수는 타입 뒤에 ? 를 붙여주어야 한다.new를 붙이지 않아야 한다.Safe Call (?) : null이 아니면 실행하고 null 이면 그대로 null(실행하지 않는다)
val str: String? = "ABC"
str.length // 불가능
str?.length // 가능
Elvis 연산자 (?:) : 앞의 연산 결과가 null 이면 뒤에 값을 사용
val str: String? = "ABC"
str?.length ?: 0
이 변수는 절대 null이 아니라고 단언
// 혹시나 null이 들어오면 NPE가 나오기 때문에
// 정말 null이 아닌게 확실한 경우에만 널 아님 단언!! 을 사용해야 한다.
fun startsWith(str: String?): Boolean {
return str!!.startsWith("A")
}
코틀린에서 자바 클래스를 가져다 사용할 경우
// 자바 코드에 Null 정보에 대한 어노테이션을 사용하면 코틀린이 이해할 수 있음.
@NotNull 혹은 @Nullable
public String getName() {
return name;
}
플랫폼 타입
코틀린이 null 관련 정보를 알 수 없는 타입 (Runtime 시 Exception이 날 수 있다.)
코틀린에서 null이 들어갈 수 있는 타입은 완전히 다르게 간주된다.
한번 null 검사를 하면 non-null임을 컴파일러가 알 수 있다.
null이 아닌 경우에만 호출되는 Safe Call (?.) 이 있다.
null인 경우에만 호출되는 Elvis 연산자 (?:) 가 있다.
null이 절대 아닐때 사용할 수 있는 널 아님 단언 (!!) 이 있다.
Kotlin에서 Java 코드를 사용할 때 플랫폼 타입 사용에 유의해야 한다.
is, !is, as, as?를 이용해 타입을 확인하고 캐스팅한다. (스마트 캐스팅 존재)Any는 Java의 Object와 같은 최상위 타입이다. null까지 포함 최상위는 Any?Unit은 Java의 void와 동일하다.Nothing은 정상적으로 끝나지 않는 함수의 반환을 의미한다.if / if - else / if - else if - else 모두 Java와 문법이 동일하다.따라서 Kotlin에서는 삼항 연산자가 없다.
fun getPassOrFail(score: Int): String {
return if (score >= 50) {
"P"
} else {
"F"
}
}
when으로 대체되었고, when은 더 강력한 기능을 갖는다.in 을 사용한다.for문 문법은 모두 동일하다.while문과 do while문은 동일하다.함수의 문법은 Java와 다르다!
접근지시어 fun 함수이름(파라미터): 반환타입 { }public fun maxV1(a: Int, b: Int): Int {
if (a > b) {
return a
}
return b
}
// Kotlin에서 public은 생략 가능하다. 기본이 public 이다.
fun maxV2(a: Int, b: Int): Int {
return if (a > b) {
a
} else {
b
}
}
fun maxV3(a: Int, b: Int): Int =
if (a > b) {
a
} else {
b
}
}
// = 을 사용하면 명시적인 반환 타입을 생략 가능
fun maxV4(a: Int, b: Int) = if (a > b) a else b
*(spread 연산자)를 붙여주어야 한다.class Person(
val name: String = "홍길동",
var age: Int = 1
)
constructor 키워드를 사용해 부생성자를 추가로 만들 수 있다.class Person(
val name: String = "홍길동",
var age: Int = 1
) {
val isAdult: Boolean
get() = this.age >= 20
// 혹은
val isAdult: Boolean
get() {
return this.age >= 20
}
}
class Person(
name: String = "홍길동",
var age: Int = 1
) {
val name = name
get() = field.uppercase() // name.uppercase()는 getter의 무한 순환으로 인해 사용 불가
}
fun getUppercaseName(): String {
return this.age.uppercase()
}
// 혹은
fun getUppercaseName() = this.name.uppercase()
var name = name
set(value) {
field = value.uppercase()
}
final : override를 할 수 없게 한다. default로 보이지 않게 존재한다.
open : override를 열어 준다.
abstract : 반드시 override를 해야 한다.
override : 상위 타입을 오버라이드 하고 있다. (어노테이션이 아니라 키워드)
상속 또는 구현을 할 때에 : 을 사용해야 한다.
상위 클래스 상속을 구현할 때 생성자를 반드시 호출해야 한다.
override를 필수로 붙여야 한다.
추상 멤버가 아니면 기본적으로 오버라이드가 불가능하다.
open을 사용해주어야 한다.상위 클래스의 생성자 또는 초기화 블럭에서 open 프로퍼티를 사용하면 예상하지 못한 버그가 생길 수 있다.
추상클래스
abstract class Animal(
protected val species: String,
protected open val legCount: Int
) {
abstract fun move()
}
class Cat(
species: String
) : Animal(species, 4) {
override fun move() {
println("고양이가 사뿐 사뿐 걸어가~")
}
}
class Penguin(
species: String
) : Animal(species, 2) {
private val wingCount: Int = 2
override fun move() {
println("펭귄이 움직인다~ 꿱꿱")
}
// 추상 클래스에서 자동으로 만들어진 getter를 override
// 추상 프로퍼티가 아닌 프로퍼티에 접근하기 위해선 open이 필요함
override val legCount: Int
get() = super.legCount + this.wingCount
}
인터페이스
interface Flyable {
// 자바의 default가 필요 없음
fun act() {
println("파닥 파닥")
}
fun fly() // 추상 메서드
}
interface Swimable {
// 코틀린에서는 backing field가 없는
// 프로퍼티를 Interface에 만들 수 있다.
val swimAbility: Int
fun act() {
println(swimAbility)
println("어푸 어푸")
}
}
class Penguin(
species: String
) : Animal(species, 2) {
private val wingCount: Int = 2
...
override fun act() {
super<Swimable>.act()
super<Flyable>.act()
}
override val swimAbility: Int
get() = 3
}
클래스를 상속받을 때 조심해야할 점
fun main() {
Derived(300)
}
open class Base(
open val number: Int = 100
) {
init {
println("Base Class")
println(number)
}
}
class Derived(
override val number: Int
) : Base(number) {
init {
println("Derived Class")
}
}
예상되는 결과는 Base Class와 3을 출력하길 기대한다.
하지만 상위 클래스에서 println(number)를 호출하면 하위 클래스에 있는 number를 가져오는데
아직 상위 클래스에 constructor가 먼저 실행된 단계라서 하위 클래스의 number의 초기화가 이루어지지 않아서
0이라는 값이 출력된다.
따라서 상위 클래스에서 constructor와 init 블럭에서는 final이 아닌 프로퍼티에는 접근하면 안된다.
자바에서의 접근 제어자
public : 모든 곳에서 접근 가능protected : 같은 패키지 또는 하위 클래스에서만 접근 가능default : 같은 패키지에서만 접근 가능private : 선언된 클래스 내에서만 접근 가능코틀린에서의 접근 제어자
public : 모든 곳에서 접근 가능
protected : 선언된 클래스 또는 하위 클래스에서만 접근 가능
internal : 같은 모듈에서만 접근 가능
private : 선언된 클래스 내에서만 접근 가능
Kotlin에서 패키지는 namespace 관리용이기 때문에 protected는 의미가 달라졌다.
Kotlin에서는 default가 사라지고, 모듈 간의 접근을 통제하는 internal이 새로 생겼다.
생성자에 접근 지시어를 붙일 때는 접근 제어자 뒤에 constructor를 명시적으로 써주어야 한다.
유틸성 함수를 만들 때는 파일 최상단을 이용하면 편리하다.
프로퍼티의 custom setter에 접근 지시어를 붙일 수 있다.
class Car(
internal val name: String,
private var owner: String,
_price: Int
) {
var price = _price
private set
}
internal과 protected는 주의해야 한다.public이 된다. 따라서 Java에서는 Kotlin 모듈의 internal 코드를 가져올 수 있다.protected 멤버에 접근할 수 있다.companion object를 사용해야 한다.companion object도 하나의 객체로 간주되기 때문에 이름을 붙일 수 있고, 다른 타입을 상속 받을 수 도 있다.object 키워드를 사용한다.object : 타입을 사용한다.static 을 사용하는 클래스static 을 사용하지 않는 클래스static을 사용하는 클래스이다.inner 키워드를 붙여야 한다.inner class에서 바깥 클래스를 참조하려면 this@바깥클래스를 사용해야 한다.class House(
var address: String
) {
var livingRoom = this.LivingRoom(10.0)
// static이 아닌 클래스로 생성, 바깥 클래스 참조 가능
inner class LivingRoom(
private var area: Double
) {
val address: String
get() = this@House.address
}
}
when과 함께 주로 사용된다.example.withIndex(), example.keys, example[1] = “test”, example.entries …null을 넣을 수도 있다.fun 확장하려는클래스.함수이름(파라미터): 리턴타입 {
// this를 이용해 실제 클래스 안의 값에 접근
}
val 확장하려는클래스.프로퍼티이름: 리턴타입 {
get() 혹은 set() = ...
private, protected 멤버 접근이 안된다!static 함수를 쓰는 것 처럼 Kotlin의 확장함수를 쓸 수 있다.infix 함수가 존재한다. 3 add 4inline 함수가 존재한다.Kotlin에서 람다를 만드는 방법
// 람다를 만드는 방법 1
val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
return fruit.name == "사과"
}
// 람다를 만드는 방법 2
val isApple2: (Fruit) -> Boolean = { fruit: Fruit -> fruit.name == "사과" }
Kotlin에서 람다를 직접 호출하는 방법
// 람다를 직접 호출하는 방법 1
isApple(Fruit("사과", 1000))
// 람다를 직접 호출하는 방법 2
isApple.invoke(Fruit("사과", 1000))
(파라티머 타입, …) → 반환 타입 이었다.filterFruits(fruits) { fruit -> fruit.name == "사과" }
// it로 생략 가능
filterFruits(fruits) { it.name == "사과" }filter / filterIndexed(필터에서 index가 필요할 시)map(객체에서 특정 필드만 꺼내볼 때) / mapIndexed / mapNotNullall (조건을 모두 만족하면 true, 아니면 false) / none (all의 반대) / any (하나라도 만족하면 true)count (List의 size랑 동일) / sortedBy / sortedbyDescending / distinctBy (람다를 가지고 중복을 제거함)first (무조건 null이 아님) / firstOrNull (첫 번째 값 또는 null) / last / lastOrNullgroupByval map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
associateBy 중복되지 않는 키를 가지고 Map을 만들 때 사용val map: Map<Long, Fruit> = fruits.associateBy { fruit -> fruit.id }
flatMap / flattern (List<List>를 List로 바꿀 때)
data class가 아닌 class 에서도 사용 가능하다.for ((key, value) in map.entries) { } 와 같은 key, value를 꺼내는 방식도 구조분해이다.break와 continue를 사용할 수 없다.takeIf (주어진 조건을 만족하면 그 값이, 그렇지 않으면 null이 반환)와 takeUnless (주어진 조건을 만족하지 않으면 그 값이, 그렇지 않으면 null이 반환)를 활용해 코드 양을 줄이고 method chaning을 활용할 수 있다.코틀린의 scope function은 일시적인 영역을 만들어 코드를 더 간결하게 하거나, method chain에 활용된다.
let하나 이상의 함수를 call chain 결과로 호출할 때
val strings = listOf("APPLE", "CAR")
strings.map { it.length }
.filter { it > 3 }
.let(::println)
non-null 값에 대해서만 code block을 실행시킬 때
val length = str?.let {
println(it.uppercase())
it.length
}
일회성으로 제한된 영역에 지역 변수를 만들 때
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first()
.let { firstItem ->
if (firstItem.length >= 5) firstItem else "!$firstItem!"
}.uppercase()
println(modifiedFirstItem)
run객체 초기화와 반환 값의 계산을 동시에 해야할 때
객체를 만들어 DB에 바로 저장하고, 그 인스턴스를 활용할 때
반복되는 생성 후 처리는 생성자, 프로퍼티, init block으로 넣는 것이 좋다.
val person = Person("홍길동", 100).run(personRepository::save)
apply객체 그 자체가 반환된다.
객체 설정을 할 때에 객체를 수정하는 로직이 call chain 중간에 필요할 때
fun createPreson(
name: String,
age: Int,
hobby: String
): Person {
return Person(
name = name,
age = age,
).apply {
this.hobby = hobby
}
}
also객체를 수정하는 로직이 call chain 중간에 필요할 때
mutableListOf("one", "two", "three")
.also { println("four 추가 이전 지금 값: $it") }
.add("four")
with특정 객체를 다른 객체로 변환해야 하는데,
모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때
// this를 생략할 수 있어 필드가 많아도 코드가 간결해진다.
return with(person) {
PersonDto(
name = name
age = age
)
}