Programming Language/Kotlin

코틀린 뽀개기

수연초이 2022. 10. 8. 19:49

코틀린이란?

  • 자바 플랫폼에서 돌아가는 새로운 프로그래밍 언어
  • 간결하고 실용적
  • 자바 코드와의 상호운용성을 중요시
  • 코틀린 컴파일러가 생성한 바이트 코드는 일반적인 자바 코드와 똑같이 효율적으로 실행되어 성능 측에서 손해가 없다. (요 부분은 나중에 더 공부해보자) 참고로 코틀린이 자바 문법으로 컴파일하는 것보단 둘다 JVM 언어이므로 코틀린으로 만들어낸 바이트 코드나 자바로 만들어낸 바이트 코드 결과물이 같다.

 

정적 타입 지정 언어

  • 굉장히 똑똑한 컴파일러. x = 1인 경우 int형으로 타입을 확정짓는다.
  • 자바와 달리 타입을 뒤에 명시한다. 타입을 생략 가능. 변수를 앞에 선언한다.
  • 세미콜론(;)을 붙이지 않아도 된다.
var x: Int = 1
var x = 1

 

함수

  • 기본적으로 자바의 메서드는 클래스가 있기 때문에 메서드가 존재한다.
  • 코틀린은 함수가 일급 객체이므로 함수를 최상위 수준에 정의 가능하다. 꼭 클래스안에 넣을 필요가 없다.
  • 코틀린은 함수를 선언할 때 `fun` 키워드를 사용한다.
  • main(String[] args)도 필요 없이 main() 으로 가능하다.
fun main() {
	println("I'm SuyeonChoi.")
}

함수와 메서드의 차이?

  • 함수: 단일 객체로도 존재 가능
  • 메서드: 기본적으로 클래스에 있는 함수. 클래스에 포함된다.

 

변수 선언

  • val: 값이 변경되지 않는 변수. 값 재할당 불가. final이 생략되어있다고 생각하면 된다.
  • var: 값이 변경될 수 있는 변수

자바에서는 기본적으로 함수는 무조건 블록(중괄호)가 있어야한다. 그러나 코틀린에서는 한줄로 표현할 수 있는 경우 중괄호를 생략할 수 있다!(리턴 대신 등호 사용 가능)

 

블록이 본문인 함수

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

식이 본문인 함수

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

 

Person 학습 테스트

Person.java

public class Person {
    private final String name;
    private final int age;
    private String nickname;

    public Person(final String name, final int age, final String nickname) {
        this.name = name;
        this.age = age;
        this.nickname = nickname;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getNickname() {
        return nickname;
    }
}

해당 자바 코드를 코틀린으로 변환하면?

Person.kt

class Person(val name: String, val age:Int, val nickname: String)
  • 클래스는 public이 기본이고 생략 가능
  • name, age, nickname은 var 타입인 경우 getter와 setter가 제공되고, val 타입인 경우 getter만 제공
  • 자바에서는 name, age, nickname을 '필드'라고 하고, getter와 setter를 포함하여 '프로퍼티'로, 클래스 및 필드 기반이다. 코틀린은 프로퍼티 기반이다. 필드라는 개념이 없다. 
  • name, age, nickname은 현재 모두 public으로 외부에서 접근 가능. name을 외부에서 접근하지 못하도록 하려면? private 키워드를 붙여주면 된다.
class Person(private val name: String, val age:Int, val nickname: String)

하지만 여기서 조금 더 코틀린 다운 코드를 만들어야한다...

 

Person 클래스 테스트

@Test
fun constructor() {
	assertDoesNotThrow {
    		Person("최수연", 2, "페퍼")
    }
}
  • Person 생성 시 new 키워드가 필요 없다.
  • (val name: String, val age:Int, val nickname: String) ⬅️ 이 부분이 주 생성자이다(Primary Constructor). 자바에서는 주 생성자가 개념적으로 있지만, 코틀린은 문법적으로 지원한다.
  • assertDoesNotThrow의 { } 중괄호는 람다. 자바에서 람다는 무조건 소괄호로 시작되는 부분이 있어야하지만, 코틀린은 생략 가능

닉네임 없이 생성할 때, 이름을 닉네임의 값으로 설정하고 싶다면?

class Person(val name: String, val age:Int, val nickname: String) {
	constructor(name: String, age: Int) : this(name, age, name)
}


class PersonTest {
	@Test
        fun constructor() {
            assertDoesNotThrow {
                    Person("최수연", 2)
            }
        }
}
  • 프로퍼티는 기본적으로 주 생성자에서 정의한다. 주 생성자의 역할은 생성자 + 프로퍼티 선언이다. 따라서 부 생성자에서는 var, val이 붙어있지 않다. 즉, var과 val는 프로퍼티에만 붙는다.
  • constructor() 로 부 생성자 생성 가능
  • 주 생성자에서도 constructor를 붙일 수 있지만 생략 가능. 주 생성자를 감추고 싶은 경우 private constructor로 사용 가능
class Person private constructor(val name: String, val age:Int, val nickname: String)

해당 코드를 더욱 코틀린스럽게 바꿀 수 있다. 코틀린은 기본 값인 default value가 존재한다.

class Person(val name: String, val age:Int, val nickname: String = name)

이렇게 두 개의 생성자가 한 줄의 문장으로 표현 가능하다.

 

코틀린은 파라미터에 이름을 지정해서 명시할 수 있다.

class PersonTest {
	@Test
        fun constructor() {
            assertDoesNotThrow {
                    Person("최수연", nickname = "페퍼", age = 2)
            }
        }
}

생성자의 순서대로 파라미터를 넣지 않아도 된다. 빌더 패턴이 필요가 없다.

 

Null

코틀린은 기본적으로 타입만 쓰는 경우, null이 들어갈 수 없다.

@Test
fun nullable() {
	Person("최수연", 2, null) // 컴파일 오류!
}

null이 들어갈 수 있게 하려면, 타입에 ?를 붙이면 된다.

class Person(val name: String, val age:Int, val nickname: String? = name)

@Test
fun nullable() {
	Person("최수연", 2, null) // OK
}

 

