Kotlin 백과사전 1탄

 

참고하면 도움될 사이트

코틀린언어 기본개념 총정리 (스압주의)

철학

  • 자료형 명시를 중요시하여 사전에 버그를 막고자 함

키워드, 타입

val // 변경 불가능(Immutable)
var // 변경 가능(Mutable)
var function: () -> Unit    // 인자로 아무것도 받지 않으며 return 하는것도 아무것도 없다
  • Null 값 아님을 보장 !!
    • 변수뒤에 !! 을 추가하면 null값이 아님을 보증
val name: String? = "과일"
val name1: String = name!!   // !!없이 그냥 붙이면 Nullable 변수를 String 타입에 넣으려고
                             // 하는것이므로 에러남
  • Nullable한 호출을 가능하게 해주는 ?.
var empty:String? = null
println("empty: ${empty?.length}") // 이렇게 구현해서 Null이 아닌것을
  • null일 경우 따로 처리해주고 싶을 때 엘비스 연산자 ?:
val str: String? = null
var upperCase = if (str != null) str else null
upperCase = str?.toUpperCase() ?: "str 변수는 초기화를 해야만 upperCase() 가 가능합니다."
println(upperCase)

변수와 함수

변수

  • 키워드 val, var
  • 전역변수로 사용할 땐 반드시 값을 초기화 해줘야함
class MainActivity: AppCompatActivity(){
    val a = 1
    var b = 2

    override fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
  • 지역변수로 사용할 땐 선언만 해주고 나중에 초기화 해줘도 괜찮음
class MainActivity: AppCompatActivity(){

    override fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val a:Int       // 값 초기화를 안해줘서 컴파일러가 타입 추론이 불가능하므로 타입 지정해줘야함
        var b:Int
    }
}
Int     
Int?        // Null을 허용함

함수

  • 키워드 fun
    fun main(): Unit{     // main 함수. Unit은 void와 같은 type이며 생략 가능
                        // 즉 : 뒤의 type은 함수의 반환타입!
      println("Hello world!")
    }
    fun sum(a: Int, b: Int): Int{
      return a + b
    }
    

Single Expression (하나의 표현식)

  • 한줄짜리 함수의 가독성을 높이기 위함
    fun sum(a: Int, b:Int): Int = a + b  
    fun sum(a: Int, b:Int) = a + b  // 이 상황에선 return 값을 Int로 추론할 수 있어서 명시 안해줘도 됌
                                  // : 기호는 타입 명시를 위해 필요한데 타입 명시를 할 필요가 없
                                  // : Int가 생략
    

when

  • if 문이 연달아 나오는걸 간결하게 바꾸기 위함
fun describe(obj: Any): String = 
    when (obj){
        1 -> "one"
        "Hello" -> "Greeting"
        is Long -> "Long"
        !is String -> "Not a string"
        else -> "Unknown"
    }
  • return 되는 값이 항상 하나이므로 = 연산자를 통해 single expression 문법 적용

리스트

  • 범위 표현방법

if (-1 !in 0..list.lastIndex){
    println("-1은 범위 안에 없다")
}

if (list.size !in list.indices){
    println("리스트 인덱스를 넘어갔다")
}

for (x in 1..5 step 2){
    println(x)
}

for (x in 9 downTo 0 step 3){
    println(x)
}
  • 링크를 통한 리스트 활용
val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
    .filter{it.startsWith("a")}     // 각 item에 대해 a로 시작하는것만 선택
    .sortedBy{it}                   // 선택된 avocado, apple을 글자순으로 정렬
    .map{it.toUpperCase()}          // 대문자로 변경
    .forEach{println(it)}           // APPLE AVOCADO가 차례로 출력됨

Array(배열)

val array: Array<int> = arrayOf(1, 2, 3)    // arrayOf() 함수로 선언
array[1] = 4
print(array.set(2, 5))                      
print(array.get(1))                         // set, get 함수로 접근 가능

