thumbnail

코틀린을 다루는 기술

<img src="https://static.podo-dev.com/blogs/images/2021/03/11/origin/5fdda13e-042f-41ab-8ecf-1e92f56140a0.jpg" alt="화면 캡처 2021-03-11 211038.jpg" style="width:217px;">

코틀린을 다루는 기술 2장에, 자바와 코틀린의 비교 내용을 정리한 내용입니다.
자바로 개발하다, 코틀린을 처음 접하면서
2장의 내용이 한눈에 비교할수 있어, 내용을 정리합니다.

자바도 조하요, 코틀린도 조하요

<br/>

2.1 필드와 변수

val name: String = "Hello!"

val라는 키워드가 맨앞에 나오면, 기본적으로 final 키워드를 가지고 불변함을 의미한다.

타입을 생략해 더 간단하게 만들기

val name = "Hello"! // 아 얘는 String 이구나!

필드에 초기화하는 값을 보고 변수의 타입을 추측한다.
이를 타입 추론 이라고 정의한다

가변 변수 사용하기

var name = "Hello!"
name = "World"

앞서 말한 것처럼, val 키워드는 불변하며,
가변을 하기위해서는 var 키워드를 사용한다.

필드의 값을 초기화한후 변경되지 않는 것은 안전함을 의미하기에, 기본으로 val 키워드를 사용하는 것을 권장한다.

일반적으로 코틀린은 초기화하지 않는 참조를 쓸 수 없도록 막는다.
이부분은 초기화 하지 않은 참조를 null로 쓸 수있는 자바와는 다르다.

지연 초기화 이해하기

참조 초기화를 위해 var를 사용 할 수 밖에 없지만,
일단 참조를 초기화해 값이 정해진 다음 부터는 변경을 막고 싶을때가 있다.
쉽게 말해 초기화를 늦게하고 싶지만, 늦게라도 초기화 한후 그값을 불변하게 하고 싶은 경우이다.

val name : String by lazy { getName() } // 최초 초기화시 getName() 함수 반환값을 주입한다. 이후 불변
val name2 : String by lazy { name } // 최초 초기화시 name 값을 주입한다. 이후 불변

<br/>

2.2 클래스와 인터페이스

코틀린의 클래스는 기본적으로 public이다.
하지만 명시 할 필요가 없다.
자바와 달리 protected는 확장(상속관계)에서만 접근 가능하고, 동일 패키지의 다른 클래스에서 접근할 수 없다
자바와 달리 package private 접근자가 없다.
internal 변경자는 클래스가 정의한 모듈안에서만 클래스에 접근할 수있다.

코틀린 클래스는 기본적으로 상속 불가 하다
상속가능하게 하려면 open 클래스를 명시해야한다.

자바와 생성자 비교하기

java

public class Person{

    private String name;
    
    Person(String name){
        this.name =name;
    }
}

kotlin

class Pesron constructor(name: String){
    val name : String
    
    init {
        this.name = name
    }
}

class Person constructor(name : String){
    val name : String = name
}

class Person constructor(val name : String){
}

class Person (val name: String)

인터페이스 구현하거나, 클래스 확장하기

class Person(
    val name : String,
    val registered : Instant
) : Serializable, Comaparable<Person> {
}

코틀린은 확장과 구현을 같은 구문을 사용한다.
클래스를 확장할때는 부모클래스 이름뒤에 인자들이 들어있는 괄호가 붙는점이 구현할때와 다른 부분이다.

class Member(name : String, registered : Instant) : Person(name, registered)

클래스 인스턴스화 하기

val Person = Person("Bob", instant.now())

new 키워드를 사용하지 않는다.

오버라이딩하기

class Person(name : String, registered : Instant = Instants.now())

class Person(name : String, registered : Instant = Instants.now()){
        constructor(name: Name) : this(name.toString()){
        }
}

data class

data class Hello(val name: String)

data 클래스는, equals(), hashcode(), toString(), copy()를 자동으로 구현해준다.

data class 구조 분해하기

data class Person(
    val name : String,
    val registered : Instant = Instant.now()
)

//다음 처럼 접근할 수 있다
fun show(person : Person){
    val name : person.component(1)
    val registered : person.component(2)
}