 Data Class

@Test
fun `data class`() {
	val person1 = Person("최수연", 2, "페퍼")
	val person2 = Person("최수연", 2, "페퍼")
    
    	assertThat(person1).isEqualTo(person2) // 실패. 주소값이 다르다
}
  • 백틱(`)을 사용하면 언더스코어(_) 없이 띄어쓰기 가능

자바와 같이 equals & hashCode를 구현하면 테스트가 통과될 것이다. 

class Person(val name: String, val age:Int, val nickname: String? = name) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Person

        if (name != other.name) return false
        if (age != other.age) return false
        if (nickname != other.nickname) return false

        return true
    }

    override fun hashCode(): Int {
        var result = name.hashCode()
        result = 31 * result + age
        result = 31 * result + (nickname?.hashCode() ?: 0)
        return result
    }
}

class PersonTest {

    @Test
    fun `data class`() {
        val person1 = Person("최수연", 2, "페퍼")
        val person2 = Person("최수연", 2, "페퍼")

        assertThat(person1).isEqualTo(person2) // success
    }
}

여기서 더 쉬운 방법이 있다. Person 클래스 앞에 data 키워드를 붙여보자.

data class Person(val name: String, val age:Int, val nickname: String? = name)

class PersonTest {

    @Test
    fun `data class`() {
        val person1 = Person("최수연", 2, "페퍼")
        val person2 = Person("최수연", 2, "페퍼")

        assertThat(person1).isEqualTo(person2) // success
    }
}
  • 데이터 클래스는 기본적으로 equals, hashCode를 오버라이딩. copy, toString까지 모두 지원한다.
  • 해당 메서드들은 생성자에 있는 프로퍼티를 기준으로 구현된다.
  • 단, 특정 값만 가지고 equals, hashCode를 구현하고 싶으면 오버라이딩을 해야한다.

 

프로퍼티 추가하기

data class Person(val name: String, val age:Int, val nickname: String? = name) {
	var phoneNumber: String // 컴파일 에러. 코틀린은 기본적으로 클래스가 인스턴스화 될 때 기본값이 필요하다.
}

기본값을 만들기 위해서 가장 쉬운 방법은 생성자를 사용하는 것이다. (자연스럽게 언어에서 객체지향적인 코드를 만들도록 한다!)

일단은 빈 문자열로 생성해보자.

data class Person(val name: String, val age:Int, val nickname: String? = name) {
	var phoneNumber: String = "" // 기본값으로 빈 문자열
}

@Test
fun setter() {
	val person = Person("최수연", 2, "페퍼")
        assertThat(person.phoneNumber).isEmpty() // getter로 값 접근
        person.phoneNumber = "01012345678" // setter로 값 지정
        assertThat(person.phoneNumber).isEqualTo("01012345678") // getter로 값 접근
}
  • 테스트 코드를 보면 필드 접근이 없다.
  • phoneNumber가 일치하지 않더라도 다른 값이 일치하면 isEqualTo가 통과한다. phoneNumber는 주 생성자에 없는 프로퍼티이기 때문이다.

함수 생성

data class Person(val name: String, val age: Int, val nickname: String? = name) {
    var phoneNumber: String = ""

    fun greeting() { // 함수
        println("Hello$name") // 문자열 템플릿
    }
}

 

IntelliJ 코틀린 컨벤션 설정하기

Gradle > help > ktlintApplyToIdea 

코틀린 컨벤션을 틀리면 커밋이 되지 않는다.