4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
4.4 object 키워드 : 클래스 선언과 인스턴스 생성
companion object(동반 객체)
- kotlin에서는 동반 객체를 사용하여 정적 멤버를 정의한다. (static 없음)
- 동반 객체는 클래스 내부에 존재하며, 클래스가 메모리에 적재될 때 함께 생성된다.
- 클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
- 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A {
companion object {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
}
fun main() {
println(A.test)
A.test2()
}
class A {
companion object AA {
val test = "companion object called!"
fun test2(){
println("companion object called!")
}
}
// companion object { 하나의 클래스에는 하나의 동반 객체만 생성가능하다!
// val test = "companion object called!"
// fun test2(){
// println("companion object called!")
// }
}
}
fun main() {
println(A.AA.test)
A.AA.test2()
A.test2() // 객체 이름을 제외해도 A.AA.test2()처럼 똑같은 결과가 나옴
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
팩토리 패턴?
- 객체를 생성하기 위한 인터페이스를 정의하고 어떤 구체적인 클래스의 인스턴스를 생성할지는 서브클래스가 결정하도록 한다.
- 객체 생성 코드를 추상화해서 관리
- 객체 생성 담당 클래스를 만들어 관리하고 객체간의 결합도를 감소시킨다.
- →그렇지않으면 객체의 생성,수정,삭제 발생했을 시 해당 코드를 전부 수정해야 함
(팩토리 패턴의 종류 : 추상 팩토리 패턴 / 팩토리 메소드 패턴)
동반 객체 안에서 팩토리 패턴 정의
class A private constructor(val name: String) { // 클래스 A의 주 생성자는 private로 접근을 제한
companion object{
val a = "companion object"
fun bar(): A{
return A("companion object") //companion object의 bar() 메서드를 통해 private 생성자에 접근 할 수 있다.
}
}
}
fun main() {
var a = A("companion object") // 실패, 주 생성자는 private 접근제한자로 설정되어 호출 할 수 없다.
var b = A.bar() // 성공, 동반객체의 bar() 메서드를 통해 private 주 생성자에 접근이 가능하다.
}
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. ( 바깥쪽 클래스의 private 생성자도 호출할 수 있다) → 팩토리 패턴을 구현하기 가장 적합하다.
3.함수 정의와 호출
3.1 코틀린에서 컬렉션 만들기
코틀린은 자신만의 컬렉션 기능을 제공하지 않음
코틀린 자체 컬렉션이 없는 이유 → 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 쉬움. 자바에서 코틀린 함수 호출/코틀린에서 자바 함수 호출할 때 서로 변환할 필요가 없음.
ex) 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값 찾기
3.2 함수를 호출하기 쉽게 만들기
3.2.1 이름 붙인 인자
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있음.
호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 함.
jointoString(collection,separator=" ",prefix=" ",postfix=".")
자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없음.
안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자를 쓸 수 없다.
3.2.2 디폴트 파라미터 값
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있음 → 오버로드 방지
대부분 아무 접두사나 접미사 없이 콤마로 원소를 구분함
//디폴트 값이 지정된 파라미터
fun <T> joinToStrlng (
co11ection : Collection<T>,
separator:String=",",
prefix:String="",
postfix:String=""
) : String
joinToString(list,",","","")
joinToString(list)
joinToString(list,";")
이름 붙은 인자를 사용하는 경우 목록의 중간에 있는 인자를 생략하고 지정하고 싶은 인자를 이름을 붙여 순서와 관계없이 지정할 수 있음
joinToString(list,postfix=';",prefix="#")
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨
함수의 디폴트 값을 바꾸고 클래스 파일을 재컴파일하면 바뀐 디폴트 값을 적용받음
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
최상위 프로퍼티
파일의 최상위 수준에 놓을 수 있음.
var opCount = 0 //최상위 프로퍼티 선언
fun performOperation() {
opCount++ //최상위 프로퍼티의 값 변경
// ...
}
fun reportOperationCount() {
println("Operation performed $opCounttimes") // 최상위 프로퍼티의 값을 읽음
}
이런 프로퍼티의 값은 정적 필드에 저장됨
3.3 메소드를 다른 클래스에 추가 : 확장 함수와 확장 프로퍼티
코틀린의 핵심 목표 : 기존 코드와 코틀린 코드를 자연스럽게 통합
확장 함수 : 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 기능을 사용하게 함
확장함수의 개념 : 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수
수신 객체 타입 : 함수가 확장할 클래스의 이름 (확장이 정의될 클래스의 타입)
수신 객체 : 확장 함수가 호출되는 대상이 되는 값 (클래스에 속한 인스턴스 객체)
함수 호출은 다른 일반 클래스 멤버 호출 구문과 같음
println("Kotlin".lastChar())
확장 함수 본문에 this 사용 가능 확장 함수 본문에서 this 생략 가능
fun String.lastChar() " Char = get(length-1)
클래스 내에서 정의한 메소드와 달리
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없음
3.3.1 임포트와 확장 함수
확장 함수를 사용하기 위해서는 함수를 다른 클래스나 함수와 마찬가지로 import 해야 함
한 클래스에 같은 이름의 확장 함수가 둘 이상 있으면 이름이 충돌할 수 있음 → 코틀린에서는 개별 함수를 임포트 할 수 있음
import string.lastChar
val c = "Kotlin".lastChar()
import string.*
val c = "Kotlin".lastChar()
import string.lastChar as last // import한 클래스나 함수를 다른 이름으로 부를 수 있음
val c = "Kotlin".lastChar()
3.3.2 자바에서 확장 함수 호출
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드
→ 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않음
3.3.3 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수 선언
separator:String=",", //파라미터 디폴트 값 지정
prefix:String="",
postfix:String=""
):String {
val result = StringBuilder(prefix)
for((index,element) in this.withIndex()) //this == 수신 객체
if (index>0) result.append(separator)
result.append(element)
}
3.3.4 확장 함수는 오버라이트 할 수 없다
확장 함수 → 오버라이드 불가
open class View{
open fun click() = println("View clicked")
}
class Button:View) { //Button은 View를 확장함
override fun click() = println("Button clicked")
}
Button은 View의 하위 타입 → View 타입 변수를 선언해도 Button 타입 변수를 변수에 대입 불가
확장 함수는 클래스의 일부가 아님! 클래스 밖에 선언됨
이름과 파라미터가 같은 확장 함수 기반 클래스와 하위 클래스에 대해 정의해도 실제로 확장 함수 호출 시 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정.
변수에 저장된 객체의 동적 타입에 의해 확장 함수 결정되지 않음.
코틀린은 호출될 확장 함수를 정적으로 결정
3.3.5 확장 프로퍼티
//확장 프로퍼티 선언
val String.lastChar:Char
get() = get(length-1)
기본 getter 구현을 제공하지 않으므로 getter는 꼭 정의해야함
초기화 불가
//변경 가능한 확장 프로퍼티 선언
val StringBuilder.lastChar: Char
get() = get(length-1) //프로퍼티 게더
set(value:Char) {
this.setCharAt(length-1,value) //프로퍼티 세터
}
3.4 컬렉션 처리 : 가변 길이 인자중위 함수 호출,라이브러리 지원
vararg : 호출 시 인자 개수가 달라질 수 있는 함수를 정의 가능함
중위 함수 호출 구문 : 인자가 하나뿐인 메소드를 간편하게 호출
구조 분해 선언 : 복합적인 값을 분해해 여러 변수에 나눠 담을 수 있음
3.4.2 가변 인자 함수 : 인자의 개수가 달라질 수 있는 함수 정의
자바의 가변 길이 인자 : 메소드 호출시 원하는 개수만큼 값을 인자로 넘김 → 컴파일러가 배열에 값을 넣음
코틀린의 가변 길이 인자 : 타입 뒤에 …을 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
fun listOf<T>(varang values: T): List<T> { ...}
3.4.3 값의 쌍 다루기 : 중위 호출과 구조 분해 선언
중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음
(객체,메소드 이름,유일한 인자 사이에 공백이 들어가야 함)
1.to("one") //to 메소드를 일반적인 방식으로 호출
1 to "one" //to 메소드를 중위 호출 방식으로 호출
3.5 문자열과 정규식
3.5.1 문자열 나누기
코틀린에서는 자바의 split 대신 split 확장 함수를 제공함
정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받음
3.5.2 정규식과 3중 따옴표로 묶는 문자열
fun parsePath(path:String) {
val regex ="""(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if(matchREsult != null) {
val (directory,filename,extension) = matchResult.destructured
println("Dir : $directory,name:$filename,ext$extension")
}
}
3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요 없음
패턴 .은 임의의 문자와 매치될 수 있음
3.5.3 여러 줄 3중 따옴표 문자열
3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이 그대로 들어감
→ 줄바꿈이 있는 프로그램 텍스트를 쉽게 문자열로 만들 수 있음
여러줄 문자열에는 줄 바꿈이 들어가지만 줄바꿈을 \n과 같은 특수 문자를 사용해 넣을 수 없음. \를 문자열에 넣고 싶으면 이스케이프할 필요가 없음.
"C://Users//yole//kotlin-book" """C:\Users\yole\kotlin-book"""
3중 따옴표 문자열 안에 $를 넣어야한다면 문자열 템플릿 안에 넣어야 함 → ‘$’
3.6 코드 다듬기 : 로컬 함수와 확장
DRY 원칙
Don’t Repeat Yourself
코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있음
→ 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조직할 수 있음
4장
4.1 클래스 계층 정의
4.1.1 코틀린 인터페이스
interface 사용
// 인터페이스 선언
interface Clickable {
fun click()
}
click 추상 메소드가 있는 인터페이스 정의
인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야함
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("clicked")
}
코틀린에서는 클래스 이름 뒤에 콜론을 붙이고 인터페이스와 클래스 이름을 적음
클래스는 인터페이스를 원하는 만큼 구현할 수 있으나 클래스는 오직 하나만 확장할 수 있음
override 변경자 : 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시
상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안 되므로 override를 붙이거나 메소드이름을 바꿔야 함
// 디폴트 구현이 있는 메소드
interface Clickable {
fun click()
fun showOff() = println("clickable!")
}
showOff 메소드는 새로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수 있음
4.1.2 open,final,abstract 변경자 : 기본적으로 final
자바에서의 final : 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있음
취약한 기반 클래스 : 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김 → 어떤 클래스가 자신을 상속하는 방법에 대한 정확한 규칙을 제공하지않으면 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있음
→ 조슈아 블로크 “상속을 위한 설계와 문서를 갖추거나,그럴 수 없다면 상속을 금지하라” : 하위 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들어야 함
코틀린의 클래스와 메소드는 기본적으로 final !
클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야함
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야함
open class RichButton : Clickable {
final override fun click() {}
}
4.1.3 가시성 변경자 : 기본적으로 공개
가시성 변경자 : 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
어떤 클래스의 구현에 대한 접근을 제한해 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있음
코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개 public
새로운 가시성 변경자 internal : “모듈 내부에서만 볼 수 있음”
→ 모듈이란? 한 번에 한꺼번에 컴파일되는 코틀린 파일들
모듈 내부 가시성 장점 : 모듈의 구현에 대한 진정한 캡슐화를 제공
4.1.4 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
코틀린의 중첨 클래스 : 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음
// 중접 클래스를 사용해 코틀린에서 view 구현하기
class Button : View {
override fun getCurrentState() : State = ButtonState()
override fun restoreState(state: State){}
class ButtonState : State {}
}
// 바깥쪽 클래스의 인스턴스를 가리키는 참조 표기
class Outer {
inner class Innter {
fun getOuterReference() : Outer = this@Outer
}
}
4.1.5 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
sealed class : 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음 . sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야함
sealed class Expr {
class Num(val value:Int) : Expr()
class Sum(val left:Expr,val right:Expr):Expr()
} // 기반 클래스의 모든 하위 클래스를 중첨 클래스로 나열
fun eval(e:Expr) : Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
} // when식이 모든 하위 클래스를 검사하므로 별도의 else분기가 필요없음
4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
4.2.1 클래스 초기화 : 주 생성자와 초기화 블록
class User(val nickname:String) // val : 파라미터에 상응하는 프로퍼티 생성
주 생성자 : 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드
→ 생성자 파라미터를 지정하고 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 사용됨.
class User(val nickname:String,
val isSubscribed:Boolean = true) // 생성자 파라미터에 대한 디폴트값 제공
클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만듦
open class Button // 인자 없는 디폴트 생성자가 만들어짐
class RadioButton : Button()
Button의 생성자는 아무 인자도 받지 않지만 Button 클래스를 상속한 하위 클래스는 반드시 생성자를 호출해야함
→ 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어감
4.2.2 부 생성자 : 상위 클래스를 다른 방식으로 초기화
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 함.
부 생성자가 필요한 이유 → 자바 상호운용성성
4.2.3 인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음
interface User {
val nickname : String
}
→ User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 함
class PrivateUser(override val vickname : String) : User
// 주생성자에 있는 프로퍼티
class SubscribingUser(val email:STring) : User {
override val nickname : String
get() = email.substringBefore('@') // 커스텀 게터
}
class FacebookUser(val acoountId:Int) : User {
override val nickname - getFacebookName(accountId) // 프로퍼티 초기화 식
}
4.2.4 게터와 세터에서 뒷받침하는 필드에 접근
class User(val name:String) {
var address :String= "unspecified"
set(value:String) {
println("""
Address was changed fro $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드 값 읽기
field=value // 뒷받침하는 필드 값 변경하기
}
}
접근자 본문에서는 field 식별자를 통해 필드에 접근가능함
게터에서는 값을 읽을 수만/세터에서는 값을 읽거나 쓸 수 있음
>>>val user = User("Alice")
>>>user.address = "Elsenfeimerstrasse 47,80687 Muenchen"
4.3 컴파일러가 생성한 메소드 : 데이터 클래스와 클래스 위임
4.3.1 모든 클래스가 정의해야 하는 메소드
코틀린 클래스도 toString/equals/hashCode등을 오버라이드 할 수 있음
// Client에 toString() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
// Client에 equals() 구현하기
class Client (val name:String,val postalCode:Int) {
override fun equals(other:Any?):Boolean {
if(other == null || toehr !is Client)
return flase
return name == other.name && postalCode == other.postalCode //두 객체의 프로퍼티 값이 같은지 검사
}
override fun toString() = "Client(name=$name,postalCode=$postalCode)"
}
Any는 코틀린의 모든 클래스의 최상위 클래스
Any?는 널이 될 수 있는 타입이므로 other은 null일 수 있음
4.3.2 데이터 클래스 : 모든 클래스가 정의해야 하는 메소드 자동 생성
클래스가 데이터를 저장하는 역할말 한다면 toString/equals/hashCode를 반드시 오버라이드 해야함. 코틀린은 data 변경자를 클래스 앞에 붙여 컴파일러가 필요 메소드를 자동으로 생성함
// data class
data class Client(val name:String, val postalCode:Int)
4.3.3 클래스 위임 : by 키워드 사용
대규모 객체지향 시스템 설계 시 문제 → 구현 상속에 의해 발생
하위 클래스가 상위 클래스 메소드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존 → 하위 클래스가 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 못하는 경우가 생김
class CountingSet<T> (
valinnerSet:MutableCo11ection<T>=HashSet<T>()
):MutableC○llection<T>by innerSet { //MutableCoIIection의 구현올 innerset에거 위임
var objectsAdded=0
override fun add(element: T) : Boolean { // 위임하지않고 새로운 구현 제공
objectsAdded++
return innerSet.add(element)
)
override fun addAll (c: Collection<T>) : Bolean { // "
objectsAdded+=c.size
returnlnnerSet.addAll(C)'프로그래밍 > kotlin in action' 카테고리의 다른 글
| 5장 (1) | 2024.05.08 |
|---|---|
| 1,2장 (3) | 2024.05.05 |