조각들

 

 

주 생성자

  • 다른 프로그램 언어와의 클래스 정의의 차이점은 주 생성자를 머리부에 정의하는 것이다.
  • 클래스 이름 옆에 constructor 키워드를 사용하여 정의할 수 있고 생략 가능하다. private일 경우 생략 불가하다. 
  • 주 생성자 내부에 매개변수를 정의할 수 있고 매개변수 앞에 val이나 var을 붙이면 속성으로 정의한다.
  • 주 생성자에 대한 초기화는 init 블록을 사용한다. 주 생성자가 호출될 때 init 블록 내부의 코드가 실행된다.

 

class Person (name:String, age:Int){
    val name = name
    var age = age
}

class People(val name: String, val age: Int)

 

보조 생성자

  • 본문에 constructor 이름으로 보조 생성자를 정의한다.
  • 보조 생성자는 함수의 오버로딩처럼 여러 개 정의할 수 있다.
  • 보조 생성자는 주 생성자처럼 매개변수를 속성으로 지정할 수 없다.
  • 주 생성자와 같이 정의된 경우 보조 생성자 중 하나는 반드시 주 생성자를 this로 호출해야 한다.
  • 보조 생성자가 여러 개 정의될 경우 다른 보조 생성자를 this로 호출할 수 있다.

 

지시자

상속지시자(modifier)

  • open: 상속 가능(반드시 지정)
  • final: 상속 불가능(기본값)

 

사용 가시성 지시자

  • private: 비공개 (상속받는 sub class여도 참조 불가)
  • protected: 상속 (상속받아서 sub 클래스를 쓸 때 참조해서 사용 가능하고 단순히 해당 클래스로 객체를 만드는 경우엔 참조 불가)
  • internal: 모듈
  • public: 공개(기본값)

 

클래스/인터페이스 상속

코틀린 클래스는 단일 상속이므로 클래스는 하나만 상속할 수 있지만 인터페이스는 여러 개 작성할 수 있다.

 

 

멤버 함수(메서드)

  • 실제 객체가 생성되어도 객체 내부에는 메서드를 가질 수 없다.
  • 자바처럼 정적 메서드 즉 클래스로 직접 접근하는 메서드는 없다.

 

 

내부 클래스/내부 object 정의

  • 클래스 내부에 내포 클래스, 이너 클래스를 정의할 수 있다.
  • obejct 정의와 동반 객체 선언을 할 수 있다.

 

 

클래스 정의

constructor(매개변수1: String, 매개변수2: Int) : this(매개변수2) {
	var 속성3 : String = 매개변수1
    }

 

  • 상속한 클래스에 대한 위임호출을 표시해서 상속한 클래스가 항상 먼저 로딩되도록 처리한다. (131pg)
  • 아무 생성자가 없으면 컴파일러가 주 생성자를 자동으로 만들어준다.
  • 여러 생성자가 정의된 경우는 호출연산자가 전달하는 인자를 확인해서 맞는 생성자를 실행시켜준다

Q. 만약 보조가 주생성자를 위임호출하면 주생성자의 init 블럭도 실행이 되는 것인지?

 

 

보조 생성자만 있는 클래스로 객체 생성

클래스를 정의할 때 주 생성자 없이 보조 생성자만 작성할 수 있다. 보조 생성자를 하나만 작성하면 주 생성자가 없어서 별도의 위임호출은 필요 없다.

 

 

주 생성자와 보조 생성자 모두 정의

  • 보조 생성자가 주 생성자를 위임호출 해서 속성을 초기화한다. 주 생성자에서 처리하지 않은 것을 보조 생성자에서 초기화 처리한다.
  • 보조생성자도 오버로딩이 가능하며 너무 많은 보조 생성자가 만들어지면 불편하므로 초깃값 등을 부여해서 보조생성자 작성을 줄일 수 있다.
  • 주 생성자에 비공개 가시성을 지정할 수 있다. (141pg)

Q. 이렇게 생성자를 제한하고 우회하도록 해서 얻는 효용이 무엇인지?

 

 

