Kotlin 문법

jiyoon·2023년 5월 14일
1

모바일 안드로이드

목록 보기
10/12

" 최근 IT 업계에서 높은 관심을 받고 있는 '코틀린'은 구글이 지원하는 프로그래밍 언어로서, 기존 Java에 비해 훨씬 간결하고 쉽게 작성할 수 있는 장점을 가지고 있다. 코틀린은 다양한 플랫폼에서 사용 가능하도록 설계되어 있어, 활용 범위가 매우 넓다. 안드로이드 개발과 관련된 유튜브 영상에서도 Kotlin을 기반으로 한 설명이 점차 증가하고 있다. 이에 따라 나도 안드로이드 앱 개발에 대한 이해를 높이기 위해 코틀린 문법을 공부할 예정이며, 배운 내용을 차근차근 정리해보겠다. "

변수와 상수

package ojy.coding.kotlin;

 //컴파일타임 상수 : 컴파일 과정에서 초기화
 const val num = 20

fun main() {
 //변수
 var i = 10;	//변수 추론 능력이 있어서 자료형을 명시하지 않아도 된다. 
 var name : String = "준석"	//이렇게 명시해줄 수도 있음.
 var point : Double = 3.3	//자료형은 다 대문자로 시작하는 클래스 타입.
 
 //상수
 val num = 20	//상수는 num=30 으로 재대입 불가함. java의 final과 동일.
}

일반 상수와 컴파일타임 상수의 차이

"일반 상수는 런타임에 초기화되며, 클래스 내부에도 선언할 수 있고, 인스턴스를 통해 접근할 수 있다.
컴파일타임 상수는 컴파일 과정에서 초기화되며, 클래스 내부에 선언할 수 없고, 인스턴스 없이 접근할 수 있다."

컴파일타임 상수는 컴파일 과정에서 초기화되어 런타임에 변경되지 않는다. 이로 인해 프로그램의 시작 시간이 단축되고, 메모리 사용이 최적화되며, 코드 실행 속도가 빨라진다. 이는 프로그램의 성능과 안정성을 향상시킨다.


형변환

	var i = 10
    var l = 20L
    l = i.toLong() //(Long) i -> 형식 안 됨.
    i = l.toInt()
    
    var name = "10"
    i = name.toInt()

String

    var name = "hello"
    
    print(name.uppercase()) //HELLO
    print(name.lowercase()) //hello
    print(name[0]) //h
    
    name = "지윤"
    //java의 + name + 와 동일 
    print("제 이름은 $name 입니다") //제 이름은 지윤 입니다.
    print("제 이름은 ${name}입니다") //제 이름은 지윤입니다.
    //중괄호 안 수식 적용 가능
    print("제 이름은 ${name + 10}입니다") //제 이름은 지윤10입니다.

max, min, random

import java.lang.Integer.max

