Develop/Kotlin

[Kotlin] 코틀린 객체지향 프로그래밍 기초

dawonny 2022. 10. 6. 05:43
728x90
반응형

ref : Udemy [Android 12 및 Kotlin 개발 완전 정복] 섹션 4


OOP(객체지향 프로그래밍)

객체지향 언어로는 자바, c#, 코틀린 등등.. 이 있다.

OOP의 특징으로

- 변수

- 흐름제어

- 함수

- 컬렉션

- 클래스

가 있겠다


클래스와 객체 개념

 

차가 클래스라고 한다면

특정 속성이 있다고 해보자

 

속성의 예시로는 최고 속도, 바퀴 개수, 창문과 문의 개수, 색깔 등이 있을 수 있다.

이런 속성들은 변수라고 한다.

 

또한 차를 skill 도 가지고 있다.

drive, break 등 이 있을 것이다.

이런걸 메서드 라고 한다.

 


클래스

 

fun main(){
    var dawon = Person("Dawon", "Seo")
    var sangwon = Person()
    var sangwonSeo = Person(lastName = "Seo")

}

class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
    init {
        println("initialized a new person object with" +
        "firstName = $firstName and lastName = $lastName")
    }
}

 

constructor는 보조 생성자 이다.

보조 생성자는 객체 생성 시 값을 추가하게 해준다.

private constructor 나 public constructor 이런식으로 정할 수도 있다.

하지만 보조생성자는 구체적으로 정하고 싶지 않다면 필수는 아니다(지워도 된다).

 

위는 Person 이라는 클래스를 만들어서 나만의 고유 타입을 만든 셈이다.

클래스를 생성하면 init을 작성할 수 있는데, 이를 이니셜라이저라고 한다.

init { } 안에 있는 코드는 자동으로 실행된다. 객체를 시작하기 위해 사용하는 거라고 볼 수 있다.

객체를 준비하는 과정인 것이다. 예시 코드에서는 성과 이름을 출력하도록 했다.

 

기본값을 설정하는 것이 가능하다.

firstName 과 lastName 으 디폴트 값을 설정해주었는데

이에 따라서 객체를 만들때 값을 설정해주기도, 기본값 그대로 만들기도 한다.

 


스코프

 

fun main(){
    myFunction(5)

}

//여기 a 는 매개변수
fun myFunction(a:Int){
    //a는 변수
    var a = 4
    println("a는 $a")
}

코틀린이 메서드에서 변수와 매개변수 이름을 왜 같게 정하는 걸까?

이런걸 섀도잉이라고 한다. 이는 현재 스코프에 이름이 속해있어서 그런 것이다.

 

fun main(){
    myFunction(5)

}

//여기 a 는 매개변수
fun myFunction(a:Int){
    //a는 변수
    var a = a
    println("a는 $a")
}

만약에 매개변수 a 자리에 5를 인자로 받았다면

이 a를 myFunction 내부에 존재하는 a라는 변수에 넣어주고

이 변수를 출력해준다.

그러면 5의 값을 확인할 수 있다.

//이 자리에 선언하는 b 변수는 모든 함수에 적용이 가능하다.
fun main(){
    myFunction(5)
    var b = 3 // myFunction에 있는 b와는 다르다.
}

//여기 a 는 매개변수
fun myFunction(a:Int){
    //a는 변수
    var b = a
    println("b 는 $b")
}

변수를 어디에 생성하느냐에 따라서 사용할 수 있는 범위가 정해져있다.

같은 이름의 변수라고 하더라도 다른 함수에 있다면 다른 변수인 것이다.

 

하지만 모든 함수 밖에 위치하는 변수는

모든 함수에서 적용이 가능한 변수가 될 것이다.


클래스의 멤버

 

멤버 변수는 다르게 말하면 속성이다.

fun main(){
    var dawon = Person("Dawon", "Seo")
    var sangwon = Person()
    var sangwonSeo = Person(lastName = "Seo")

    dawon.stateHobby()

}

class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
    //멤버변수 or 속성
    var age : Int? = null
    var hobby : String = "watch TV"

    init {
        println("initialized a new person object with" +
                "firstName = $firstName and lastName = $lastName")
    }

    //멤버 함수 or 메서드
    fun stateHobby(){
        println("my hobby is $hobby ")
    }
}

속성으로 age와 hobby를 만들었고 

