[Android]/[Kotlin]

Kotlin 기본문법 정리

Hevton 2022. 12. 4. 13:57
반응형

 

이 글에서는

JAVA와 조금 다른, Kotlin의 기본 문법을 정리한다.

 

코틀린은 Python + JAVA + Dart 느낌이 물씬 풍긴다.

 

코틀린은 간결하고, 세미콜론도 생략 가능하며, Null Safty하다.

 

 

1. 함수

함수의 지시 예약어는 fun 키워드이다.

 

fun helloWorld() {
    println("Hello World!")
}

이것을 자바로 보면 아래 코드와 같다.

// JAVA
void helloWorld() {
    System.out.println("Hello World!");
}

 

 

코틀린 함수에서는, 리턴 타입을 지정해 줄 때 아래와 같이 사용한다

// 리턴 => 함수 맨뒤에 : 자료형
fun add() : Int {
    return 10
}

맨 위에 있던 helloWorld 함수에서는, 리턴값이 없으니 리턴 자료형을 따로 지정해 주지 않았지만

사실 생략 시에 기본적인 리턴 타입이 지정되어 있다.

fun helloWorld() {
    println("Hello World!")
}

// 위 함수와 아래 함수는 동일하다
fun helloWorld() : Unit {
    println("Hello World!")
}

리턴 타입을 생략 시, Unit 이라고 기본적으로 컴파일러가 지정해준다.

괜찮은 비유일 지 모르겠지만, 마치 자바에서 class A {} 하면, 암묵적으로 extends Object를 달아주는 것과 같은 느낌이라고 볼 수도 있다.

 

아무튼 리턴할 것이 명시적이지 않다면, 생략이 가능하나, 생략할 시 컴파일러가 기본적으로 : Unit을 달아준다고 생각하면 된다.

// 리턴값이 없으면 Unit을 붙인다
fun sampel() : Unit {

}
// 또는 생략가능
fun sample() {

}

 

또한 이번엔 함수의 인자를 넣어줄 때, 코틀린에서는 이렇게 넣어준다

// 함수의 인자 => 변수명 : 자료형
// 리턴 => 함수 맨뒤에 : 자료형
fun add(a : Int, b : Int) : Int {

    return a + b
}

 

실행할 때엔 add() 이런식으로 실행한다.

fun main() {
    print(add(10, 20))
}

 

파라미터 변수를 직접 지정해 줄 수도 있다.

fun main() {
    print(add(a = 10, b = 20))
}

 

 

자바로 이와 동일한 기능의 함수는 이렇다

// JAVA
int add(int a, int b) {

    return a + b;
}

 

코틀린에서 함수의 인자를 지정해 줄 때, a : Int 처럼

변수명 : 자료형 으로 선언해준다.

 

또한 코틀린에서는 Int를 소문자로 시작하지 않고 대문자로 시작한다는 것에 주의한다!

 

 

코틀린의 메인 함수는 이렇게 생겼다.

메인 함수는 프로그램의 진입점(Entry Point)이다.

fun main() {

}

 

자바에서 메인 함수는 익숙할 것이다

// JAVA
public static void main(String args[]) {

}

 

 

2. val vs var

val은 value를 의미하며, 값이 바뀌지 않는 지시자이다.

var는 variable을 의미하며, 값이 바뀌는, 우리가 아는 변수 지시자이다.

fun val_and_var() {

    val a : Int = 10

    var b : Int = 9

    // a = 100 에러.. 이유는, val은 상수이다 바뀌지 않아야 한다.
    
    b = 100 // b는 variable이니까 가능


    // 바로 초기화를 진행한다면 ': 자료형' 을 해주지 않아도 된다.
    val c = 100
    var d = 100

    // 바로 초기화를 진행하지 않고, 선언만 한다면 타입 선언을 해야 한다
    val e : Int

    var name = "hevton"
}

 

조금 헷갈릴 수 있는데, 일반적인 변수 선언에서는 이렇게 var와 val을 명시해주어야 하지만

아까 1에서 봤던 함수의 인자에서는 val이나 var를 명시할 수 없다.

 

 

3. String Template

기본적인 String 사용 방식이다.

