1. 변수, 상수

- var : 변수(변경 가능)

- val : 상수(변경 불가)

fun main() {
    var r : Int = 1
    val l : Int = 1
    r = 2
    l = 2  // 오류발생
}

2. null safety(언래핑)

1) ? (Optional)

값이 있을수도 없을수도 있다(null인지 아닌지 모르는 변수)
주로 null이 될 수 있는 변수를 명시적으로 표시하기 위해 사용

var name : String? = null
// 위처럼 자료형에 ?를 붙임

 

2) ?. (Safe calls)

null이 아니라면 이어지는 내용 수행, null이라면 수행하지 않음

println(a?.length)
// 만약 a가 null이 아니라면 a.length 출력, a가 null이라면 length 확인안하고 바로 null 출력

 

3) ?: (Elvis operator)

null이면 이 값/동작으로 대체

val value : String = name ?: "홍길동"
// 길게 풀어쓰면 val value : String = if (name == null) "홍길동" else name

 

4) as?(Safe casts)

타입 캐스트, 변환 못한다면 null을 반환

val other = o as? p ?: return false
// o를 p로 형변환, 성공하면 other에 넣지만 실패하면 null 리턴되니까 ?:에 걸려서 return false

 

5) !!(No-null assertions)

어떤 값이든 null이 될 수 없는 타입으로 강제 변경
근데 만약 null을 넣으면? 예외 발생함
즉 컴파일러에게 이 값은 절대 null이 아님을 개발자가 보장한다, 만약 내가 틀리면 예외발생 감수한다 라는 의미

fun ignoreNulls(s: String?) {
    val sNotNull: String = s!!
    println(sNotNull.length)
}
// ignoreNulls(null)을 호출한다면 NullPointerException 발생

 

6) let과 ?. ?: 연계

name에 값이 있다면 let 수행, 없다면 "이름없음"을 대입

val value : String = name?.let {
    it + "- 데이터 있음"
}?: "이름없음"

// 풀어쓴다면 val value : String = if (name != null) name else "이름없음"

3. 조건문

1) if

- C++과 완전히 동일

- if문법 뿐 아니라 !으로 not 걸거나, ==으로 비교하는 등 모두 같다.

 

2) when

- C++의 switch문과 대응되는 방식

- 값 -> 행동 으로 작성하며, default 값의 경우 else -> 행동 으로 작성

fun main() {
    var r = 1
    when(r) {
        1 -> print(1)
        2 -> print(2)
        else -> print(0)
    }
}

 

3) 표현식(조건 변수)

- 값 자체를 if-else를 이용해서 지정할 수 있다.

fun main() {
    var r = 1
    var result = if(r == 1) 1 else if(r == 2) 2 else 0
    print(result)
}

4. 함수

fun으로 선언

fun foo() {
    println("foo")
}

// 매개변수 있는 함수
fun foo1(text : String) {
    println("text : $text")
}

// 디폴트 매개변수
fun foo2(text : String = foo2) {
    println("text : $text")
}

// return이 있는 함수
fun foo3() : String {
    return "a"
}

5. 배열

1) listOf

값 변경이 안되는 배열

val a = listOf("a", "b", "c")
println(a)

 

2) mutableListOf

값 변경이 가능한 배열

val b = mutableListOf("a", "b", "c")
b[0] = "d"
println(b)



※ 제네릭

listOf와 mutableListOf는 제네릭이라서 아래처럼 쓰는게 생략되는 것
listOf<Any>, mutableListOf<Any> // String이나 int나 다 들어갈 수 있음
만약 특정 자료형만 들어가게 하고 싶다면 listOf<String>으로 만들면 여긴 String만 들어갈 수 있음

 

3) 배열 원소 접근

val friends = listOf("영희", "철수", "제임스")
- friends.get(0)
- friends[0]
- 혹은 friends.first() 로 첫번째 요소를 가져올 수 있음

 

4) arrayOf

이건 mutable 개념 없이 그냥 변경 가능한 배열

val nums = arrayOf(1, 2, 3)

 

5) arrayListOf

