Kotlin 기본문법 정리
이 글에서는
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();
}