val name = "Hevton"
val lastName = "PYRON"
println("My name is ${name + lastName}")


OUTPUT :
My name is Hevton PYRON

 

 

 

4. 조건식 & 삼항연산자

자바에서 if문과 switch문이 있듯

코틀린에서는 if문과 when문이 있다.

fun max(a : Int, b : Int) : Int {
    if( a > b ) {
        return a
    } else {
        return b
    }
}

두 숫자 중 큰 수를 리턴해주는 함수이다.

 

코틀린에서는 위 함수를 굉장히 짧게 정의할 수도 있다.

코틀린에서의 삼항연산자 를 사용하면 된다.

fun max(a : Int, b : Int) = if(a > b) a else b

두 max함수는 같은 기능을 하는 함수이다. 코틀린의 아주 강력한 기능이다.

 

 

if문의 코드 영역이 여러 줄일 경우에는, 마지막 값을 반환값으로 이용하게 활용할 수 있다.

var a = 5
var b = 3

var bigger = if (a > b) {
    var c = 30
    a // 마지막 줄의 a 값이 변수 bigger에 저장된다.
} else {
    b
}

 

 

이번엔 자바에서의 switch와 같은, 코틀린에서의 when 이다

fun checkNum(score : Int) {

    // when은 switch와 같다
    when(score) {
        0 -> println("this is 0")
        1 -> println("this is 1")
        2, 3 -> println("this is 2 or 3")
        else -> println("I don't know")
    }
    
    // max를 만들 때 처럼, 이렇게 만들 수도 있다.
    var b = when(score) {
        1 -> 1
        2 -> 2
        else -> 3
    }

    when(score) {
        in 90..100 -> println("You are genius") // 90 ~ 100
        in 10..80 -> println("not bad")
        else -> println("okay")
    }
}

 

 

5. Expression vs Statement

Expression은 값을 만들어내는 구문을 의미

Statement는 단지 실행만 하는 구문을 의미

 

조금 헷갈릴 수도 있다.

바로 위 코드블럭에서, 

// when은 switch와 같다
when(score) {
    0 -> println("this is 0")
    1 -> println("this is 1")
    2, 3 -> println("this is 2 or 3")
    else -> println("I don't know")
}

에서 when은 Statement로 쓰인 것이고

var b = when(score) {
    1 -> 1
    2 -> 2
    else -> 3
}

에서 when은 Expression으로 쓰인 것이다.

 

 

코틀린의 모든 함수는 Expression (값을 만들어냄) 이다.

자바에서는 리턴값이 없다 할 떄 void 이지만,

코틀린에서는 리턴값이 없더라도 Unit을 지정해주기 때문이다.

 

if문이 자바에서는 Statement로만 사용할 수 있었는데

// JAVA
if(a > b)
    System.out.println(a)
else
    System.out.println(b)

코틀린에서는 Expression으로도 사용이 가능하다.

var d = if(a > b) a else b

 

 

6. Array and List

Array는 초기에 크기를 지정해주어야한다. 크기가 고정적이다.

 

List는 두가지가 있다.

1. InMutableList : 수정이 불가능. 읽기전용 = List

2. MutableList : 수정이 가능. 읽기/쓰기전용 = MutableList, ArrayList

 

fun array() {
    val array = arrayOf(1, 2, 3)
    val list = listOf(1, 2, 3)

    val array2 = arrayOf(1, "d", 3.4f) // 파이썬처럼 이렇게 가능
    val list2 = listOf(1, "d", 11L) // 파이썬처럼 이렇게 가능

    array[0] = 3
    // list[0] = 4 불가능. 기본적으로 InMutableList

    // MutableList 대표적인 것 = ArrayList
    val arrayList = arrayListOf<Int>() // 헷갈리기 쉬운게, val이라고 해서 리스트 내 값을 못바꾼다? 아님. 참조변수가 고정되는 것 뿐
    arrayList.add(10)
    arrayList.add(20)
    arrayList[0] = 20
    
    val mutable = mutableListOf<Int>()
}

내용을 보면 알 수 있겠지만,, 코틀린에서는 자바와 달리, 그리고 파이썬과 같이