arrayOf와 같다고 함. 근데 왜 있음? 뭐가 다른지 좀 찾아봐야할듯


6. 배열 외의 컬렉션

별도 글에서 정리

https://smallpants.tistory.com/336


7. 배열의 반복문

1) range based for

for (n in nums) {
    println(n)
}

 

2) forEach

nums.forEach { n ->      // n이 nums내의 원소 하나하나
    println(n)
}

 

3) withIndex

배열에서 인덱스와 값 함께 획득

val nums = listOf(1, 2, 3, 4)
for ((i, n) in nums.withIndex()) {
    println("index : $i, num : $n")
}

// 결과는 index : 0, num : 1 형식

 

4) forEachIndexed

val nums = listOf(1, 2, 3, 4)

nums.forEachIndexed { i, n ->
    println("index : $i, num : $n")
}

8. 스코프 함수(범위 지정 함수)

1) 개요

- let, run, with, also, apply
스코프 함수라고 부르며 특정 객체에 대한 작업을 블록안에서 실행하는 함수
사용 예시로 객체 생성 후 객체에 대한 후처리를 하게된다면 어디까지 해당 객체에 대한 후처리인지 알아보기 힘듬
이걸 {} 블록내에서 하니까 명확해짐
또한 한줄한줄을 변수만들고 저장하면서 해야하는것들을 체이닝으로 처리하므로 변수 생성을 줄일 수 있음

 

2) 각 함수의 용도

각 함수의 기능차이가 있긴한데, 그래봤자 서로가 서로를 대체해서 사용가능하고 구분이 모호함
그래서 어떤 상황에는 어떤 함수를 사용한다를 먼저 알고 그때그때 적용하는 것이 더 이해하기 쉽다

let
- null이 아닐때만 코드 실행할때

object?.let {
    // do something...
}


- nullable한 객체를 다른 nullable한 객체로 변환할때(null이면 null로 대입, null이 아니면 객체로 대입할때)
- 지역변수 1개의 범위를 제한할때

run
- nullable한 값이 아닐때
- 수신객체에 대한 특정 동작 수행 후 결과값을 리턴받아야 할때

- 어떤 값을 계산할때
- 여러개의 지역변수 범위를 제한할때

with
- 다른 변수에 접근해서 뭔가 조작해야 할때
- null이 될 수 없는 객체이고, return이 없어도 될때
- 이것보단 run을 더 자주쓰게 됨
- 다른 함수들은 객체.func 형태인데 with는 with(객체) 형태로 쓴다

also
- 주로 로그찍거나 디버깅할때
- 수신객체를 전혀 사용하지않고 멤버변수도 변경하지 않을 때

apply

- 주로 객체 초기화시점에 객체의 멤버변수들 값을 변경한 후 해당 객체를 다시 리턴할때

- 람다 내부에서 수신객체의 함수를 사용하지않고, 수신객체를 다시 반환할때

 

3) this, it

apply, run, with는 this

  - 수신객체를 람다의 수신객체로 전달해서
also, let은 it

  - 수신객체를 람다의 파라미터로 전달해서

 

수신객체니 파라미터니 이런건 이해 못하겠으면 넘겨도 되고 사용법과 특징만 알아도 될 듯
this는 생략가능하다는 장점이 있고, it은 다른 이름으로 변경해서 사용 가능하다는 장점이 있다

 

4) return

also, apply는 객체를 반환
run, let, with는 block의 마지막줄의 결과를 반환


9. 클래스

1) 클래스 선언

class Toy

fun main() {
    val t : Toy = Toy()
}


위처럼 정의없이 선언만 할 수도 있음

 

2) 멤버변수, 멤버함수

class Toy {
    var name : String = ""      // 멤버 변수
    var price: Int = 0          // 멤버 변수

    fun info() : String {       // 멤버 함수
        return "[name : $name, price : $price]"
    }
}

fun main() {
    val t = Toy()
    t.name = "a"
    t.price = 10
    println(t.info())
}

 

3) 생성자

가. 주 생성자
- class Toy {} 형태면 주생성자가 없는거고, class Toy(...) {} 형태면 주생성자가 있는 것
- 멤버변수를 노출시키면서 바로 값을 넣을 수 있는 것

