Java 와 다른 kotlin 문법
프로그래밍/kotlin 2023. 7. 4. 01:07kotlin을 사용하면서 java와 다른 점들과 샘플 코드들을 정리 한다.
0. main 함수가 클래스 안에 있을 필요가 없다.
main 함수를 클래스 없이 정의 하면 파일명으로 클래스가 생성되고 그안에 main 함수가 들어가게 된다.
fun main(args: Array<String>) { | |
println("Hello World!") | |
} | |
//// java code | |
//public final class MainKt { | |
// public static final void main(@NotNull String[] args) { | |
// System.out.println("Hello World!"); | |
// } | |
//} |
1. 기본 타입들이 모두 클래스 이다.
기본 타입들은 java와 크게 다르지 않으나 모두 클래스 형태로 되어 있다.
// Base type is class | |
fun main(args: Array<String>) { | |
val a: Byte = 0 // 8 | |
val b: Short = 0 // 16 | |
val c: Int = 0 // 32 | |
val d: Long = 0 // 64 | |
val e: UByte = 0u | |
val f: UShort = 0u | |
val g: UInt = 0u | |
val h: ULong = 0u | |
val i: Float = 0F | |
val j: Double = 0.0 | |
val k: Float = 0F | |
val l: Double = 0.0 | |
val bool: Boolean = true | |
val ch: Char = 'a' | |
val str: String = "abc" | |
} |
2. 컴파일 시점에 기본 타입의 범위에 대한 체크가 이루어진다.
val a: Byte = 500 처럼 최대 값을 벗어나는 값으로 초기화를 할때 에러를 발생 시킨다.
초기 잘못된 타입에 대한 overflow를 예방 할수 있다.
// If out of the range according to the type, an error occurs. | |
// The type is automatically set according to the given value. | |
fun main(args: Array<String>) { | |
// val a: Byte = 500 // compile error | |
val c = 0 | |
val bool = true | |
val ch = 'a' | |
val str = "abc" | |
c.toString() // has method | |
} |
3. 함수 정의는 fun, 변수 정의는 val, var을 앞에 붙인다.
fun foo(a: Int): Int // fun 함수명(파라메터): 반환타입 형식으로 함수를 정의 한다.
변수앞에는 val, var을 붙이고 val 은 value로 변경 되지 않는 값, var은 variable로 변경 되는 값을 의미한다.
// define function | |
fun foo(a: Int): Int { | |
return 0 | |
} | |
// val : value, var : variable | |
fun main(args: Array<String>) { | |
val a = foo(0) | |
// val, var | |
val c = 0 | |
c = 1 // compile error | |
var d = 0 | |
d = 1 | |
} |
4. for 루프 사용하기
여러 스타일로 사용이 가능하다.
// for loop | |
fun main(args: Array<String>) { | |
for (x in 1..5) { | |
// 1, 2, 3, 4, 5 | |
} | |
for (x in 1 until 5) { | |
// 1, 2, 3, 4 | |
} | |
for (x in 5 downTo 1) { | |
// 5, 4, 3, 2, 1 | |
} | |
for (x in 1..5 step 2) { | |
// 1, 3, 5 | |
} | |
for (item in args) { | |
} | |
for (item in args.indices) { | |
} | |
} |
5. if, when
val c = if (a == 5) "a is 5" else "a is not 5" 처럼 if 문을 값으로 사용할수 있다. 이때 항상 else가 있어야 한다.
when은 switch 대신 사용한다고 생각하면 된다. default 대신 else를 사용한다.
when에서 사용하는 값은 기본 타입에서 클래스 객체까지 사용되고,
','를 사용하여 여러 값을 하나의 항목으로 묶을수 있다.
// if, when | |
class Cat(n: String, w: Int) { | |
val name = n | |
var weight = w | |
} | |
fun main(args: Array<String>) { | |
val a = 5 | |
val b = "abc" | |
val c = if (a == 5) "a is 5" else "a is not 5" | |
println(c) | |
when (a) { | |
1 -> println("1") | |
2,3,4 -> { println("2,3,4") } | |
5 -> println("5") | |
else -> println("else") | |
} | |
when (b) { | |
"abc" -> println("1") | |
else -> println("else") | |
} | |
val cat = Cat("a", 0) | |
val cat1 = Cat("a", 0) | |
// val cat1 = cat | |
when (cat) { | |
cat1 -> println("cat 1") | |
else -> println("cat else") | |
} | |
} |
6. 멤버 변수 정의 하기
클래스 명 파라메터를 val, var로 정의를 하면 멤버 변수가 된다. data class 타입에서 주로 사용 된다.
멤버 변수를 전달 받은 파라메터로 초기화 가능 하고, init 블럭을 사용해서 초기화 할수도 있다.
var 변수의 경우 lateinit 을 사용하여 초기화 시점을 늦출수 있다.(사용하기 전에 초기화 하면 된다.)
lateinit은 val 변수에는 사용할수 없다. val은 값 변경이 되지 않아 생성시점에 초기화 되어야 한다.
// class | |
class Dog(val name: String, var weight: Int) { | |
} | |
class Cat(n: String, w: Int) { | |
val name = n | |
var weight = w | |
} | |
class Wolf(n: String, w: Int) { | |
val name: String | |
var weight: Int | |
init { | |
name = n | |
weight = w | |
} | |
} | |
class Duck(n: String, w: Int) { | |
val name = n | |
// lateinit var weight: Int // lateinit - Not applicable to basic types | |
var weight: Int = 0 | |
lateinit var dog: Dog | |
fun foo() { | |
weight = 0 | |
dog = Dog("aaa", 0) | |
} | |
} |
7. getter, setter 정의 하기
Java 에서는 get~, set~ 메소드를 정의 해야 하나 kotlin에서는 멤버변수 밑에 get(), set(value)를 붙여 주면 된다.
간단하게 getter, setter가 정의 되고, 변수를 사용하듯이 값을 할당 하거나 사용하면 자동으로 set, get이 호출된다.
// getter setter | |
class Cat() { | |
val name: String | |
get() { return "abc" } | |
var weight: Int = 0 | |
get() { | |
return if (field < 0) | |
-1 | |
else | |
field | |
} | |
set(value) { | |
println("Hello") | |
field = value | |
} | |
} |
8. 상속 사용하기
kotlin의 class는 자바의 final class 이다. 상속이 되지 않는다.
kotlin의 open class는 자바의 class 이다. 상속이 가능하다.
overriding 가능한 함수는 앞에 "open"을 붙여준다. 사용할때는 "override" 를 앞에 붙인다.
추상 클래스는 "abstract" 키워드를 사용하여 정의 한다.
// open class | |
open class Animal() { | |
open val name: String = "" | |
open fun print() { | |
println("Animal $name") | |
} | |
private fun print2() { | |
println("Animal2 $name") | |
} | |
} | |
class Cat(): Animal() { | |
override val name: String = "cat" | |
override fun print() { | |
println("Cat $name") | |
} | |
fun print2() { | |
println("Cat2 $name") | |
} | |
} | |
fun main(args: Array<String>) { | |
val cat = Cat() | |
val animal: Animal = cat | |
cat.print() | |
cat.print2() | |
animal.print() | |
animal.print2() | |
} | |
// =================================================== | |
// abstract class | |
abstract class Animal() { | |
abstract val name: String | |
abstract fun print() | |
private fun print2() { | |
println("Animal2 $name") | |
} | |
} | |
class Cat(): Animal() { | |
override val name: String = "cat" | |
override fun print() { | |
println("Cat $name") | |
} | |
fun print2() { | |
println("Cat2 $name") | |
} | |
} | |
fun main(args: Array<String>) { | |
val cat = Cat() | |
val animal: Animal = cat | |
cat.print() | |
cat.print2() | |
animal.print() | |
// animal.print2() | |
println("${animal is Cat}") | |
val animal2 = cat as Animal | |
} |
9. null 안정성 - 1
변수를 정의 할때 "?"를 붙여 null 사용여부를 결정한다.
var testName: String? = null
사용 할때 "!!"를 붙이면 java에서 사용할때처럼 null 값일 경우 null pointer exception이 발생한다.
"?"를 붙여서 사용한다면 null 값을 경우 아무런 동작을 하지 않는다.
dog.printName() // compile error
cat2.printName()
둘다 if 문에서 null 체크를 했으나 하나는 컴파일 에러가 발생하고, 하나는 에러가 발생하지 않는다.
차이는 변수의 값이 if 문 이후 변경이 가능한지 여부이다.
dog은 멤버 변수로 멀티 스레드로 동작할 경우 if문을 지난 직후 다른 스레드에서 변경하는 상황이 발생할수 있다.
cat2는 로컬 변수로 함수내에서 변경이 불가능 하여 null 체크 후 "." 만으로 사용이 가능 하다.
// null safety - 1 | |
class Dog(n: String, w: Int) { | |
val name = n | |
var weight = w | |
var testName: String? = null | |
fun printName() { | |
testName = "aa" | |
} | |
} | |
class Cat(n: String, w: Int) { | |
val name = n | |
var weight = w | |
var dog: Dog? = null | |
fun printName() { | |
dog = Dog("a", 5) | |
} | |
fun testPrintName() { | |
if (dog != null) { | |
// dog.printName() // compile error | |
} | |
dog!!.printName() // if null, exception | |
dog?.printName() | |
} | |
} | |
fun testPrint1() { | |
var cat2: Cat? = null | |
if (cat2 != null) { | |
cat2.printName() | |
} | |
} |
10. null 안정성 - 2
dog?.let 은 dog이 null 이 아닐 경우에만 동작하는 블럭을 정의 하고 dog을 "it"을 사용하여 접근한다.
엘비스 연산자 "?:"는 if 문을 간소화 해준다.
// null safety - 2 | |
class Dog(n: String, w: Int) { | |
val name = n | |
var weight = w | |
} | |
class Cat(n: String, w: Int) { | |
var dog: Dog? = null | |
fun testPrintName() { | |
var num: Int? = null | |
if (dog != null) { | |
println("dog is not null") | |
} | |
dog?.let { | |
println("dog is not null ${it.name}") | |
} | |
val aa = if (num != null) { | |
num | |
} else { | |
-1 | |
} | |
val bb = num ?: -1 | |
} | |
} |
11. data 클래스
class 명 앞에 data를 붙여 정의 하는 것으로 생성자 파라메터에 정의 값이 동일할 경우 == 연산시 true를 리턴한다.
동일한 객체인지를 확인 할때는 "==="를 사용한다.
data 클래스는 내부적으로 copy, equals 등 몇가지 메소드를 정의한다.(java 코드로 변환해서 비교 하기 추천)
data class Book(val title: String, val isNew: Boolean) | |
class BookNotData(val title: String, val isNew: Boolean) | |
fun main(args: Array<String>) { | |
val nr1 = BookNotData("Snow White", false) | |
val nr2 = BookNotData("Snow White", false) | |
// val nr3 = nr1.copy(title = "Cinderella") // compile error | |
println("nr1 hash code : ${nr1.hashCode()}") | |
println("nr2 hash code : ${nr2.hashCode()}") | |
println("nr1 toString : ${nr1.toString()}") | |
println("nr2 toString : ${nr2.toString()}") | |
println("nr1 == nr2? ${nr1 == nr2}") | |
println("nr1 === nr2? ${nr1 === nr2}") | |
println("#####################################################") | |
val r1 = Book("Snow White", false) | |
val r2 = Book("Snow White", false) | |
val r3 = r1.copy(title = "Cinderella") | |
println("r1 hash code : ${r1.hashCode()}") | |
println("r2 hash code : ${r2.hashCode()}") | |
println("r3 hash code : ${r3.hashCode()}") | |
println("r1 toString : ${r1.toString()}") | |
println("r2 toString : ${r2.toString()}") | |
println("r3 toString : ${r3.toString()}") | |
println("r1 == r2? ${r1 == r2}") | |
println("r1 === r2? ${r1 === r2}") | |
println("r1 == r3? ${r1 == r3}") | |
val (title, isNew) = r1 | |
println("title is $title and it is new $isNew") | |
} |
12. 배열 사용하기
arrayOf 를 통해 값이 설정된 배열을 리턴한다.
배열의 값은 변경이 가능하다(List 와 비교 하면 어떤 의미인지 알수 있다.)
값 설정외 갯수를 정의 하는 방법도 있고 "IntArray(10) { index -> index }" 처럼 초기값을 설정하는 것이 가능하다.
Int 타입 처럼 숫자 관련 배열의 경우 sum, average 등의 자주 사용하는 연산 함수들이 추가 된다.
fun mainTest1(args: Array<String>) { | |
val array = arrayOf(1, 3, 2) | |
val array2 = arrayOf('a', 'b', 'c') | |
val array3 = arrayOf("ab", "bc", "ca") | |
var array4: Array<String?> = arrayOfNulls(2) | |
val array5 = IntArray(10) | |
val array6 = IntArray(10) { index -> index } | |
print("array5 : ") | |
for (item in array5) { | |
print("$item, ") | |
} | |
println() | |
print("array6 : ") | |
for (item in array6) { | |
print("$item, ") | |
} | |
println() | |
} | |
fun mainTest2(args: Array<String>) { | |
val array = arrayOf(1, 3, 2) | |
val size = array.size | |
for (item in array) { | |
print("$item, ") | |
} | |
println() | |
array.reverse() | |
for (item in array) { | |
print("$item, ") | |
} | |
println() | |
val isIn = array.contains(1) | |
val sum = array.sum() | |
val average = array.average() | |
println("$sum, $isIn, $average") | |
array.sort() | |
for (item in array) { | |
print("$item, ") | |
} | |
println() | |
} |
13. List, MutableList
주요 데이터 저장 타입에는 순서가 있는 List, 중복값 없는 Set, key 와 연결 되는 Map 이 있다.
이 타입들은 값 변경이 불가능하고 변경을 원할 경우 Mutable 이 붙어 있는 타입으로 정의 한다.(MutableList...)
fun main(args: Array<String>) { | |
val muList = mutableListOf("tea", "egg") | |
val list = listOf("tea", "egg") | |
// list[0] = "a" | |
// list.add("test") | |
val list2 = muList.toList() | |
muList.add("apple") | |
val list3 = muList.toList() | |
print("list2 : ") | |
for (item in list2) { | |
print("$item, ") | |
} | |
println() | |
print("list3 : ") | |
for (item in list3) { | |
print("$item, ") | |
} | |
println() | |
muList[0] = "milk" | |
print("muList : ") | |
for (item in muList) { | |
print("$item, ") | |
} | |
println() | |
println("muList size : ${muList.size}") | |
muList.remove("apple") | |
println("muList size : ${muList.size}") | |
muList.removeAt(0) | |
println("muList size : ${muList.size}") | |
for (item in muList) { | |
print("mu $item, ") | |
} | |
println() | |
} |
14. Lambda 식
요즘 들어 개발 언어들을 보면 lambda 식을 지원한다는 내용이 자주 보인다.(c++, java 등)
그만큼 알고 사용하면 유용한 개발 방식이다.
주로 사용하는 방식은 생성된 객체의 변수를 다른 객체에서 접근하여 원하는 결과를 만들고 싶을때 사용한다.????
말하고 보니 설명이 쉽지 않은 부분이지만 여러 샘플 코드들을 보면서 이해 하길 추천한다.
예제 코드는 { num -> list.contains(num) } 가 내부 객체로 생성되고 그 참조가 LamdaTest 에 전달된다.
lambda 식이 내부 객체로 동작하는 부분을 이해하는게 중요하다.

