코틀린 정리 1장, 2장

손현수·2022년 11월 4일

MainActivity.kt 코드 파일 알아보기

package com.example.helloworld

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
  • package com.example.helloworld: MainActivity.kt 파일이 어느 패키지에 속하는지 알려줍니다. 점(.) 기호를 사용해서 패키지의 계층 구조를 표현합니다.
  • import: 해당 .kt 파일이 담긴 패키지 밖에 구현된 객체(라이브러리나 모듈)를 임포트하는 구문입니다.
  • MainActivity 클래스는 AppCompatActivity 클래스를 상속받는 클래스입니다. 그리고 AppCompatActivity 클래스는 Activity 클래스를 상속받습니다. 안드로이드 스튜디오 1.5 버전부터는 새로 생성하는 모든 액티비티 클래스가 기본적으로 AppCompatActivity 클래스를 상속받습니다.
  • onCreate()는 클래스가 생성될 때 맨 처음에 호출되는 콜백 함수입니다. 여기에 초기화 코드를 넣으면 됩니다.
  • super란 상위 클래스를 뜻하며, 상위 클래스의 onCreate()를 먼저 실행하라는 의미입니다. 이 줄이 없으면, 오직 여러분이 적은 코드만 실행되어 SuperNotCalledException과 같은 에러를 만나게 됩니다.
  • setContentView()는 보여줄 레이아웃을 지정해줍니다. 여기서는 R.layout.activity_main을 지정했습니다. activity_main.xml 파일을 지칭합니다.
    R은 무엇일까?
    R은 자동 생성된 클래스로서 어디서나 접근할 수 있는 상수들로 구성되어 있습니다. R 클래스 덕분에 여러분이 res(리소스) 폴더에 정의한 여러 값을 프로젝트에서 사용할 수 있습니다. R 뒤에 .을 붙이고 그 귀에 리소스 종류를 씁니다. 예를 들어 R.layout은 레이아웃 리소스, R.anim은 애니메이션 리소스, R.color는 색상 리소스입니다 그 외에도 R.string, R.menu 등이 있습니다.

변수와 상수

  • 변수를 var로, 상수값을 val로 선언합니다.
val pi: Double = 3.14	//val 변수명: 자료형 = 값
val name = "gil-dong"	//형추론 (String)
  • 문맥상 추론이 가능하면 자료형을 생략할 수 있습니다.
  • val은 상수이므로 값을 재할당하면 컴파일 오류가 납니다. 값을 변경하고 싶을 때는 var을 사용해야 합니다.
var age = 21 //형추론 (Int)
age = 25 //재할당

기본 자료형

  • 정수 자료형: byte, short, int, long
val numByte: Byte = 100
val numShort: Short = 20
val numInt: Int = 1
val numLong: Long = 2L
  • 실수 자료형: double, float
val numDouble: Double = 3.2
val numFloat: Float = 3.2f
  • 코틀린은 형추론을 통해서 자료형을 명시하지 않아도 알아서 추론한다. 자료형을 명시하지 않으면 정수의 경우 Int, 실수의 경우 Double형이 된다.
  • 문자 자료형: char, string
val char1: Char = 'H'
val string1: String = "Hi, This is String"
  • 논리 자료형: Boolean
val isTrue: Boolean = true
  • 배열 자료형: Array
val stringArray: Array<String> = arrayOf("apple", "banana", "grape")
val intArray: Array<Int> = arrayOf(1, 2, 3)

println(stringArray[0])//apple
println(stringInt[2])//3

함수

fun printAge(age: Int): Unit {
	println(age)
}

fun printAge(age: Int) {//Unit 생략
	println(age)
}
  • 매개변수를 선언할 때는 매개변수명: 자료형 형태로 선언
  • 이 함수는 반환 자료형이 없으므로 Unit형을 적었다. Unit형은 자바의 void에 대응하며, 생략할 수 있다.
fun addNum(a : Int, b : Int) : Int {
	return a + b
}
  • 이 함수의 반환값은 Int형이므로 반드시 반환형을 명시해주어야 한다. 반환 자료형을 생략할 수 있는 경우는 Unit형이거나 단일 표현식일 경우에 생략 가능. 단일 표현식 함수는 실행할 코드가 표현식 하나로 이루어진 함수를 말한다.
fun minusNum(a : Int, b : Int) = a - b

println(minusNum(minusNUm(1000, 200), 100))//700

문자열 템플릿

  • 우리는 가끔 변수를 포함해 문자열을 만들 때가 있다. 코틀린에서는 $를 변수명 앞에 덧붙이면 문자열로 만들 수 있다. 변수가 하나면 $, 더 많다면 ${}로 감싸주면 된다.