메서드 참조

  • 객체가 메서드를 호출하면 자동으로 자기 자신의 객체인 this를 받아서 바운드 되지만 객체를 생성하지 않고 직접 클래스에서 함수를 호출하면 자기 자신의 객체인 this가 바운드 되지 않아 직접 객체를 전달해야 해서 바운드 처리를 해야 한다.
  • 함수 참조는 두 개의 콜론 다음에 함수 이름을 사용했지만 메서드 참조는 클래스, object 선언 등에 지정해서 클래스 이름, 객체, object 등이 두 개의 콜론 앞에 오고 그다음에 메서드 이름을 사용한다. 
  • :: 연산자 앞에 클래스나 객체, 연산자 뒤에 메서드 이름을 사용하면 로딩된 메서드의 레퍼런스를 가져온다.
  • 메서드도 메서드 참조를 통해 매개변수의 인자로 전달할 수 있다.

 

class Klass {
	companion object {
    	fun foo() {}
    }
    fun bar() {
    	println("bar 실행")
    }
}

//unbound reference, 객체를 전달해서 바운드 필요 case
println(Klass::bar)
(Klass::bar)(Klass())

//bound reference, 직접 객체가 메서드 실행 case
println(Klass()::bar)
(Klass()::bar)()

// bar 실행
// bar 실행

 

 

 

 

코틀린 클래스의 특징

  • 코틀린 클래스는 기본으로 final과 public으로 만들어지기 때문에 상속이 필요한 경우 반드시 final을 open으로 제한을 풀어서 사용해야 한다.
  • 이때 클래스만 open 하는 것이 아니라 내부의 속성이나 메서드도 기본이 final이라 서브클래스에서 재정의하려면 open 지시자를 붙여줘야 한다.

 

상속

보통 세 단계 이상 상속하면 사람이 이해하기가 어려우므로 세 단계 정도만 상속하는 것이 좋다.

 

open 뿐만 아니라 override도 하위 클래스에서 재정의가 가능하다. 따라서 상속을 못 하게 제약하려면 override 앞에 final을 추가해야 한다. 

 

 

상속에 따른 생성자 호출

  • 생성자 호출 순서도 부모 클래스의 생성자가 호출되고 그다음에 자식 클래스의 생성자가 호출되어야 한다. 그래서 생성자를 정의할 때 이런 연결을 명확히 지정해야 한다.
  • 서브클래스의 주 생성자와 보조 생성자가 정의되면 머리부에 주 생성자와 슈퍼클래스 위임호출이 있어서 부생성자는 주 생성자를 반드시 위임호출 처리해야 한다.
  • 생성자의 실행순서는 슈퍼클래스의 생성자 -> 서브클래스의 주 생성자 -> 서브 클래스의 보조 생성자 순으로 처리된다.

 

 

생성자 간의 보조 생성자로 연결

(슈퍼 클래스의) 보조 생성자도 위임호출을 할 수 있다. 슈퍼클래스의 위임호출은 보조 생성자에서 직접 슈퍼클래스의 보조 생성자를 호출해야 한다. 자기 클래스에서는 this로 생성자의 위임호출을 처리했다. 상속관계에서는 super로 생성자 위임호출을 수행한다.

 

 

내포 클래스와 이너 클래스의 차이점

  • 내포 클래스는 외부 클래스의 이름으로 접근해서 객체를 생성할 수 있지만, 이너 클래스는 멤버처럼 객체로 접근해서 객체를 생성한다. 
  • 내포 클래스는 실제 아웃 클래스와 밀접한 관계가 없다. 따라서 외부 클래스의 속성을 참조할 수 없지만, 이너 클래스는 외부 클래스의 속성을 참조할 수 있다.

Q1. 밀접한 관계가 없으면 어째서 굳이 내부에다 적는 것인지? 그냥 밖으로 빼는 게 낫지 않을까요?

Q2. 기능적인 차이는 무엇인지?

 

내포 클래스

class Outer {
	private val bar: Int = 1 //외부 클래스의 비공개 속성
    
    class Nested { //내포 클래스
    	private val nestVar = 100
        fun foo() = 999
        //fun foo() = this@Outer.bar //외부 클래스의 멤버 참조 시 예외 발생
        }
}

val demo = Outer.Nested() //내포된 객체 생성은 외부 클래스로 접근해서 생성

println(demo.foo()) // 내포된 객체의 메서드 실행