class Toy(var name : String = "기본값", var price : Int = 기본값) {
    // 클래스 내부 구현
}


- 위처럼 클래스 생성해두면 Toy() 로는 못만들고 Toy("a", 1) 처럼만 생성 가능
- 단 default 값을 넣어뒀다면 Toy()로 생성 가능

 

나. 추가생성자

여러개 만들 수 있음(매개변수로 오버로딩)

클래스 내에 constructor()로 만들고, 생성자 호출 시점에 추가적으로 할 수 있는 일을 넣을 수 있음

class Toy(var name : String = "", var price: Int = 0) {

    var nickname : String = ""
    constructor(nickname : String) : this("toy", 100) {
        println("constructor call, name : $name, price : $price, nickname : $nickname")
    }

    fun info() : String {
        return "[name : $name, price : $price]"
    }
}

fun main() {
    val t = Toy("nick")
}

 

아래처럼 추가생성자에 매개변수를 안넣고 기본생성자만 호출하게 되면 추가 생성자까지 호출돼서 constructor 호출 ~~ 문구 출력된다.

class Toy(var name : String = "", var price: Int = 0) {
    constructor() : this("toy", 100) {
        println("constructor 호출, name : $name, price : $price")
    }

    fun info() : String {
        return "[name : $name, price : $price]"
    }
}

fun main() {
    val t = Toy()
}


연계해서도 사용 가능

constructor(a : int) : this () {

}
// 위에꺼가 이미 있다면

constructor(s : String) : this(a) {

}
// 이렇게 쓰면 위 생성자 먼저 호출 후 아래꺼까지 추가로


다. init

주생성자 호출 직후 수행하는 로직

주생성자 -> init -> 보조생성자 순으로 호출되므로 init은 주생성자의 일부로 생각해도 됨
여러 블록 둘 수 있고 여러개면 위에서부터 아래로 순차적 수행

class Toy(var name : String = "", var price: Int = 0) {

    var nickname : String = ""

    init {
        println("Toy 생성")
    }

    fun info() : String {
        return "[name : $name, price : $price]"
    }
}

fun main() {
    val t = Toy("nick")
}

 

4) companion object

클래스 내 static 변수/함수를 모아두는 공간(해당 클래스로 만든 모든 객체가 공유하는 변수/함수)
클래스에 하나만 있을 수 있음

멤버변수는 객체 생성시점에 메모리할당되지만 companion object는 앱 로딩시점에 할당된다.
이 때문에 companion object 외에서는 companion object 변수/함수 사용가능한데 내에서는 멤버변수 사용 불가능

  - 아직 안만들어졌을 수 잇으니

 

companion object에 이름을 붙일 수 있음

이름을 Factory라고 붙였다고 치면 외부에서 클래스명.Factory.변수 로 접근

 

TAG 같은걸 여기에 쓰면 좋을듯하다

class Toy {
    var name : String = ""
    var price: Int = 0
    var nickname : String = ""

    init {
        count++
        println("count : $count")
    }

    companion object Factory {
        var count : Int = 0
    }

    fun info() : String {
        return "[name : $name, price : $price]"
    }
}

fun main() {
    val t1 = Toy()
    val t2 = Toy()
    val t3 = Toy()
    val t4 = Toy()
}

 

5) 접근제한자

코틀린 기본접근제한자는 public

멤버변수, 멤버함수 모두 public이므로 외부에 공개하고 싶지 않으면 private, protected, internal로 변경해야함


10. 클래스 상속

상속을  허용하려면 class 앞에 open 써줘야한다.
상속받는법은 class 자식클래스 : 부모클래스() { }
- 다만 부모클래스 주생성자가 매개변수 필요하면 매개변수도 넣어줘야한다.

open class Base(val name : String) {
    fun sayMyName() {
        println("sayMyName() name : $name")
    }
}

class Person(_name : String) : Base(_name) {
    init {
        println("Person init")
    }
}

fun main() {
    var p : Person = Person("a")
    p.sayMyName()
}