개인적으로 별로인듯 싶다

정적 메소드 구현하기

코틀린에는 정적 멤버(static member)가 없다.
그 대신 동반 객체(companion object)라는 특별한 요수를 사용해 같은 효과를 얻을 수 있다.

class Person{
    companion object{
        // LIKE static function
        fun create(name : String){
        }
    }
}

싱글톤 사용하기

어떤 클래스에서, 속한 객체를 단 하나만 만들어야 때가 있다. (싱글톤)
코틀린에서는 classobject로 바꾸기만 하면, 쉽게 싱글톤을 만들 수 있다.

object OnlyOnePerson{
    fun hello(){
    }
}

유틸리티 클래스 인스턴스화 방지하기

자바에서는 정적 메소드만 포함된 유틸리티 클래스를 생성한다.
그런 경우 보통, 유틸리티클래스의 인스턴스화를 방지한다.

코틀린은 이런 경우, 패키지 수준의 함수를 사용하면된다.

// Util.kt

package com.podo.dev.utill

fun toDateTimeStr(datetime : LocalDateTime) : String{
}
val dateTimeString = toDateTimeStr(LocalDateTime.now())

<br/>

2.3 원시 타입이 없음

  • 코틀린은 원시타입이 없다.
  • 따라서 박싱, 언박싱의 개념이 없으며, 관련 함수도 없고 단순해진다.
  • Integer 대신 Int를 사용한다.
  • Long은 L을 붙인다
  • Float은 F를 붙인다.
  • 0x를 앞에 붙여 16진수를 표현한다.
  • 0b를 앞에 붙여 2진수를 표현한다.

<br/>

2.4 컬렌션의 두 유형

코틀린의 컬렉션은 자바의 컬렉션을 사용하지만, 코틀린이 제공하는 기능이 추가되었다.

코틀린에서 가장 중요한점은

컬렉션에 불변과, 가변이라는 두가지 유형이 있다는 점이다.

다음 코드는 불변 list를 만든다

// listOf()는 패키지 수준의 함수이다.
val list = listOf(1,2,3)

불변성이 리스트에 연산을 수행 할 수 없다는 뜻은 아니다

val list1 = listOf(1,2,3) // list 생성 
val list2 = list1 + 4 // 새로운 리스트 
val list3 = list1 + list2 // 새로운 리스트!!

println(list1) // [1,2,3]
println(list2) // [1,2,3,4]
println(list3) // [1,2,3,1,2,3,4]

가변 컬렉션이 필요하면, 이를 명시해야한다.

val list1 = mutableListOf(1,2,3)
val list2 = list1.add(4)
val list3 = list1.addAll(list1)

println(list1) // [1,2,3,4,1,2,3,4]
println(list2) // true
println(list3) // true

하지만 가변 컬렉션도 + 연산을 사용하면 새로운 컬렉션을 만든다.

val list1 = mutableListOf(1,2,3) // list 생성 
val list2 = list1 + list1 // 새로운 리스트 

println(list1) // [1,2,3]
println(list2) // [1,2,3,1,2,3]

<br/>

2.5 패키지

코틀린의 패키지 사용방식은, 자바의 사용방시과 다르다

  • 코틀린의 패키지가, 저장된 디렉토리 구조와 일치할 필요가 없다 (path 일치 X)
  • 코틀린의 클래스는, 파일명과 일치할 필요가없다.
  • 코틀린에서의 패키지는 단지 식별자에 지나지 않는다.
  • 코틀린에는 하위 패키지라는 개념이 없다.
  • 자바와 코틀린을 혼합한다면, 자바 파일은 패키지 path에 일치하는 파일 경로에 넣어야한다. 이 경우 코틀린도 자바와 같은 방식을 택해야한다.
  • 하지만 아무래도, 코틀린 소스파일만 사용하는 경우라도, 패키지와 소스 경로를 일치시키면 찾기가 더쉬울것이다.

<br/>

2.6 가시성

코틀린의 가시성은 자바와 차이가 있다.
코틀린으 모듈이라는 단위의 가시성을 제공한다.
모듈은 한꺼번에 컴파일되는 묶음 말하는데, 이에 대한 가시성은 internal 접근자를 사용한다

  • private
  • public ( 아무 접근자가 없으면, public이 기본이다)
  • internal
  • protected

