반응형
본 포스트는 지인들과 스터디한 내용을 정리한 포스트입니다
http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931463422
1. 객체와 클래스
- 코틀린은 클래스 작성 등을 위한 장황한 보잉러플레이트 코드 없이 바로 객체지향 프로그래밍을 할 수 있도록 해준다
- 코틀린은 클래스 생성자를 함수처럼 사용할 수 있다( 자바처럼 new 키워드가 필요없다 )
- 개발자는 속성만 정의하면 코틀린이 필요한 부븐에 백킹 필드를 만들어준다 ( 백킹필드: 클래스의 속성 정보 )
- 데이터 클래스를 이용하면 데이터를 처리하는 것보다 데이터를 보여주는데 집중할 수 있다
2. 객체와 싱글톤
- 코틀린은 싱글톤을 직접 지원함으로써 싱글톤을 구현할 때 발생할 수 있는 부담과 구현이 잘못될수 있는 리스크를 제거했다
- 개발자가 필요하다면 클래스 정의 없이 객체를 생성할 수 있다
- 객체 표현식으로 사용하는 익명 객체
- 익명객체가 지닌 한계
- 익명 객체의 내부 타입은 함수나 메소드의 리턴타입이 될 수 없다
- 익명 객체의 내부 타입은 함수나 메소드의 파라미터가 될 수 없다
- 클래스 안에 저장된 속성들이 있다면, 해당 속성들은 Any로 간주된다. 그러면 모든 속성이나 메소드에 직접 접근할 수 없게 된다
- Java Style vs Kotlin Style
// anonymous.kts fun createRunnalble(): java.lang.Runnable { val runnable = object : java.lang.Runnable { override fun run() { println("You called...") } } return runnable } val aRunnable = createRunnalble() aRunnable.run() // You called... fun createRunnable2(): java.lang.Runnable = java.lang.Runnable { println("You called...") } val aRunnable2 = createRunnable2() aRunnable2.run() // You called...
- 익명 내부 클래스에 의해 구현된 인터페이스가 싱글 추상 메소드 인터페이스(함수형 인터페이스)라면 다음과 같이 메소드 이름을 명명하지 않고 바로 구현할 수 있다
- 익명 내부 클래스가 둘 이상의 인터페이스를 구현해야 한다면 리턴이 필요한 경우에는 반드시 리턴할 인스턴스 타입을 명시해줘야 한다
- 익명객체가 지닌 한계
3. 객체 선언을 이용한 싱글톤
- object 키워드와 {} 블록 사이에 이름을 넣는다면, 코틀린은 이를 표현식이 아니라 명령문 또는 선언으로 인식한다
- 익명 이너클래스의 인스턴스를 만들 땐 객체 표현식을 사용하고, 싱글톤을 만들 땐 객체 선언을 사용하라
- 대표적인 싱글톤 Unit
- 예제코드1
// util.kts object util { fun numberOfProcessors() = Runtime.getRuntime().availableProcessors() } println(util.numberOfProcessors())
- 객체 선언을 이용해서 만들 Util 객체가 싱글톤이다
- Util로는 객체를 생성할 수 없다
- 코틀린 컴파일러는 Util을 클래스 취급하지 않고, 객체 자체로 취급한다
- 즉 Java에서 private 생성자와 static 메서드만 가지고 있는 클래스라고 생각하면 된다
- 예제코드2
// singleton.kts object Sun : java.lang.Runnable { val radiusInKM = 696000 var coreTemperatureInC = 15000000 override fun run() { println("spin...") } } fun moveIt(runnable: java.lang.Runnable) { runnable.run() } println(Sun.radiusInKM) // 696000 moveIt(Sun) // spin...
4. 탑 레벨 함수 vs 싱글톤
- 패키지 정의
// UtilTest.kt fun unitSupported() = listOf("Metric", "Imperial") fun precision(): Int = throw RuntimeException("Not implemented yet") object Temperature { fun c2f(c: Double) = c * 9.0 / 5 + 32 fun f2c(f: Double) = (f - 32) * 5.0 / 9 } object Distance { fun milesToKm(miles: Double) = miles * 1.609344 fun kmToMiles(km: Double) = km / 1.609344 }
- 패키지 import 및 사용
// UseUtil.kt package chapter7 import Temperature.c2f import unitSupported fun main() { println(unitSupported()) println(Temperature.f2c(75.253)) println(c2f(24.305)) }
- 탑레벨 함수 선택
- 사용할 함수들이 하이레벨
- 일반적이거나 넓게 사용될 예정
- 반면 함수들이 연관되어 있다면 싱글톤을 사용하는 게 좋다
5. 클래스 생성
- class 키워드를 이용해 정의
class Car
- 읽기 전용 속성 부여
class Car(val yearOfMake: Int)
- yearOfMake라는 이름으로 Int 타입의 읽기전용 속성을 만들었다
- 코틀린 컴파일러는 생성자를 작성했고, 필드를 정의하고 해당 필드에 접근하게 해주는 getter를 추가했다
- 인스턴스 생성하기
- 코틀린은 new 키워드를 쓰지 않는다
- 함수를 사용하듯 그냥 클래스 이름을 이용한다
// property.kts class Car(val yearOfMonth: Int) var car = Car(2019) println(car.yearOfMonth)
- 읽기-쓰기 속성
- 예제코드
// readwrite.kts class Car(val yearOfMake: Int, var color: String) val car = Car(2019, "Red") car.color = "Green" println(car.color) // Green
- 읽기전용 속성을 만들 때는 val
- 변경 가능한 속성을 만들때는 var
- 예제코드
- 들여다보기 - 필드와 속성
- 코틀린에서는 클래스에 필드가 없다 ( public으로 열리는 멤버변수가 없다..? )
- 코틀린에선 getter, setter 대신 속성의 이름을 이용해서 속성에 접근할 수 있다
- 속성 제어 변경
- 예제코드
// setter.kts import java.lang.RuntimeException class Car(val yearOfMake: Int, theColor: String) { var fuelLevel = 100 var color = theColor set(value) { if (value.isBlank()) { throw RuntimeException("no empty, please") } field = value } get() = field + "hi" } val car = Car(2021, "Green") println(car.color) println(car.fuelLevel)
- 코틀린은 정의한 속성에 사용되는 getter와 setter를 생성한다
- 전달받은 값이 사용 가능하다면 값을 스페셜 키워드인 field에 의해서 참조되고 있는 필드에 할당한다
- 코틀린이 필드를 내부적으로 만들었기 떄문에 코드에서 필드에 접근할 수 있는 방법이 없다
- 즉 개발자는 getter나 setter에 있는 field 키워드를 통해서만 필드를 사용할 수 있다
- 예제코드
- 접근 제어자
- 코틀린에서 클래스의 속성과 메소드는 public이 기본이다
- 코틀린에는 4개의 접근 제어자가 있다
- public
- private
- protected
- internal
- 같은 모듈에 있는 모든 코드에서 속성이나 메소드에 접근이 가능
- 모듈이라 함은 함께 컴파일된 모든 소스 코드를 뜻한다
- 바이트 코드에 직접 나타나지 않는다
- 네이밍 컨벤션에 의해서 코틀린 컴파일러에 의해 다뤄진다
- getter의 접근 권한은 속성의 접근 권한과 동일하다
- setter의 경우 개발자가 원하는 대로 접근 권한을 설정할 수 있다
// mySetter.kts class myClass(param: Int) { var fuelLevel = param private set(value) { field = value } } val testObject = myClass(1000) println(testObject.fuelLevel) //testObject.fuelLevel = 3000 // Cannot assign to 'fuelLevel': the setter is private in 'myClass'
- setter를 다시 구현할 생각이 없다면 파라미터인 value와 메소드 바디는 생략하면 된다
- setter 역시 작성하지 않거나 접근 제어자 설정을 하지 않는다면 속성과 동일한 권한으로 접근이 가능하다
- 초기화 코드
- 생성자를 위해서 파라미터와 속성이 파라미터 리스트로 정의된다 ( 주 생성자, 첫 번째 줄에 정의 )
- 객체를 초기화하는 코드가 값들을 설정하는 것보다 더 복잡하다면 생성자용 바디를 만들 필요가 있다
- 클래스는 0개 이상의 init 블록을 가질 수 있다
- init 블록의 코드는 top-down으로 순차적으로 실행된다
- 주 생성자에서 선언된 속성과 파라미터는 클래스 전체에서 사용 가능
- 클래스 내부에서 선언된 속성을 사용하기 위해서는 init 블록을 해당 속성 아래에 위치시켜야 한다
- 예제 코드
// initialization.kts import java.lang.RuntimeException class Car(val yearOfMonth: Int, theColor: String) { var fuelLevel = 100 private set var color = theColor set(value) { if(value.isBlank()) { throw RuntimeException("no empty, please") } field = value } init { if (yearOfMonth < 2020) { fuelLevel = 90 } } }
- 예제코드 ( init 블록을 표현식으로 )
// initialization.kts import java.lang.RuntimeException class Car(val yearOfMonth: Int, theColor: String) { var fuelLevel = if(yearOfMonth < 2020) 90 else 100 private set var color = theColor set(value) { if(value.isBlank()) { throw RuntimeException("no empty, please") } field = value } }
- 가급적 init 블록은 1개만 만들고, 가능하다면 1개도 만들지 않도록 하라 ( 생성자에서 최대한 아무런 작업도 안 하는 것이 프로그램의 안정성과 퍼포먼스 측면 모두에서 더 장점이 크다 )
- 보조 생성자
- 주 생성자를 작성하지 않았다면 코틀린은 아규먼트가 없는 기본 생성자를 생성한다
- 예제코드
// seondary.kts class Person(val first: String, val last: String) { private var fulltime = true private var location: String = "-" constructor(first: String, last: String, fte: Boolean): this(first, last) { fulltime = fte } constructor(first: String, last: String, loc: String): this(first, last, false) { location = loc } override fun toString(): String { return "$first $last $fulltime $location" } } println(Person("Jane", "Doe")) // Jane Doe true - println(Person("John", "Doe", false)) // John Doe false - println(Person("Baby", "Doe", "home")) // Baby Doe false home
- Person의 주 생성자는 2개의 속성 first, last를 val로 선언했다
- 주 생성자에서 constructor 키워드는 선택사항이다
- 두 개의 보조 생성자는 constructor 키워드로 선언
- 모든 보조 생성자는 주 생성자나 보조 생성자를 호출할 수 있다, 단 생성자끼리 서로를 호출하는 순환은 일어나선 안된다
- 인스턴스 메소드 정의
- 클래스 안의 메소드를 정의할 때는 fun 키워드를 사용한다
- 기본적으로 public, fun 키워드 앞에 private, protected, internal 키워드를 이용해서 메소드의 권한을 설정할 수 있다
- 인라인 클래스
- inline 클래스는 primitive type과 class 생성간 발생하는 오버헤드(객체 생성과 메모리 사용에 관한)를 균형잡게 해주는 좋은 기능이다
- 컴파일 시간에는 클래스의 장점을 취할 수 있고, 실행 시간에는 primitive type으로 취급된다 ( 바이트 코드로 변환되었을 때 primitive type으로 변경되는 것 )
- 예제코드
// ssn.kt package chapter7 @JvmInline value class SSN(val id: String) fun receiveSSN(ssn: SSN) { println("Received $ssn") }
- 코틀린 컴파일러는 receiveSSN() 함수를 호출할 때 raw 문자열이 아닌 SSN 인스턴스를 이용했는지를 검증한다
- inline 클래스는 속성과 메서드를 가질 수 있고, 인터페이스를 구현할 수도 있다
- 내부를 살펴보면 메소드는 primitive type을 받는 static 메소드가 inline 클래스로 둘러싸여있다
- inline 클래스는 final이 되어야 하고, 다른 클래스에 의해서 확장될 수 없다
6. 컴패니언 객체와 클래스 멤버
- 지금까지 만든 클래스는 속성과 인스턴스 메소드만 가지고 있었다
- 클래스 레벨(static, 클래스 멤버 변수, 메소드)의 속성이나 메소드를 컴패니언 객체로 만든다
- 컴패니언 객체는 클래스 안에 정의한 싱글 톤이다
- 예제코드
// companion.kts class MachineOperator(val name: String) { fun checkIn() = checkedIn++ fun checkOut() = checkedIn-- companion object { var checkedIn = 0 fun minimumBreak() = "15 minutes every 2 hours" } } MachineOperator("Mater").checkIn() println(MachineOperator.minimumBreak()) // 15 minutes every 2 hours println(MachineOperator.checkedIn) // 1
- 컴패니언에 접근하기
- 컴패니언 객체가 인터페이스를 구현하고 있는 등의 이유로 컴패니언 객체에 참조가 필요한 경우가 생긴다
- 클래스 .Companion을 붙여서 접근할 수 있다 ( C는 반드시 대문자로 )
- companion object {name} 을 통해 companion 객체에 적절한 이름도 지어줄 수 있다
- 팩토리로 사용하는 컴패니언
- 컴패니언 객체는 클래스의 팩토리로 사용할 수 있다
- 객체를 사용 가능한 상태로 만들기까지 몇 가지 단계를 두고 진행을 해야 할 때가 있다 ( 객체를 생성 후 이벤트 핸들러에 등록하거나 타이머에 등록하는 등 )
- 위에서 소개한 필수 작업들이 진행된 후에 객체를 사용할 수 있기에 생성자의 작업 완료 이후 등록 메소드를 호출하는 것은 실수와 빼먹음을 유발할 수 있어 좋은 선택이 아니다.
- 또한 생성자에 해당 기능들을 넣는 것 또한 좋은 생각이 아니다
- 이런 경우 팩토리처럼 동작하는 클래스의 컴패니언 객체를 설계하는 것을 고려해볼 수 있다
- 예제코드
// companionFactory.kts class MachineOperator private constructor(val name: String) { var num = 0 fun checkIn() = num++ fun checkOut() = num-- companion object { fun create(name: String): MachineOperator { val instance = MachineOperator(name) instance.checkIn() return instance } } } val operator = MachineOperator.create("hi") println(operator.num) // 1 operator.checkIn() println(operator.num) // 2
- 컴패니언 객체의 멤버에 접근하면 코틀린 컴파일러는 싱글톤 객체로 라우팅을 한다
7. 제네릭 클래스 생성
- 제네릭 클래스는 타입 안정성을 지키면서 일반화를 할 때 사용된다
- 코틀린의 제네릭 기능은 이와 비슷하지만 가변성과 제약들은 다르게 선언된다
- 예제코드
// prioritypair.kts class PriorityPair<T: Comparable<T>>(member1: T, member2: T) { val first: T val second: T init { if (member1 >= member2) { first = member1 second = member2 } else { first = member2 second = member1 } } override fun toString(): String { return "$first, $second" } } println(PriorityPair(2, 1)) // 2, 1 println(PriorityPair("A", "B")) // B, A
- 제네릭 클래스는 일반적인 클래스를 만드는 문법을 기반으로 가변성과 제약조건을 추가해서 정의한다
8. 데이터 클래스
- 코틀린의 data class는 특정한 행동, 동작보다는 데이터를 옮기는 데 특화된 클래스이다
- 각각의 데이터 클래스에 코틀린은 자동으로 equals(), hashCode(), toString() 메소드를 만들어준다
- 추가적으로 셀렉트 속성의 업데이트된 값을 제공하면서 인스턴스를 복사해주는 copy() 메소드도 제공해준다
- 주 생성자에 의해서 정의된 각각의 속성에 접근할 수 있게 해주는 메소드인 이름이 component로 시작되는 특별한 메소드도 제공해준다
- 예제코드
// taskdataclass.kts data class Task(val id: Int, val name: String, val completed: Boolean, val assigned: Boolean) val task1 = Task(1, "Create Project", false, true) println(task1) // Task(id=1, name=Create Project, completed=false, assigned=true) println("name: ${task1.name}") // name: Create Proejct val task1Completed = task1.copy(completed = true, assigned = false) println(task1Completed) // Task(id=1, name=Create Project, completed=true, assigned=false ) println(task1 === task1Completed) // false val (id, _, _, isAssigned) = task1 println("Id: $id Assigned: $isAssigned") //Id: 1 Assigned: true
- copy 메서드
- primitive type과 참조에 대한 쉘로우 카피만 가능하다, deep copy는 되지않는다 ( 조심해야함 )
- componentN()
- 마지막 구조분해를 살펴보자
- 코틀린에서는 주 생성자에 전달되는 프로퍼티 순서에 기반한다 ( 즉 순서가 섞이거나, 추가, 제거된다면 다른 결과를 낼 수 있다... )
- copy 메서드
- 데이터 클래스 선택을 고려해야하는 선택지
- 행동, 동작보다는 데이터 자체에 집중된 모델링을 할 경우
- equals(), hashcode(), toString()과 copy()가 생성되길 원하거나 copy()만 생성되길 원할 경우
- 주 생성자에 적어도 하나 이상의 속성이 포함되어야 할 경우 ( 아규먼트가 없는 생성자는 데이터 클래스에서 불가능 )
- 주 생성자를 속성만으로 구성해야 할 경우
- 구조분해 기능을 이용해서 데이터를 쉽게 추출하고 싶은 경우 ( 데이터 추출은 속성의 이름이 아닌 순서 기반이다, 한계... )
728x90
반응형
'Study > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
Day 9. 델리게이션을 통한 확장 (0) | 2021.11.27 |
---|---|
Day 8. 클래스 계층과 상속 (0) | 2021.11.16 |
Day 6. 오류를 예방하는 타입 안정성 (0) | 2021.11.02 |
Day 5. 컬렉션 사용하기 (0) | 2021.10.31 |
Day 4. 외부 반복과 아규먼트 매칭 (0) | 2021.10.27 |