본문 바로가기

책/오준석의 안드로이드 생존코딩 코틀린편

4. 코틀린

REPL 사용하기

Tools -> Kotlin -> Kotlin REPL(Read-eval-print loop) 을 선택

윈도우 에서는 Ctrl + Enter를 사용하여 실행하고, 맥에서는 Command + Enter를 사용하여 실행한다.

스크래치 사용하기

File -> New -> Scratch File 을 선택 후 Kotlin을 클릭

REPL처럼 한 줄 단위로 코드를 실행하는 것과는 달리 *.kts 라는 파일에 저장하여 파일 단위로 실행 할 수 있다.

문법

기본적인 문법은 다른 프로그래밍 언어와 비슷하기 때문에 특화적인 부분만 소개하기로 한다.

when

// java의 switch문 처럼 사용 가능
val x = 1
when(x){
    1 -> println("x == 1") // 값 하나
    2, 3 -> println("x == 2 or x == 3") // 여러 값은 콤마로
    in 4..7 -> println("4 부터 7사이") // in 연산자로 범위 지정
    !in 8..10 -> println("8부터 10사이가 아님")
    else -> {
        println("x는 1이나 2가 아님")
    }
}

// 식 처럼 사용 가능
val number = 1
var numStr = when( number%2 ) {
    0 -> "짝"
    else -> "홀"
}
println(numStr)

// 함수의 반환 값으로도 사용 가능
fun isEven(num: Int) = when( num%2 ) {
    0 -> "짝"
    else -> "홀"
}
println(isEven(1))

for

for(i in 1..3) {
    print(" ${i}")  // 1 2 3
}

for(i in 1..10 step 2) {
    print(" ${i}")  // 1 3 5 7 9
}


for(i in 10 downTo 0 step 2) {
        print(" ${i}")  // 10 8 6 4 2 0

}

클래스 선언 및 사용

//클래스 선언
class Person {

}
// 인스턴스 생성
val person = Person()

///////////////////////////////////////////////////////////
class Person(var name: String) {

}
class Person {
    constructor(name: String) {
    
    }
}
class Person(name: String) {
    init {
        print(name)
    }
}
///////////////////////////////////////////////////////////
// 프로퍼티
// 클래스 선언
class Person(var name: String) {

}

// 인스턴스 생성
var person = Person("멋쟁이")
person.name = "키다리"   // 읽기
println(person.name)     // 쓰기


접근제한자

  • public (생략가능) : 전체공개. 아무것도 안 쓰면 기본적으로 public
  • private : 현재 파일 내부에서만 사용 가능
  • internal : 같은 모듈 내에서만 사용 가능
  • protected : 상속받은 클래스에서 사용가능
class A {
    val a = 1 // public
    private val b = 2
    protected val c = 3
    internal val d = 4
}

상속

코틀린에서 기본적으로 상속은 금지되어 있다. 상속이 가능하게 하려면 open 키워드를 클래스 선언 앞에 추가해야 한다.

open class Animal {

}

class Dog: Animal() {

}

///////////////////////////////////////////////////////////////////
open class Animal(val name: String) {

}
class Dog(name: String): Animal(name) {

}

내부 클래스

class OuterClass {
    var a = 10
    
    // 내부 클래스
    inner class OuterClass2 {
        fun something() {
            a = 20 // 접근가능
        }
    }
}

추상 클래스

abstract class A {
    abstract fun func() 
    
    fun func2() {
    
    }
}

class B: A() {
    override fun func() {
        println("hello")
    }
}

val a = A()  // 에러
val b = B()  // OK

인터페이스

// 인터페이스 선언
interface Runnable {
    // 인터페이스에서는 abstract 키워드를 생략 할 수 있다.
    fun run()
    
    // 구현된 메서드도 포함할 수 있다. 자바 8의 default 와 대응
    fun fastRun() = println("빨리 달린다.")
}
// 인터페이스 구현
class Human : Runnable {
    override fun run() {
        println("달린다.")
    }
}

//상속과 인터페이스를 함께 구현
open class Animal {

}
interface Eaterable {
    fun eat()
}

// 상속은 하나의 클래스만 상속하지만 인터페이스는 콤마로 구분하여 
// 여러 인터페이스를 구현할 수 있다.
class Dog: Animal(), Runable, Eatable {
    override fun eat(){
        println("먹는다")
    }
    override fun run(){
        println("달린다")
    }
}

var dog = Dog()
dog.run()
dog.eat()

null 가능성

val a: String          // 에러 : 초기화를 반드시 해야 함

val a: String = null   // 에러 : 코틀린은 기본적으로 null을 허용하지 않음

val a: String? = null  // OK

lateinit 키워드로 늦은 초기화