class LambdaTest(val test: (Int) -> Boolean) { | |
var checkNum = 1 | |
fun isOk(): Boolean { | |
return test(checkNum) | |
} | |
} | |
class LambdaUser(arg: List<Int>) { | |
var list = arg | |
fun test() { | |
val lambdaTest1 = LambdaTest() { num -> list.contains(num) } | |
println("test : ${lambdaTest1.isOk()}") | |
val lambdaTest2 = LambdaTest() { num -> list.contains(num) == false } | |
println("test : ${lambdaTest2.isOk()}") | |
} | |
} | |
fun main(args: Array<String>) { | |
val lambdaUser1 = LambdaUser(listOf(1, 2, 3, 4, 5)) | |
lambdaUser1.test() | |
println("#######") | |
val lambdaUser2 = LambdaUser(listOf(2, 3, 4, 5)) | |
lambdaUser2.test() | |
} |
15. coroutine 은 멀티 스레드가 아니다.
처음 봤을때는 가벼운 동시 작업 이라고 해서 멀티 스레드 개념으로 이해 했으나
context switching 과 관련이 없이 수행되는 동시 작업이라 동작이 완전히 다르다.
coroutine 용으로 사용되는 delay를 호출할 경우 그 제어권이 scope 관리로 넘어 간다.
delay 시간에 맞춰 각 coroutine 이 호출 될것으로 생각 하지만 멀티 스레드와 달리 호출이 될것에 대한 보장이 약하다.
coroutine 실행이 짧고 delay 가 호출되어 scope 관리로 넘어 갈 경우는 크게 문제가 되지 않지만
coroutine 실행이 길어질 경우 동일 scope내 coroutine들이 제시간에 실행 되는 것이 보장 되지 않는다.
sleep을 넣어 보면 sleep 끝날때까지 다른 coroutine 들이 동작 하지 않는다.
실행 시간이 짧은 비동기 실행이 필요할때 사용한다.
import kotlinx.coroutines.* | |
import java.lang.Thread.sleep | |
import java.time.Instant | |
import java.time.ZoneOffset | |
import java.time.format.DateTimeFormatter | |
fun main() = runBlocking { | |
printTime() | |
doWorld() | |
printTime() | |
println("Done") | |
} | |
fun printTime() { | |
println( | |
DateTimeFormatter | |
.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS") | |
.withZone(ZoneOffset.UTC) | |
.format(Instant.now()) | |
) | |
} | |
suspend fun doWorld() = coroutineScope { | |
launch { | |
delay(2000L) | |
printTime() | |
println("World 2 ") | |
} | |
launch { | |
delay(1000L) | |
printTime() | |
println("World 1") | |
// sleep(3000) | |
} | |
launch { | |
printTime() | |
println("Hello") | |
} | |
} |