stateHobby 라고 하는 메서드를 만들었다. hobby 변수의 값을 출력해준다.

fun main(){
    var dawon = Person("Dawon", "Seo")
    var sangwon = Person()
    var sangwonSeo = Person(lastName = "Seo")
    dawon.hobby = "drawing"
    dawon.stateHobby()
    sangwon.hobby = "play game"
    sangwon.stateHobby()

}

class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
    //멤버변수 or 속성
    var age : Int? = null
    var hobby : String = "watch TV"

    init {
        println("initialized a new person object with" +
                "firstName = $firstName and lastName = $lastName")
    }

    //멤버 함수 or 메서드
    fun stateHobby(){
        println("my hobby is $hobby ")
    }
}

객체마다의 hobby 속성값을 따로 지정해줄 수 있다.

fun main(){
    var dawon = Person("Dawon", "Seo", 23)
    var sangwon = Person()
    var sangwonSeo = Person(lastName = "Seo")
    dawon.hobby = "drawing"
    dawon.age = 25
    println("dawon is ${dawon.age} years old")
    dawon.stateHobby()
    sangwon.hobby = "play game"
    sangwon.stateHobby()

}

class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
    //멤버변수 or 속성
    var age : Int? = null
    var hobby : String = "watch TV"

    init {
        println("initialized a new person object with" +
                "firstName = $firstName and lastName = $lastName")
    }
    //두번째 생성자
    constructor(firstName: String, lastName: String, age:Int) : this(firstName, lastName){
        this.age = age //객체 생성시 생성자에 전달된 age 값이 this 클래스의 age 값으로 지정되어야
    }

    //멤버 함수 or 메서드
    fun stateHobby(){
        println("my hobby is $hobby ")
    }
}

두번째 생성자를 만들었다. 이 생성자는 age 라고 하는 추가 정보를 가지고 있게 된다.

객체를 생성할 때에 전달된 age 의 값이 this 클래스의 age 값으로 지정되어야 하기에 this.age = age 라는 코드를 작성했다.

 

fun main(){
    var dawon = Person("Dawon", "Seo", 23)
    var sangwon = Person()
    var sangwonSeo = Person(lastName = "Seo")
    dawon.hobby = "drawing"
    dawon.age = 25
    println("dawon is ${dawon.age} years old")
    dawon.stateHobby()
    sangwon.hobby = "play game"
    sangwon.stateHobby()

}

class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
    //멤버변수 or 속성
    var age : Int? = null
    var hobby : String = "watch TV"
    var firstName : String? = null

    init {
        this.firstName = firstName //인자로 받은 firstName을 변수에 저장
        println("initialized a new person object with" +
                "firstName = $firstName and lastName = $lastName")
    }
    //두번째 생성자
    constructor(firstName: String, lastName: String, age:Int) : this(firstName, lastName){
        this.age = age //객체 생성시 생성자에 전달된 age 값이 this 클래스의 age 값으로 지정되어야
    }

    //멤버 함수 or 메서드
    fun stateHobby(){
        println("$firstName hobby is $hobby ")
    }
}

누구의 hobby 인지 출력하기 위해 Person 클래스 내에 firstName 이라는 멤버 변수를 만들어줬다.

객체를 생성할 때에 인자로 firstName 을 받아서 this.firstName 의 값으로 지정해줬다.

여기 까지하면 메서드를 작성할 때에 firstName 변수를 사용할 수있게 될 것이다.


lateinit(늦은 초기화), getter, setter

 

lateinit은 속성을 선언(초기화)할 때에 당장 초기화 하지않고

나중에 init 함수에서 초기화를 하겠다는 의미이다.

 

주의할 부분은 초기화 되지 않은 상태로 이 객체의 속성에 접근하려고 하면 안된다는 것이다(오류난다)

이건 비어있는 데이터에 접근하는 것과 같다.

fun main(){
    var myCar = Car()
    println("owner is ${myCar.owner}")
    println("brand is ${myCar.myBrand}")

}

class Car(){
    lateinit var owner : String

    val myBrand : String = "BMW"
    get() {
        return field.toLowerCase()
    }

    init {
        this.owner = "Dawon"
    }
}

커스텀 getter 를 만들어 보았다.

myBrand 라는 속성을 get 할때에 소문자로 리턴해주는 함수 getter 이다.

