Kotlin] 기본 문법(1)

Kotlin

해당 내용은 테크과학! DiMo Youtube 내용을 보고 정리하였습니다.

학습에 사용한 Web Tool: play.kotlinlang.org

1
2
3
fun main() {
println("Hello, world!!!")
}

클래스 이름: 파스칼 표기법

파스칼 표기법

모든 단어를 대문자로 시작

ClassName

함수나 변수 이름: 카멜 표기법

CamelCase

첫 단어만 소문자로 시작

functionName

변수 선언법

var

일반적으로 통용되는 변수

언제든지 읽기 쓰기가 가능함

val

선언시에만 초기화 가능

중간에 값을 변경할 수 없음

(const나 final과 같은 변수 -> 상수)

클래스에 선언된 변수: Property(속성)

1
2
3
4
fun main() {
var a: Int
println(a)
}

위의 코드를 실행하면 에러가 발생하게 된다.

코틀린은 기존의 언어와는 다르게 값이 할당되지 않으면 Error를 표시하게 된다

Variable 'a' must be initialized

코틀린은 기본 변수로 Null값을 허용하지 않아서 원천적으로 의도치 않은 동작이나 null point exception을 막아준다.

1
2
3
4
fun main() {
var a: Int = 20210325
println(a) // 출력값: 20210325
}

프로그래밍을 하면서 Null이 필요할 때가 있는데 이러할 때는 아래와 같이 하면 된다

1
2
3
4
fun main() {
var a: Int? = null
println(a)
}

자료형 뒤 물음표를 붙이면 nullable 변수로 선언해 줄 수 있다.

:warning:nullable 변수는 null인 상태로 연산할 시 null point exception이 발생할 수 있으므로 주의해서 사용 필요

변수의 초기화를 늦추는 lateinit이나 lazy속성은 클래스에 대한 지식 필요

Primitive Type

기본 자료형은 Java와의 호환을 위해 거의 동일

숫자형 타입

정수형

참고, Kotlin은 8진수의 표기는 지원하지 않음

1
2
3
4
5
6
fun main() {
var intValue:Int = 1234
var longValue:Long = 1234L
var hexValue:Int = 0x4d2
var binValue:Int = 0b10011010010
}

실수형

1
2
3
4
5
fun main() {
var doubleValue:Double = 123.5
var doubleValueWithExp:Double = 123.5e10 // 지수 표현
var floatValue:Float = 123.5f
}

문자형

코틀린 문자열은 UTF-16 BE 규칙을 따르고 있으며 글자 하나가 2bytes의 메모리 공간 사용

1
2
3
4
5
fun main() {
// UTF-16 BE: 코틀린 문자열 관리 규칙
// 글자 하나가 2bytes의 메모리 공간 사용
var charValue:Char = 'k'
}

Boolean

true(참) / false(거짓)

1
2
3
fun main() {
var booleanValue:Boolean = true
}

문자열

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
var stringValue:String = "Kotlin"
println(stringValue)
stringValue = "Hello"
print(stringValue)

stringValue = """Multiline
Kotlin
Hello"""
println(stringValue)
/*
* Multiline
* Kotlin
* Hello
*/
}

형변환과 배열

Type Casting

형변환은 하나의 변수에 지정된 자료형을 호환되는 다른 자료형으로 변환하는 기능

형변환 함수

  • toByte()
  • toShort()
  • toInt()
  • toLong()
  • toFloat()
  • toDouble()
  • toChar()
  • toString()

명시적 형변환(Explicit type casting)

변환될 자료형을 개발자가 직접 지정함

1
2
3
4
fun main() {
var intValue = 1234
var longValue = intValue.toLong()
}

:man_teacher:코틀린은 형변환시 발생할 수 있는 오류를 막기 위해 암시적 형변환은 지원하지 않음

Array