lateinit 는 다음 조건에서만 사용가능

  • var 변수에서만 사용
  • null 값으로 초기화 할 수 없음
  • 초기화 전에는 변수를 사용할 수 없음
  • Int, Long, Double, Float에서는 사용할 수 없음
lateinit var a: String // OK

a = "hello"
println(a) // hello

lazy로 늦은 초기화

lazy는 다음 조건에서만 사용가능

  • val에서만 사용 가능
var str: String by lazy {
    println("초기화")
    "hello"
}
println(str) // 초기화; hello
println(str) // hello

null 값이 아님을 보증( !! )

val name: String? = "키다리"

val name2: String = name   // 에러
val name3: String? = name  // OK

//null 값이 아님을 보증
var name4: String = name!! // OK

안전한 호출 ( ?. )

안전한 호출을 사용하면 복잡한 if 문을 한 줄로 줄일 수 있다.

val str: String? = null

var upperCase = if( str != null) str else null // null
upperCase = str?.toUpperCase                   // null

엘비스 연산자 ( ?: )

안전한 호출 시 null 이 아닌 기본값을 반환하고 싶을 때 사용한다.

val str: String? = null

var upperCase = if( str != null) str else null                  // null
upperCase = str?.toUpperCase ?: "초기화하시오"                  // 초기화하시오

컬렉션

// 리스트 : 배열처럼 같은 자료형의 데이터들을 순서대로 가지고 있는 자료구조

var foods:  List<String> = listOf("라면", "갈비", "밥")
// 형추론으로 자료형 생략
var foods = listOf("라면", "갈비", "밥")

//요소를 변경할 경우는 mutableListOf 메서드를 사용
var foods = mutableListOf("라면", "갈비", "밥")
foods.add("초밥")
foods.removeAt(0)
foods[1] = "부대찌개" // foods.set(1, "부대찌개") 1번째 아이템을 부대찌개로 변경

println(foods)       // [갈비, 부대찌개, 초밥]
println(foods[0])    // 갈비
println(foods[1])    // 부대찌개
println(foods[2])    // 초밥


//----------------------------------------------------------------
// 맵: 키와 값 쌍으로 이루어진 키가 중복될 수 없는 자료구조

// 읽기 전용 맵
val map = mapOf("a" to 1, "b" to 2, "c" to 3)

// 변경 가능한 맵
val citiesMap = mutableMapOf("한국" to "서울", "일본" to "동경", "중국" to "북경")
citiesMap["한국"] = "서울특별시"  // 요소에 덮어쓰기
citiesMap["미국"] = "워싱턴"      // 추가

// 맵의 키와 값을 출력
for ((k, v) in map){
    println("$k -> $v")
}

//----------------------------------------------------------------
// 셋(집합): 중복되지 않은 요소들고 구성된 자료구조

// 읽기 전용 셋
var citySet = setOf("서울", "수원", "부산")

// 수정 가능한 셋
var citySet2 = mutableSetOf("서울", "수원", "부산")
citySet2.add("안양")      // [서울, 수원, 부산, 안양]
citySet2.remove("수원")   // [서울, 산, 안양]

// 셋의 크기
println(citySet2.size)              // 3
// '서울'이 포함되어 있는지 
println(citySet2.contains("서울"))  // true

람다식

람다 식은 익명 클래스나 익명 함수를 간결하게 표현 하게 해준다.

fun add(x: Int, y: Int): Int {
    return x + y
}

fun add(x: Int, y: Int) = x + y