Generic(제네릭)

  • 사전적 의미는 “일반적인, 포괄적인”
  • 아래는 간단 예시
    fun main(){
      val list1 = ArrayList<Int>()
      val list2 = ArrayList<String>()
    }
    
  • 좀더 구체적으로 말하면 클래스나 메서드, 프로퍼티를 정의할 때 데이터 타입을 변수로 지정하고, 사용할 때 그 타입을 정해줄 수 있는것
    • 변수로 지정한 데이터 타입을 Type parameter(타입 파라미터) 라고 함
    • 사용할 때 타입 명시해주는데, 이때 명시해 주는 타입을 Type argument(타입 아규먼트) 라고 함

      class Box<T>
      class Apple
      class Banana
      
      fun main(){
          val appleBox = Box<Apple>()
          val bananaBox = Box<Banana>()
      }
      

Scope Function(범위 지정 함수)

Kotlin 공식 문서
잘 정리된 블로그

Kotlin이 문맥(context)안에서 코드가 실행되게 해주며, 이를 다시 말하면 객체 하나에 대해 코드 블럭을 선언해서 그 내부에 코드를 작성하는것을 의미한다. 이러한 작업은 this 혹은 it 키워드를 활용하여 가능하며 이를 통해 코드가 직관적으로 보이게 한다.

  • 적용 코드 예시

      Person("Alice", 20, "Amsterdam").let {
          println(it)     // 초기화된 Person 객체를 가리킴
          it.moveTo("London")
          it.incrementAge()
          println(it)
      }
    
  • 일반적인 코드 예시

      val alice = Person("Alice", 20, "Amsterdam")
      println(alice)
      alice.moveTo("London")
      alice.incrementAge()
      println(alice)
    

코드블록과 불필요한 naming 작업 제거를 통해 훨씬 직관적인 코드 작성이 가능하다.
이제 비슷한 여러 scope function을 알아볼 것인데, 서로 비슷하기 때문에 다음의 2가지 포인트를 잘 기억하면서 각 scope function을 이해해야 올바르게 활용할 수 있다.

  1. context object(this, it)을 참조하는 방법
    • this: 생략 가능하기 때문에 무분별하게 사용하면 멤버 변수에 접근하는건지 다른 변수에 접근하는건지 헷갈릴 수 있다. 따라서 객체 멤버변수를 초기화하거나, 객체의 멤버함수를 호출하는 용도로 사용하는것을 권장한다.

         val adam = Person("Adam").apply { // apply는 this를 참조 키워드로 사용함
             age = 20                       // same as this.age = 20 or adam.age = 20
             city = "London"
         }
         println(adam)
      
  • 사용되는 scope function: run, with, apply

  • it: 생략은 불가능하지만, it 대신 다른 이름을 지정해 줄 수 있어서 코드 활용성을 높일 수 있다.

       fun getRandomInt(): Int {
           return Random.nextInt(100).also {
               writeToLog("getRandomInt() generated value $it")
           }
       }
    
       val i = getRandomInt()
       println(i)
    
       INFO: getRandomInt() generated value 66
       66
    
       fun getRandomInt(): Int {
           return Random.nextInt(100).also { value ->      // it 대신 value가 객체를 가리키는 변수가 됨
               writeToLog("getRandomInt() generated value $value")
           }
       }
    
       val i = getRandomInt()
       println(i)
    
       INFO: getRandomInt() generated value 80
       80
    