Array<T>

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
var intArr = arrayOf(1, 2, 3, 4, 5)
var nullArr = arrayOfNulls<Int>(5) // 비어있는 배열 만들기

println(intArr[1]) // 2
intArr[1] = 7
println(intArr[1]) // 7

nullArr[0] = 5
println(nullArr[0]) // 5
println(nullArr[1]) // null
}

타입 추론과 함수

타입 추론

1
2
3
4
fun main() {
var a = 5
println(a.javaClass.kotlin.simpleName) // 타입확인: Int
}

var a:Int = 5라고 사용하지 않아도 Kotlin이 타입을 추론하여 Int형태로 선언해주는 것을 알 수 있다.

함수

function의 fun을 이용하여 선언

:runner:연습. 3가지 숫자를 받아서 합해주는 함수를 만들어보기

1
2
3
4
5
6
7
8
fun plusCalc(a:Int, b:Int, c:Int):Int {
return a + b + c
}

fun main() {
var result:Int = plusCalc(5, 3, 2)
println(result) // 10
}

단일 표현식 함수(Single-Expression Function)

1
2
3
4
5
6
fun plusCalc(a:Int, b:Int, c:Int) = a + b + c

fun main() {
var result:Int = plusCalc(5, 3, 2)
println(result)
}

단일 표현식 함수에서는 반환형의 타입을 생략할 수 있다.

함수형 프로그래밍 언어를 이해하기 위해서 함수는 자료형이 결정된 변수라는 개념으로 접근하는 것이 좋다.

조건문

when

다른 언어의 switch문을 when으로 변경하여 사용함

다른 언어의 break문 사용없이 위에 조건에 맞게 되면 아래의 조건은 실행하지 않음

:warning:등호나 부등호의 사용은 불가능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 조건에 따른 출력 */
fun main() {
doWhen(3.toLong())
}

fun doWhen(a: Any) { // Any: 어떤 자료형이든 상관없이 호환되는 코틀린 최상위 자료형
when(a) {
1 -> println("숫자 1입니다")
"Kotlin" -> println("문자열 Kotlin입니다. Hello, Kotlin")
is Long -> println("Long 타입입니다")
!is String -> println("String 타입이 아닙니다")
else -> print("어떤 조건도 맞지 않습니다")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 조건에 따른 변수값 지정 */
fun main() {
var result = doWhen(3.toLong())
println(result)
}

fun doWhen(a: Any):Any { // Any: 어떤 자료형이든 상관없이 호환되는 코틀린 최상위 자료형
var result = when(a) {
1 -> "숫자 1입니다"
"Kotlin" -> "문자열 Kotlin입니다. Hello, Kotlin"
is Long -> "Long 타입입니다"
!is String -> "String 타입이 아닙니다"
else -> "어떤 조건도 맞지 않습니다"
}
return result
}

반복문

파이썬과 다르게 마지막 값까지 포함하여 반복

1
2
3
4
5
fun main() {
for (i in 0..9) {
println(i)
}
}

1씩 증가하는 것이 아닌 단계별 증가를 시키기 위해서는 step을 사용하면 된다

1
2
3
4
5
fun main() {
for (i in 0..9 step 3) {
println(i)
}
}

..이 아닌 downTo를 이용하여 감소를 시킬 수 있다

1
2
3
4
5
fun main() {
for (i in 9 downTo 0) {
println(i)
}
}
1
2
3
4
5
fun main() {
for (i in 'a'..'e') {
println(i)
}
}

step을 사용할 수도 있다

character형태가 숫자로도 나타낼 수 있기에…

흐름제어

break문과 continue문은 동일하나 코틀린에서 label 기능을 사용 할 수 있다

다중 반복문을 통해서 label의 기능을 살펴보자

먼저, 일반적인 이중 반복문에서 break를 이용한 방식을 보면 첫번째 for문으로 인해 이후의 값들도 지속됨을 알 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
for (i in 1..10) {
for (j in 1..10) {
print(i)
print("\t")
println(j)
if (i == 1 && j == 2) {
break
}
}
}
}

다음의 label의 기능을 쓴 방식은 처음 for문을 빠져나가게 해줌으로서 이후의 값이 출력되지 않는다

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
loop@for (i in 1..10) {
for (j in 1..10) {
print(i)
print("\t")
println(j)
if (i == 1 && j == 2) {
break@loop
}
}
}
}