// { 인수1: 타입1, 인수2: 타입2 -> 본문 }
var add = { x: Int, y: Int -> x + y }
println(add(2, 5) // 7

SAM(Single Abstract Method) 변환

안드로이드에서 onClick() 추상 메서드 하나만 가지고 있는 View.OnClickListener 인터페이스를 익명클래스로 구현한 예이다.

button.setOnClickListener(object: View.OnClickListener { 
    override fun onClick(v: View?) {
        // 클릭 시 처리
    }
}

위의 코드는 람다 식을 이용하여 간단하게 변경할 수 있다.

button.setOnClickListener({ v: View? -> 
    // 클릭 시 처리
})

메서드 호출 시 맨 뒤에 전달되는 인수가 람다식인 경우 람다식을 괄호 밖으로 뺄수 있다.

button.setOnClickListener() { v: View? -> 
    // 클릭 시 처리
}

메서드 호출 시 전달되는 인수가 하나인 람다식인 경우 메서드의 괄호를 생략할 수 있다.

button.setOnClickListener { v: View? -> 
    // 클릭 시 처리
}

컴파일러가 자료형을 추론하는 경우 자료형을 생략할 수 있다.

button.setOnClickListener { v -> 
    // 클릭 시 처리
}

위에 인수 v를 사용하지 않고 코드를 작성할 수 있다면 다음과 같이 변경이 가능하다.

button.setOnClickListener { _ -> 
    // 클릭 시 처리
}

람다식에서 인수가 하나인 경우에는 아예 생략하고 블록 내에서 인수를 it로 접근할 수 있다.

button.setOnClickListener { 
    // 클릭 시 처리
    it.visibility = View.GONE
}

기타기능

확장함수

  • 기존 클래스에 상속 없이 함수를 추가가 가능
  • 확장함수 내부에서는 이 객체를 this로 접근할 수 있고 이러한 객체를 리시버 객체라고 함
fun Int.isEven() = this % 2 == 0

val a = 5
val b = 6

println(a.isEven()) // false
println(b.isEven()) // true

형변환

// 숫자형 끼리는 to자료형() 메서드 사용
val a = 10L
val b = 20

val c = a.toInt() // Long -> Int
val d = b.toDouble() // Int -> Double
val e = a.toString() // Long -> String


// 숫자 형태의 문자열을 숫자로 바꿀때는 자바 메서드 사용
val intStr = "10"
var realInt = Integer.parseInt(intStr)


// 일반 클래스간의 형변환 시 as 키워드 사용
open class Animal
class Dog: Animal()
val dog = Dog()

val animal = dog as Animal

형 체크

is 키워드를 사용하여 형 체크 자바의 instanceOf와 대응

val str = "hello"

if (str is String) {
  println(str.toUpperCase())
}

고차함수

함수의 인수로 함수를 전달하거나 함수를 반환하는 함수를 고차함수라고 한다.

// 인수: 숫자, 숫자, 하나의 숫자를 인수로 하는 반환값이 없는 함수
fun add(x: Int, y: Int, callback: (sum: Int) -> Unit) {
    callback(x + y)
}

// 함수는 { } 로 감싸고 내부에서는 반환값을 it로 접근할 수 있음
add(5, 3, { println(it) })    // 8

동반객체(companion object)

자바의 static과 같은 정적인 메서드를 만들 수 있는 키워드 대신 companion object(동반객체) 키워드를 제공한다.

class Fragment {
    companion object {
        fun newInstance() : Fragment {
            println("생성됨")
        }
    }
}
var wfragment = Fragment.newInstance()

let() 함수

  • 블록에 자기 자신을 인수로 전달하고 수행된 결과를 반환한다.
  • 안전한 호출연산자( ?. ) 와 함께 사용하면  null 값이 아닐 때만 실행하는 코드를 다음과 같이 나타낼 수 있다.

이 코드는 str이 null이 아닐 때만 정수로 변경하여 출력하는 코드이다. 

// fun <T, R> T.let(block: (T) -> R): R
val result = str?.let {    // Int
    Integer.parseInt(it)
}

with() 함수

  • 인수로 객체를 받고 블록에 리시버 객체로 전달한다. 그리고 수행된 결과를 반환한다.
  • 리시버 객체로 전달된 객체는 this로 접근 가능하다.
  • with 에 인수는 null 이 아닌경우에만 사용해야 한다.
// fun <T, R> with(receiver: T, block T.() -> R): R

// this(리시버객체)는 생략이 가능하다.
// str 은 null이 아니어야 한다.
with (str) {
    println(toUpperCase())
}

apply() 함수

  • 블록에 객체 자신이 리시버 객체로 전달되고 이 객체가 반환됨
  • 객체의 상태를 변화시키고 그 객체를 다시 반환할 때 주로 사용한다.
// fun <T> T.apply(block: T.() -> Unit): T
val result = car?.apply {
    car.setColor(Color.RED)
    car.setPrice(1000)
}

run() 함수

  • 익명함수처럼 사용할 때는 블록의 결과를 반환한다.
  • 복잡한 계산에 임시변수가 많이 필요할때 유용하다.
// fun <R> run(block: () -> R): R
val avg = run {
    val korean = 100
    val english = 80
    val math = 50
    
    (korean + english + math) // 3.0
}

 

  • 객체에서 호출하는 방법은 객체를 블록의 리시버 객체로 전달하고 블록의 결과를 반환한다.
  • 안전한 호출을 할 수 있어서 with() 함수 보다는 더 유용하다.
// fun <T, R> run(block: T.() -> R): R
str?.run {
    println(toUpperCase())
}

 

' > 오준석의 안드로이드 생존코딩 코틀린편' 카테고리의 다른 글

8. 수평 측정기  (0) 2020.06.16
7. 나만의 웹 브라우저  (0) 2020.06.15
6. 스톱워치  (0) 2020.06.11
5. 비만도 계산기  (0) 2020.06.09
3. 첫 번째 앱 만들기  (0) 2020.06.07