11. interface

인터페이스에 변수/함수를 구현해두면 상속받은 클래스에서는 반드시 다 override해야한다.
다만 강제성이 주목적이 아니라 상속이라는 top-down 방식에서 좀 더 가볍게 특정 속성들만 넣는 클래스가 필요할때 인터페이스로 사용한다.
예를들어 사람클래스 - 귀여운 사람 클래스를 만들때 귀여운이라는 속성을 interface로 만들어서 붙이는 느낌
그러면 나중에 귀여운 동물 클래스를 만들때도 동물 클래스 + 귀여운 interface로 클래스 만들기 좋으니까

 

1) 인터페이스 선언

interface i {
    fun func()
}

 

2) 인터페이스 추가

보통 상속 다음에 작성

class 쥬피썬더(name: String, age: Int) : 이브이(name, age), Fighting, Lightening {
    override fun sayInfo() {
        super.sayInfo()
        println("쥬피썬더")
    }

    override fun chainAttack() {
        super.chainAttack()
        println("연쇄공격")
    }
}

12. abstract class

open class는 객체로 생성이 가능하다
그런데 진짜 상속 목적으로만 만들고 객체 생성은 막고 싶다면?
abstract class로 만든다


13. enum class

enum class Pet {
    CAT, DOG, BIRD
}

val pet = Pet.DOG


enum class도 매개변수를 가질 수 있음

enum class Pet(val info : String) {
    CAT("고양이"), DOG("강아지"), BIRD("새")
}

val pet = Pet.DOG
println(${pet.info}) // "강아지" 출력

14. data class

데이터를 다룰때 특화된 클래스

1) 특징

- 일반적인 객체는 동일 클래스에서 찍어낸 객체이고 멤버변수 값이 모두 같아도 서로 다른 객체이다.
- 반면 data class에서 찍어낸 객체는 멤버변수 값이 모두 같으면 같은 객체 판정이다.

val data01 = SomeDataClass("홍길동", 20)
val data02 = SomeDataClass("홍길동", 20)

val areSame2 = (data01 == data02)
println(areSame2)

println("data01.hashCode() : ${data01.hashCode()}")
println("data02.hashCode() : ${data02.hashCode()}") // 멤버 변수 값이 모두 같으므로 둘의 hashCode도 일치한다

 

2) data class의 값 참조

data class의 멤버변수들을 아래 형태로 가져올 수 있다
- 다른 클래스에서는 사용 불가능하고 data class에서만 지원하는 기능

 

가. 구조 분해 방식

val data03 = SomeDataClass("빡코딩", 30)
val (name, age) = data03
println(name)
println(age)


나. 멤버변수 순서대로 참조

data03.component1()
data03.component2()
data03.component3()

 

 

3) data class copy

data class를  copy만 하고 값을 아무것도 안바꾸면 data class 특징에 따라 같은 객체로 판단한다

val data01 = SomeDataClass("홍길동", 20)
val data02 = data01.copy(name = "강호동") // 복사해와서 이름만 변경
val data03 = data01.copy() // 복사하면서 아무것도 변경 안함 -> 동일한 객체를 가리킴
println("data01.hashCode() : ${data01.hashCode()}")
println("data02.hashCode() : ${data02.hashCode()}") // data01과 다른 값
println("data03.hashCode() : ${data03.hashCode()}") // data01과 같은 값

 

4) data class 출력

일반적인 객체는 print하면 주소값이 출력된다
data class 객체는 print하면 멤버변수들 값이 출력된다.

SomeDataClass(name=홍길동, age=20) // 이런식으로 출력된다다

 

5) data class 상속

data class가 일반 open class를 상속받을 수 있다
단, data class는 open이나 abstract가 될 수 없다

6) data class 예시

Pair나 Triple같은 객체도 data class로 구현되어 있다.

7) data 클래스 내의 함수

data class 내에도 생성자, 추가생성자, 함수를 구현할 수 있다
다만 목적자체가 객체의 행동보다는 값에 있다보니 값을 set/get하는 정도로만 구현해서 사용한다


15. object class