fun main(){
    var myCar = Car()
    println("owner is ${myCar.owner}")
    println("brand is ${myCar.myBrand}")
    println("maxSpeed is ${myCar.maxSpeed}")
    myCar.maxSpeed = 200
    println("maxSpeed is ${myCar.maxSpeed}")


}

class Car(){
    lateinit var owner : String

    val myBrand : String = "BMW"
    get() {
        return field.toLowerCase()
    }
    var maxSpeed : Int = 250
    /*
    이 코드는 자동으로 생성되기 때문에 작성할 필요 없음.

    get()  = field
    set(value) {
        field = value
    }
    */


    init {
        this.owner = "Dawon"
    }

maxSpeed 라는 속성을 새로 만들어보았다.

getter 와 setter 는 자동으로 생성이 되기 때문에 따로 커스텀할 거 아니면 별도로 작성해줄 필요는 없다.

따로 작성하지 않아도 속성에 접근하고, 값을 새로 set 해주는 과정이 모두 가능하다.

import java.lang.IllegalArgumentException

fun main(){
    var myCar = Car()
    println("owner is ${myCar.owner}")
    println("brand is ${myCar.myBrand}")
    println("maxSpeed is ${myCar.maxSpeed}")
    myCar.maxSpeed = -10
    println("maxSpeed is ${myCar.maxSpeed}")


}

class Car(){
    lateinit var owner : String

    val myBrand : String = "BMW"
    get() {
        return field.toLowerCase()
    }
    var maxSpeed : Int = 250
    /*
    이 코드는 자동으로 생성되기 때문에 작성할 필요 없음.
    get()  = field
    set(value) {
        field = value
    }
    */
    set(value) {
        field = if(value > 0) value else throw IllegalArgumentException("MaxSpeed cannot be less than 0")
    }


    init {
        this.owner = "Dawon"
    }
}

setter 를 커스텀해준 모습이다. 만약에 value가 0보다 크지 않을 경우 오류가 나도록 커스텀해줬다.

class Car(){
    lateinit var owner : String

    val myBrand : String = "BMW"
    get() {
        return field.toLowerCase()
    }
    var maxSpeed : Int = 250
    /*
    이 코드는 자동으로 생성되기 때문에 작성할 필요 없음.
    get()  = field
    set(value) {
        field = value
    }
    */
    set(value) {
        field = if(value > 0) value else throw IllegalArgumentException("MaxSpeed cannot be less than 0")
    }

    var myModel : String = "m5"
    private set //비공개 setter, car 클래스 내에서만 사용 가능.
                //getter 는 private 이 아니기 때문에 main 함수에서 사용 가능

    init {
        this.owner = "Dawon"
    }
}

예시 코드에서 private setter 는 다른 클래스에서는 사용이 불가능하다.

(getter 는 private 으로 선언한거 아니니 상관없이 가능하다)

private 으로 선언한 것은 해당 클래스 내에서만 사용 가능.

따라서 init 함수 안에서는 사용이 가능하다.


data class

 

data class 에 대해서 알아보자

data class User(val id:Long, var name:String) //매개변수는 val 이나 var 이어야

fun main(){
    val user1 = User(1, "Dawon")

    val name = user1.name
    println(name)
    user1.name = "Sangwon"
    var user2 = User(1, "Sangwon")
    println(user1 == user2) //true 라고 출력됨

    println("User Details : $user1")

    val updatedUser = user1.copy(name = "Dawon Seo")
    println(user1)
    println(updatedUser)

    println(updatedUser.component1()) //print 1
    println(updatedUser.component2()) //print Dawon Seo


}

User 라는 데이터 클래스를 만든다. 매개변수는 id 와 name 이 있다.

main 함수에서 id가 1이고 이름이 "Dawon" 인 객체를 만들어서 user1 에 넣었다.

객체 뒤에 . 을 적고 속성이름을 적어서 속성에 접근이 가능하다.

 

copy() 를 이용해서 객체의 복사본을 만들 수도 있다.

component를 통해 속성에 접근할 수도 있다.

(현재 위 예시코드에서는 component1 이 id를 의미하고, component2가 name 을 의미한다)

    val (id, name) = updatedUser // 두 변수를 updated User 에 좌우되게 하도록 함
    println("id=$id, name=$name")

id와 name 이라는 변수를 updatedUser 에 좌우되도록 지정할 수도 있다.

이러면 updatedUser 가 가지고있는 속성 id와 name 의 값이

변수 id와 name 에 들어갈 것이다.


상속

 

부모 클래스와 자식 클래스가 있다.

//super class, parent class, base class
open class Vehicle{ // 상속을 위해서는 open 으로 선언되어야 한다.
                    // 상속을 원하지 않으면 open 말고 sealed 를 넣으면 됨.
    //속성