private 자바와 약간다르다.
Inner Class에서의 private을 외부 클래스에서 볼 수 없지만,
코틀린은 반대로, 외부클래스에서 private 접근된 멤버를 내부 클래스에서 볼 수없다.

코틀린은 패키지(package)내 가시성은 제거하ㅕㅆ따.

<br/>

2.7 함수

코틀린의 함수는 자바의 메소드와 동등하다.
하지만 함수형 프로그래밍이나, 수학의 함수와 코틀린의 함수는 의미가 다르다.

함수 선언하기

코틀린에서 함수를 선안할때는 fun이라는 키워드를 사용한다.

fun add(a : Int, b : Int): Int{
    return a + b
}

fun add(a : Int, b : Int): Int = a + b

fun add(a : Int, b : Int) = a + b

// 이건 틀렸다!!!!!!!!!!!
fun add(a: Int, b : Int) = {
     a + b
} 

로컬 함수 사용하기

코틀린은 함수 내부엣도 함수를 정의 할 수 있다.

fun hello(){
    val str = "hello"
    fun world(){
        return str + "world"
    }
}

word() 함수를 hello() 함수 밖에서 정의 할 수 없을것이다.
그것은 str 변수를 가두었기 때문이다. 이런 구조를 클로저(Closure)라 정의한다.
이런 특성을 원할 수 도 있고, 원하지 않을 수도 있다. (이 내용은 뒷장에 이어진다)

여기서 world()라는 hello() 바깥에서 쓸 가능성이 별로 없다.
그럴경우 함수 내부에 함수를 선언하는것이 더 나을 것이다.

함수 오버라이드 하기

코틀린에서는 함수를 오버라이딩하기 위해
override 키워드를 반드시 사용해야한다.

override fun eqauls() = ...

확장 함수 사용하기

확장함수 마치 클래스의 정의된 인스터스 함수인 것처럼 객체를 호출 할 수 있는 함수를 말한다.
코틀린에서는 확장 함수를 자주 사용한다

다음 예제를 보자.

// 확장함수
fun <T> List<T>.length() = this.size

val ints = listOf(1,2,3,4,5)
val listLength = ints.length()

람다 사용하기

자바와 마찬가지로 람다익명 함수
코틀린의 람다 구문은 자바 구문과 약간 다르다.

코틀린에서는 중괄호 사이에 람다가 위치한다.

fun triple(list : List<Int>): List<Int> = list.map({a -> a*3})

// map 코드에서 람다는 마지막 인자 일뿐만아니라, 유일한 인자이다, 따라서 괄호를 없앨 수 있다.
fun triple(list : List<Int>): List<Int> = list.map {a -> a*3}

// 람다의 파라미터 주변에는 괄호를 치지 않는다, 여기에 괄호를 치면 뜻이 완전히 달라지므로 괄호를 치면 안된다.

fun product(list : List<Int>): Int = list.map {a, b -> a * b}

// 괄호를 칠 경우, 컴파일 에러가 발생한다.
// fun product(list : List<Int>): Int = list.map {(a, b) -> a * b}

코틀린은 람다의 파라미터 타입을 추론한다.
하지만 코틀린은 타입 추론을 위해 최선을 다하지 않는다.
하지만 타입을 제대로 추론하는데 시간이 아주 오래 걸릴 경우, 타입을 코드 작성자에게 미룬다.

다음 예제 처럼 란다 구현을 여러 줄에 걸쳐 작성 할 수 있다.

fun List<Int>.product(): Int = this.fold(1) { a, b ->
    val result = a * b
    result
}

코틀린은 파라미터가 단 하나뿐인, 람다를 편하게 쓸 수 있는 간이 구문을 ㅔ공한다.
간이 구문에서 유일한 파라미터를 it으로 부른다.

fun tirple(list : List<Int>) : List<Int> = list.map { it * 3}

하지만 간이 구문이 항상 좋은건 아니다.
람다 안에 람다가 있다면 혼란을 가져온다. it이 가리키는 대상이 추측하기 어려워 질 수 있다.