val price = 3000
val tax = 300

val originalPrice = "The original price is $price"
val totalPrice = "The total price is ${price + tax}"

범위 클래스

  • 범위 클래스로 특정 범위의 값들을 간편하게 표현할 수 있다. 범위 클래스로는 IntRange, LongRange, CharRange 등이 있다.

  • 숫자의 범위

val numRange : IntRange = 1..5

println(numRange.contains(3)) //true
println(numRange.contains(10)) //false
  • 알파벳의 범위
val charRange : CharRange = 'a'..'e'

println(charRange.contains('b'))//true
println(charRange.contains('z'))//false

for문

for (i in 1..5) {
	println(i) //1, 2, 3, 4, 5
}

for (i in 5 down to 1) {
	println(i) //5, 4, 3, 2, 1
}

for (i in 1..10 step 2) {
	println(i) //1, 3, 5, 7, 9
}
  • for문을 이용해 배열의 요소 출력
val students = arrayOf("jun-gi", "jun-su", "yeon-seo", "jun-seo")

for (name in students) {
	println(name) //jun-gi, jun-su, yeon-seo, jun-seo
}
  • withindex() 함수를 이용하면 요소의 인덱스도 함께 가져올 수 있다.
val students = arrayOf("jun-gi", "jun-su", "yeon-seo", "jun-seo")

for ((index, name) in studetns.withIndex()) {
	println("Index: $index Name : $name")
}

while문

var num = 1

while (num < 5) {
	println(num)
    num++
}
  • do while문
var num = 1
do {
	num++
    println(num)
} while (num < 5)

if문

  • 자바와 같은 방식
val examScore = 60
val isPass = false

if (examScore > 80) {
	isPass = true
}

println("시험 결과 : $isPass")
  • if-else문도 자바와 같은 방식

when문

  • 코틀린에는 값에 따라서 코드를 실행하는 switch문이 없다. 대신 when문이 있다.
val weather = 15

when (weather) {
	-20 -> {println("매우 추운 날씨")}//값 하나
    11, 12, 13, 14 -> {println("쌀쌀한 날씨")}//값 여러 개
    in 15..26 -> {println("활동하기 좋은 날씨")}//범위 안에 들어가는 경우
    
    //범위 안에 안 들어가는 경우
    !in -30..50 -> {println("잘못된 값입니다. -30 ~ 50 가운데 값을 적어주세요")}
    else -> {println("잘 모르겠는 값")}//위 경우가 모두 아닐 때
}
  • when문도 값을 반환하는 표현식으로 사용할 수 있다. 값을 무조건 할당해야 하므로 else문이 필수로 들어가야 한다.
val essayScore = 95
val grade = when(essayScore) {
	in 0..40 -> "D"
    in 41..70 -> "C"
    in 71..90 -> "B"
    else -> "A"
}

println("에세이 학점 : $grade")

리스트

  • 리스트는 순서가 있는 자료구조이다. 먼저 읽기 전용 리스트를 만들 것인지, 읽기 쓰기 모두 가능한 리스트를 만들 것인지를 정한 후, 목적에 맞는 함수를 사용해 리스트를 만들어야 한다.
  • 읽기 전용 listOf()
val numImmutableList = listOf(1, 2, 3)
numImmutableList[0] = 1//읽기 모드이므로 오류 발생
  • 일기 쓰기 모두 가능한 리스트를 만드려면 mutableListOf() 함수를 사용하면 된다.
val numMutableList = mutableListOf(1, 2, 3)
numMutableList[0] = 100

println(numMutableList)//[100, 2, 3]
println(numMutableList[0])//100
  • 리스트에 어떤 요소가 있는지 확인할 때는 contains() 함수를 사용한다.
val numMutableList = mutableListOf(1, 2, 3)
numMutableList[0] = 100

println(numMutableList.contains(5))//false

셋(Set)

  • 셋은 순서가 없다. 또한 중복되지 않은 요소들로 만들어지므로 같은 값을 추가하더라도 해당 값은 하나만 저장된다. 읽기 전용 셋과 읽기 쓰기 모두 가능한 셋 두가지를 제공한다. setOf(), mutableSetOf() 함수로 객체 생성
//일기 전용 셋
val immutableSet = setOf(1,1,2,2,3,3,3)
println(immutableSet)
//읽기 쓰기 전용 셋
val mutableSet = mutableSetOf(1,2,3,3,3,3)
mutableSet.add(100)
mutableSet.remove(1)
mutableSet.remove(200)