fun main() {
	var i = 10
    var j = 20
    
    print(max(i,j)) //20
    //Math.max(i,j) = kotlin.math.max(i,j)
    
    val randomNumber = Random.nextInt()
    print(randomNumber) //840539650
    
    val randomNumber2 = Random.nextInt(0,100) //0~99
	val randomNumber3 = Random.nextDouble(0.0,1.0) //0.0~0.9


키보드 입력 받기

	val reader = Scanner(System.`in`)
    
    reader.nextInt() //숫자 받음
    reader.next() //글자 받음

`in` 백틱 쓰는 이유

in은 코틀린에서 특별한 키워드다. 그래서 이 키워드를 변수나 식별자로 사용하려면 충돌을 피하기 위해 ` 백틱(backtick) 기호를 사용하여 묶어줘야 한다.

System.in은 자바에서 표준 입력 스트림을 나타내는 변수다. 코틀린에서는 자바와의 상호 운용성(interoperability)을 유지하기 위해 이를 그대로 사용할 수 있게 한다. 하지만 코틀린에서 in은 범위(range)를 나타내는 키워드로 사용되기 때문에, 이를 식별자로 사용하려면 백틱으로 감싸주어야 한다.



조건문

코틀린에서는 if 문을 식(expression)처럼 사용할 수 있다. 이를 통해 코드를 더 간결하게 작성할 수 있다. 이와 달리, Java에서는 if 문이 값을 반환할 수 없기 때문에 이러한 방식이 불가능하다. 대신 Java에서는 삼항 연산자를 사용해야 한다.

fun main() {
	var i =5
    
    var result = if(i > 10) { "10보다 크다." }
    else if (i > 5) { "5보다 크다" }
    else {"!!!!"}
    
    //when문으로 똑같이 치환 가능!!
    var result = when {
        i > 10 -> "10보다 크다."
        i > 5 -> "5보다 크다"
        else -> "!!!!"
    }
}

when 문 : 코틀린의 강력한 조건 처리

Java에 익숙한 개발자들은 여러 조건을 처리하기 위해 switch 문을 사용했다. 하지만 코틀린은 Java의 switch 문보다 더 강력하고 유연한 조건 처리 기능을 제공하는 when 문을 도입했다.

코틀린의 when 문 특징

코틀린의 when 문은 여러 가지 강력한 기능을 제공하여 조건 처리를 더 간결하고 가독성 높게 만든다.

  • 여러 조건 처리 : 각 분기에서 여러 조건을 쉼표로 구분하여 표현할 수 있다.
val i = 5
val result1 = when (i) {
    1, 3, 5 -> "홀수"
    2, 4, 6 -> "짝수"
    else -> "알 수 없는 숫자"
}
  • 범위 사용 : 조건에 범위(range)를 사용할 수 있다.
val score = 85
val result2 = when {
    score in 90..100 -> "A등급"
    score in 80..89 -> "B등급"
    else -> "F등급"
}
  • 표현식 사용 : 조건에 함수 결과나 표현식을 사용할 수 있다.
fun isEven(n: Int) = n % 2 == 0

val number = 4
val result3 = when {
    isEven(number) -> "짝수"
    else -> "홀수"
}
  • 스마트 캐스팅 : 추가적인 타입 변환 없이 사용할 수 있도록 스마트 캐스팅(smart casting)을 지원한다.
fun describe(obj: Any): String = when (obj) {
    is String -> "문자열"
    is Int -> "정수"
    is Double -> "실수"
    else -> "알 수 없는 타입"
}

val x: Any = "코틀린"  //Any 타입의 변수 x에 문자열 "코틀린"을 할당
val result4 = describe(x) //describe 함수에 x를 전달하여 반환되는 문자열을 result4에 할당.
// result4 변수는 "문자열"이라는 값을 가짐

Any 타입은 코틀린에서 모든 타입의 상위 클래스로, 어떠한 타입의 객체도 저장할 수 있는 변수 타입이다. Java에서 Object 클래스와 유사한 역할을 수행한다.

Java와의 차이점

Java에서 비슷한 기능을 사용하려면 복잡한 if-else 구문이나 조건 연산자를 사용해야 했다. 이런 복잡한 처리를 해결하기 위해 코틀린은 when 문을 도입했다. 이를 사용하면 코드가 더 간결해지고 가독성이 높아진다.



반복문

val items = listOf(1,2,3,4,5)
items.forEach { 		//forEach 함수는 람다를 인자로 받아 컬렉션의 각 요소에 대해 람다를 실행
	item -> print(item) //람다
    } 
    
// for (int i=0; i<items.length; i++)
for (i in 0..(items.size - 1)) {
	print(items[i])  // 결과 : 12345
}

//범위 이용
for (i in 1..5) {
    print("$i ") // 결과: 1 2 3 4 5
}

for (i in 1 until 10 step 2) {
    print("$i ") // 결과: 1 3 5 7 9
}

for (i in 10 downTo 1) {
    print("$i ") // 결과: 10 9 8 7 6 5 4 3 2 1
}

val names = listOf("철수", "영희", "민수")
for (name in names) {
    print("$name ") // 결과: 철수 영희 민수
}



List와 MutableList

코틀린에서는 리스트를 다루기 위해 List와 MutableList 두 가지를 제공한다. List는 변경이 불가능한(읽기 전용) 리스트이고, MutableList는 변경이 가능한 리스트이다.

// 변경이 불가능한 List 생성
val items = listOf(1, 2, 3, 4, 5)

// 변경이 가능한 MutableList 생성
val itemsMutable = mutableListOf(1, 2, 3, 4, 5)
// 타입 추론으로 인해 생략 가능한 타입 선언
// val itemsMutable: MutableList<Int> = mutableListOf(1, ...)

itemsMutable.add(6)  // 리스트에 요소 추가
itemsMutable.remove(3)  // 리스트에서 요소 제거

// 인덱스를 이용해 요소 접근
try {
    val item = itemsMutable[6]
} catch (e: Exception) {
    print(e.message) // Index 6 out of bounds for length 5
}

Null Safety

코틀린은 Null Pointer Exception에 대비하기 위해 Null 안전성을 기본적으로 제공한다. 변수를 선언할 때 타입 뒤에 물음표(?)를 붙여 해당 변수가 null 값을 허용하도록 지정할 수 있다.

var name: String? = null // `?`를 사용하여 null 값이 허용되는 변수로 선언
name = "지윤"

name = null

var name2: String = ""

name2 = name //오류 : 별개의 타입이라 대입 안 됨

if (name != null) {
	name2 = name //name이 String 타입으로 변환되서 오류 안 남
}
//name2 = name!! 개발자가 널이 아님을 보증하는 !! 연산자를 사용할 수 있지만, 권장되지 않는다.

//let을 사용하여 널이 아닌 경우에만 코드를 실행
name?.let { //name이 널이 아니라면 실행하자!
	name1 = name
}

함수

코틀린에서 함수를 작성하는 방법은 매우 간결하다. 기본적인 함수 정의 및 호출 방법을 살펴보자.

fun main() {
	var c = sum(10,20)
}

//Top-Level 함수 : 파일의 맨 바깥에 작성함. 어느 파일에서나 사용 가능
fun sum(a: Int, b: Int) : Int {
	return a+b
}

//한 줄짜리 함수는 더 간단히 가능
fun sum(a: Int, b: Int) : Int = a + b

//return 타입 생략 가능 <- 타입 추론 가능하기 때문
fun sum(a: Int, b: Int) = a + b

Top-Level 함수

코틀린에서 Top-Level 함수란 클래스나 객체에 종속되지 않는 함수를 말한다. 이 함수들은 파일의 최상위에 정의되며, 어느 파일에서든 사용이 가능하다.
Top-Level 함수는 전역적으로 사용할 수 있는 기능을 쉽게 구현할 수 있도록 도와준다. 이를 통해 클래스를 생성하지 않고도 독립적인 함수를 만들어 코드 구조를 간결하게 만들 수 있다.

// TopLevelFunction.kt
fun sayHello() {
    println("안녕하세요!")
}

// 다른 파일에서 사용하기
import TopLevelFunction.sayHello

fun main() {
    sayHello() // 안녕하세요!
}

오버로딩이 필요한 경우

fun main() {
	var c = sum(10,20)
    var d = sum(b=10, a=20)
}
// 인자 3개짜리 필요하다고 오버로드할 필요 없음
fun sum(a: Int, b: Int, c: Int = 0) = a + b + c


Class, data class

코틀린에서는 클래스를 간편하게 생성할 수 있다. 생성자를 따로 만들 필요 없이 매개변수처럼 직접 작성하고, val이나 var를 사용하여 getter와 setter를 자동으로 구현한다.

fun main() {
	val john = Person("John", 20)
    print(john.name)
    print(john.age)
    
    john.age = 23
}

class Person(val name: String, var age: Int) 

만약 생성자에서 전달한 값이 외부에 노출되지 않도록 하려면, private을 사용하여 getter를 제공하지 않게 할 수 있다.

class Person(private val name: String, var age: Int) 

이 경우 main 함수에서 print(john.name)은 오류가 발생한다.

data class

데이터 클래스는 주로 데이터를 담기 위한 클래스로, 자동으로 일반적인 함수들을 생성해준다.

fun main() {
	val john = Person("John", 20)
    val john2 = Person("John", 20)
    
	println(john)
	println(john2)
    print(john == john2)
    
}

class Person(val name: String, var age: Int)

결과


위 예제에서 == 연산자로 비교한 결과는 false이다. 하지만 데이터 클래스를 사용하면 자동으로 equals와 hashCode 함수가 구현되어 있어, 값에 따라 비교할 수 있다.

data class Person(val name: String, var age: Int)

결과

이제 == 연산자로 비교한 결과가 true로 나온다. 이렇게 데이터 클래스는 데이터를 담는데 편리한 기능을 제공한다.



getter와 setter

코틀린에서는 getter와 setter를 쉽게 사용할 수 있다. 주로 클래스의 프로퍼티에 적용된다

fun main() {
    val john = Person("John", 20)

    john.hobby = "독서"
}

class Person(val name: String, var age: Int) {
    var hobby = "축구"
        private set // 외부에서 값을 변경할 수 없게 한다
        get() = "취미: $field" // getter를 재정의하여 반환값을 수정한다

    init {  // 클래스 생성 시 실행되는 초기화 블록
        print("init")
    }

    fun some() {
        hobby = "농구"
    }
}

init : 초기화 블록

코틀린에서 클래스의 생성자는 주 생성자와 보조 생성자로 구분된다. 주 생성자의 본문에는 로직을 넣을 수 없기 때문에, 초기화 블록(init)을 사용하여 클래스 생성 시 실행되는 로직을 작성할 수 있다. 필요한 경우 여러 개의 초기화 블록을 사용할 수 있, 이들은 선언된 순서대로 실행된다.



상속 : extends, interface

//추상 클래스
abstract class Animal {
	fun move() {
	    print("이동")
   }
   
   //오버라이딩 가능하게 하기 위해선 open 해줘야 함
    open fun run() {
    	print("뛴다")
    }
}

class Dog : Animal() {
	override fun run() {
    	print("달린다")
    }

class Cat : Animal() {
	override fun run() {
    	print("점프한다")
    }

open class

코틀린에서 클래스를 상속받기 위해서는 부모 클래스가 open 키워드로 선언되어야 한다.

open class Person

class SuperMan : Person()

interface

인터페이스는 클래스와 비슷한 개념이지만, 추상 메서드를 포함한다. 클래스는 인터페이스를 상속받아 해당 인터페이스의 메서드를 구현해야 한다.

interface Drawable {
	fun draw()
}

class Dog : Animal(), Drawable {
	override fun draw() {	//인터페이스 상속 받았기 때문에 오버라이딩 해줘야 함.
    	print("그린다.")
    }

추상 클래스와 인터페이스 차이

  1. 상속 제한 : 클래스는 하나의 추상 클래스만 상속받을 수 있지만, 여러 인터페이스를 동시에 구현할 수 있다.
  2. 프로퍼티 및 생성자 : 추상 클래스는 프로퍼티와 생성자를 가질 수 있지만, 인터페이스는 가질 수 없다.
  3. 기본 구현 : 추상 클래스는 일부 메서드에 기본 구현을 제공할 수 있지만, 인터페이스는 모든 메서드가 추상 메서드이다. 코틀린에서는 인터페이스의 메서드에 기본 구현을 제공할 수 있다.

추상 클래스와 인터페이스 선택 기준

  1. 공통된 기본 구현이 필요하면 추상 클래스를 사용한다.
  2. 여러 인터페이스를 동시에 구현해야 하는 경우 인터페이스를 사용한다.
  3. 변경 가능성이 높은 API를 정의할 때 인터페이스를 사용하여 유연성을 확보한다.


타입체크 is와 강제 타입변환 as

fun main() {
    val dog: Animal = Dog()
    val cat = Cat()

    // 타입 체크를 통해 dog가 Dog 타입인지 확인
    if (dog is Dog) {
        dog.draw() // Dog 타입이므로 사용 가능
        println("멍멍이")
    }

    // 타입 체크를 통해 dog가 Animal 타입인지 확인
    if (dog is Animal) {
        // dog.draw() // 사용 불가능, Animal 타입에는 없음. move는 가능.
    }

    // cat을 강제로 Dog 타입으로 변환 (실행 시 오류 발생)
    cat as Dog
}

제네릭

제네릭을 사용하면, 클래스나 함수에서 다양한 타입을 처리할 수 있도록 만들어 준다.

fun main() {
    val box = Box(10) // Int 타입의 Box
    val box2 = Box("dfdfd") // String 타입의 Box
    
    println(box.value) // 10 출력
}

// 제네릭을 사용한 클래스 정의
class Box<T>(var value: T)

T라는 타입 매개변수 사용해 사용자 원하는 타입 지정 가능하게 했다. 이를 통해 Box 클래스 인스턴스 생성 시 각기 다른 타입 값 저장 가능하게 됐다.


콜백 함수 (고차함수)

콜백 함수는 함수를 매개변수로 받아 실행하는 고차함수를 이용하여 비동기 처리나 특정 이벤트에 대한 응답을 처리할 수 있다.

fun main() {
    // 콜백 함수 사용, 출력: "함수 호출"
    myFunc { println("함수 호출") }
}

// 고차함수 정의: 콜백 함수를 매개변수로 받는다.
fun myFunc(callBack: () -> Unit) {
    println("함수 시작!!!")
    callBack() // 콜백 함수 실행
    println("함수 끝!!!")
}

myFunc 함수는 고차함수로써, 매개변수로 콜백 함수(callBack)를 받는다. 이 콜백 함수는 main 함수에서 정의된 람다식으로 "함수 호출"을 출력한다. myFunc 함수 안에서 callBack()을 호출하면 전달받은 람다식이 실행되어 원하는 동작을 수행한다. 이런 방식으로 함수 호출 순서나 이벤트 처리를 유연하게 조작할 수 있다.


suspend 함수

suspend 함수는 코루틴에서 사용되며, 비동기 작업을 수행할 때 블로킹 없이 대기할 수 있게 해준다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val result = fetchData()
    println("결과: $result")
}

// suspend 함수 fetchData
suspend fun fetchData(): String {
    delay(1000L) // 네트워크 호출 등 시간이 걸리는 작업
    return "데이터"
}
  1. kotlinx.coroutines 라이브러리를 임포트합니다. 이 라이브러리는 코루틴을 쉽게 사용할 수 있는 다양한 기능을 제공합니다.
  2. main 함수를 runBlocking 블록 안에서 실행합니다. runBlocking은 블록 내부의 모든 코루틴이 완료될 때까지 기다리며, 이를 통해 비동기 작업을 동기적으로 처리할 수 있게 합니다.
  3. fetchData() 함수를 호출하고 결과를 받아 출력합니다. fetchData()는 suspend 함수이므로 코루틴 또는 다른 suspend 함수 내에서 호출해야 합니다.
  4. fetchData() 함수에서는 delay(1000L)을 사용해 시간이 걸리는 작업을 시뮬레이션합니다. 이 시간 동안 다른 코루틴이나 작업을 처리할 수 있습니다.
  5. fetchData() 함수가 작업을 완료하면 "데이터" 문자열을 반환하고, 이를 출력합니다.

코루틴

코루틴은 비동기 작업을 쉽게 처리할 수 있도록 도와주는 프레임워크이다. 코루틴을 사용하여 동시에 여러 작업을 수행할 수 있다.


출처

https://www.youtube.com/watch?v=OtHkb6wAI5U - Kotlin 문법 총 정리 - 1시간

profile
주니어 개발자

0개의 댓글