앞서 배운 클로저를 람다에 적용 할 수도 있다.

val multiplier = 3
fun tirple(list : List<Int>) : List<Int> = list.map { it * multiplier}

하지만 일반적으로 클로저를 함수 인자로 넣을 수 있음을 인지하자.

fun tirple(list : List<Int>, mutlplier : Int) : List<Int> = list.map { it * multiplier}

<br/>

2.8 null

코틀린의 null 참조를 독특한 방식으로 다룬다.
null 참조는 컴퓨터 프로그램에서 버그를 가장 많이 발생하는 원인이다.

코틀린을 반드시 null 참조를 처리하도록 강제함으로써 이런 문제를 해결한다.

코틀린에서는 null이 될 수 있는 타입null이 될 수 없는 타입을 구분한다.

예를 들어,

Int는 null이 될 수 없는 타입이지만,
Int?는 null이 될 수 있는 타입니다.

즉 모든 타입에는 null 이 될 수 있는/없는 타입으로 정의 할 수 있다.

여기서 흥미로운점은 null이 될 수 없는 타입이, null 이 될수 있는 타입의 자식탑입이라는 것이다

// 가능!
val x :Int = 3
val y : Int? = x

// 불가능
val x : Int? = 3
val y : Int = x 

Null이 될 수 있는 타입 다루기

Null이 될 수 있는 타입을 다루면 NPE 예외가 발생하지 않는다.

val s : String? = getNullableStr()
val length = s.length

위의 코드는 NPE가 발생할 수 있어 . 연산자를 사용할 수 없다.
그대신 다음과 같이 작성해야한다.

val s : String? = getNullableStr()
val length = if(s != null) s.length  else null

하지만 이를 ?를 사용하여 짧게 표현 할 수있다
코틀린의 length를 Int?로 추론함을 유의하자.

val s : String? = getNullableStr()
val length = s?.length

이 구문은 연쇄 호출을 할 때 더 강력하다.

val hello : String? = a?.bb?.ccc?.dddd?.helloValue

엘비스 연산자와 기본값

Null 이 아닌 기본값을 사용하고 싶을 때가 있다.
이는 자바의 Optional.getOrElse()에 해당한다.
이때 엘비스 연산자를 사용하면 된다.

val hello: String = a?.bb?.ccc?.dddd? ?: "UNKNOWN"

<br/>

2.9 프로그램 흐름과 제어구조

제어구조는 프로그램의 흐름을 제어하는 요소이다.
제어구조는 버그를 발생시키는 주요원인이며, 제어구조를 피함으로써 안전하게 만들수 있음을 의미한다.
코틀린은 먼저 제어흐름이라는 개념을 무시할 수 있따. 식과 함수로 바쑬수있다.
코틀린은 자바와 비슷한 제어 구조를 제공하고, 그 제어 구조를 대신하고 싶을 때 함수를 제공한다.

조건 선택 사용하기

자바의 다음 분기를

int a; 
int b;

String s
if (a > b){
    s = "Hello World";
} else{
    s = "Hello Hello"
}

다음과 같이 사용할 수 있다

val s = if (a> b) "Hello World" else "Hello Hello"

다중 조건 사용하기

String country = "A";
String capital = "";
switch(country){
    case "A" :
        captial = "a";
        break;
    case "B" :
        captial = "b";
        break;
}

코틀린은 when 구문을 사용한다.
when 구문은 제어 흐림이 아닌 하나의 식이다.

val capital = when(country){
    "A" -> "a"
    "B" -> "b"
    else -> "don't know"
    
}

코틀린은 when 구문에서, 빠진 케이스를 허용하지 않는 점을 주의하자.
enum 이라면, 하나의 케이스가 빠졌다면 컴파일에러가 발생하며,
위와 같은 경우는 else가 필수적으로 있어야한다.

when은 또한 다음 구문처럼 사용 할 수 도 있다.

val country = ...

val captial = when{
    country == "A" -> "a"
    country == "B" -> "b"
    else -> "don't know"
}

루프 사용하기

자바에서는 여러가지 루프가 있다

  • 인덱스를 사용하는 루프
  • 컬렉션에 안에있는 루프, 이터레이션
  • 조건이 성립하는 루프