println(mutableSet)
println(mutableSet.contains(1))

맵(Map)

  • 맵은 키와 값을 짝지어 저장하는 자료구조이다. 키는 중복되지 않도록 해야 한다. 읽기 전용 맵과 읽기 쓰기 모두 가능한 맵 두가지 종류가 있다.
//읽기 전용 맵
val immutableMap = mapOf("name" to "junsu", "age" to 13, "age" to 15, "height" to 160)
println(immutableMap)//{name=junsu, age=15, height=160}, 키를 중복으로 정의하면 나중에 정의한 것을 기준으로 저장됨

//읽기 쓰기 모두 가능한 맵
val mutableMap = mutableMapOf("돈까스" to "일식", "짜장면" to "중식", "김치" to "중식")
mutableMap.put("막국수", "한식")
mutableMap.remove("돈까스")
mutableMap.replace("김치", "한식")
println(mutableMap)//{짜장면=중식, 김치=한식, 막국수=한식}

클래스 선언 및 객체 생성

class Car(val color : String)

val car  = Car("red")
println("My car color is ${car.color}")

클래스 생성자

  • 코틀린에는 주 생성자와 보조 생성자가 있다.

주 생성자

  • 클래스 이름 옆에 괄호로 둘러쌓인 코드를 주 생성자라고 한다.
class Person(val name : String) {}

보조 생성자

  • 보조 생성자는 클래스 바디 내부에서 constructor 키워드를 이용해 만들며 객체 생성 시 실행할 코드를 작성해 넣을 수 있다.
class Person {
    constructor(age : Int) {
        println("I'm $age years old")
    }
}
  • 주 생성자가 존재할 때는 반드시 this 키워드를 통해 주 생성자를 호출해야 한다.
class Person(name : String) {
    constructor(name : String, age : Int) : this(name) {
        println("I'm $age years old")
    }
}

초기화 블록

  • 객체 생성 시 필요한 작업을 하는 것이 초기화 블록, init{} 안의 코드들은 객체 생성 시 가장 먼저 실행되고 주 생성자의 매개변수를 사용할 수 있다. 주로 주 생성자와 같이 쓰인다.
class Person(name : String) {
    val name : String
    init {
        if (name.isEmpty()) {//매개변수 문자열이 비어 있는 경우 에러 발생
            throw IllegalArgumentException("이름이 없어요.")
        }
        this.name = name//문자열이 안 비어 있으면 이름 저장
    }
}

클래스의 상속

  • 코틀린에서 클래스를 상속받으려면 부모 클래스에 open 키워드를 추가해야 한다. 메소드도 자식 클래스에서 오버라이드하려면 부모 클래스의 메소드에 open 키워드를 추가해야 한다. 다음은 Flower 클래스를 상속받는 Rose 클래스이다.
open class Flower{
    open fun waterFlower() {
        println("water flower")
    }
}

class Rose : Flower() {
    override fun waterFlower() {
        super.waterFlower()
        println("Rose is happy now")
    }
}

val rose = Rose()
rose.waterFlower()
  • 부모 클래스 생성자를 실행시키려면 자식 클래스에서 반드시 부모 클래스의 생성자를 명시적으로 호출해주어야 한다.
open class Flower(val name : String) {}

class Rose(name : String, color : String) : Flower(name) {}

접근 제한자

  • 코틀린 클래스의 기본 속성과 메소드는 public이다. 이외에 private, protected, internal이 있다.
    public: 코틀린의 기본 접근 제한자. 어디에서나 접근 가능
    internal: 같은 모듈 내에서 접근 가능. 안드로이드 개발 시에는 한 프로젝트 안에 있으면 같은 모듈이라고 보면 된다. 만약 한 프로젝트에 여러 모듈을 만든다면 이는 모듈 간 접근이 제한된다.
    protected: 자식 클래스에서는 접근할 수 있다.
    private: 해당 클래스 내부에서만 접근할 수 있다.

companion 키워드

  • 자바에서 static 역할. 객체를 만들지 않고도 접근 가능
class Dinner {
    companion object {
        val MENU = "pasta"
        fun eatDinner() {
            println("$MENU is yummy")
        }
    }
}

println(Dinner.Companion.MENU)
println(Dinner.MENU)
Dinner.eatDinner()

추상 클래스

  • 추상 메소드가 포함된 클래스. 추상 클래스, 추상 메소드에는 abstract 키워드를 붙인다.
abstract class Game {
    fun startGame() {
        println("게임을 시작했습니다.")
    }
    
    abstract fun printName()
}