대표적으로 세 종류가 있다
- singleton
- companion object
- anonymous(이름없는 객체)

1) singleton

- object 키워드로 선언

object UserInfo {
    var name : String = "강호동"
}

- 일반 open class나 interface 상속받을 수 있다

- 주생성자 사용하지 않는 편이다
- 그 외는 싱글턴 특성 따라간다
  - 한개만 생성
  - 전역객체라 어디서든 사용 가능
  - 처음 사용하는 시점에 객체가 생성되어 이때 memory에 올라간다

2) companion object

9. 클래스 항목 참고

 

3) anonymous

- 이름없는 object 생성
- 클래스 없이 임시객체 만들어쓸때 사용
- 익명 object도 상속이나 인터페이스 채택 가능하다

var somInstance = object {
    var name : ~~
    var age : ~~
}

 

4) data object

- data class와 동일하지만 싱글턴의 특성을 가진다
- 일반적으로로 object를 print하면 주소값이 나오는데 data object를 print하면 이름이 출력된다.
  - 디버깅할때 편리하다


16. 오버라이딩

클래스를 상속할 수 있게 해주려면 open class처럼 open을 붙여야하는데, 이는 함수도 마찬가지다.

open fun처럼 붙여줘야 해당 함수를 override 할 수 있다.
상속받는 child쪽은 fun 앞에 override 붙이면 된다.

 

1) 자식클래스에서 부모클래스의 함수 호출

child에서 부모의 함수 호출하고 싶으면 super.func() 처럼 사용한다.

open class Base {
    open fun attack() {
        println("attack-p")
    }
}

class water : Base() {
    override fun attack() {
        super.attack()
        println("attak-w")
    }
}

fun main() {
    var w : water = water()
    w.attack()
}

 

한번에 여러개의 interface를 상속받을 수 있는데, 만약 서로 다른 interface에 동일한 이름의 함수가 있다면
super<인터페이스 이름>.func() 처럼 구분해서 호출할 수 있다.

 

2) getter / setter 오버라이딩

특정 변수를 override해서 getter / setter를 오버라이딩할 수 있다.

override var name: String
    get() {
        return "하고싶은말~~ : ${super.name}"
    }
    set(value) {
        if (value != "a") {
            super.name = value
        }
    }

17. 제네릭

1) 제네릭 함수

제네릭 함수 생성

fun<T> func(param : T) {}

 

제네릭 함수 호출

val result = sayHello("야채") // T가 String


2) 제네릭 클래스

제네릭 클래스 생성

class Restaurant<T>(val food : T) { }


제네릭 클래스 호출

val restaurant = Restaurant(food = "짜장면") // T가 String


※ 만약 제네릭 변수를 여러개 쓰고싶다면 <A, B, C>처럼 여러개 쓰면 된다

 

3) 범위 제약

T에는 아무 자료형이나 들어갈 수 있지만, 어느정도 제약을 두고 싶다면 아래처럼 사용할 수 있다

fun <T : A>


예를들어 A -> B -> C 구조로 상속했다면
fun<T : A>로 선언 시 A와 A의 자식클래스들만 들어갈 수 있다.

다른 방법으로는 sql처럼 where로 사용 가능하다

fun <T> func() : T where T : String, T : Int  
// T에는 String, Int만 들어갈 수 있다


만약 클래스에 T의 범위를 정해두면 클래스 내의 멤버변수/멤버함수에 있는 T에도 똑같게 적용된다.


18. 정리 전

정리 전인 내용

# 객체는 주소값을 전달한다
- 코틀린도 객체는 리턴하거나할때 객체의 주소값을 리턴한다
- 기본타입은 값이 복사되어 전달된다.
- 그러므로 객체는 리턴해서 넘기면 다른 함수에서 변경시 원본 객체에도 영향을 끼친다
- 이걸 막으려면 불변객체로 넘기거나, 객체.copy()해서 넘겨야한다.

# 스코프함수
특징
- apply, also : 객체 자신 반환
- run, let, with : 마지막줄 반환

this, it
- also, let : it
- apply, run, with : this

