[Kotlin] Ch1. 클래스와 객체

leeeha·2022년 8월 4일
0

코틀린

목록 보기
17/29
post-thumbnail

출처: https://www.boostcourse.org/mo234/lecture/154220?isDesc=false

객체지향 프로그래밍

  • OOP (Object-Oriented Programming)

    • 프로그램의 구조를 객체 간의 상호작용으로 표현하는 프로그래밍 방식
    • 절차적 프로그래밍의 한계를 극복하기 위해 나온 언어의 한가지 방법론
    • 객체와 관계를 표현하고 이를 통해 확장과 재사용이 용이해짐.
  • 자바와 코틀린에서는 OOP 지원 (C언어는 대표적인 절차적 프로그래밍 언어)

  • 객체지향의 기본 용어

    • 추상화 (abstraction)
    • 인스턴스 (instance)
    • 상속 (inheritance)
    • 다형성 (polymorphism)
    • 캡슐화 (encapsulation)
    • 메시지 전송 (message sending)
    • 연관 (association)

클래스와 객체의 정의

클래스

  • 분류, 계층, 등급
  • 특정 대상을 분류하고 특징(속성)과 동작할 활동(함수)을 기록

추상화

  • 목표로 하는 것에 대해 필요한 만큼만 속성과 동작을 정의하는 과정

객체지향 개념의 동의어들

객체지향 개념상의 용어가 언어마다 약간씩 다르다!

자바에서 사용하는 필드는 코틀린에서 프로퍼티라고 부른다.

클래스 다이어그램

클래스의 선언

class Car {
    val wheel: Int = 4
    fun start() {
        println("Engine Start!")
    }
}

fun main() {
    val sonata = Car()
    println(sonata.wheel)
    sonata.start()
}
class Bird {
    var name: String = "mybird"
    var wing: Int = 2
    var beak: String = "short"
    var color: String = "blue"

    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}

fun main() {
    val coco = Bird()
    coco.color = "yellow"

    println("coco.color: ${coco.color}")
    coco.fly()
    coco.sing(3)
}

클래스와 객체의 차이점

Bird 클래스일종의 선언일 뿐, 실제 메모리에 존재하여 실행되고 있는 것이 아니다!

반면에, 객체 물리적인 메모리 영역에서 실행되고 있는 클래스의 실체이다.

따라서 클래스로부터 객체를 생성해내며, 이를 인스턴스화 (instantiation)라고 한다. 즉, 메모리에 올라간 객체를 인스턴스 (instance)라고도 부른다.


생성자 (Constructor)

생성자란?

  • 클래스를 통해 객체가 만들어질 때, 기본적으로 호출되는 함수
  • 객체 생성 시 필요한 값을 인자로 설정할 수 있다.
  • 생성자를 위해 특별한 함수인 constructor()를 정의한다.

주 생성자, 부 생성자

  • 주 생성자 (Primary Contructor): 클래스명과 함께 기술되며, 보통의 경우 constructor 키워드를 생략할 수 있다.
  • 부 생성자 (Secondary Constructor): 클래스 본문에 기술되며, 하나 이상의 부 생성자를 정의할 수 있다.
package chap01.section2

class Bird {
    var name: String
    var wing: Int
    var beak: String

	// 부 생성자 
    constructor(name: String, wing: Int, beak: String){
        this.name = name
        this.wing = wing
        this.beak = beak
    }

    fun fly() = println("Fly wing: $wing")
}

fun main() {
    val coco = Bird("coco", 2, "long")
    coco.fly()
    println(coco.name)
}
package chap01.section2

class Bird constructor(_name: String, _wing: Int, _beak: String) {
    var name: String = _name
    var wing: Int = _wing
    var beak: String = _beak

    fun fly() = println("Fly wing: $wing")
}

fun main() {
    val coco = Bird("coco", 2, "long")
    coco.fly()
    println(coco.name)
}

주 생성자의 constructor 키워드는 생략할 수 있다.

package chap01.section2

class Bird(var name: String, var wing: Int, var beak: String) {
    fun fly() = println("Fly wing: $wing")
}

fun main() {
    val coco = Bird("coco", 2, "long")
    coco.fly()
    println(coco.name)
}

이렇게 프로퍼티 선언을 주 생성자의 선언부에서 해주면, 코드량을 더 줄일 수 있다.

init 블록

package chap01.section2