class Overwatch : Game() {
    override fun printName() {
        println("오버워치입니다.")
    }
}

val overwatch = Overwatch()
overwatch.startGame()//Game 클래스 메소드
overwatch.printName()//Overwatch 클래스 메소드

데이터 클래스

  • 코틀린의 데이터 클래스는 특정한 메소드의 실행보다는 데이터 전달이 목적이다. 코틀린은 데이터 전달용 객체를 간편하게 생성하도록 data class라는 키워드를 제공. 주 생성자에는 val이나 var를 사용한 property 정의가 적어도 하나 이상 필요하며, val, var가 아닌 매개변수는 사용 불가.
data class Memo(val title : String, val content : String, var isDone : Boolean)
var memo1 = Memo("마트 가기", "계란, 우유, 빵", false)
var memo2 = memo1.copy(content = "칫솔, 과자")

println(memo1.toString())
println(memo2.toString())
  • 데이터 클래스는 각각의 property에 대한 toString(), copy()와 같은 메소드를 자동으로 생성.
    - toSrting(): 객체에 포함되어 있는 데이터를 출력하여 보여줌. 생성자에 포함된 property만 출력됨
    - copy(): 객체의 속성들을 복사하여 반환하는 메소드, 인수로 받는 property만 해당 값으로 바뀌어 복사해줌

인터페이스 정의

  • interface 키워드를 사용하면 만들 수 있음. 인터페이스에서는 추상 메소드에 abstract 키워드 생략 가능.
interface Car{
    abstract fun drive()
    fun stop()
}

디폴트 메소드

  • 인터페이스에서 기본적으로 구현하는 메소드를 제공할 수 있다. 해당 인터페이스를 구현하는 클래스들은 디폴트 메소드만큼은 오버라이드 안 해도 됨.
interface Car{
    abstract fun drive()
    fun stop()
    fun destroy() = println("차가 파괴되었습니다.")
}

인터페이스 구현

interface Car{
    abstract fun drive()
    fun stop()
    fun destroy() = println("차가 파괴되었습니다.")//디폴트 메소드
}

class Ferrari : Car {
    override fun drive() {
        println("페라리가 달립니다.")
    }
    override fun stop() {
        println("페라리가 멈춥니다.")
    }
}

val myFerrari = Ferrari()
myFerrari.drive()
myFerrari.stop()
myFerrari.destroy()
  • 한 클래스에서 클래스는 단 한 개만 상속받을 수 있다. 하지만 인터페이스는 2개 이상 구현할 수 있다.

클래스 상속과 인터페이스 구현

interface Animal {
    fun breath()
    fun eat()
}

interface Human{
    fun think()
}

open class Name(val name : String) {
    fun printName() {
        println("제 이름은 $name")
    }
}

class Korean(name : String) : Name(name), Animal, Human {
    override fun breath() {
        println("후-하")
    }
    override fun eat() {
        println("한식 먹기")
    }
    override fun think() {
        println("생각하기")
    }
}

val joyce = Korean("정아")
joyce.breath()
joyce.printName()

Null 처리하기

var myName : String// 초기화를 해주지 않아 에러
var myName : String = null//non-nullable 자료형에 null을 넣어서 에러

var myName : String? = null//자료형 뒤에 ?를 붙여서 null이 올 수 있음을 알려주면 된다.
myName
  • 이제 myName은 공식적으로 null이 가능한 String형이 되었다. 하지만 다음 코드를 실행하면 에러가 발생함.
myName = "Joyce"
println(myName.reversed()) // 에러 발생
  • 이 코드가 에러가 발생하는 이유는 myName이 null이 될 수도 있기 때문에 코틀린 컴파일러가 개발자에게 확인하라고 알리는 것이다.

셰이프 콜 연산자?

  • ? 연산자를 이용하면 메소드 호출, 혹은 객체 property 접근과 null 체크를 한 번에 할 수 있다. ? 연산자를 셰이프 콜 연산자라고 한다. 만약 객체 참조가 null이면 셰이프 콜 연산자의 반환값은 null이 된다.
fun reversedName(name : String?) : String? {//인수, 반환값 모두 null 가능
    return name?.reversed()//name이 null이라면 null 반환
}

println(reversedName("joyce"))//ecyoj
println(reversedName(null))//null

엘비스 연산자 ?:

  • 엘비스 연산자는 셰이프 콜을 할 시 null을 반환하지 않고, 기본값을 반환한다.
fun reversedName(name : String?) : String? {
    return name?.reversed() ?: "이름을 확인해주세요."
}

println(reversedName("joyce"))
println(reversedName(null))