- 사용되는 scope function: `let`{:.success}, `also`{:.success}
  1. 반환 타입
  • 동작 원리
    • block이 lambda 문법으로 선언한 람다 변수(이렇게 말해도 될지는 잘 모르겠음)
      • 따라서 람다 변수의 Input type, return type을 지정해줘야함
        • (Input type) -> return type
        • 위 그림에선 Input type이 확장함수
          • 확장함수여서 입력인자의 타입을 추가로 받을 수 있도록 ( )가 존재하는것으로 생각됨
    • Single Expression 문법으로 함수 정의를 { } 대신 = 로 함
    • 아직 확장함수 타입과 람다가 어떻게 엮이는지 잘 모르겠음...

  • with, apply, run, also, let 함수
    • 참고하면 좋을 사이트

    • apply
      • 인스턴스 생성 후 초기화 과정 수행할 때 많이 쓰임
      • 스코프 내에서 참조연산자 사용하지 않아도 됨
      • 객체 자신이 리시버 객체로 전달되며(with는 인자가 필요하고 apply는 자기 자신!!) 이 객체가 반환됨
      • 객체의 상태를 변화시키고 변화시킨 객체를 다시 반환할 때 주로 사용

        class Book(var name: String, var price: Int){
            fun discount(){
                price -= 2000
            }
        }
        
        fun main(){
        
            // apply 적용 안할때
            var a = Book("Kotlin basic", 10000)
            a.name = "Extra " + a.name
            a.discount()
            println("${a.name}, ${a.price}")
        
        
            // apply 적용할 때
            var b = Book("Java basic", 10000).apply{
                name = "Extra " + name
                discount()
            }
            println("${b.name}, ${b.price}")
        }
        

    • run
      • apply와 마찬가지로 참조연산자를 사용하지 않음
      • apply와 달리 객체를 반환하는것이 아니라 마지막 구문 반환함

        var b = a.run{      // b 변수에는 a 인스턴스의 name이 할당됨
            println(price)
            name
        }
        
      • run은 주로 아래와 같이 이미 만들어진 인스턴스의 함수나 속성을 scope 내에서 사용해야 할 때 씀

        a.run{
            println(name)
        }
        
      • 메인함수의 스코프에서 인스턴스의 속성, 함수에 접근하는것 보다 가독성을 올릴 수 있다!
    • with
      • run과 동일한 기능을 가지지만 인스턴스를 참조연산자 대신 인자(파라미터)로 받는다는 차이만 존재

        a.run {...}     // a를 참조연산자로 사용
        with(a) {...}   // a를 인자로 받음
        
      • 인자로 객체를 전달받으며 이 객체는 블록 내에 Receiver 객체 형태로 전달됨
      • this로 접근 가능하며 생략가능. ?.을 이용한 안전한호출 불가능

        var strWorld = "world"
        with(strWorld){
            println(this.toUpperCase())
            println(toUpperCase())
        }
        
    • also (apply와 같은 역할), let (run과 같은 역할)
      • apply, run은 scope 내에서 참조연산자 없이 인스턴스의 변수, 함수를 사용할 수 있었다면 also, let은 인자로 인스턴스를 넘긴것처럼 it을 통해서 인스턴스의 변수 함수에 접근함
      • 대체 왜 이렇게 귀찮게 할까?
        • 같은 이름의 변수나 함수가 scope 바깥에 존재할 경우 혼란 방지를 위해서임!

            var price = 5000
          
            var a = Book("kotlin basic", 10000)
          
            a.run{
                println(price)      // 5000원이 출력됨. 인스턴스의 속성보다 main의 속성을 우선시하기 때문
            }
          
            a.let{
                println(it.price)   // 10000 출력
            }
          

receiver(수신객체)

  • 확장함수 개념 선행 필요

Extension Function(확장함수)

아래 람다식 챕터에서 더 자세히 설명함

  • 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에서 선언된 함수
    • 메소드 개념은 함수에 포함되어 있으며 클래스, 구조체 등에 속하는 함수일 경우 메소드라고 함
      fun String.lastChar(): Char = this[this.length-1]
      // String 클래스의 확장함수를 정의한 것
      // String 클래스에 lastChar() 이라는 메소드는 원래 없음
      // String 클래스를 통해 문자열의 마지막 문자 출력 해주는 메소드 정의함
      // 여기서 this는 String 클래스의 "객체"
      // 선언할 땐 fun 키워드와 함수 이름 사이에 확장할 클래스의 이름과 점을 붙임
      

클래스

