List, Set, Map을 제공한다.//컬렉션 생성
var immutableList : List<String> = listOf("사과", "딸기") //immutable List
var mutableList = mutableListOf<String>() //mutable List
var immutableSet = setOf(1,2,3,4) //immutable list
var mutableList = mutableSetOf<Int>() //mutable set
var immutableMap = mapOf("one" to 1, "two" to 2) //immutable map
var mutableMap = mutableMapOf<String, Int>() // mutable map
// 값 추가
mutableList.add("수박")
mutableSet.add(5)
mutableMap["three"] = 3apply 함수를 이용해 가독성 좋은 값 추가 가능하다.mutableList.apply{
add("배")
add("참외")
}
val numberList: List<Int> = buildList {
add(1)
add(2)
add(3)
}
코틀린의 컬렉션은 Iterable의 구현체이므로 순차적 반복이 가능하다.
앞서 학습한 for loop를 사용하면 암시적으로 이터레이터 사용한다.
val iterator = list.iterator()
while(iterator.hasNext()) {
println(iterator.next())
}
for (current in list) {
println(current)
}
또한 코틀린 표준 라이브러리는 컬렌션 사용시 자주 사용되는 패턴인 forEach, map, filter와 같은 유용한 인라인 함수를 제공한다.
tempList.forEach { println(it) } //forEach
var upperList = tempList.map {it.uppercase() } //map
var filterList = tempList.filter { it == 'A' || it == 'C' } //filter
위에서 처럼 자바8은 중간 연산자(map,filter 등)만 사용했을때 아무 동작도 하지 않고 최종 연산자(terminal operator)를 사용해야 값을 얻을 수 있다.
코틀린에서는 인라인 함수가 동작할때마다 조건에 맞는 컬렉션을 생성한다.
하지만 대량의 데이터를 처리할 때는 컬렉션이 새로 생성되는 것이 부담이다.
이를 위해 코틀린에서는 아래와 같이 시퀀스 API를 지원하는데 벤치마크상 일반적으로 인라인 함수가 빠르고 대량일 경우 시퀀스 API를 사용해야 한다.
var filteredList = upperList
.asSequence()
.filter {it == 'A' || it == 'C' }
.toList()
data class Person(val name: String, val age: Int)구조분해할당을 사용해 좀 더 쉽고 안전하게 변수를 선언할 수 있다. val person = Person(name="tony", age=12)
println("이름=${person.component1()}, 나이=${person.component2()}")
val (name, age) = person
println("이름=${name}, 나이=${age}")
싱글톤 패턴은 클래스의 인스턴스를 하나의 단일 인스턴스로 제한하는 디자인 패턴이다.
싱글톤 패턴을 구현할때는 몇가지 제약사항을 통해 구현한다.
- 직접 인스턴스화 하지 못하도록 생성자를 private로 숨긴다.
- getInstance()라는 클래스의 단일 인스턴스를 반환하는 static 메서드를 제공한다.
- 멀티-스레드 환경에서도 안전하게유일한 인스턴스를 반환해야한다.
자바에서 많이 쓰이는 구현 방식
//이른 초기화
public class Java_Singleton {
private static final Java_SingleTon INSTANCE = new Java_Singleton();
private Java_Singleton() {
//do nothing
}
public Java_SingleTon getInstance() {
return INSTANCE;
}
}
//지연 초기화
public class Java_Singleton {
private Java_Singleton() {
//do nothing
}
public Java_Singleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final Java_Singleton INSTANCE = new Java_Singleton();
}
}
코틀린은 언어에서 객체 선언을 통해 싱글톤을 기본 지원한다.
객체 선언은 object 키워드를 사용한다.
object Singleton { }
함수나 변수를 사용할때는 클래스 한정자를 사용한다. (클래스명.함수명)
object Singleton {
val a = 1234
fun printA() = println(a)
}
fun main() {
println(Singleton.a)
Singleton.printA()
}
객체 선언을 사용하면 자바의 static 유틸리티를 대신해 쉽게 싱글톤 기반의 유틸리티를 만들 수 있다.
companion 키워드를 사용해 클래스 내부에 객체 선언을 사용할 수 있다.
class MyClass {
companion object { }
}
동반객체의 멤버는 object로 선언한 객체와 마찬가지로 클래스 한정자를 사용해 호출할 수 있다.
class MyClass {
private constructor()
companion object {
val a = 1234
fun newInstance() = MyClass()
}
}
fun main() {
println(MyClass.a)
println(MyClass.newInstance())
//이렇게도 호출가능
println(MyClass.Companion.a)
println(MyClass.Companion.newInstance())
}
실드 클래스의 하위 클래스는 같은 파일 내에서만 정의가능
하나의 상위 클래스 또는 인터페이스에서 하위 클래스의 정의를 제한할 수 있는 방법
실드 클래스는 하위 클래스를 제한 조건에 따라 정의해야하고 이렇게 만들어진 실드 클래스의 하위 클래스는 컴파일 시점에 판단할 수 있다
sealed class Developer {
abstract val name: String
abstract fun code(language: String)
}
data class BackendDeveloper : Developer() { //구현 생략 }
data class FrontendDeveloper : Developer() { //구현 생략 }
object DeveloperPool() {
val pool = mutableMapOf<String, Developer>()
//sealed 정의가 아니라 일반 클래스였다면
//아래 함수에서 when의 else 구문이 없어서 컴파일 에러!!
//sealed 정의 덕분에 하위 클래스가 2종인 걸 컴파일러가 인지해서 컴파일 오류 발생X
fun add(developer : Developer) = when (developer) {
is BackendDeveloper -> pool[developer:name] = developer
is frontendDeveloper -> pool[developer:name] = developer
}
}
fun String.first(): Char { return this[0] }
fun String.addFirst(char: Char): String { return char + this.substring(0) }
public final class Example {
public static final char first(@NotNull String $this) {
return $this.charAt(0);
}
}타입 파라미터를 가질 수 있다타입 아규먼트를 제공하면 된다.class MyGenerics<T> (val t: T)
val generic1 = MyGenerics<String>("test") //인스턴스 생성
val generic2 = MyGenerics("test") //타입 아규먼트 생략
val generic3: MyGeneric<*> = MyGenetrics<String>("test") //스타 프로젝션
제네릭에서 파라미터화된 타입이 서로 어떤 관계에 있는지 설명하는 개념
변성은 크게 공변성, 반공변성, 무공변성으로 나뉜다.
이펙티브 자바에선 공변성과 반공변성을 설명할때 PECS 규칙을 언급
- Producer - Extends (코틀린 out)
- Consumer - Super (코틀린 in)
공변성 예시
- 아래 예시에서 CharSequence가 String의 상위 타입일 때 MyGenerics<CharSequence> 를 MyGenerics<String>의 상위 타입으로 만들어 공변성이다.
class MyGenerics<out T>(val t: T) //공변성을 위해 out 사용
fun main() {
val generics = MyGenerics("test")
val charGenerics : MyGenerics<CharSequence> = generics //out이 없으면 에러 발생
}
반공변성 예시
- 아래 예시에서 CharSequence가 String의 상위 타입일 때 Bag<String>가 Bag<CharSequence>의 상위 타입이므로 반공변성이다.
class Bag<T> {
fun saveAll (to: MutableList<in T>, from: MutableList<T>) { //반공변성을 위해 in 사용
to.addAll(from)
}
}
fun main() {
val bag = Bag<String>()
//in이 없으면 에러 발생
bag.savaAll(mutableListOf<CharSequence>(""), mutableListOf<String>(""))
}
위 예시들에서 in, out 어떠한 것도 지정하지 않은 경우 String은 CharSequece의 하위 타입이지만 MyGenerics<String>과 MyGenerics<CharSequence>는 아무 관계도 아니므로 무공변성이다.
변수를 선언한 시점엔 초기화하지 않다가 특정 시점에 초기화가 필요할때
사용 시점에 1회만 초기화 로직이 동작한다.
멀티 스레드 환경에서도 안전하게 동작한다. (by Lazy(LazyThreadSafetyMode.SYNCHRONIZED))
LazyThreadSafetyMode.NONE으로 설정하면 멀티 스레드 환경에서 동기화X
class HelloBot {
val greeting: String by Lazy { getHello() } //지연 초기화
fun sayHello() = println(greeting)
}
fun getHello() = "안녕하세요"
fun main() {
val helloBot = HelloBot()
//..
helloBot.sayHello()
}
가변 프로퍼티에 대한 지연초기화가 필요한 경우
특정 프레임워크나 라이브러리에서 지연초기화가 필요한 경우
lateinit을 사용할 경우에는 가변 변수이며 non-null이어야 한다.
초기화 여부를 파악하고 사용하려면 isInitialized 프로퍼티 사용 (내부에서만 사용 가능)
class lateinitTest {
lateinit var text: String
fun printText() {
if (this::text.isInitialized) {
println("초기화됨")
println(text)
} else {
text = "안녕하세요"
println(text)
}
}
}
fun main() {
val test = lateinitTest()
test.printText()
}
페어를 통해 2개의 요소가 있는 튜플을 기본 제공한다. first, 두번째인자는 second로 사용할 수 있다.fun plus(pair: Pair<Int, Int>) = pair.first + pair.second
val newPair = Pair("A", 1).copy(first = "B") 트리플을 통해 3개의 요소가 있는 튜플을 기본 제공한다. first, second, third를 사용할 수 있다.val newTriple = Triple("A", "B", "C")
val (a, b, c) = newTriple
println("$a, $b, $c") // A, B, CcomponentN 함수를 사용한다. componentN은 앞선 5개 요소에 대해서만 제공한다. to는 내부적으로 페어를 사용한다.val map = mutableMapOf("test" to "개발자")
for ( (key, value) in map) {
println("${key}의 직업은 ${value}")
}변수명을 사용하지 않고 객체에 접근할 수 있는데 그 이유는 수신자 객체(receiver)에 접근할 수 있기 때문이다.
fun main() {
val str: String? = "안녕"
val result = str?.let {
println(it)
}
println(result) //let 함수 마지막 코드가 결과로 반환
}
class DatabaseClient {
val url: String? = null
var username: String? = null
var pw: String? = null
fun connect(): Boolean {
println("접속중...")
println("접속 완료")
return true
}
}
fun main() {
var connected = DatabaseClient().run {
url = "localhost:3306"
username = "mysql"
pw = "1234"
connect()
}
println(connected) //true
}
val str = "안녕"
val length = with(str) {
length
}
println(length) //2
val client: DatabaseClient = DatabaseClient().apply {
url = "localhost:3306"
username = "mysql"
pw = "1234"
connect()
}
class User(val name: String, val pw: String) {
fun validate() {
if (name.isNotEmpty() && pw.isNotEmpty()) {
println("검증 성공")
} else {
println("검증 실패")
}
}
}
fun main() {
User(name= "tony", pw= "1234).also {
it.validate()
}
}
try-with-resources 구문을 사용하면 자동으로 리소스를 close 처리해준다.Closeable, Autocloseable 인터페이스의 구현체에 대해 자동으로 close 메서드를 호출한다. try (PrintWriter writer = new PrintWriter("test.txt")) {
writer.println("Hello world");
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
}use라는 확장 함수를 제공한다. FileWriter("test.txt")
.use { it.write("Hello Kotlin") }코틀린은 try-catch를 통한 예외처리외에도 함수형 스타일의 Result 패턴을 구현한 runCatching을 제공한다.
Result 패턴이란 함수가 성공하면 캡슐화된 결과를 반환하거나 예외가 발생하면 지정한 작업을 수행하는 패턴이다.
runCatching 사용
val result = runCatching { getStr() }
.getOrElse {
println(it.message)
"기본값"
}
//getStr()에서 예외 발생하여 result = "기본값"으로 초기화
runCatching은 결과에 따라 Result.success(), Result.failure()를 호출하는데 해당 메서드들은 Result 내부 동반 객체에서 성공 상태와 실패 상태에 따라 Result 객체를 생성함.
Result 객체에는 다양한 기능들이 존재한다.
- getOrNull : 실패인 경우 null 반환
- exceptionOrNull : 성공인 경우 null, 실패인 경우 Throwable 반환
- getOrDefault : 성공시엔 성공값을 반환하고 실패시엔 지정한 기본값을 반환
- getOrElse : 실패시 수신자 객체로 Throwable을 전달받고 let, run과 같이 함수의 결과를 반환
- getOrThrow : 성공시엔 값을 반환 실패시엔 예외를 발생시킨다.
- map : 성공인 경우 원하는 값으로 변경할 수 있다. (map 내부 예외 재처리 불가)
- mapCatching : map처럼 성공인 경우 원하는 값으로 변경할 수 있다. 예외가 발생하면 재처리 가능
- recover : map은 성공시 원하는 값으로 변경하지만 recover는 실패시에 원하는 값으로 변경할 수 있다. (recover 내부 예외 재처리 불가)
- recoverCatching : recover처럼 실패시 원하는 값으로 변경할 수 있다. 예외가 발생하면 재처리 가능
//1. getOrNull()
val result = runCatching { //에러 발생 }
.getOrNull()
println(result) //null 반환
//2. exceptionOrNull()
val result = runCatching { //에러 발생 }
.exceptionOrNull()
println(result) //Throwable 반환
//3. getOrDefault()
val result = runCatching { //에러 발생 }
.getOrDefault("기본값")
println(result) //"기본값" 반환
//4. getOrElse
val result = runCatching { //에러 발생 }
.getOrElse {
println(it.message)
"기본값"
}
println(result) //"기본값" 반환
//5. getOrThrow()
val result = runCatching { //에러 발생 }
.getOrThrow()
println(result) //예외 발생
//6. map
val result = runCatching { "안녕" }
.map{
it + "하세요"
}.getOrThrow()
println(result) //안녕하세요
//7. mapCatching
val result = runCatching { "안녕" }
.map{
//예외 발생
}.getOrDefault("기본값")
println(result) //"기본값"
//8. recover
val result = runCatching { //예외발생 }
.recover{
"복구"
}.getOrNull()
println(result) //"복구"
//9. recoverCatching
val result = runCatching { //예외발생 }
.recoverCatching{
//예외 발생
}.getOrDefault("복구")
println(result) //"복구"
() -> Unit 화살표 포기법을 사용한다.val printHello: () -> Unit = { println("Hello") }
val list = mutableListOf(printHello) //데이터 구조에 함수 저장
list[0]() //함수 호출 익명함수라고 한다. fun outerFunc(): () -> Unit {
return fun() {
println("이것은 익명함수")
}
}
outerFunc()( //"이것은 익명함수" 출력 //람다 표현식
fun outerFunc(): () -> Unit = { println("이것은 람다함수") } 후행 람다 전달을 사용할 수 있다. (filter, map, forEach 사용 방식) fun forEachStr(collection: Collection<String>, action: (String) -> Unit) {
for (item in collection) {
action(item)
}
}
val list = listOf("a", "b", "c")
forEachStr(list) { //후행 람다 전달
println(it)
} 람다 레퍼런스를 사용하면 좀 더 가독성 좋게 함수를 인자로 넘길 수 있다.
//일반 함수값 스타일
val callReference: () -> Unit = { printHello() }
callReference()
val numberList = listOf("1", "2", "3")
numberList.map{ it.toInt() }.forEach{ println(it) }
val callReference = ::printHello //탑 레벨 함수는 ::함수명 사용
callReference()
val numberList = listOf("1", "2", "3")
numberList.map(String::toInt).forEach(::println)