public final class
접근 제어자인 public
과 상속을 막는 변경자 final
, class를 선언하는 키워드 class
kotlin에서 접근 제어는 public
이 default 설정이다. 접근제어자로는 다음과 같다
private
: 파일 내부에서만 사용 가능
protected
: 파일 내부나 상속한 경우에만 사용 가능.
internal
: 프로젝트 내의 컴파일 단위의 모듈에서만 사용
public
: 어디서나 사용 가능
final
이 있는 자리는 상속 관련 변경자로 default 값이 final
이다
final
은 상속을 막는 키워드고 open
은 상속을 허용, 추상 클래스 설정인 abstract
가 있다.
Kotlin에서
final
이 기본 설정인 이유에 대해서 공식적인 답변은 없다.
하지만 "Effective Java" 책에는 쓰여진 내용을 참고할 수 있다.
상속은 캡슐화를 깨트리게 되며, 상위 클래스에 의존적이여서 결합도가 높아진다.
관련 토론 글
public constructor(public var argInt: Int)
Class의 주 생성자이다
어노테이션이나 접근 제어자를 설정하지 않는다면(public으로 지정한다면) constructor
키워드를 생략할 수 있다.
주 생성자의 인자 부분인 var argInt: Int
의 경우 var
키워드를 생략할 수 있다.
다만 그럴 경우 생성 메서드의 매개 변수로만 쓰이는, 클래스의 멤버 변수로는 쓰이지 않는다는 뜻이다.
따라서 init
블럭이나 멤버 변수의 초기화 과정에서는 쓰일 수 있으나 클래스 내의 함수에서는 사용하지 못한다.
val
이나 var
을 작성하면 클래스 내에 멤버 변수로써 속성을 선언하게 된다
class User(val id: Long, email: String) {
val hasEmail = email.isNotBlank() //email can be accessed here
init {
//email can be accessed here
}
fun getEmail(){
//email can't be accessed here
}
}
: Any()
:
콜론 키워드 뒤에는 상속 또는 구현할 클래스를 지정한다
Any
class는 모든 클래스의 가장 상위 클래스로 자바에서의 Object
와 같다
보다 자세한 내용은 아래 참조
2번 ~ 8번 라인
var argStr: String
클래스의 멤버변수(property)로 선언한다
기본적으로 생성과 같이 초기화를 해야하는데 init
블럭에서 초기화를 하거나 lateinit
을 통해 나중에 초기화가 가능하다
lateinit
변수는 나중에 초기화되므로val
을 사용할 수 없다.
초기화 이전에 접근하면 런타임 에러가 난다.::변수명.isInitialized
를 사용해 초기화 여부 확인 가능
Int, Long 등과 같은 기본 유형 속성에는lateinit
을 사용할 수 없다.
lazy
의 경우val
이 가능하다
get() { ... }
set(value) { ... }
kotlin에서는 java로 컴파일 시 멤버 변수의 캡슐화를 자동으로 진행한다.
변수는 private으로 하고, 해당 getter와 setter를 자동으로 만들어 준다.
val 변수는 값 변경이 불가하니 get만 자동 생성되고, private으로 설정 시 둘 다 생성되지 않는다.
그래서 get과 set을 추가적인 로직이나 접근 제어를 변경하는 등 커스텀 할 수 있다.
자주 사용되는 예시는 다음과 같다.
private val _liveData = MutableLiveData<String>()
val liveData: LiveData<String> get() = _liveData
10번 ~ 17번 라인
init { ... }
init 블럭은 보통 class 내 상단부분에 넣지만, 중간에 넣어도 되며, 여러개를 넣어도 생성시 전부 호출
constructor(argInt: Int, argStr: String): this(argInt) { ... }
예시와 같이 보조 생성자들은 주 생성자를 호출해야 한다
class에는 위와 같이 property
(argStr), init
, constructor
를 통해 초기화를 진행한다.
이때 진행하는 순서는 다음과 같다
class Test{
constructor(){
println("set primary constructor")
}
constructor(msg: String): this(){
println("set secondary constructor")
}
val a = println("set property 1")
init{
println("set init 1")
}
val b = println("set property 2")
init{
println("set init 2")
}
}
fun main(){
Test("")
}
// prints
set property 1
set init 1
set property 2
set init 2
set primary constructor
set secondary constructor
따라서 처음에 나온 18줄의 코드를 아래의 한 줄짜리 코드로 표현할 수 있다.
Kotlin의 Class는 default로 final
이라서 open
키워드를 선언해야 상속할 수 있다.
내부 메서드 및 필드도 마찬가지로 open
키워드를 선언해야한다.
final이 default인 이유는 위 내용 참조
클래스의 계층 구조를 만드는 기법은 다음이 있다.
is-a
: 슈퍼 클래스와 서브 클래스가 하나로 묶여서 사용.has-a
: 클래스를 사용하는 관계, 클래스 내부에 다른 클래스를 속성으로 처리.implements
: 인터페이스, 추상 클래스를 구현상속관계
open
키워드를 제외하곤 Java와 유사하다
open class Base {
open fun baseOpenFun() { ... }
fun baseNotOpenFun() { ... }
}
class Derived() : Base() {
override fun baseOpenFun() { ... }
}
연관관계, Composition
kotlin의 의도에 맞게 상속보다 연관관계로 계층 구조를 풀어나가는 것이 대부분의 경우 좋다
방법으로는 Composition, 즉 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 메서드를 호출하는 기법
class Progress {
fun showProgress() { ... }
fun hideProgress() { ... }
}
class ImageLoader(private val progress: Progress) {
fun load() {
progress.showProgress()
...
progress.hideProgress()
}
}
구현관계
자바에서는 클래스의 상속과 인터페이스의 구현을 extends 와 implements로 구분하지만, 코틀린에서는 이를 구분하지 않고 콜론(:) 뒤에 상속한 클래스나 구현한 인터페이스를 표기
interface Clickable {
fun click()
fun showOff() = println("Clickable showoff") //디폴트 메소드
}
class Button : Clickable {
override fun click() {
...
}
}
기존 클래스를 수정하지 않고 새로운 기능을 추가하는 방법
확장되는 유형을 참조하는 수신자 유형을 해당 이름 앞에 붙인다
fun String.hello() : String {
return "Hello, $this"
}
fun main(args: Array<String>) {
val whom = "cwdoh"
println(whom.hello()) // print: Hello, cwdoh
}
Extension은 다음의 규칙을 가진다
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(s: Shape) {
println(s.getName())
}
printClassName(Rectangle()) // print: Shape
호출된 확장 함수가 매개변수의 선언된 유형에만 의존하기 때문에 Shape 를 print 한다
class Example {
fun printFunctionType() {
println("Class method")
}
}
fun Example.printFunctionType() {
println("Extension function")
}
Example().printFunctionType() // print: Class method
class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}
class Connection(val host: Host, val port: Int) {
fun printPort() { print(port) }
// 확장함수
fun Host.printConnectionString() {
printHostname() // calls Host.printHostname()
print(":")
printPort() // calls Connection.printPort()
}
fun connect() {
/*...*/
host.printConnectionString() // calls the extension function
}
}
fun main() {
Connection(Host("kotl.in"), 443).connect() // print: kotl.in:443
//Host("kotl.in").printConnectionString() // error, the extension function is unavailable outside Connection
}
Nullable 확장함수 정의
안전호출연산자로 nullable한 class를 확장할 수 있다
class Person(val name: String)
fun Person?.getName() {
this?.let { println(it.name) } ?: println("null")
}
fun main() {
val p1: Person? = null
p1.getName() // print: null
}
확장 속성
확장 함수를 지원하는 것처럼 확장 속성을 지원한다.
val <T> List<T>.lastIndex: Int
get() = size - 1
class 앞에 data
를 붙여 데이터 보관 목적으로 클래스를 생성할 수 있다.
data class User(val name: String, val age: Int)
특징은 다음과 같다
val
또는 var
으로 선언해야 한다.abstract
open
sealed
inner
를 붙일 수 없다.equals()
hashCode()
toString()
componentN()
copy()
를 자동으로 구성해준다.Data Class가 상속이 안되는 이유로는
equals()
를 제대로 정의할 수 없기 때문이다
sealed 클래스는 추상 클래스로, 상속받는 서브 클래스의 종류를 제한할 수 있다.
특징으로는 다음과 같다
abstract
클래스임protected
(default) 또는 private
생성자만 갖게 됨sealed interface Error
sealed class IOError(): Error
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()
object RuntimeError : Error
상속하는 타입이 정해져 있기 때문에 다음과 같은 이점을 가진다.
fun log(e: Error) = when(e) { is FileReadError -> { println("Error while reading file ${e.file}") } is DatabaseError -> { println("Error while reading from database ${e.source}") } is RuntimeError -> { println("Runtime error") } // the `else` clause is not required because all the cases are covered }
Class 안에 Class를 정의한 형태
inner
키워드를 선언하지 않으면 기본이 중첩 클래스
이너 클래스 내부에서는 외부 클래스의 속성에 접근 가능.
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
val demo = Outer.Nested().foo() // == 2
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}
val demo = Outer().Inner().foo() // == 1
이펙티브 자바와 코틀린 인 액션 책을 참고하면, 자바의 Inner Classes에는 크게 3 가지 문제가 있음을 알 수 있다.
- Inner classes를 사용할 경우 직렬화에 문제가 있다.
- Inner classes 내부에 숨겨진 Outer class 정보를 보관하게 되고, 결국 참조를 해지하지 못하는 경우가 생기면 메모리 누수가 생길 수도 있고, 코드를 분석하더라도 이를 찾기 쉽지 않아 해결하지 못하는 경우도 생긴다.
- Inner classes를 허용하는 자바는 Outer를 참조하지 않아도 기본 inner classes이기 때문에 불필요한 메모리 낭비와 성능 이슈를 야기한다.
object
정의는 하나의 싱글턴 패턴을 만드는 방법이다
object
정의를 처음으로 사용될 때 메모리에 적재fun main(args: Array<String>) {
Counter.increment()
println(Counter.count)
}
object Counter {
var count: Int = 0
private set
fun increment() = ++count
}
fun main() {
CompanionClass.test() // 동반 객체 선언 : 2
ObjectClass.ObjectTest.test() // object 선언 : 1
}
class ObjectClass {
object ObjectTest { // 싱글턴 객체
const val CONST_STRING = "1"
fun test() { println("object 선언 : $CONST_STRING") }
}
}
class CompanionClass {
companion object { // 동반
const val CONST_TEST = 2
fun test() { println("동반 객체 선언 $CONST_TEST") }
}
}