필드, 프로퍼티

  • Java
    • 필드: 클래스 내의 멤버변수
    • 프로퍼티: 필드 + get함수, set함수
  • Kotlin
    • 코틀린에서는 필드 = 프로퍼티
      • 클래스 내의 멤버변수 선언 키워드에 따라 set, get이 자동으로 생성되기 때문
    • 코틀린의 프로퍼티는 생성자에 val, var 키워드의 유무에 따라 결정된다

      class Person(val name: String, var isMarried: Boolean)
      
      • name, isMarried가 Person 클래스의 프로퍼티가 된다
      class Person(name: String, isMarried: Boolean)
      
      • name, isMarried 파라미터는 단지 값만 가지고 있을 뿐 프로퍼티가 되지 않는다
        • 이걸 C++에 대입해서 생각해보면, 생성자를 통해 값을 받기는 하지만 멤버변수에 값 대입을 하지는 않은것으로 생각하면 된다
      class(Person val name: String, var isMarried: Boolean) // 이 코드를 풀어서 쓰면
      
      class Person {      
      
      val name: Int
          get() {
              return this.age
          }
      
      var isMarried: Boolean
          get() {
              return this.isMarried
          }
          set(isMarried: Boolean) {
              this.isMarried = isMarried
          }
      }
      
      // 실제로 사용할 땐
      fun main(){
          val p0 = Person("Hojun", 16)
              
          println(p0.name)        // 이게 getName() 위 name 뒤에있는 get함수 호출하는 것임
      }
      
      • val 키워드는 get만 생성되고, var 키워드는 set, get 모두 생성된다

생성자

초기화

  • 코틀린의 클래스는 하나의 Primary constructor와 다수의 Secondary constructor를 가질 수 있다
class Person constructor(val name: String) { }
class Person(val name: String) { }      // primary constructor가 어노테이션이나 접근 제한자(public, private)
                                    // 를 가지고 있지 않다면 constructor 키워드 생략 가능
                                    // 조금 어색하겠지만 클래스명 이름 옆에 있는 괄호가 주 생성자임

// 코틀린에서 주 생성자에는 아무런 코드를 넣을 수 없으므로 만약 초기화를 위해 코드가 필요하다면 init 블록을 사용한다
// 주로 주 생성자내의 val, var을 통해 초기화를 진행하지만 그렇지 않을 경우 init 블록을 사용해서 초기화를 진행하는 방식이다.
class Person(name:String){      
    init{
        if (name.isEmpty()){
            println("이름이 비었습니다")
            this.name = name        // init블록에서 property 접근하려면 this 키워드 사용
        }
    }
}

// 이렇게 선언해주는것이 primary constructor을 통해 property 선언, 초기화 동시에 하는것
class Person(val firstName: String, val lastName: String, var age: Int) { /*...*/ }


class Person(val name: String) {        // primary constructor가 선언되어있음
                                        // 기본적으로 public이며 다른 접근제한자를 두고싶은 경우
                                        // constructor 키워드 앞에 붙여주면 됨
                                        // 또한 this() 생성자를 이용해 직간접적으로
                                        // primary constructor에 위임해야 함

var age: Int = 26
constructor(name: String, age: Int) : this(name) {
    // 보조생성자는 val, var 선언 안하는것 주목. 단지 값을 전달하는 파라미터 역할을 하는 것
    // 위 코드를 더 해석하자면, 보조 생성자가 받아온 name값을 주 생성자를 통해 초기화 시긴다는 것이다
        this.age = age
    }
}
class E {       // 주 생성자를 생략한 경우엔 중괄호 안에있는 constructor가 주 생성자가 되는 것

    var name: String
    var age: Int = 1
    var height: Int = 2

    init {
        println("call Init Block!")
    }

    constructor(name: String) {     // 여기선 이게 주 생성자임
                                    // 클래스 이름 E 옆에 주 생성자가 없기 때문
        this.name = name
        println("call Name Constructor!")
    }

    constructor(name: String, age: Int) : this(name) {      // 여기부턴 보조 생성자 이므로
                                                            // 주 생성자에게 생성을 위임해야함
        this.age = age
        println("call Name, Age Constructor!")
    }