//Outer.Nested().nestVar //내포 클래스의 비공개 속성 접근 시 예외 발생

 

 

이너 클래스

  • 이너 클래스의 객체는 this이고 외부 클래스의 객체는 this@외부 클래스 이름으로 사용한다. 그래서 이너 클래스 내부에서 외부 클래스의 속성에 접근할 수 있다. 외부 클래스의 상속관계는 this 대신 super@외부 클래스를 사용한다.
  • 외부 클래스에서 이너 클래스를 사용하려면 객체를 생성해서 사용한다.

 

class Outer {
	private val bar: Int = 1 //외부 클래스의 비공개 속성
    inner class Inner {
    	private val bar = 100 //동일한 이름의 속성을 가지고 있음
        fun foo() = this@Outer.bar //외부 클래스의 비공개 속성에 접근 가능
        fun fbar() = bar 
    }
    fun getBar() = println(Inner().fbar())
}

val demo = Outer().Inner().foo() // 이너 클래스가 멤버 클래스라서 객체로 접근

println(demo)
Outer().getBar()

//1
//100

Q. this 키워드를 활용해 내/외 리소스에 편하게 접근 가능하고 Inner가 Outer의 scope 안에 한정돼서 같이 묶이므로 비슷한 도메인 안에서 객체의 디테일을 조금씩 다르게 커스텀 해야할 때 쓰일 수 있을 것 같은데 어떻게 생각하시는지 궁금합니다!

 

 

지역 클래스

보통 함수로 팩토리 즉 객체를 생성할 때 이런 패턴을 많이 사용한다. (154pg)

 

 

Object

코틀린에 class 예약어 외에 obejct 예약어가 추가되었다. 최상위 클래스도 Any이다. 이 object 예약어는 클래스 정의와 하나의 객체 생성을 동시에 한다. 하나의 객체를 만드는 패턴을 싱글턴이라고 한다.

 

object 예약어는 3가지 경우에 사용된다.

 

1) 익명 객체를 생성한다.

Q. 비용적인 이점이 있는지? 혹시 다른 데서 쓰일 수도 있으니 그냥 하나의 클래스로 관리하는 게 낫지 않을까요?

2) 하나의 객체만 만들어서 사용한다.

3) 클래스와 같이 동반해서 사용할 수 있다. (companion object)

 

함수 내부에 여러 개의 변수를 하나의 목적으로 사용할 때 object 표현식에 묶어서 같이 사용한다.

Q,뉘양스가 헷갈리는데 data class와 같은 역할도 한다는 것인지?

 

 

인터페이스 구현체 등록(매개변수로 전달)

interface Personnel {
	val name: String
    val age: Int
}

fun getObject(p: Personnel) : Personnel {
	return p
}

//매개변수로 전달 case
val p = getObject(object: Personnel {
	override val name = "달문"
  	override val age = 55
	}
)

println("객체 반환 이름 = ${p.name} 나이 = ${p.age}")

//객체 반환 이름 = 달문 나이 = 55

Q. (162pg) 책에는 변수에 짧게 값 할당하는 식이니까 괜찮지만 함수가 있고 담아낼 logic이 복잡하다면 가독성 측면에서 그냥 override로 따로 빼서 관리하고 this만 등록해주는 게 깔끔하지 않을까요? 일회성 인터페이스 구현체 등록으로 얻을 수 있는 효용이 큰지 잘 모르겠습니다.

 

 

메서드의 반환값 사용

interface StrType

class C { //메서드의 반환값으로 바로 사용하는 case
	private fun getObject() = object : StrType {
    	val x: String = "내부 속성의 값"
    }
    
    fun printX() {
    	println(getObject().x)
    }
}

val cc = C()
cc.printX()

//내부 속성의 값

 

상속에서의 활용

//A는 open class, Add는 interface

val a : A = object : A(10), Add {
	override val y = super.y
    override fun add(x:Int, z:Int) = x+z
}

 

 

object

  • 클래스와 객체 생성이 정의에서 만들어져서 별도의 생성자가 필요 없다.
  • object 정의 내부에 const val 사용이 가능하다.
  • object 정의를 처음으로 사용할 때 메모리에 로딩된다.
  • 클래스 상속과 인터페이스 구현이 가능하다.

 

 