class Bird(var name: String, var wing: Int, var beak: String) {
    init {
        println("------- init start -------")
        name = name.capitalize()
        println("name: $name, wing: $wing, beak: $beak")
        println("------- init end ---------")
    }

    fun fly() = println("Fly wing: $wing")
}

fun main() {
    val coco = Bird("coco", 2, "long")
    coco.fly()
    println(coco.name)
}

------- init start -------
name: Coco, wing: 2, beak: long
------- init end ---------
Fly wing: 2
Coco

생성자 오버로딩

package chap01.section2

class Bird {
    var name: String
    var wing: Int
    var beak: String

    constructor(name: String, wing: Int, beak: String){
        this.name = name
        this.wing = wing
        this.beak = beak
    }

    constructor(name: String, beak: String){
        this.name = name
        this.wing = 2
        this.beak = beak
    }

    fun fly() = println("Fly wing: $wing")
}

fun main() {
    val coco = Bird("coco", 4,"long")
    val coco2 = Bird("coco2", "short")

    println("coco  - name: ${coco.name}, wing: ${coco.wing}, beak: ${coco.beak}")
    println("coco2 - name: ${coco2.name}, wing: ${coco2.wing}, beak: ${coco2.beak}")
}

coco - name: coco, wing: 4, beak: long
coco2 - name: coco2, wing: 2, beak: short


상속과 다형성

상속 (inheritance)

  • 자식 클래스를 만들 때, 상위 클래스 (부모 클래스)의 속성과 기능을 물려받아 계승한다.
  • 상위(부모) 클래스의 프로퍼티와 메서드가 자식에 적용된다.

package chap01.section3

open class Bird(var name: String, var wing: Int, var beak: String) {
    fun fly(){
        println("Fly")
    }
}

class Lark(name: String, wing: Int, beak: String) : Bird(name, wing, beak){
    fun sing(){
        println("sing high tone")
    }
}

class Parrot : Bird {
    var lang: String

    // 부 생성자에서는 프로퍼티 선언할 수 없음.
    constructor(name: String, wing: Int, beak: String, lang: String)
            : super(name, wing, beak) {
        this.lang = lang
    }

    fun speak(){
        println("Speak: $lang")
    }
}

fun main() {
    val lark = Lark("mylark", 2, "short")
    val parrot = Parrot("myparrot", 2, "long", "English")
    println("lark - name: ${lark.name}")
    println("parrot - name: ${parrot.name}")

    lark.fly()
    lark.sing()

    parrot.fly()
    parrot.speak()
}

다형성 (polymorphism)

  • 다형성 (polymorphism)이란 같은 이름을 사용하지만, 구현 내용이 다르거나 매개변수가 달라서 하나의 이름으로 다양한 기능을 수행할 수 있는 개념이다.
  • 정적 다형성: 컴파일 타임에 결정되는 다형성 (메서드 오버로딩)
  • 동적 다형성: 런타임에 결정되는 다형성 (메서드 오버라이딩)

오버로딩과 오버라이딩

  • 오버로딩 (overloading) : 기능은 같지만, 매개변수의 타입이나 개수를 다르게 하여 여러 경우를 처리하는 것
    ex) print(123), print("Hello") → 매개변수의 타입은 다르지만, 출력의 기능은 동일함.
package chap01.section3

class Calc {
    fun add(x: Int, y: Int): Int = x + y
    fun add(x: Double, y: Double): Double = x + y
    fun add(x: Int, y: Int, z: Int): Int = x + y + z
    fun add(x: String, y: String): String = x + y
}

fun main() {
    val calc = Calc()
    println(calc.add(3, 2))
    println(calc.add(3.2, 2.2))
    println(calc.add(3, 2, 3))
    println(calc.add("Hello", "World"))
}
  • 오버라이딩 (overriding) : 기능을 완전히 다르게 바꾸어 재정의 하는 것
    ex) 부모 클래스로부터 sing() 메서드를 상속 받은 자식 클래스들은, 자신의 메커니즘에 맞게 메서드를 재정의하여 부모 클래스와는 다른 기능을 하도록 만들 수 있다.
  • 오버라이딩을 하기 위해서 부모 클래스는 open 키워드, 자식 클래스는 override 키워드를 사용해야 한다. (메서드 및 프로퍼티에 사용 가능)
package chap01.section3