    constructor(name: String, age: Int, height: Int) : this(name, age) {
        this.height = height
        println("call Name, Age, Height Constructor!")
    }
}
  • 보조 생성자는 클래스를 사용하는 사람에게 인스턴스를 다양하게 초기화 시킬 수 있도록 도와주는 역할

상속

  • 상속이 필요한 경우는 2가지
    1. 기존 클래스의 기능을 기본으로 몇개 더 붙인 클래스들이 필요할 때

    2. 클래스간의 공통된 코드들이 있을 때 코드 관리의 용이함을 위해서

  • 지켜야 하는 규칙 2가지

    1. 수퍼 클래스에 존재하는 속성과 같은 이름의 속성으 가질 수 없음
    2. 서브 클래스가 생성될 때 반드시 수퍼클래스의 생성자까지 호출되어야 함
      open class Animal(var name:String, var age:Int, var type:String){
          fun introduce(){
              println("I'm ${name}, ${type} and ${age} years old")
          }
      }
    
      class Dog (name:String, age:Int) : Animal(name, age, "개"){
            
      }
    
    • 선언한 클래스 이름 옆에 콜론을 붙이고 수퍼 클래스의 생성자 호출
      • 이때 수퍼 클래스, 서브 클래스는 공통된 속성 가질 수 없으므로 Dog의 주 생성자에서 var, val 키워드를 붙이지 않은것을 주목
      • 조금 더 자세히 설명해보면 Dog의 주 생성자를 통해 들어온 값을 그대로 Animal 주 생성자의 프로퍼티에 대입한다는 의미로 봐도 됨

오버라이딩

  • 수퍼 클래스, 자식 클래스가 같은 이름의 함수를 가질 수 있게 해줌
fun main(){
    val t = Tiger()
    t.eat()
}

open class Animal{
    open fun eat(){
        println("eat food")
    }
}

class Tiger: Animal(){
    override fun eat(){
        println("eat meet")
    }
}
  • Animal 클래스의 eat 함수에 open 키워드가 붙은것을 주목
    • 저 키워드가 없다면 override 불가능

추상화

  • 오버라이딩은 기능 구현이 의무가 아님

  • 오버라이딩과 달리 Animal 클래스를 상속하는 모든 서브 클래스는 반드시 eat 가지며, 직접 구현해야함을 명시

  • 추상화는 추상 클래스라는 요소를 통해 가능해짐

    • 추상함수가 하나라도 있으면 추상 클래스
    • 추상 클래스는 일부 기능이 구현 안되었기 때문에 단독으로 인스턴스화 불가능
인터페이스
  • 다른 언어에서의 인터페이스 (예외도 물론 존재)
    • 추상 함수로만 이루어져 있는 순수 추상화 기능
  • 코틀린에선
    • 추상함수, 일반함수, 속성 모두 가질 수 있음
    • 클래스와 달리 생성자를 가질 순 없음
    • 서브 클래스에서 모든 함수의 구현 및 재정의 가능 (어떻게 가능한가?)
      • 구현부가 있으면 open 키워드 붙은것으로 간주
      • 구현부가 없으면 abstract 키워드 붙은것으로 간주
    • 여러 인터페이스를 상속받을 수 있으므로 코드 설계가 더 유연해짐

fun main(){
    var d = Dog()

    d.run()
    d.eat()
}

interface Runner {
    fun run()
}

interface Eater {
    fun eat(){
        println("음식을 먹습니다")
    }
}

class Dog: Runner, Eater{
    override fun run(){
        println("우다다다 뜁니다")
    }

    override fun eat(){
        println("허겁지겁 먹습니다")
    }
}
  • 위 코드 해석할 때 Dog class의 중괄호 안에 constructor { }가 생략된 것으로 봐야할 듯
    • 즉, 주 생성자에서 어떠한 속성도 만들지 않고, 값을 받지도 않겠다! 라는걸로 보임

open, final, abstract