클래스 상속

object 정의에 클래스를 상속한다. 슈퍼클래스의 위임호출을 할 때는 생성자가 없어서 실제 값을 전달한다.

open class Value(open val x:Int, open val y: Int)

object Operation: Value(100, 200){
    override val x = super.x //직접 값 전달
    override val y = super.y
    
    fun add() = x + y
    fun sub() = x - y
    fun mul() = x * y
    fun div() = x / y
}

println(Operation.add())
println(Operation.sub())
println(Operation.mul())
println(Operation.div())

//300
//-100
//20000
//0

 

 

클래스 내부에 object 정의 사용

내부 object 정의는 외부 클래스와 아무런 연관이 없어서 이 객체를 부를 때도 클래스 이름으로 직접 접근해서 object 이름에 접근하여 메서드를 실행한다.

 

class Person(val name: String, val age: Int){
	
    object Inner {
    	fun foo() = "bar"
        fun getPerson(p: Person) = p.info()
        fun create(name: String, age: Int) = Person(name, age)
       }
    fun info() = "이름 = $name 나이 = $age"
}

println(Person.Inner.foo()) //클래스의 이름만으로 내포 객체 접근
val p = Person.Inner.create("남궁성", 50)
println(p.info())
println(Person.Inner.getPerson(p))

//bar
//이름 = 남궁성 나이 = 50
//이름 = 남궁성 나이 = 5

 

 

Companion Object

  • 클래스와 companion object는 하나처럼 움직이도록 구성되었다. 그래서 클래스에 compnaion 객체를 정의하면 다양한 기능을 클래스 이름으로 처리할 수 있다.
  • 클래스 내부의 동반 객체는 하나만 만들어진다. 그래서 클래스의 여러 객체에서 동반객체는 공유해서 사용한다.

 

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")}
    }
}

CompanionClass.test()
ObjectClass.ObjectTest.test()

//동반 객체 선언: 2
//object 선언: 1

 

 

object, companion object 차이

object companion object 모두 싱글톤 패턴을 구현하는데 사용되지만 몇가지 차이점을 가집니다.

먼저 object는 클래스 전체가 하나의 싱글톤 객체로 선언되지만, companion object는 클래스 내에 일부분이 싱글톤 객체로 선언되는 것입니다. 또한 둘은 초기화 시점이 다릅니다. object로 선언된 클래스는 해당 클래스가 사용될 때 초기화가 되지만, companion object는 해당 클래스가 속한 클래스가 load될 때 초기화가 이루어집니다

 

 

확장

  • 확장은 단점도 있다. 내부적으로는 백킹 필드(field)를 사용하지 못하고 확장 메서드는 클래스에 정의된 비공개 속성을 사용할 수 없다. 또한 확장은 클래스 멤버가 아니므로 확장을 선언한 패키지나 클래스도 함께 import해야 사용할 수 있다.
  • 확장 속성일 때 get, set 메서드는 직접 정의해서 처리한다.
  • 속성에는 정보를 관리하는 영역인 field가 있다. 이 필드를 배킹필드(backing field)라고 한다. 확장 속성도 이 필드가 없지만 interface도 이 필드가 없다. 그래서 backing field 대신 실제 값을 처리하도록 구현할 필요가 있다.
  • 확장할 때 가장 중요한 것은 어느 클래스에 확정할지 결정하는 것이다. 그래야 이 클래스의 객체를 전달받아서 처리할 수 있다. 이런 객체를 리시버라고 하며, 클래스를 리시버(receiver) 클래스라고 한다.
  • 최상위 클래스 Any를 리시버 클래스로 사용해서 확장 속성을 지정하면 하위 모든 클래스는 이 확장 속성을 사용할 수 있다.
  • object나 동반 객체에도 확장 가능하다.

 

 

'Kotlin > 스터디' 카테고리의 다른 글

코틀린 스터티 - 8장  (0) 2023.09.03
코틀린 스터디 - 6장  (0) 2023.08.20
코틀린 스터디 - 4장  (0) 2023.08.06
코틀린 스터디 3장  (0) 2023.07.30
코틀린 스터디 - 2장  (0) 2023.07.23