    //메서드

}

//sub class, child class
open class Car : Vehicle(){

}

class ElectricCar: Car(){

}

예를 들어 Vehicle 이라는 부모클래스가 있고 그 안에 속성과 메서드들이 있다

 

이  Vehicle 클래스를 상속받는 Car 이라는 자식 클래스가 있고

ElectricCar 이라고 하는 자식클래스는 또 Car 이라고 하는 클래스를 상속받는 모습이다.

open class Car(val name:String, val brand:String) {
    open var range : Double = 0.0 //자식클래스에서 오버라이드할거면 open으로 선언해야

    fun extendRange(amount : Double){
        if(amount > 0)
            range += amount

    }

    open fun drive(distance : Double){ //override 할거면 open으로 선언해야
        println("drove for $distance km")
    }
}
//sub class, child class
class ElectricCar(name:String, brand:String, batteryLife:Double): Car(name, brand){
    override  var range = batteryLife * 6 //속성을 override
    var chargerType = "type1"
    override fun drive(distance: Double) { //부모클래스에서 override 한 함수
        println("drove for $distance km")
    }

    fun drive(){ //상속에서 생성한 drive 함수
        println("drove for $range km on electricity")
    }

}


fun main(){
    var myCar = Car("A3", "Audi")
    var myECar = ElectricCar("s-model", "Tesla", 85.0)

    myCar.drive(distance = 200.0)
    myECar.drive(distance = 200.0) // 전기차에는 drive 함수가 없지만 부모클래스의 특성을 상속 받아서 가능
    myECar.drive()
    myECar.chargerType = "type2"
}

자식 클래스에서 부모클래스의 속성과 함수를 override 할 수 있다.

대신 override 하려면 상속해주는 부분(부모클래스 부분)에 open으로 선언되어 있어야 한다.


interface

interface Drivable{
    val maxSpeed : Double
    fun drive():String
    fun brake(){
        println("the drivable is braking")
    }
}

interface 안에는 함수랑 속성이 있다.

만약 클래스가 이 interface 의 기능을 확장한다면 interface 에 만들어진 속성을 입력해야한다.

그리고 그 변수 오버라이딩을 위해서 override 키워드를 추가해야한다.

package org.techtown.kotlinbasics

import java.lang.IllegalArgumentException


interface Drivable{
    val maxSpeed : Double
    fun drive():String
    fun brake(){
        println("the drivable is braking")
    }
}

open class Car(override val maxSpeed : Double, val name:String, val brand:String) : Drivable {
    open var range : Double = 0.0 //자식클래스에서 오버라이드할거면 open으로 선언해야

    fun extendRange(amount : Double){
        if(amount > 0)
            range += amount

    }
    override fun drive() :String{ //이 drive 는 Drivable interface 에서 온 것
        return "driving the interface drive"
    }
    open fun drive(distance : Double){ //override 할거면 open으로 선언해야
        println("drove for $distance km")
    }
}
//sub class, child class
class ElectricCar(maxSpeed: Double, name:String, brand:String, batteryLife:Double): Car(maxSpeed, name, brand){
    override  var range = batteryLife * 6 //속성을 override
    var chargerType = "type1"
    override fun drive(distance: Double) { //부모클래스에서 override 한 함수
        println("drove for $distance km")
    }

    override fun drive(): String {
        return "drove for $range km on electricity"
    }