주요 사용 시점
- apply : 변수에 담는 시점에 주로 씀
- also : 로그 찍거나 유효성 검사할때
- let : Null체크할때(?.let)
- run : 객체값으로 어떤 결과를 만들어야 할때(예를들어 int list가 있을때 "이사람의 나이는 this.age입니다" 같은 문자열을 반환해야할때)
- with : 확장함수로 생각하지말고 그냥 일반함수로 생각하기(다만 스코프함수다 정도로...) 이름 반복해서 부르기 귀찮을때 this 생략하기 위해 쓴다고 생각하기


# 싱글턴 사용 법
애초에 그냥 class보다는 object를 사용해서 구현한다.
1) 클래스 생성자를 private으로 만들어야하므로 class Name private constructor() {} 처럼 쓴다.
2) 객체 instance를 제공해야하므로 companion object내에 getInstance나 instance 함수로 return this한다.

# init
- 객체 생성할때마다 호출되고, 주생성자 > init > 부생성자 순으로 호출된다.
- 최초 1회만 호출하게 하려면 companion object의 init 활용
- 객체 생성할때마다 호출할거면 생성자에 넣어도 되지 않나 싶은데 C++은 생성자에 코드작성할 수 있지만 코틀린은 클래스명쪽에 한줄로 쓰니까 적을 자리가 없음

# 형변환 관련
double을 int로 는 toInt()
list를 set으로는 toSet()이다. toSetOf 아니다. 이런거 없다.
map으로 바꿀땐 list가 pair들로 구성된 list라면 toMap()이 가능한데 대부분 아니니까 associate 활용한다.
문자열로는 toString
mutable 붙이고 싶으면 l.toMutableList()
=> 이 to를 이용한 형변환들은 모두 새로운 객체가 생기는거라서 기존값에는 영향을 주지 않는다.





___
# 번외
kotlin은 객체 생성시 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다.
가끔 생성만 해두고 나중에 초기화하고 싶을때 "lateinit"을 사용
```kotlin
class MyClass {
    private lateinit var a : String
   
    fun setUp() {
        s = "abc";
    }
}

val myClass = MyClass()
println(myClass.a)            // 초기화 안했으므로 컴파일 오류 발생, 적어도 런타임 에러가 아닌게 이점
```

___
# 번외

## 1. 코틀린 자료형

1) 코틀린은 암시적 형변환 허용하지 않음
그래서 var l : Long = 100 하면 안되고 100L 붙여야함
실수형은 기본자료형이 double이라서 float 쓸거면 3.14f 처럼 f 붙여야함

2) 코틀린 char는 2바이트다
안드로이드는 유니코드(UTF-16)을 기본값으로 쓰기때문에 2바이트이고, 숫자로 처리되지 않아서 'A' == 65같은거 불가능하다

3) unsigned는 UByte, UInt, ULong처럼 쓴다

4) 형변환 방법
val d : Double = 3.14
val f : Float = d.toFloat()
이처럼 .to자료형() 으로 함수로 바꿔야한다.
toInt(), toChar() 등등

문자열을 정수로 변환하는 stoi같은 기능도 있는데 "123".toInt()처럼 사용하면 123이 된다, 다만 변환 실패하면 Exception 발생해서 위험하다
이런 경우 "123".toIntOrNull()을 사용하면 정수로 변환하되 실패하면 null을 반환하게 된다

5) as를 이용한 형변환
내가 만든 객체들은 toA() toB()같은 함수가 없음
- 상속관계인 경우 : as를 사용
val objB : B = A as? B 처럼 하면 A를 B로 형변환해서 objB에 넣음
- 상속관계 아닌 경우 : as 못씀
___
# 향후 확인 필요
1) isNullOrEmpty, isNullOrBlank
2) arrayof와 listof의 차이점 -> array는 고정길이배열, list는 가변길이배열
3) 왜 set은 add인데 map은 put인지?
4) 객체를 원본 유지하면서 쓰려면 copy함수 사용해야한다는데?
5) 제네릭 추가 내용 확인
6) 인라인 함수 관련 상세 내용
7) 코루틴