이렇게 보았을 때 시간 복잡도상으로 따진다면 큰 이득을 얻을 수 있다

보통은 함수처리하여 return으로 마무리 했었는데 좋은 기능이 추가 되었음을 알 수 있다

Class

고유의 특징값(속성) + 기능의 구현(함수)로 이루어짐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
var a = Person("오세훈", 1961)
var b = Person("박영선", 1960)
var c = Person("안철수", 1962)

a.introduce()
b.introduce()
c.introduce()
}

class Person(var name:String, val birthYear: Int) {
fun introduce() {
println("안녕하십니까 시민 여러분 저는 ${birthYear}년생 ${name} 입니다")
}
}

여기서 클래스 옆에 var name:String, val birthYear: Int는 클래스의 속성들을 선언함과 동시에 생성자 역시 선언하는 방법

생성자(Constructor)

새로운 인스턴스를 만들기 위해 호출하는 특수한 함수

생성자를 호출하면 클래스의 인스턴스를 만들어 반환 받을 수 있음

  • 인스턴스의 속성을 초기화
  • 인스턴스 생성시 구문을 수행

init함수를 통해 구문 수행 가능

init 함수는 파라미터나 반환형이 없는 특수한 함수로 생성자가 생성될 때 실행됨

1
2
3
4
5
6
7
8
9
10
11
fun main() {
var a = Person("오세훈", 1961)
var b = Person("박영선", 1960)
var c = Person("안철수", 1962)
}

class Person(var name:String, val birthYear: Int) {
init {
println("안녕하십니까 시민 여러분 저는 ${this.birthYear}년생 ${this.name} 입니다")
}
}

기본 생성자: 클래스를 만들 때 기본으로 선언init

보조 생성자: 필요에 따라 추가적으로 선언constructor

:man_teacher:보조 생성자를 만들 때는 반드시 기본 생성자를 통해 속성을 초기화 해주어야 한다

보조 생성자가 기본 생성자를 호출하도록 하려면 아래와 같이 :this라는 키워드를 사용하고 기본 파라미터가 필요한 부분을 괄호 안에 넣어주면 된다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {
var a = Person("오세훈", 1961)
var b = Person("박영선", 1960)
var c = Person("안철수", 1962)

var baby1 = Person("신생아1")
var baby2 = Person("신생아2")
}

class Person(var name:String, val birthYear: Int) {
init {
println("안녕하십니까 시민 여러분 저는 ${ this.birthYear }년생 ${ this.name } 입니다")
}

constructor(name:String):this(name, 2021) {
println("보조 생성자가 실행되었습니다")
println("생성된 보조 생성자 ${ name }입니다")
}
}

상속(Inheritance)

코틀린은 상속 금지가 기본이기에 상속을 하기 위해서는 부모 클래스가 open이어야 한다

open: 클래스가 상속될 수 있도록 클래스 선언시 붙여줄 수 있는 키워드

  1. 서브 클래스는 슈퍼 클래스에 존재하는 속성과 ‘같은 이름’의 속성을 가질 수 없다
  2. 서브 클래스가 생성될 떄는 슈퍼 클래스의 생성자까지 호출하여야 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
fun main() {
var a = Animal("John", 3, "DOG")
var b = Dog("John", 3)

a.introduce()
b.introduce()

// a.bark() // 부모클래스에서 자식클래스 함수 접근X
b.bark()

var c = Cat("Loi", 2)
c.introduce()
c.meow()
}