// 상속을 위해 open 
open class Bird(var name: String, var wing: Int, var beak: String) {
    open fun fly(){ // 오버라이딩을 위해 open 
        println("Fly")
    }
}

class Lark(name: String, wing: Int, beak: String) : Bird(name, wing, beak){
    override fun fly(){ 
        println("Fast Fly")
    }
}

class Parrot : Bird {
    var lang: String
    constructor(name: String, wing: Int, beak: String, lang: String)
            : super(name, wing, beak) {
        this.lang = lang
    }
    override fun fly(){
        println("Slow Fly")
    }
}

fun main() {
    // 컴파일 타임에 정적으로 Bird 타입이라고 인식됨.
    val lark: Bird = Lark("mylark", 2, "short")
    val parrot: Bird = Parrot("myparrot", 2, "long", "English")

    lark.fly() // 오버라이딩 (동적 바인딩)
    //lark.sing() // error

    parrot.fly() // 오버라이딩 (동적 바인딩)
    //parrot.speak() // error
}

Fast Fly
Slow Fly

동적 바인딩에 의해 lark와 parrot은 런타임에 각각 Lark, Parrot 타입으로 인식되어서 부모 클래스의 fly()가 아니라 자신의 fly() 메서드를 호출한다. 이를 동적 다형성이라 부른다.

open class Lark() : Bird() {
   // 하위 클래스에서 재정의 할 수 없음. 
  final override fun sing() { /* 구현부를 새롭게 정의 */ } 
}

하위 클래스에서 오버라이딩을 금지하고 싶을 때는 final 키워드를 붙이면 된다.

super와 this

상위 클래스는 super, 현재 클래스는 this 키워드로 참조할 수 있다.

open class Person {
    constructor(firstName: String){
        println("[Person] $firstName")
    }
    constructor(firstName: String, age: Int){
        println("[Person] $firstName, $age") // 1
    }
}

class Developer: Person {
    constructor(firstName: String) : this(firstName, 10){
        println("[Developer] $firstName") // 3
    }
    constructor(firstName: String, age: Int): super(firstName, age){
        println("[Developer] $firstName, $age") // 2
    }
}

fun main() {
    val sean = Developer("Sean")
}

[Person] Sean, 10
[Developer] Sean, 10
[Developer] Sean

class Person(firstName: String,
             out: Unit = println("[Primary] Parameter")) { // 3
    val firstName = println("[Property] firstName: $firstName") // 4

    init { // 5
        println("[init] Person init block")
    }

    constructor(firstName: String, age: Int,
                out: Unit = println("[Secondary] Parameter")): this(firstName){ // 2
        println("[Secondary] Body: $firstName, $age") // 6
    }
}

fun main() {
    val person = Person("Kildong", 30) // 1
}

[Secondary] Parameter
[Primary] Parameter
[Property] firstName: Kildong
[init] Person init block
[Secondary] Body: Kildong, 30

class Person(firstName: String,
             out: Unit = println("[Primary] Parameter")) { // 2
    val firstName = println("[Property] firstName: $firstName") // 3

    init { // 4
        println("[init] Person init block")
    }

    constructor(firstName: String, age: Int,
                out: Unit = println("[Secondary] Parameter")): this(firstName){
        println("[Secondary] Body: $firstName, $age")
    }
}

fun main() {
    val person = Person("Dooly") // 1
}

[Primary] Parameter
[Property] firstName: Dooly
[init] Person init block

@ 기호

inner 클래스에서 outer 클래스의 상위 클래스를 호출하려면, super 키워드와 함께 @ 기호 옆에 outer 클래스의 이름을 작성하여 호출하면 된다.

open class Base {
    open val x: Int = 1
    open fun f() = println("Base Class f()")
}

class Child: Base() {
    // 상위 클래스 프로퍼티 및 메서드 오버라이딩
    override val x: Int = super.x + 1
    override fun f() = println("Child Class f()")

    // 이너 클래스
    inner class Inside {
        fun f() = println("Inside Class f()")
        fun test() {
            f() // 현재 이너 클래스의 메서드
            Child().f() // Child 클래스의 메서드
            super@Child.f() // Base 클래스의 메서드
            println("[Inside] super@Child.x: ${super@Child.x}") // 1
        }
    }
}

fun main() {
    val c1 = Child()
    c1.Inside().test()
}

Inside Class f()
Child Class f()
Base Class f()
[Inside] super@Child.x: 1