하나의 배열 또는 리스트에서 원소들의 타입이 달라도 무방하다. 이게 아주 강력하다.

 

 

6. 반복문 (For / While)

코틀린에도 for문과 while문이 똑같이 존재한다.

fun forAndWhile() {

    val students = arrayListOf("Kim", "John", "Steve", "Hevton")

    for (name in students) {
        println(name)
    }

    // 리스트를 순회하는 name in students에서, 인덱스도 함께 사용할 수 있다
    for((index, name) in students.withIndex()) {
        println("${index + 1} 번쨰 학생 : ${name}")
    }

    var sum = 0
    for(i in 1..10) { // 1부터 10까지 반복
        sum += i
    }
    println(sum)


    // step 2 : 2칸씩
    // 1 3 5 7 9
    for(i in 1..10 step 2) {

    }

    // until : 전 까지
    for(i in 1 until 100) { // 1..100 과 다른점은, 100을 포함하지 않는 것

    }
    
    // while문
    var index = 0
    while(index < 10) {
        println("current index : ${index}")
        index++
    }
}

다 직관적이다.

 

 

8. NonNull과 Nullable

자바와 코틀린의 가장 큰 차이점이라고 생각한다.

아마 자바도 Null Safty를 지원하는 날이 오지 않을까 싶긴 한데..

 

자바에서는 NPE 를 많이 경험했을 것이다.

NPE : Null Pointer Exception

자바에서는 컴파일 시점에 Null을 잡을 수 없으니 런타임에만 잡아왔고, 

컴파일 시점에서 잡을 수 있도록 코틀린에서 지원한다. Dart의 Null Safty가 생각난다.

fun nullcheck() {

    var name : String = "Hevton" // 기본적으로 NonNull
    
    var nullName : String? = null; // 물음표 붙이면 Nullable


    // 기본적으로 자료형 생략을 하기도 했는데, 그러면 기본적으로 NonNull 타입이다
    var sample_name = "Hevton" // sample_name 은 String 타입
    // Nullable로 선언하고 싶으면 자료형을 생략하지 않고 물음표를 붙여서 명시해주어야 한다
    var sample_name_2 : String? = null // sample_name_2 는 String? 타입


    var nameInUpperCase = name.uppercase() // 대문자로 올려주는 함수가 uppercase


    // 만약에 nullName이 null이면, 함수 수행 없이 null로 대체됨
    // 만약에 nullName이 null이 아니면, 정상적으로 진행
    ( = null이면 수행 안함. null이 아니면 수행함 )
    
    // 이게 추론되어서, nullNameInUpperCase는 String? 타입으로 자동으로 인지됨
    var nullNameInUpperCase = nullName?.uppercase() // nullNameInUpperCase 는 String? 타입

    // Java에서는 if(nullName != null) nullName.uppercase() 해야하는 것 보다 수고스러움이 덜어짐



    // 엘비스(프레슬리) 연산자
    // ?:
    // 위에서는 nullName?.uppercase() 에서 nullName이 null이면 그냥 uppercase 수행 없이 바로 전체가 null로 대체되는데
    // 엘비스 프레슬리 연산자는, null일 경우 기본값을 지정해 주는 방법이다
    val lastName : String? = null
    val fullName = name + " " + (lastName?: "No lastName") // Dart에서 lastName ?? "No lastName" 과 같음

    println(fullName)



    // !!
    // Dart에서의 !와 같음
    // ?와 엘비스 연산자를 수행하는 것을 권고하고, !!는 NPE 발생 가능성이 있으니 사용을 지양

    val aa : String? = "Hello"

//    val notNull : String = aa 에러
    val notNull : String = aa!!

}


// let : 자신의 리시버 객체를 람다식 내부로 옮겨서 실행
// ? 연산자와 같이 사용한다
fun ignoreNulls() {

    val email : String? = "hevton@gmail.com"

    // email이 null이 아니면 람다식 수행되는 로직이다.
    email?.let {
        println("my email is ${email}")
    }

// email이 null이 아니면 {} 구문이 실행되어println이 실행되고
// email이 null이면 실행되지 않는다.

// 아까 위에서
nullName?.uppercase()
처럼, uppsercase()의 함수 수행 대신 {} 를 익명 블록으로 구현한다고 보면 된다.

}