open class Animal (var name:String, var age:Int, var type:String) {
fun introduce() {
println("${type} ${name} ${age}")
}
}

class Dog (name:String, age:Int) : Animal (name, age, "DOG") {
// 클래스의 자체 속성으로 만들어주는 var을 붙이지 말것
// 같은 속성을 붙이게 되면 서브 클래스의 속성이 되기에 상속 규칙 무시됨
fun bark() {
println("Bark! Bark!")
}
}

class Cat (name:String, age:Int) : Animal (name, age, "CAT") {
fun meow() {
println("Meow~")
}
}

오버라이딩과 추상화

Overriding

슈퍼 클래스에서 함수가 open이 되어 있는 것은 서브 클래스에서 override를 사용하여 재정의를 할 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fun main() {
var a = Animal("John", 3, "DOG")
var b = Dog("John", 3)
var c = Cat("Loi", 2)

a.sound()
b.sound()
c.sound()
}

open class Animal (var name:String, var age:Int, var type:String) {
fun introduce() {
println("${type} ${name} ${age}")
}

open fun sound() {
println("Sound")
}
}

class Dog (name:String, age:Int) : Animal (name, age, "DOG") {
override fun sound() {
println("Bark! Bark!")
}
}

class Cat (name:String, age:Int) : Animal (name, age, "CAT") {
override fun sound() {
println("Meow~")
}
}

Abstraction

추상화는 선언부만 있고 기능이 구현되지 않은 추상함수(abstraction function)와 추상클래스(abstraction class)로 구성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {
var rabbit = Rabbit()
rabbit.sniff()
rabbit.eat()

}

abstract class Animal {
abstract fun eat()
fun sniff() {
println("킁킁")
}
}

class Rabbit : Animal() {
override fun eat() {
println("당근을 먹습니다")
}
}

Interface

다른 언어에서 인터페이스는 추상함수로만 이루어져 있는 순수 추상화 기능

코틀린에서는 인터페이스도 속성, 추상함수, 일반함수를 가질 수 있다

다만, 추상함수는 생성자를 가질 수 있는 반면에 인터페이스는 생성자를 가질 수는 없다

구현부가 있는 함수: open함수로 간주

구현부가 없는 함수: abstract함수로 간주

별도의 키워드가 없어도 포함된 모든 함수를 서브클래스에서 구현 및 재정의 가능

한번에 여러 인터페이스를 상속할 수 있기에 유연한 설계 가능

[정리]

오버라이딩: 이미 구현이 끝난 함수의 기능을 서브클래스에서 재정의 할 때

추상화: 형식만 선언하고 실제 구현은 서브클래스에 일임할 때

인터페이스: 서로 다른 기능들을 여러개 물려주어야 할 때

접근제한자

Access Modifier

  • public
  • internal
  • private
  • protected

패키지 스코프

키워드 접근 범위
public(기본값) 어떤 패키지에서도 접근 가능
internal 같은 모듈 내에서만 접근 가능
private 같은 파일 내에서만 접근 가능

*protected는 사용하지 않음

클래스 스코프

키워드 접근 범위
public(기본값) 클래스 외부에서 늘 접근 가능
private 클래스 내부에서만 접근 가능
protected 클래스 자신과 상속받은 클래스에서 접근 가능

*internal은 사용하지 않음

고차함수와 람다함수

고차함수

(String)->Unit: 반환형에는 값이 없다라는 의미로 Unit를 사용하며, 람다식에 주로 사용

function1(::function2): 일반 함수를 고차 함수로 변경해 주는 연산자로 ::를 사용

1
2
3
4
5
6
7
8
9
10
11
fun main() {
b(::a) // 일반 함수를 고차 함수로 변경해 주는 연산자
}

fun a (str: String) {
println("$str 함수 a")
}

fun b (function: (String)->Unit) { // Unit: 반환형에는 값이 없다
function("b가 호출함")
}

Lambda Function