하나의 부모클래스를 여러 자식 클래스가 상속한다고 했을 때 부모 클래스를 수정할 경우 프로그램에 치명적인 오류가 발생할 수 있으며 이를 취약한 기반 클래스라고 함. open, final, abstract 키워드는 이러한 사태를 미연에 방지하고자 존재

접근 방법은 상속을 금지하는 것

  • final
    • 상속 금지
    • 자식 클래스에서 override하게 의도한 클래스 혹은 메소드가 아니면 final로 만들것을 권유함. 클래스 필드의 default도 final임 (interface는 default가 public)
  • open
    • 클래스 혹은 메소드의 상속을 허용하기 위해 붙여주는 키워드
  • 코드 예시

      interface Clickable { 
          fun click() fun showOff() = println("I'm clickable!") 
      }
    
      open class RichButton : Clickable { 
          fun disable() {} 
          open fun animate() {} 
          override fun click() {} 
      }
    
      class childBtn : RichButton() { 
          override fun click() { super.click() } 
          override fun animate() { super.animate() } override fun showOff() { super.showOff() } 
      }
    
    • childBtn에서 disable() 메소드는 override 불가능 (open 키워드가 없어서)
    • RichButton에서 Clickable을 확장(interface는 extend 키워드를 자바에서 사용. 그러니 확장이라는 단어를 사용했음)할 때 final override fun click() {} 으로 선언할 경우 childBtn 에서 click() 메소드 override 불가능
  • abstract
    • abstract 클래스는 인스턴스화 불가능
    • abstract 클래스내의 메소드들은 항상 open이여서 따로 open 선언 안해줘도 됨

      abstract class Animated{
          abstract fun animate()
          open fun stopAnimating(){
      
          }
          fun animateTwice(){ // 이게 open이란 뜻이 아님! abstract 키워드가 붙어있으면 open까지 겸해진다는 것
          // 즉 animateTwice는 abstract 키워드가 없어서 상속 불가능
      
          }
      }
      
      open class RichButton : Animated() {
          override fun animate(){
      
          }
      
          override fun stopAnimating(){
              super.stopAnimating()
          }
      }
      
      • animate()는 반드시 override 해줘야 함
      • stopAnimating()은 override를 해줄수도 있고 안해줘도 됨
      • animateTwice()는 override 불가능

다형성(Polymorphism)

다형성은 기능임 하나의 객체가 여러가지 타입을 가질 수 있도록 하는 기능

  • 어떻게 다형성 기능을 구현할 수 있는가?
    • 상속과 Up-Casting, Down-Casting을 통해!
      • Up-Casting: super class의 자료형에 child class 인스턴스를 담는 것

          var a:Drink = Cola()
        
        • 콜라 인스턴스를 콜라 타입이 아닌 음료 타입에 담았음
        • 이럴경우 콜라의 기능은 사용하지 못하며 음료의 기능만 사용 가능
          • 단 콜라 클래스에서 음료 함수를 override 했을 경우 이렇게 override 된 함수는 사용가능

      • Down-Casting: Up-Casting 된 인스턴스를 다시 child 자료형으로 변환하면 Down-Casting 이며 이를 위한 연산 필요 (as, is)

        • as: 변수를 호환되는 자료형으로 변환해주며 변환된 자료형을 return도 해줌
        • is: 조건문내에서 사용되며 조건문의 scope에서만 자료형을 변환시킴
      fun main(){
          val a = Cola()
          a.drink()
      
          val b:Drink = Cola()
          // b.washDishes() // 에러
      
          b as Cola
          b.washDishes()
      }
      
      open class Drink(){
          // constructor(){
          //     println("Drink constructor called..")
          // }
          var name = "drink"
      
          open fun drink(){
              println("drink ${name}.")
          }
      }
      
      class Cola(): Drink(){      // 상속할 땐 부모 생성자 호출이 반드시 필요한 것으로 생각하자
                                  // Cola의 주생성자는 생략해도 괜찮음
          var type = "Cola"
              
          override fun drink(){
              println("drink drink of Cola")
          }
      
          fun washDishes() {
              println("washing dishes with ${type}")
          }
      }