과거에 안드로이드 프로그래밍을 하며 정리해둔 코틀린 문서를 업로드합니다. 틀린 부분이 있을 수 있습니다.
언어 특성
- 세미콜론 없어도 됨
- 강타입(정적 타입)언어로 형변환 필수
- Java의 라이브러리를 사용할 수 있으나 코틀린 라이브러리 쓰는게 좀 더 낫다함
Java와 차이점
- 고차 함수 지원(Java는 직접적으로 지원하지 않는다)
- 함수형 프로그래밍에 더 능함
- Java는 SAM 변환 지원, Kotlin은 지원하지 않음(람다, 고차함수 적극 사용)
- SAM(Simple Abstract Method) 변환 : 매개변수로 전달하거나 반환하는 인터페이스 객체가 하나의 추상 메서드만을 가지고 있을 때 이를 해당 메서드(익명함수) 객체로 자동 변환
main 함수
- Java와 달리 빌드 시 클래스 필수적이지 않다
- mian 함수는 필수적이다
fun main(){
print("Hello World!!")
println("Hello World!!") //개행
}
변수
- Kotlin은 자동으로 문맥 상 변수타입 추론이 지원된다
- var i = 10 //타입 추론 var j: Int = 20 //타입 지정 //자바의 Wrapper 클래스와 같이 대문자로 시작 val max = 100 //상수(final), 런타임 할당
- 상수
- const val max = 100 //컴파일 타임에 할당되어 성능상 유리, 변경 불가 fun main(){ //... }
- 기본 자료형 형변환
- var i = 10 //정수 var l = 20L //Long 타입 var name = "" l = i.toLong() name = i.toString() i = name.toInt()
- 문자열 처리
- var name = "Taehyeon Kim" print(name.uppercase()) //대문자 print("the Name is" + name + '\n') print("my Name is $name My Men.") print("my Name is ${name.uppercase()}")
- lateinit : 초기 값을 정확히 알 수 없는 변수를 미리 정의할 때 (null 사용 지양)
- class MyClass{ lateinit var str: String fun myFunc(){ str = "Hello World!" } }
입력 받기
//Java Style
import java.util.Scanner;
var sc = Scanner(System.'in');
var num = sc.nextInt();
//Kotlin Style
var input = readln()
//var input: Int = readln().toInt()
기타 메서드, 함수
- 최대 최소
- import kotlin.math.max var i = 10 var l = 20 //println("MAX = ${kotlin.math.max(i, l)}") println(max(i, l))
- 난수
- val ranInt = Random.nextInt(0, 100) //0 ~ 99
- 소수점 자리수
- var num = 3.1415 println(String.format("%.3f".num))
- measureTimeMillis() : 시간 측정, ms단위
- var measuredTime = measureTimeMillis{ //측정하고자 하는 동작 }
조건문
- alt+enter를 통해 when, if문 상호치환
- Java Style 가능
- var i=5 //Java Style if(i < 5) { ... } else if(i > 5) { ... } else{ ... } //Kotlin Style when { i < 5 -> { ... } i > 5 -> { ... } else -> { ... } }
- if문의 return 값
- var i = 5 var result = when { i < 5 -> { "1" } i > 5 -> { "2" } else -> { "3" } } //삼항연산자 var result = if(i > 10) true else false
반복문
- for문
- var items = listOf(1, 2, 3, 4, 5) for(item in items){ print(item) } items.forEach { item -> print(item) } for(i in 0..items.size){ print(items[i]) } for(i in 0..10 step 2){ print(i) } //0246810
- while문, break/continue는 Java와 완전 동일
자료구조
- List
- List, MutableList는 서로 다른 타입
- List : immutable, 수정 불가
- //var items: List<Int> = listOf<Int>(1, 2, 3, 4, 5) var items = listOf(1, 2, 3, 4, 5) //타입추론으로 인한 생략 //[1, 2, 3, 4, 5]
- MutableList : mutable, 수정 가능
- var items = mutableListOf<Int>() //빈 리스트 items.add(1) items.add(2) //[1, 2] var items = mutableListOf<MutableList<Int>>() //빈 2차원 정수 리스트 //[]
- 메서드
- items.add(mutableListOf(1, 2, 3, 4, 5) items.add(mutableListOf(5, 6, 7, 8 ,9) //[[1, 2, 3, 4, 5], [5, 6, 7, 8, 9]] items.remove(5) //값이 5인 원소 하나 삭제 items.removeAt(5) //index가 5인 원소 하나 삭제 items.size //길이
- Array (List를 사용하는게 일반적)
- var items = arrayOf(1, 2, 3, 4, 5) print(items[3]) //4 //size, set, get 등 메서드 존재
예외 처리
- try-catch
- var items = listOf(1, 2, 3) try { var item = items[3] } catch(e: Exception){ //Java는 catch(Exception e){ ... } println(e.message) } finally{ println("Error Handling Completed") } //var e: Exception = Exception("This is an Error") //임의의 에러 //throw e throw Exception("This is an Error") //임의로 에러 발생
- null Safety
- 선언 시 ‘?’가 없으면 null 대입 불가
- var name: String? = null //var name: String = null 은 에러
- Null Checking : ‘?’가 붙은 것과 아닌 것은 서로 다른 타입, Null이 아닐 경우 상호작용 가능
- var name: String? = "" var name2: String = "" if(name != null){ //if문 name2 = name } name?.let{ name2 = it //또는 name2 = name } name2 = name!! //개발자 임의로 null이 아님을 보증
함수
- main 함수 밖에 있는 함수는 Top-Level 함수로 어느 파일에서도 사용 가능
- 기본형
- fun sum(a: Int, b: Int): Int{ //Return값 추론 가능할 경우 리턴 타입 생략 가능 return a+b } fun main(){ print(sum(10, 20)) //30 }
- 축약
- fun sum(a: Int, b: Int): Int = a + b
- 오버로딩 : Java Style로 오버로딩 가능, 매개변수에 기본값 지정하여 축약 가능
- //Java Style fun sum(a: Int, b: Int): Int { ... } fun sum(a: Int, b: Int, c: Int): Int { ... } //Kotlin Style fun sum(a: Int, b: Int, c: Int = 0): Int { ... }
- 함수 사용 시 매개변수 지정 가능 (명시적으로 지정 시 순서 상관x)
- fun sum(a: Int = 0, b: Int = 0, c: Int = 0) = a + b - c fun main(){ print(sum(10, c=20)) //-10 print(sum(c=20, a=10, b=20)) //10 }
Class
- 기본적으로 멤버에 대한 getter, setter가 존재
- 자바와 달리 Getter, Setter 메서드 지양
- 기본형
- class Person( //기본생성자(필수) var name: String, val race: String, //한번만 할당 가능 private var age: Int ){ init { //... 생성자 } //... Methods, Property }
- Data 클래스 : 객체의 값이 같으면 같은 객체로 인식
- 프로퍼티는 상관 없다
- Java에선 Equals, HashCode 이용
//Kotlin data class Person( //... ) - 그 외 프로퍼티 조정
- field는 해당 프로퍼티의 값
class Person( //... ){ var name = "KTH" private set //setter 막을 때 get() = "Name:${field}" set(str: String){ field = str } } - 상속
- open 키워드가 있어야 overriding 가능
- interface 외에는 모두 단일 상속만 가능
- 일반 상속
- open class Person(var name: String, var age: Int){ } class Male(menName: String, menAge:Int) : Person(menName, menAge){ //Java의 super(menName, menAge); 와 같은 개념 }
- Abstract Class
- abstract class Person{ abstract fun walk() //추상 메서드, 상속 시 반드시 오버라이딩하여 구현해야함 open fun greet(){ println("Hello") } } class Men : Person(){ override fun walk(){ println("I'm walking") } override fun greet(){ println("What's up") } }
- Interface : 다중 상속 가능
- interface Moveable{ fun walk() } interface Talkable{ fun saySom() } open class Animal : Moveable{ override fun walk(){ //... } } class Human : Animal, Talkable{ override fun saySom(){ //... } }
- Generic
- class Container<T>(var value: T){ //... } fun main(){ var container = Container(10) //var container: Container<Int> = Container<Int>() }
- Type Check (is)
- var men: Animal = Human() //Animal Type의 Human Instance 생성 men.saySome() //에러 //men 객체는 Animal Type이므로 Human의 속성을 이용할 수 없다 if(men is Human){ //men의 Type을 검사하여 Human인게 확인될 경우 men.saySome() //Human의 속성을 사용할 수 있다 }
- 강제 Type Casting (as) : 같은 타입이 아니라면 에러난다
- var men: Animal = Human() men as Human men.saySome() //가능
- object
- 싱글톤 객체
- 생성자가 없음, 단일 객체를 가짐
- 명시적 선언
- object obj { val name = "KTH" fun talk(){ println("Hello") } } class MyClass(){ object DataSource{ var baseUrl = "/etc/" } } fun main(){ obj.age = 20 obj.talk() myClass.DataSource.baseUrl //값 : /etc/ }
- Companion object
- 클래스 수준의 싱글톤 객체
- 자바의 정적 메서드, 정적 프로퍼티와 같은 개념(static)
- class MyClass{ companion object{ var baseUrl = "/etc/" } } fun main(){ myClass.baseUrl //값 : /etc/ }
고급
- 함수형 인터페이스 : 추상 메서드를 하나만 가진 인터페이스
- 매개변수로 함수를 넘겨줄 때 사용
- fun 키워드는 메서드가 여럿일 경우 에러 띄움
//안드로이드의 Event Listener(예: OnClickListener 같은 것)와 비슷한 구조의 사용 예시 interface Listener{ fun run() } fun setListener(listener: Listener){ listener.run() } setListener( object : Listener{ override fun run(){ ... } }) //Java Legacy Style - → Java의 @FunctionalInterface 어노테이션과 같다
- Runnable 인터페이스 : Java와 Kotlin에서 지원하는 함수형 인터페이스
- fun runMyFunc(runnable: Runnable){ runnable.run() } runMyFunc(object : Runnable{ override run(){ ... } }) /* Java runMyFunc(new Runnable(){ @Override public void run(){ ... } }) */
- Lambda
- Kotlin의 람다식은 return 키워드는 쓰지 않음(마지막 표현식이 반환됨)
- 반환값을 고차함수에 전달할 경우 return@[고차함수] [반환값] 형식으로 명시 가능 (필수x)
- func: () -> Unit = { … }
- func : 람다식이 할당된 변수명
- () -> Unit : 람다식의 타입
- { ... } : 람다식의 내용
//Unit은 void와 비슷한 타입(아무것도 반환하지 않음) val func: () -> Unit = { ... } //람다식의 타입 부분을 생략(타입 추론) //매개변수 x, y를 명시적으로 초기화 val func = { x: Int, y: Int -> ... } //Int형 매개변수 2개와 Int형 리턴 타입을 가진 예제 //아래 둘은 같은 동작을 한다 val sum: (x: Int, y: Int) -> Int = { x, y -> //시그니처 타입 모두 명시 x + y } val sum = { x: Int, y: Int -> x + y } - 고차 함수 (Higher-Order Function) : 콜백 함수를 인자로 받거나 리턴 값으로 사용 가능한 함수
- 콜백 함수 : 다른 함수에 의해 호출되는 함수
- 기본 형태
- fun myFunc(callBack: () -> Unit){ callBack() } //원형 myFunc({ ... }) //고차 함수의 마지막 인자가 콜백함수이므로 //인자로 넘겨주는 람다식의 중괄호를 밖으로 뺄 수 있다 myFunc(){ ... } //고차 함수가 받는 인자가 콜백 함수 뿐인 경우 //아래와 같이 소괄호도 생략할 수 있다 myFunc{ ... }
- 매개변수, 리턴이 있는 콜백 함수를 인자로 받을 때
- fun myFunc(callBack: (x: Int) -> Int){ var ret = callBack(5) } myFunc(){ x -> //매개변수의 타입은 추론이 되므로 생략 가능 ... } //콜백 함수의 매개변수가 하나 이하일 경우 //해당 매개변수를 명시적으로 초기화하지 않아도 ‘it’로 접근할 수 있다 myFunc(){ println(it) //결과 : 5 }
- 콜백 함수 오버로딩
- //콜백 함수의 기본값을 빈 람다로 정의 fun myFunc(callBack: () -> Unit = {}){ callBack() } myFunc() //콜백 함수 전달하지 않아도 호출 가능
- ‘::’ : 일반 함수를 인자로 넘길 때 또는 클래스 참조를 전달할 때
- Kotlin에서 MyClass::class, 클래스 참조는 klass 타입
- Java에서 MyClass.class, Class타입
MyClass::class //klass 타입 반환 MyClass::class.java //Java의 Class 타입 반환 - fun printNum(num: Int){ println(num) } fun myFunc(callBack: (x: Int) -> Unit){ callBack(5) } myFunc(::printNum) //결과 : 5
- 람다와 고차함수로 인한 Event Listener의 간결화
fun interface Listener{ fun run() } //Legacy fun setListener(listener: Listener){ listener.run() } //Legacy fun setListener(run: () -> Unit){ run() } //고차함수를 통한 간결화 가능 setListener{ ... } - #Java에선 Event Listener가 함수형 인터페이스 구조로 구현되어있다.
Listener를 Set하는 함수에 익명 내부 클래스와 람다식을 둘 다 전달할 수 있는 건 SAM(Single Abstract Method) 변환이 지원되기 때문에 EventListner의 구현을 Legacy로 두더라도 람다로도 전달할 수 있는 것이다.
하지만 Kotlin은 SAM 변환을 지원하지 않으므로 함수형 인터페이스 구조와 고차 함수 구조의 오버로딩을 통해 구현하여 Java의 Event Listener와 같은 동작을 하게끔 구현했을 것
Thread-Coroutine
Thread
- start() 메서드로 쓰레드 생성 및 시작
- Thread 클래스 상속 받아 생성
- class SimpleThread(val num: Int, var latency: Long) : Thread(){ override fun run(){ Thread.sleep(latency) //latency만큼 해당 Thread 대기(ms) println("${num} Thread is Running") } } val simpleThread = SimpleThread(1, 3000) simpleThread.start() SimpleThread(2, 2000).start() //익명객체로 인스턴스 생성하여 실행 SimpleThread(3, 1000).start() /* 결과 3 Thread is Running 2 Thread is Running 1 Thread is Running */
- Runnable 인터페이스 상속
- Runnable을 implement
- 생성된 객체를 Thread에 인자로 넘겨 Thread 객체 생성
- 이렇게 하는 이유
- Thread 클래스는 쓰레드의 실행의 역할만 있고 Runnable 인터페이스로 작업을 구현하도록 역할이 나뉘어 있음
- Thread 클래스를 상속 받을 경우 단일 상속 원칙으로 다른 클래스 상속 불가
class SimpleRunnable(val latency: Long) : Runnable { override fun run(){ Thread.sleep(latency) println("${num} Thread is Running") } } val simpleRunnable = SimpleRunnable(4, 3000) val thread = Thread(simpleRunnable) thread.start() Thread(SimpleRunnable(5, 2000)) //익명객체로 인스턴스 생성하여 실행 Thread(SimpleRunnable(6, 1000)) - Thread 익명 객체
- var thread = object : Thread(){ override fun run(){ println("Thread is Running") } } thread.start() /* Java Thread thread = new Thread(){ @Override public void run(){ System.out.println("Thread is Running"); } }; thread.start(); */
- 람다식 : Thread 클래스는 람다식을 매개변수로 생성자에 전달할 수 있다
- //원형 var thread = Thread({ println("Thread is Running") }) thread.start() //kotlin식 축약, 람다 매개변수로 넣을 시 소괄호 빼기 var thread = Thread{ println("Thread is Running") } thread.start() //최대한 축약하여 쓰레드 실행 Thread{ println("Thread is Running") }.start() /* Java new Thread(()->{ System.out.println("Thread is Running"); }).start(); */
- 헬퍼 함수 만들기 : 쓰레드를 생성, 실행하는 함수 직접 만들기
- fun makeThread( start: Boolean = true, name: String? = null, callBack: () -> Unit ): Thread{ val thread = object : Thread(){ override run(){ callBack() } } name?.let{ thread.name = it } if(start==true) thread.start() } makeThread(){ ... }
Coroutine
- 병행성(Concurrency)
- Single Core 사용으로 병렬이 아닌 순차적 동작, Logical Level
- Time-Sharing으로 CPU를 나눠 사용함으로 동시에 실행하는 것처럼 보임
- Thread는 병렬성(Parallelism)을 지원하므로 실제로 동시에 작업이 처리되나(Physical Level), Thread간 작업 교환에 System call, Blocking call 등 비용 발생, 즉 Thread에 비해 비용이 적다
- 스코프 : 코루틴의 실행 범위
- GlobalScope : 전역 스코프, 앱의 생명주기와 독립적, 안드로이드에서 사용 지양
- GlobalScope.launch{ ... }
- CoroutineScope : 해당 코루틴이 관리하는 범위 내에서만 코루틴을 실행하고 관리
- CoroutineScope(Dispatchers.Default).launch{ ... }
- lifecycleScope : Jetpack의 수명 주기 라이브러리에 포함, 안드로이드 액티비티나 프래그먼트의 수명주기와 연결되어있어 Active 상태일 때만 실행하여 메모리 누수 및 생명 주기 이슈x
- lifecycleScope.launch{ ... }
- 코루틴 빌더 : 코루틴 생성 및 실행을 위한 구문, 코루틴 내부에서 사용된다
- launch : 반환 값이 없는 비동기 작업 수행 시 사용
- launch{ ... }
- async : 반환 값이 있는 비동기 작업 수행 시 사용, Deferred 객체로 결과 반환됨
- var deferred = async{ return@async "This is Data" } var result = deferred.await() //
- runblocking : 내부의 모든 코루틴이 수행되기 전까지 부모 스레드를 차단시킴, 테스트 목적
- runBlocking{ ... } fun main() = runBlocking{ ... }
- 디스패쳐(Dispatcher) : 코루틴이 실행될 스레드나 스레드 풀을 지정
- Dispatchers.Default : CPU 바운드 작업에 적합한 디스패쳐, 고정 크기의 스레드 풀
- Dispatchers.IO : I/O 바운드 작업에 적합한 디스패쳐, 백그라운드 스레드 풀
- Dispatchers.Main : 안드로이드에서 UI 작업을 수행하는 메인 스레드에 대한 디스패쳐
- Dispatchers.Unconfined : 현재 스레드에서 즉시 실행
- Suspend 함수
- 일시 정지(async-await, delay 등) 가능한 함수
- → 해당 함수가 일시정지 상태일 경우 쓰레드는 다른 작업을 진행
- 스레드의 중단 없이 부모 루틴의 상태를 저장하고 불러오는 방식으로 성능저하를 최소화함
- suspend 함수의 사용은 코루틴 또는 suspend 함수 내에서만 가능
- suspend fun readData() : Int{ var job = CoroutineScoupe(Dispatchers.IO).async{ delay(2000) //Reading Data return@async 433 } return job.await() } fun main(){ CoroutineScope(Dispatchers.Default).launch{ readData() } }
- withContext : 부모 루틴의 Dispatcher와 다른 Dispatcher를 호출하여 사용
- suspend 함수이므로 동작이 끝낼 때까지 부모 루틴 정지됨
CoroutineScope(Dispatchers.Default){ withContext(Dispatchers.IO){ ... } } - Methods
- join() : 동작이 완료될 때까지 쓰레드 일시정지
- var job1 = launch{ for(i in 0..5){ delay(100) println("Running ${i}") } } delay(200) println("Main Coroutine") job1.join() println("Main Done") /* Running 0 Running 1 Main Coroutine Running 2 Running 3 Running 4 Running 5 Main Done */
- delay() : 시간만큼 일시정지, 정지된 시간동안 다른 루틴에 양보
- yield() : 한번 양보
- launch{ for(i in 0..10 step 2){ print("${i} ") yield() } } launch{ for(i in 1..10 step 2){ print("${i} ") yield() } } /* 결과(스케줄링 결과에 따라 랜덤할 수 있음) 0 1 2 3 4 5 6 7 8 9 10 */
- cancel() : 작업 중지
- launch{ for(i in 0..10){ println("Running ${i}") if(i == 5) this@launch.cancel() } } /* Running 0 Running 1 Running 2 Running 3 Running 4 Running 5 */
- 비동기 작업의 효율성(작업시간 차이 비교)
- fun main()=runBlocking{ println("Main's On") launch{ var time = measureTimeMillis { var name = async{ readName() } var age = async{ readAge() } println("Name : ${name.await()}, Age : ${age.await()}") } println("Running time : ${time}ms") } launch{ var time2 = measureTimeMillis { var name = readName() var age = readAge() println("Name : ${name}, Age : ${age}") } println("Running time : ${time2}ms") } println("Main's Down") } suspend fun readName() : String{ var job = CoroutineScope(Dispatchers.IO).async{ delay(3000) return@async "James" } return job.await() } suspend fun readAge() : Int{ var job = CoroutineScope(Dispatchers.IO).async{ delay(1000) return@async 25 } return job.await() }