위의 내용을 람다식을 써서 이렇게 나타낼 수도 있다

1
2
3
4
5
6
7
8
fun main() {
var a: (String) -> Unit = { str:String -> print("$str 함수 a") }
b(a)
}

fun b (function: (String)->Unit) { // Unit: 반환형에는 값이 없다
function("b가 호출함")
}

위의 람다함수를 이렇게 간략하게 쓸수도 있다

위의 람다함수에서는 String을 하나 받지만 반환값이 없기에 아래와 같이 쉽게 나타낼 수 있다

1
2
3
4
5
6
7
8
fun main() {
var a = { str:String -> print("$str 람다함수") }
b(a)
}

fun b (function: (String)->Unit) { // Unit: 반환형에는 값이 없다
function("b가 호출함")
}

람다식에서도 여러줄을 사용할 수 있다

1
2
3
4
5
6
7
8
fun main() {
val calculate:(Int, Int) -> Int = { a, b ->
println(a)
println(b)
a + b // return값, return 타입 있을시에는 맨 아래줄 리턴 대상
}
println(calculate(3, 5))
}

파라미터가 없을 시,

1
2
3
4
5
6
fun main() {
val calculate:() -> Unit = {
println("계산값 없음")
}
calculate()
}

파라미터가 하나일 경우 다른방법으로 파라미터 가져오기: it

1
2
3
4
5
6
fun main() {
val calculate:(Int) -> Unit = {
println("${it} 파라미터 하나 가져오기")
}
calculate(7)
}

Scope Function

함수형 언어의 특징을 조금 더 편리하게 사용할 수 있도록 기본 제공하는 함수들

  • apply
  • run
  • with
  • also
  • let

apply: 인스턴스를 생성한 후 변수에 담기 전에 ‘초기화 과정’을 수행할 때 많이 사용됨

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
var book = Book("Kotlin", 30000).apply {
name = "[Sale]" + name
discount()
}
println(book.price)
}

class Book(var name: String, var price: Int) {
fun discount() {
price = (price * 0.7).toInt()
}
}

main함수와 별도의 스코프에서 인스턴스의 변수와 함수를 조작하므로 코드가 깔끔해진다

run: 스코프 안에서 참조연산자를 사용하지 않아도 된다는 점은 apply와 같지만, 일반 람다함수처럼 인스턴스 대신 마지막 구문에서 값 반환

이미 인스턴스가 만들어진 후에 인스턴의 함수나 속성을 scope 내에서 사용해야할 떄 유용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
var book = Book("Kotlin", 30000).apply {
name = "[Sale]" + name
discount()
}
book.run {
println("Product: ${name}, Price: ${ price }")
// Product: [Sale]Kotlin, Price: 21000
}
}

class Book(var name: String, var price: Int) {
fun discount() {
price = (price * 0.7).toInt()
}
}

with: run과 동일한 기능을 가지지만 단지 인스턴스를 참조연산자 대신 파라미터로 받는다는 차이

처리가 끝나면 인스턴스를 반환: apply, also

처리가 끝나면 최종값을 반환: run, let

참조연산자 없이 인스턴스의 변수와 함수를 사용: apply, run

파라미터로 인스턴스를 넘긴 것처럼 it을 통해서 인스턴스 사용: also, let

also와 let이 파라미터를 통해서 인스턴스를 받는 이유

같은 이름의 변수나 함수가 scope 바깥에 중복되어 있는 경우에 혼란을 방지하기 위해서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main() {

var price = 5000

var book = Book("Kotlin", 30000).apply {
name = "[Sale]" + name
discount()
}
book.run {
println("Product: ${name}, Price: ${ price }")
}
}

class Book(var name: String, var price: Int) {
fun discount() {
price = (price * 0.7).toInt()
}
}

이러한 경우 27000이 나와야하지만, 위의 price와 혼동이 생겨 5000이 나오게 됨