다트의 Null Safty를 코틀린에서도 지원한다.

 

그리고 let 함수는 알아두면 좋겠다.

 

 

여기까지 꽤나 간단하면서 중요한 내용을 되짚자면

코틀린 팁

1. 세미콜론 안쓴다
2. val은 상수 var는 변수
3. Null Safty

 

 

9. Class

자바와 코틀린의 차이점을 생각해보면서 코드를 보면 더 좋겠다.

class Sample {

}

코틀린에는 주생성자와 부생성자가 있다.

class Sample() { // 여기 () 있는건 주 생성자
    
    // 부 생성자
    // 부 생성자는 주 생성자에게 위임을 받아야 함
    constructor(id : String) : this() {
        // constructor에서는 ':' 이후의 부분이 '리턴 자료형'의 의미가 아니다
        // fun 에서는 ':' 이후의 부분이 리턴 자료형을 의미함!
    }
}

 

Default 생성자

생성자를 작성하지 않을 경우 파라미터가 없는 주 생성자가 하나 있는 것과 동일하다.

class Sample {

}

 

초기화 코드를 정의하려면, init 안에 작성하면 된다.

init은 생성자 이후 호출된다.

class Sample {
    
    init {
    
    }
}

 

 

주 생성자가 빈 생성자인 예시인데, 다르게도 할 수도 있따

class Sample(id : String) {

    init {
        Log.d("class", "생성자로부터 전달받은 값은 ${value}")
    }
}

그리고 클래스의 생성자가 호출되면 init 블록의 코드가 실행되고, init 블록에서는 생성자를 통해 넘어온 파라미터에 접근할 수 있다.

 

 

부 생성자를 정의할 때엔, 주 생성자에서 갖고 있는 것을 토대로 추가해야한다.

class Sample(id : String) {

    constructor(id : String, name : String) : this(id) {
    
    }
}

 

 

여기 주 생성자에 초기값을 넣어줄 수도 있다.

class Sample(id : String = "GOOD") {

    constructor(id : String, name : String) : this(id) {
    
    }
}

이렇게 주 생성자에 초깃값을 넣어주면, 기본 생성자까지 추가된 기능을 하게 된다.

참고로 세컨더리 생성자(부 생성자)는 init보다 늦게 호출된다.

 

class Sample(id : String) {

    constructor(id : String, name : String) : this(id) {
    
    }
}

fun main() {
    // val s = Sample() 오류
}

위에서는 기본 생성자로 생성할 수 없는데, 아래에서는 가능하다.

id 기본값은 GOOD으로 들어가 있을 것이다.

class Sample(id : String = "GOOD") {

    constructor(id : String, name : String) : this(id) {
    
    }
}

fun main() {
    val s = Sample()
}

 

또한, 하나 추가적으로 기능이 있다면

주 생성자 변수에 val이나 var을 지시해줄 수 있다.

class Sample(var id : String = "GOOD") {

    constructor(id : String, name : String) : this(id) {

    }
}

이렇게 말이다. 그러면 이 id는 이제부터 클래스 내의 필드(멤버변수) 역할을 한다.

 

이렇게 var이나 val 키워드를 붙이기 전에는,

자바에서 생성자가 따로 존재하듯, 생성할 때에만 id가 지역변수로 사용되고 그 이후에는 사용할 수 없다는 로직과 같다.

// JAVA
class Sample {


    Sample() {}
    Sample(String id) {}
}


public static void main(String args[]) {
    Sample s = new Sample("GOOD");
    // s.id 찾을 수 없음
}

 

var이나 val 을 붙이게 되면, 생성 이후에도 사용할 수 있는 멤버변수로 자리잡게 된다.

아래에서 id는 number처럼 멤버변수의 역할까지 되는 것이다.

class Sample(var id : String = "GOOD") {

    var number : String
    
    constructor(id : String, name : String) : this(id) {

    }
}

 

 

여기까지 헷갈릴 만한 점 한가지가 있다