    override fun brake() {
        super.brake()
    }
}


fun main(){
    var myCar = Car(200.0,"A3", "Audi")
    var myECar = ElectricCar(240.0,"s-model", "Tesla", 85.0)

    myCar.drive(distance = 200.0)
    myECar.drive(distance = 200.0) // 전기차에는 drive 함수가 없지만 부모클래스의 특성을 상속 받아서 가능
    myECar.drive()
    myECar.chargerType = "type2"
    myECar.brake()
    myCar.brake()
}

Car 과 ElectricCar 클래스에 Drivable 인터페이스를 적용한 모습이다.

인터페이스에 있던 함수를 override 하고 있다.

 

 

왜 인터페이스를 사용해야할까?

나중에 구현하고 싶은 특정 함수와 클래스 속성이 있다면 유용하다(바로 구현하고 싶지 않은 경우)


추상 클래스

 

추상 클래스는 인터페이스와 비슷한 점이 있다.

객체를 만들 수 없다는 점이다.

하지만 추상 클래스로부터 서브 클래스를 상속 받을 수 있다.

 

추상 클래스에서 property와 method 같은 멤버는 abstract 키워드를 사용하지 않는 한 추상적인 것이 아니다.

 

package eu.tutorials.interfacestest

// An abstract class cannot be instantiated
// (you cannot create objects of an abstract class).
// However, you can inherit subclasses from can them.
// The members (properties and methods) of an abstract class are non-abstract
// unless you explicitly use the abstract keyword to make them abstract.
abstract class Mammal(val name: String, val origin: String,
    val weight: Double) {   // Concrete (Non Abstract) Properties

    // Abstract Property (Must be overridden by Subclasses)
    abstract var maxSpeed: Double

    // Abstract Methods (Must be implemented by Subclasses)
    abstract fun run()
    abstract fun breath()

    // Concrete (Non Abstract) Method
    fun displayDetails() {
        println("Name: $name, Origin: $origin, Weight: $weight, " +
                "Max Speed: $maxSpeed")
    }
}

class Human(name: String, origin: String, weight: Double,
            override var maxSpeed: Double): Mammal(name, origin, weight) {

    override fun run() {
        // Code to run
        println("Runs on two legs")
    }

    override fun breath() {
        // Code to breath
        println("Breath through mouth or nose")
    }
}

class Elephant(name: String, origin: String, weight: Double,
               override var maxSpeed: Double): Mammal(name, origin, weight) {

    override fun run() {
        // Code to run
        println("Runs on four legs")
    }

    override fun breath() {
        // Code to breath
        println("Breath through the trunk")
    }
}

fun main() {
    val human = Human("Denis", "Russia",
        70.0, 28.0)
    val elephant = Elephant("Rosy", "India",
        5400.0, 25.0)

    human.run()
    elephant.run()

    human.breath()
    elephant.breath()

}

 

추상 클래스가 있다.(이 예시 코드에서는 Mammal 이다)

추상클래스에는 property와 method가  abstract 로 선언되어 있는데 

이것은 꼭 서브클래스에서 overriding 해야한다.

추상클래스에서는 body 가 구현되어있지 않아도 된다.

 

추상클래스와 인터페이스. 인터페이스는 대략적인 설계 명세를 구현하고

인터페이스를 상속하는 하위 클래스에서 이를 구체화 하는 거는 동일하다.

 

그럼 추상클래스와 인터페이스의 차이점은 뭘까?

 

인터페이스에서는 property의 상태정보를 저장할 수 없다

다시 말하면 인터페이스에서는 property의 초기화가 불가능하다는것이다.

 

그리고 인터페이스는 다중 상속이 가능한데 추상클래스는 불가능하다

 

정리하자면..

- 추상클래스 : 형식만 선언하고 실제 구현은 서브클래스에 일임할 때

- 인터페이스 : 서로 다른 기능들을 여러개 물려주어야 할때

 


형 변환(타입 캐스팅)

 

리스트를 만들려면 ListOf 를 사용하면 된다.

리스트에 들어갈 요소들의 타입들을 지정해줄 수도 있는데, Any로 지정하면 여러 타입들의 요소를 넣을 수 있다.

val stringList: List<String> = listOf("Denis", "Frank", "Michael", "Garry")
val mixedTypeList: List<Any> = listOf("Denis", 31, 5, "BDay", 70.5, "weighs", "Kg")

for(value in mixedTypeList){
	if (value is Int){
    	println("Integer: '$value'")
    }else if(value is Double){
    	println("Double: '$value' with Floor value ${floor(value)}")
    }else if(value is String){
    	println("String: '$value' of length ${
        value.length} ")
    }else{
    	println("Unknown Type")
    }
}

그리고 요소들의 타입에 따라 조건을 달고 실행할 코드를 작성했다.

when 표현식으로 나타낼 수도 있겠다.

 

val obj3: Any = 1337
val str3: String? = obj3 as? String //works
println(str3) //prints null

위 코드처럼 키워드 as? 를 사용해서 explicit safe 캐스팅을 할 수 있다.

728x90
반응형