확정 연산자 !!

  • 셰이프 콜 연산자와 엘비스 연산자만으로 코드를 안전하게 작성 가능. 여기에 더하여 null이 아님을 보증하는 확정 연산자 !!도 존재. 컴파일러에게 "이게 null이 가능한 자료형이긴 한데, null이 아니니까 걱정마!"라고 알리는 것.
fun reversedName(name : String?) : String {//반환 자료형 null 불가능
    return name!!.reversed() //절대 null이 아님을 보증
}

println(reversedName("joyce"))
  • null이 아닌 값을 함수의 인수로 넣으면 문제가 발생하지 않는다. 하지만 null을 넣으면 NullPointerException 에러가 발생한다. 따라서 !! 연산자를 남용하면 코틀린이 제공하는 자료형 안전성을 제대로 누리는 것이 아니다. 편리하지만 무분별한 사용은 하지 말자.

lateinit 키워드

  • 코틀린에서는 기본적으로 모든 변수는 null이 아니기 때문에 반드시 선언과 동시에 초기화돼야 한다. 만약 값을 나중에 넣고 싶으면 lateinit 키워드를 통해 일단 변수를 선언하고 나중에 값을 할당할 수 있다.
lateinit var lunch : String
lunch = "waffle"
  • lateinit 변수를 사용할 때 주의해야 할 것
  1. var 변수에만 사용
  2. nullable 자료형과 함께 사용 불가
  3. 초기화 전에 변수를 사용하면 에러 발생
  4. 원시 자료형(Int, Double, Float) 등에는 사용 불가
  5. ::변수명::isInitialized() 함수로 초기화되었는지 확인 가능

lazy 키워드

  • lazy를 이용하면 변경할 수 없는 변수인 val의 늦은 초기화를 할 수 있다. 객체가 생성될 때 초기화되는 것이 아니라 처음 호출될 때 lazy{} 안의 코드가 실행되면서 초기화 됨

람다식

  • 람다식은 마치 값처럼 다룰 수 있는 익명 함수다.
val sayHello = fun() {println("안녕하세요.")}
sayHello()

람다식 정의

val squareNum : (Int) -> (Int) = {number -> number * number}
println(squareNum(12))//144

  • 자료형은 2, 3 자리에 넣어도 되지만 4에서 명시해주어도 된다.
val squareNum2 = {number : Int -> number * number}
  • 또한 람다식의 인수가 한 개이면 인수를 생략하고 it로 지칭할 수 있다.
val squareNum3 : (Int) -> (Int) = {it * it}

람다를 표현하는 다양한 방법

fun invokeLambda (lambda : (Int) -> Boolean) : Boolean {//람다를 인수로 받음
    return lambda(5)
}

val paramLambda : (Int) -> Boolean = {num -> num == 10}

println(invokeLambda(paramLambda))//람다식의 인수로 넣은 5 != 10이므로 false

invokeLambda({num -> num == 10})//람다식 바로 넣어주기
invokeLambda({it == 10})//인수가 하나일 때 it으로 변경 가능
invokeLambda() {it == 10} //만약 함수의 마지막 인수가 람다일 경우 밖으로 뺄 수 있음
invokeLambda{it == 10}//그 외 인수가 없을 때 () 생략 가능

SAM(Single Abstract Method) 변환

  • 안드로이드 개발을 하다 보면 다음과 같은 코드를 많이 작성하게 된다.
button.setOnClickListener{
	//버튼이 눌렸을 때 작동할 코드
}
  • 함수의 마지막 인수가 람다식인 경우에 ()을 생략하고 {}에 코드를 작성할 수 있다. invokeLambda{it == 10}처럼 말이다.
  • setOnClickListener 함수의 경우 마지막 인수가 람다식이 아닌 OnClickListener 인터페이스를 인수로 받고 있다. OnClickListener는 추상 메소드가 하나 있는 인터페이스이다. setOnClickListener는 이와 같이 람다식이 아님에도 람다식처럼 취급되고 있다. 이것이 가능한 이유는 자바 8에서 소개된 SAM 변환이다. SAM 변환에는 두 가지 조건이 있다.
  1. 코틀린 인터페이스가 아닌 자바 인터페이스여야 할 것
  2. 인터페이스 내에는 추상 메소드만 존재할 것
  • 이 조건을 만족하는 경우 익명 인터페이스 객체 생성에 람다식을 사용할 수 있다. 이런 경우에 람다식을 사용하면 코드가 훨씬 간결해지고 가독성이 높아진다.
profile
안녕하세요.

0개의 댓글