<> (angle bracket)

open class A {
    open fun f() = println("A Class f()")
    fun a() = println("A Class a()")
}

interface B { // 인터페이스는 기본적으로 open 되어 있음.
    fun f() = println("B interface f()")
    fun b() = println("B interface b()")
}

class C: A(), B {
    override fun f() = println("C Class f()")
    fun test(){
        f() // 현재 클래스
        b() // 인터페이스 B
        super<A>.f() // 클래스 A
        super<B>.f() // 인터페이스 B
    }
}

fun main() {
    val c = C()
    c.test()
}

C Class f()
B interface b()
A Class f()
B interface f()


정보 은닉과 캡슐화

캡슐화 (encapsulation)

클래스를 작성할 때 외부에 속성이나 기능을 숨기는 것을 캡슐화 라고 한다.
가시성 지시자 (visibility modifiers)를 통해 외부 접근 범위를 결정할 수 있다.

  • private: 외부에서 접근 불가
  • public: 어디서든 접근 가능 (디폴트)
  • protected: 외부에서 접근할 수 없으나, 하위 상속 요소에서는 접근 가능
  • internal: 같은 정의의 모듈 (프로젝트) 내부에서는 접근 가능 (자바에서 사용되던 package 키워드를 대체함.)

가시성 지시자의 선언 위치

cf) 가시성 지시자가 붙은 생성자는 constructor 키워드를 생략할 수 없다.

가시성 지시자의 공개 범위

private

package chap01.section5

private class PrivateClass {
    private var i = 1
    private fun privateFunc(){
        i += 1
        println(i)
    }
    fun access(){ // 기본은 public
        privateFunc()
    }
}

class OtherClass { // 기본은 public
    //val opc = PrivateClass() // public으로 생성 불가

    // public 함수이긴 하지만, OtherClass 내부에서 한번 더 가려지므로
    // private 클래스 생성 가능
    fun test(){
        val pc = PrivateClass() // 생성 가능
        pc.access()
    }
}

fun main() { // 최상위 함수
    val pc = PrivateClass() // 생성 가능 
    //pc.i = 3 // 접근 불가
    //pc.privateFunc() // 접근 불가
    pc.access() // ok
}

// 최상위 함수에서 private 클래스 생성 가능
fun TopLevelFunc(){
    val tpc = PrivateClass() // 생성 가능
    tpc.access()
}

protected

package chap01.section5

open class Base {
    protected var i = 1
    protected fun protectedFunc(){
        i += 1
        println("Base: $i")
    }
    fun access(){
        protectedFunc()
    }
    protected class Nested
}

class Derived: Base(){
    fun test() {
        protectedFunc() // 접근 가능
        i += 1 // 프로퍼티 값 변경 가능
        println("Derived: $i")
    }
}

class Other {
    fun other(){
        val base = Base()
        //base.i = 3 // 프로퍼티 값 변경 불가
    }
}

fun main() {
    val base = Base() // 생성 가능
    // 외부에서는 접근 불가
    //base.i
    //base.protectedFunc()
    base.access() // 접근 가능

    val derived = Derived()
    derived.test()
}

Base: 2
Base: 2
Derived: 3

internal

internal class InternalClass { // 같은 모듈 내에서 접근 가능 
    internal var i = 1
    internal fun icFunc(){
        i += 1
        println(i)
    }
    fun access(){
        icFunc()
    }
}

class Other {
    internal val ic = InternalClass()
    fun test(){
        ic.i
        ic.icFunc()
    }
}

fun main() {
    val mic = InternalClass()
    println(mic.i) // 1
    mic.icFunc()   // 2 
    mic.access()   // 3 
}

가시성 지시자와 클래스의 관계

cf) UML 다이어그램에서의 표기법
- private
# protected
~ package, internal
+ public

package chap01.section6

open class Base {
    // a, b, c, d, e 접근 가능 
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4
    
    protected class Nested {
        // a, b, c, d, e, f 접근 가능 
        val e: Int = 5
        private val f: Int = 6
    }
}

class Derived: Base() {
    // b, c, d, e 접근 가능 
    override val b = 5 
}

class Other(val base: Base) {
    fun test(){
        //base.a 
        //base.b 
        base.c 
        base.d 
        //Base.Nested
        //Nested::e
    }
}
profile
습관이 될 때까지 📝

0개의 댓글