// 헷갈릴 만한 점 주의!!!!
class A(val field : String) {
    // 클래스 생성자에 val, var을 붙여주면, 클래스 내 필드처럼 사용할 수 있다.
    // 클래스 생성자에 val, var을 붙여주지 않으면, 클래스 생성만 할 때만 쓰인다고 보면 된다
}


// 함수 인자로는 val이나 var을 붙일 수 없다
fun hello(a : String) {

 

함수 인자는 모두 읽기 전용 키워드 val이 생략된 형태이다.

 

 

 

 

코틀린에서 상속은 자바와 같이 최대 하나만 가능하다.

추가로 주의할 점이 있는데, 코틀린에서는 class가 기본적으로 final이기 때문에 상속을 할 수 없다.

 

따라서 상속하고자 하는 클래스는 open 키워드를 class 앞에 붙여줘야 한다.

마찬가지로 override 경우에도, 함수를 open 키워드를 붙여줘야 override가 가능하다.

open class Human() { // 참고로 open class Human이나 open class Human()은 상관없이 같음
    open fun singASong() {
        println("lalala")
    }
}

// 상속하려면 대상 클래스가 open 상태여야 한다  -> 기본적으로 Kotlin에서 클래스는 final이기 때문이다
class Korean : Human() { // 자바와 같이 상속은 하나밖에 못함. 상속할 때에는 생성자 표시를 해주어야 한다.

    // 오버라이딩하려면 마찬가지로 함수도 open 해야 함
    override fun singASong() {
        super.singASong() // 부모 것도 호출 가능
        println("라라랄")
    }
}

 

Human 클래스를 정의할 때, class Human()이나 class Human이나 같다.

주 생성자를 따로 정의하지 않고 class Human 하게 되면 알아서 기본 빈 생성자 () 가 자바처럼 생성된다고 보면 된다.

open class ss {
    // 주 생성자 ss() 있는 것임.
}

// 상속할 때에는 생성자를 명시해주어야함.
class bb : ss() {

}

 

끄읕

 

아래는 클래스 관련실습 코드 예시이다. 참고!

class Sample(var id : String = "GOOD") { // 여기 있는건 주 생성자

    // 부 생성자
    // 부 생성자는 주 생성자에게 위임을 받아야 함
    constructor(id : String, name : String) : this(id) {
        // constructor에서는 ':' 이후의 부분이 '리턴 자료형'의 의미가 아니다
        // fun 에서는 ':' 이후의 부분이 리턴 자료형을 의미함!
    }
}

// 자바와 코틀린의 생성자 생성 차이점
open class Human(n : String = "Anonymous") { // 여기 있는건 주 생성자
    // 이렇게 주 생성자에 초기값까지 넣어주면, 디폴트 생성자 () 까지 추가된 기능을 하게된다.

    /*
    JAVA
    Human(String n) { }
    Human(String n, int a) { }
     */

    // 부 생성자
    // 부 생성자는 주 생성자에게 위임을 받아야 함
    constructor(n : String, age : Int) : this(n) {
        // constructor에서는 ':' 이후의 부분이 리턴 자료형의 의미가 아니다
        // fun 에서는 ':' 이후의 부분이 리턴 자료형을 의미한다
    }

    val name = n


    init {
        // 객체 생성시 호출되는 코드블록
        println("new Human has been born!!")
    }

    fun eatingCake() {
        println("This is so YUMMMYYY ~~~")
    }

    open fun singASong() {
        println("lalala")
    }

}

// JAVA에서는 상속이 extends, Kotlin은 ':'
// 상속하려면 대상 클래스가 open 상태여야 한다  -> 기본적으로 Kotlin에서 클래스는 final이기 때문이다
class Korean : Human() { // 자바와 같이 상속은 하나밖에 못함

    // 오버라이딩하려면 마찬가지로 함수도 open 해야 함
    override fun singASong() {
        super.singASong() // 부모 것도 호출 가능
        println("라라랄")
    }
}

fun main() {

    // 코틀린에서는 자바와 달리 new 키워드 필요 없음
    val human = Human("Hevton")
    human.eatingCake()

    println("this human's name is ${human.name}")

    val k = Korean();
    k.singASong();
}

 

반응형