참고하면 도움될 사이트
철학
- 자료형 명시를 중요시하여 사전에 버그를 막고자 함
키워드, 타입
val // 변경 불가능(Immutable)
var // 변경 가능(Mutable)
var function: () -> Unit // 인자로 아무것도 받지 않으며 return 하는것도 아무것도 없다
- Null 값 아님을 보장
!!
- 변수뒤에 !! 을 추가하면 null값이 아님을 보증
val name: String? = "과일"
val name1: String = name!! // !!없이 그냥 붙이면 Nullable 변수를 String 타입에 넣으려고
// 하는것이므로 에러남
- Nullable한 호출을 가능하게 해주는
?.
var empty:String? = null
println("empty: ${empty?.length}") // 이렇게 구현해서 Null이 아닌것을
- null일 경우 따로 처리해주고 싶을 때 엘비스 연산자
?:
val str: String? = null
var upperCase = if (str != null) str else null
upperCase = str?.toUpperCase() ?: "str 변수는 초기화를 해야만 upperCase() 가 가능합니다."
println(upperCase)
변수와 함수
변수
- 키워드
val
,var
- 전역변수로 사용할 땐 반드시 값을 초기화 해줘야함
class MainActivity: AppCompatActivity(){
val a = 1
var b = 2
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
- 지역변수로 사용할 땐 선언만 해주고 나중에 초기화 해줘도 괜찮음
class MainActivity: AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val a:Int // 값 초기화를 안해줘서 컴파일러가 타입 추론이 불가능하므로 타입 지정해줘야함
var b:Int
}
}
Int
Int? // Null을 허용함
함수
- 키워드
fun
fun main(): Unit{ // main 함수. Unit은 void와 같은 type이며 생략 가능 // 즉 : 뒤의 type은 함수의 반환타입! println("Hello world!") } fun sum(a: Int, b: Int): Int{ return a + b }
Single Expression (하나의 표현식)
- 한줄짜리 함수의 가독성을 높이기 위함
fun sum(a: Int, b:Int): Int = a + b fun sum(a: Int, b:Int) = a + b // 이 상황에선 return 값을 Int로 추론할 수 있어서 명시 안해줘도 됌 // : 기호는 타입 명시를 위해 필요한데 타입 명시를 할 필요가 없 // : Int가 생략
when
- if 문이 연달아 나오는걸 간결하게 바꾸기 위함
fun describe(obj: Any): String =
when (obj){
1 -> "one"
"Hello" -> "Greeting"
is Long -> "Long"
!is String -> "Not a string"
else -> "Unknown"
}
- return 되는 값이 항상 하나이므로 = 연산자를 통해 single expression 문법 적용
리스트
- 범위 표현방법
if (-1 !in 0..list.lastIndex){
println("-1은 범위 안에 없다")
}
if (list.size !in list.indices){
println("리스트 인덱스를 넘어갔다")
}
for (x in 1..5 step 2){
println(x)
}
for (x in 9 downTo 0 step 3){
println(x)
}
- 링크를 통한 리스트 활용
val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
.filter{it.startsWith("a")} // 각 item에 대해 a로 시작하는것만 선택
.sortedBy{it} // 선택된 avocado, apple을 글자순으로 정렬
.map{it.toUpperCase()} // 대문자로 변경
.forEach{println(it)} // APPLE AVOCADO가 차례로 출력됨
Array(배열)
val array: Array<int> = arrayOf(1, 2, 3) // arrayOf() 함수로 선언
array[1] = 4
print(array.set(2, 5))
print(array.get(1)) // set, get 함수로 접근 가능
Generic(제네릭)
- 사전적 의미는 “일반적인, 포괄적인”
- 아래는 간단 예시
fun main(){ val list1 = ArrayList<Int>() val list2 = ArrayList<String>() }
- 좀더 구체적으로 말하면 클래스나 메서드, 프로퍼티를 정의할 때 데이터 타입을 변수로 지정하고, 사용할 때 그 타입을 정해줄 수 있는것
- 변수로 지정한 데이터 타입을 Type parameter(타입 파라미터) 라고 함
-
사용할 때 타입 명시해주는데, 이때 명시해 주는 타입을 Type argument(타입 아규먼트) 라고 함
class Box<T> class Apple class Banana fun main(){ val appleBox = Box<Apple>() val bananaBox = Box<Banana>() }
Scope Function(범위 지정 함수)
Kotlin이 문맥(context)안에서 코드가 실행되게 해주며, 이를 다시 말하면 객체 하나에 대해 코드 블럭을 선언해서 그 내부에 코드를 작성하는것을 의미한다. 이러한 작업은 this
혹은 it
키워드를 활용하여 가능하며 이를 통해 코드가 직관적으로 보이게 한다.
-
적용 코드 예시
Person("Alice", 20, "Amsterdam").let { println(it) // 초기화된 Person 객체를 가리킴 it.moveTo("London") it.incrementAge() println(it) }
-
일반적인 코드 예시
val alice = Person("Alice", 20, "Amsterdam") println(alice) alice.moveTo("London") alice.incrementAge() println(alice)
코드블록과 불필요한 naming 작업 제거를 통해 훨씬 직관적인 코드 작성이 가능하다.
이제 비슷한 여러 scope function을 알아볼 것인데, 서로 비슷하기 때문에 다음의 2가지 포인트를 잘 기억하면서 각 scope function을 이해해야 올바르게 활용할 수 있다.
- context object(
this
,it
)을 참조하는 방법-
this
: 생략 가능하기 때문에 무분별하게 사용하면 멤버 변수에 접근하는건지 다른 변수에 접근하는건지 헷갈릴 수 있다. 따라서 객체 멤버변수를 초기화하거나, 객체의 멤버함수를 호출하는 용도로 사용하는것을 권장한다.val adam = Person("Adam").apply { // apply는 this를 참조 키워드로 사용함 age = 20 // same as this.age = 20 or adam.age = 20 city = "London" } println(adam)
-
-
사용되는 scope function:
run
,with
,apply
-
it
: 생략은 불가능하지만, it 대신 다른 이름을 지정해 줄 수 있어서 코드 활용성을 높일 수 있다.fun getRandomInt(): Int { return Random.nextInt(100).also { writeToLog("getRandomInt() generated value $it") } } val i = getRandomInt() println(i)
INFO: getRandomInt() generated value 66 66
fun getRandomInt(): Int { return Random.nextInt(100).also { value -> // it 대신 value가 객체를 가리키는 변수가 됨 writeToLog("getRandomInt() generated value $value") } } val i = getRandomInt() println(i)
INFO: getRandomInt() generated value 80 80
- 사용되는 scope function: `let`{:.success}, `also`{:.success}
- 반환 타입
- 동작 원리
- block이 lambda 문법으로 선언한 람다 변수(이렇게 말해도 될지는 잘 모르겠음)
- 따라서 람다 변수의 Input type, return type을 지정해줘야함
- (Input type) -> return type
- 위 그림에선 Input type이
확장함수
임- 확장함수여서 입력인자의 타입을 추가로 받을 수 있도록 ( )가 존재하는것으로 생각됨
- 따라서 람다 변수의 Input type, return type을 지정해줘야함
- Single Expression 문법으로 함수 정의를 { } 대신 = 로 함
-
아직 확장함수 타입과 람다가 어떻게 엮이는지 잘 모르겠음...
- block이 lambda 문법으로 선언한 람다 변수(이렇게 말해도 될지는 잘 모르겠음)
- with, apply, run, also, let 함수
- apply
- 인스턴스 생성 후 초기화 과정 수행할 때 많이 쓰임
- 스코프 내에서 참조연산자 사용하지 않아도 됨
- 객체 자신이 리시버 객체로 전달되며(with는 인자가 필요하고 apply는 자기 자신!!) 이
객체가 반환됨
-
객체의 상태를 변화시키고 변화시킨 객체를 다시 반환할 때 주로 사용
class Book(var name: String, var price: Int){ fun discount(){ price -= 2000 } } fun main(){ // apply 적용 안할때 var a = Book("Kotlin basic", 10000) a.name = "Extra " + a.name a.discount() println("${a.name}, ${a.price}") // apply 적용할 때 var b = Book("Java basic", 10000).apply{ name = "Extra " + name discount() } println("${b.name}, ${b.price}") }
- run
- apply와 마찬가지로 참조연산자를 사용하지 않음
-
apply와 달리 객체를 반환하는것이 아니라 마지막 구문 반환함
var b = a.run{ // b 변수에는 a 인스턴스의 name이 할당됨 println(price) name }
-
run은 주로 아래와 같이 이미 만들어진 인스턴스의 함수나 속성을 scope 내에서 사용해야 할 때 씀
a.run{ println(name) }
- 메인함수의 스코프에서 인스턴스의 속성, 함수에 접근하는것 보다 가독성을 올릴 수 있다!
- with
-
run과 동일한 기능을 가지지만 인스턴스를 참조연산자 대신 인자(파라미터)로 받는다는 차이만 존재
a.run {...} // a를 참조연산자로 사용 with(a) {...} // a를 인자로 받음
- 인자로 객체를 전달받으며 이 객체는 블록 내에 Receiver 객체 형태로 전달됨
-
this로 접근 가능하며 생략가능. ?.을 이용한 안전한호출 불가능
var strWorld = "world" with(strWorld){ println(this.toUpperCase()) println(toUpperCase()) }
-
- also (apply와 같은 역할), let (run과 같은 역할)
- apply, run은 scope 내에서 참조연산자 없이 인스턴스의 변수, 함수를 사용할 수 있었다면 also, let은 인자로 인스턴스를 넘긴것처럼 it을 통해서 인스턴스의 변수 함수에 접근함
- 대체 왜 이렇게 귀찮게 할까?
-
같은 이름의 변수나 함수가 scope 바깥에 존재할 경우 혼란 방지를 위해서임!
var price = 5000 var a = Book("kotlin basic", 10000) a.run{ println(price) // 5000원이 출력됨. 인스턴스의 속성보다 main의 속성을 우선시하기 때문 } a.let{ println(it.price) // 10000 출력 }
-
receiver(수신객체)
- 확장함수 개념 선행 필요
Extension Function(확장함수)
아래 람다식 챕터에서 더 자세히 설명함
- 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에서 선언된 함수
- 메소드 개념은 함수에 포함되어 있으며 클래스, 구조체 등에 속하는 함수일 경우 메소드라고 함
fun String.lastChar(): Char = this[this.length-1] // String 클래스의 확장함수를 정의한 것 // String 클래스에 lastChar() 이라는 메소드는 원래 없음 // String 클래스를 통해 문자열의 마지막 문자 출력 해주는 메소드 정의함 // 여기서 this는 String 클래스의 "객체" // 선언할 땐 fun 키워드와 함수 이름 사이에 확장할 클래스의 이름과 점을 붙임
- 메소드 개념은 함수에 포함되어 있으며 클래스, 구조체 등에 속하는 함수일 경우 메소드라고 함
클래스
필드, 프로퍼티
- Java
- 필드: 클래스 내의 멤버변수
- 프로퍼티: 필드 + get함수, set함수
- Kotlin
- 코틀린에서는 필드 = 프로퍼티
- 클래스 내의 멤버변수 선언 키워드에 따라 set, get이 자동으로 생성되기 때문
-
코틀린의 프로퍼티는 생성자에
val
,var
키워드의 유무에 따라 결정된다class Person(val name: String, var isMarried: Boolean)
- name, isMarried가 Person 클래스의 프로퍼티가 된다
class Person(name: String, isMarried: Boolean)
- name, isMarried 파라미터는 단지 값만 가지고 있을 뿐 프로퍼티가 되지 않는다
- 이걸 C++에 대입해서 생각해보면, 생성자를 통해 값을 받기는 하지만 멤버변수에 값 대입을 하지는 않은것으로 생각하면 된다
class(Person val name: String, var isMarried: Boolean) // 이 코드를 풀어서 쓰면 class Person { val name: Int get() { return this.age } var isMarried: Boolean get() { return this.isMarried } set(isMarried: Boolean) { this.isMarried = isMarried } } // 실제로 사용할 땐 fun main(){ val p0 = Person("Hojun", 16) println(p0.name) // 이게 getName() 위 name 뒤에있는 get함수 호출하는 것임 }
- val 키워드는 get만 생성되고, var 키워드는 set, get 모두 생성된다
- 코틀린에서는 필드 = 프로퍼티
생성자
초기화
- 코틀린의 클래스는 하나의 Primary constructor와 다수의 Secondary constructor를 가질 수 있다
class Person constructor(val name: String) { }
class Person(val name: String) { } // primary constructor가 어노테이션이나 접근 제한자(public, private)
// 를 가지고 있지 않다면 constructor 키워드 생략 가능
// 조금 어색하겠지만 클래스명 이름 옆에 있는 괄호가 주 생성자임
// 코틀린에서 주 생성자에는 아무런 코드를 넣을 수 없으므로 만약 초기화를 위해 코드가 필요하다면 init 블록을 사용한다
// 주로 주 생성자내의 val, var을 통해 초기화를 진행하지만 그렇지 않을 경우 init 블록을 사용해서 초기화를 진행하는 방식이다.
class Person(name:String){
init{
if (name.isEmpty()){
println("이름이 비었습니다")
this.name = name // init블록에서 property 접근하려면 this 키워드 사용
}
}
}
// 이렇게 선언해주는것이 primary constructor을 통해 property 선언, 초기화 동시에 하는것
class Person(val firstName: String, val lastName: String, var age: Int) { /*...*/ }
class Person(val name: String) { // primary constructor가 선언되어있음
// 기본적으로 public이며 다른 접근제한자를 두고싶은 경우
// constructor 키워드 앞에 붙여주면 됨
// 또한 this() 생성자를 이용해 직간접적으로
// primary constructor에 위임해야 함
var age: Int = 26
constructor(name: String, age: Int) : this(name) {
// 보조생성자는 val, var 선언 안하는것 주목. 단지 값을 전달하는 파라미터 역할을 하는 것
// 위 코드를 더 해석하자면, 보조 생성자가 받아온 name값을 주 생성자를 통해 초기화 시긴다는 것이다
this.age = age
}
}
class E { // 주 생성자를 생략한 경우엔 중괄호 안에있는 constructor가 주 생성자가 되는 것
var name: String
var age: Int = 1
var height: Int = 2
init {
println("call Init Block!")
}
constructor(name: String) { // 여기선 이게 주 생성자임
// 클래스 이름 E 옆에 주 생성자가 없기 때문
this.name = name
println("call Name Constructor!")
}
constructor(name: String, age: Int) : this(name) { // 여기부턴 보조 생성자 이므로
// 주 생성자에게 생성을 위임해야함
this.age = age
println("call Name, Age Constructor!")
}
constructor(name: String, age: Int, height: Int) : this(name, age) {
this.height = height
println("call Name, Age, Height Constructor!")
}
}
- 보조 생성자는 클래스를 사용하는 사람에게 인스턴스를 다양하게 초기화 시킬 수 있도록 도와주는 역할
상속
- 상속이 필요한 경우는 2가지
-
기존 클래스의 기능을 기본으로 몇개 더 붙인 클래스들이 필요할 때
-
클래스간의 공통된 코드들이 있을 때 코드 관리의 용이함을 위해서
-
-
지켜야 하는 규칙 2가지
- 수퍼 클래스에 존재하는 속성과
같은 이름
의 속성으 가질 수 없음 - 서브 클래스가 생성될 때 반드시 수퍼클래스의 생성자까지 호출되어야 함
open class Animal(var name:String, var age:Int, var type:String){ fun introduce(){ println("I'm ${name}, ${type} and ${age} years old") } } class Dog (name:String, age:Int) : Animal(name, age, "개"){ }
- 선언한 클래스 이름 옆에 콜론을 붙이고 수퍼 클래스의 생성자 호출
- 이때 수퍼 클래스, 서브 클래스는 공통된 속성 가질 수 없으므로 Dog의 주 생성자에서 var, val 키워드를 붙이지 않은것을 주목
- 조금 더 자세히 설명해보면 Dog의 주 생성자를 통해 들어온 값을 그대로 Animal 주 생성자의 프로퍼티에 대입한다는 의미로 봐도 됨
- 수퍼 클래스에 존재하는 속성과
오버라이딩
- 수퍼 클래스, 자식 클래스가 같은 이름의 함수를 가질 수 있게 해줌
fun main(){
val t = Tiger()
t.eat()
}
open class Animal{
open fun eat(){
println("eat food")
}
}
class Tiger: Animal(){
override fun eat(){
println("eat meet")
}
}
- Animal 클래스의 eat 함수에
open
키워드가 붙은것을 주목- 저 키워드가 없다면 override 불가능
추상화
-
오버라이딩은 기능 구현이 의무가 아님
-
오버라이딩과 달리 Animal 클래스를 상속하는 모든 서브 클래스는 반드시 eat 가지며, 직접 구현해야함을 명시
-
추상화는 추상 클래스라는 요소를 통해 가능해짐
- 추상함수가 하나라도 있으면 추상 클래스
- 추상 클래스는 일부 기능이 구현 안되었기 때문에 단독으로 인스턴스화 불가능
인터페이스
- 다른 언어에서의 인터페이스 (예외도 물론 존재)
- 추상 함수로만 이루어져 있는 순수 추상화 기능
- 코틀린에선
- 추상함수, 일반함수, 속성 모두 가질 수 있음
- 클래스와 달리 생성자를 가질 순 없음
- 서브 클래스에서 모든 함수의 구현 및 재정의 가능 (어떻게 가능한가?)
- 구현부가 있으면
open
키워드 붙은것으로 간주 - 구현부가 없으면
abstract
키워드 붙은것으로 간주
- 구현부가 있으면
-
여러 인터페이스를 상속받을 수 있으므로 코드 설계가 더 유연해짐
fun main(){
var d = Dog()
d.run()
d.eat()
}
interface Runner {
fun run()
}
interface Eater {
fun eat(){
println("음식을 먹습니다")
}
}
class Dog: Runner, Eater{
override fun run(){
println("우다다다 뜁니다")
}
override fun eat(){
println("허겁지겁 먹습니다")
}
}
- 위 코드 해석할 때 Dog class의 중괄호 안에 constructor { }가 생략된 것으로 봐야할 듯
- 즉, 주 생성자에서 어떠한 속성도 만들지 않고, 값을 받지도 않겠다! 라는걸로 보임
open, final, abstract
하나의 부모클래스를 여러 자식 클래스가 상속한다고 했을 때 부모 클래스를 수정할 경우 프로그램에 치명적인 오류가 발생할 수 있으며 이를 취약한 기반 클래스라고 함. open, final, abstract 키워드는 이러한 사태를 미연에 방지하고자 존재
접근 방법은 상속을 금지하는 것
- final
- 상속 금지
- 자식 클래스에서 override하게 의도한 클래스 혹은 메소드가 아니면 final로 만들것을 권유함. 클래스 필드의 default도 final임 (interface는 default가 public)
- open
- 클래스 혹은 메소드의 상속을 허용하기 위해 붙여주는 키워드
-
코드 예시
interface Clickable { fun click() fun showOff() = println("I'm clickable!") } open class RichButton : Clickable { fun disable() {} open fun animate() {} override fun click() {} } class childBtn : RichButton() { override fun click() { super.click() } override fun animate() { super.animate() } override fun showOff() { super.showOff() } }
- childBtn에서 disable() 메소드는 override 불가능 (open 키워드가 없어서)
- RichButton에서 Clickable을 확장(interface는 extend 키워드를 자바에서 사용. 그러니 확장이라는 단어를 사용했음)할 때 final override fun click() {} 으로 선언할 경우 childBtn 에서 click() 메소드 override 불가능
- abstract
- abstract 클래스는 인스턴스화 불가능
-
abstract 클래스내의 메소드들은 항상 open이여서 따로 open 선언 안해줘도 됨
abstract class Animated{ abstract fun animate() open fun stopAnimating(){ } fun animateTwice(){ // 이게 open이란 뜻이 아님! abstract 키워드가 붙어있으면 open까지 겸해진다는 것 // 즉 animateTwice는 abstract 키워드가 없어서 상속 불가능 } } open class RichButton : Animated() { override fun animate(){ } override fun stopAnimating(){ super.stopAnimating() } }
- animate()는 반드시 override 해줘야 함
- stopAnimating()은 override를 해줄수도 있고 안해줘도 됨
- animateTwice()는 override 불가능
다형성(Polymorphism)
다형성은
기능
임 하나의 객체가 여러가지 타입을 가질 수 있도록 하는 기능
- 어떻게 다형성 기능을 구현할 수 있는가?
- 상속과 Up-Casting, Down-Casting을 통해!
-
Up-Casting: super class의 자료형에 child class 인스턴스를 담는 것
var a:Drink = Cola()
- 콜라 인스턴스를 콜라 타입이 아닌 음료 타입에 담았음
- 이럴경우 콜라의 기능은 사용하지 못하며 음료의 기능만 사용 가능
- 단 콜라 클래스에서 음료 함수를 override 했을 경우 이렇게 override 된 함수는 사용가능
-
Down-Casting: Up-Casting 된 인스턴스를 다시 child 자료형으로 변환하면 Down-Casting 이며 이를 위한 연산 필요 (as, is)
- as: 변수를 호환되는 자료형으로 변환해주며 변환된 자료형을 return도 해줌
- is: 조건문내에서 사용되며 조건문의 scope에서만 자료형을 변환시킴
fun main(){ val a = Cola() a.drink() val b:Drink = Cola() // b.washDishes() // 에러 b as Cola b.washDishes() } open class Drink(){ // constructor(){ // println("Drink constructor called..") // } var name = "drink" open fun drink(){ println("drink ${name}.") } } class Cola(): Drink(){ // 상속할 땐 부모 생성자 호출이 반드시 필요한 것으로 생각하자 // Cola의 주생성자는 생략해도 괜찮음 var type = "Cola" override fun drink(){ println("drink drink of Cola") } fun washDishes() { println("washing dishes with ${type}") } }
-
- 상속과 Up-Casting, Down-Casting을 통해!