코틀린에서도 루프에 인덱스를 사용할 수 있다.
하지만 실제로는 컬렉션 안에 루프를 사용한다.

// 다음 코드는
for(i in 0 until 10 step 2){
    println(i)
}

//실제로 범위를 만들어서 동작한다.
val range = 0 until 10 step 2
for(i in range) println(i)

범위 연사자인 .. 키워드도 있다.
until과 같지만, 마지막 값도 포함한다.

until과 반대로 downTo 도 존재한다. 줄어드는 케이스에서 사용하자.

<br/>

2.10 비검사 예외

자바와 달리 코틀린은 Checked Exception 이 존재하지 않는다.
모든예외는 Unchecked Exception 이다.

또한 코틀린은 자바의 가장 큰차이는 try/catch/finally 구조가 값을 돌려주는 식이라는 점이다.

val num : Int = try{
    args[0].toInt()
} catch{
    0
} finally{
    // 항상 실행됨
}

catch 문에서 return 하더라도, finally 구문을 진행한다.

<br/>

2.11 사용한 자원 자동으로 닫기

자바에서 자원을 사용하는 try로 할 수 있는 것처럼,
코틀린도 자원을 자동으로 닫을 수 있다.

Closable, AutoClasble 인터페이스를 구현함으로써 자동으로 닫을 수 있다.
또 주목할점은, 코틀린은 use라는 함수를 사용한다는 점이다.

File("myFile.txt").inputStream()
    .use{
        it.bufferReader().lineSequence().forEach(::println)
    }

위코드는 자원을 자동으로 닫는다.
use 밖에서는, 스트림이 닫히 때문에 자원을 더이상 사용할 수 없다.
코틀린은 지연 계산을 사용한다. lineSequence는 지연 컬렉션을 반환하는데
실제 그 라인을 사용할때 실제 줄을 읽어온다는것이다.
println()을 매줄 호출하는데, 매줄 호출할떄 실제줄을 읽어온다는 뜻이다.

<br/>

2.12 스마트 캐스트

코드를 작성하다보면, 강제형변환을 해줘야하는 경우가 있다.
참조 대상 객체가 지정한 형태로 변환이 불가능하면 ClassCastException이 발생한다.
따라서 instanceOf로 타입 변환이 가능한지 검증하였다.

이는 코드를 매우 지저분하게하는데, 코틀린은 스마트 캐스트라는 특별한 기능을 제공한다.

val length : Int = if(payload is String ) payload.length else -1

첫번째 분기문에서, payload가 String이라는 점을 알고, 자동으로 형변환 해주었다.

when 구조에서도 스마트 캐스팅이 가능한다.

val result : Int = when(payload){
    is String -> payload.length
    is Int -> payload
    else -> -1
}

반대로 as 연산자를 이용하여 안전하지 않은 방식으로 타입전환을 할 수 있다.

val result : String = payload as String
val result : String? = payload as? String // nullable

<br/>

2.13 동등성과 동일성

자바에서는 동등성과 동일성을 혼동하는데
원시 타입, 문자열, Integer를 자바에서 처리하는 방식때문에 더욱더 혼란스럽다.

코틀린은 더 단순하다.
참조 동등성===, !==로 검증한다.
동등성, 즉 구조 동등성(equals)==, !=로 검증한다.

<br/>

2.14 문자열 인터폴레이션

자바에서 문자열과 값을 혼용하기 위해서 String.format()을 사용했지만,
코틀린에서는 다음과 같이 사용 할 수 있다.

val world = "Korea"
println("Hello $world")
println("Hello ${world.toUpperCase()}")

<br/>

2.15 여러줄 문자열

코틀린에서는 """를 사용해 여러줄 문자열도 쉽게 사용할 수있다.

println("""Hello
            |World""")

<br/>

2.16 변성/공변성

코틀린의 변성, 공변성의 얘기를 다룬다.
제네릭에서 extends, super와 관련된 내용이다.
추후 뒤의 챕터로 내용을 정리한다.

책에서는 자바는 안된다고하는데, 가능하다.
extends, super 관련 내용이다.

CommentCount 0
이전 댓글 보기
등록
TOP