업데이트:

카테고리:

/

태그: ,

객체 지향 프로그래밍(Object Oriented Programming)

5가지의 기본 정의가 있다.

  1. 변수와 타입
    • 저장 공간에서의 위치: 저장 공간을 나타내기 위해서 고유의 이름을 정해야 한다.
    • 왜냐면 고유의 이름으로 데이터에 접근할 수 있기 때문에
     fun main(args: Array<String>){
       var age = 15
    
       println(age) // 15
     }
    
  2. 흐름 제어
    • 조건이 맞는 경우에만 코드를 실행
    • 조건이 맞으면 코드를 여러 번 반복 실행한다.
     fun main(args:Array<String>){
       var age = 17
    
       // 21보다 크거나 같을 때 투표할 수 있다는 출력을 한다.
       if(age >= 21)
         println("You may vote now")
     }
    
  3. 함수
    • 코드를 나눠주고 필요시 코드 블록을 실행하게 한다.
    • 코드를 쉽게 나누고 정리해준다.
     fun main(args:Array<String>){
       // 함수 호출
       myFunction()
     }
    
     fun myFunction(){
       println("my function was called")
     }
    
  4. 컬렉션
    • 많은 요소를 한 군데에 저장해준다.
    • 흐름 제어의 도움을 받아 여러 요소를 반복 실행하게 해준다.
  5. 클래스
    • 직접 데이터 타입을 만든다.
    • 데이터 멤버와 메서드를 한 곳에 있게 해준다.
    • 메서드는 쉽고 유지 가능한 코드를 만들게 해준다.

객체

라는 청사진, 클래스가 존재한다.
차의 props는 최고 속도, 바퀴 개수, 창문과 문의 갯수… 이 있다.
차의 method은 drive, break가 있다.

차의 최고속도는 322km
차의 바퀴는 4개, 문은 2개
drive, break, turbo buster가 있다.

이렇게 생산된 차는 객체라고 불린다.

클래스

코틀린의 MainActivity는 AppCompatActivity로 부터 상속받은 것이다.

fun main() {
  // 이런식으로 클래스를 이용하여 객체를 생성한다.
  var sohee = Person(Cheon, Sohee) 
}

// 메서드나 속성이 없어도 클래스가 된다.
// 매개변수에 기본값을 지정해줄 수 있다.
class Person(firstName: String = "John", lastName: String = "Doe"){
  init {
    println("Initialized a new Person object with firstName = $firstName and lastName = $lastName")
  }
}

constructor: 보조 생성자

  • 객체 생성 시 값을 추가해준다.
  • 클래스 안에 어떤 속성이 들어가는 지 미리 정할 수 있다.
  • 구체적으로 정하고 싶지 않다면 필수는 아니다.

init: 이니셜라이저

  • 객체가 생성되는 순간 호출된다.

스코프

함수의 파라미터에는 값을 지정할 수 없다.
변수와 파라미터는 다르다.

// 이런식으로 제일 바깥에 변수를 지정해주면 어느 곳에서든 사용가능하다.
var b = 3
fun main () {
  myFcuntion(5)
  // 함수 내의 변수는 전역으로 확인 할 수 없다.
  b = 3
}

fun myFunction(a: Int){
  // 변수 a와 파라미터 a는 다르다.
  b = a

  println("a = $a") // 4
}

함수 내에서 변수로 값을 지정하고 호출을 하면 파라미터가 아닌 변수가 호출된다.

코틀린에서는 왜 메서드에서 변수와 매개변수 이름을 같게 정할까?
이것을 shadowing이라 부르고, 시스템 내 다른 곳으로부터 코드를 디커플링 하는 것에 유용하다.

유의사항

  • 다른 2개의 변수나 파라미터가 있을 때 이름이 같다면 읽기가 힘들다.
  • 한번 쉐도잉을 하면, 스코프 안에서 더는 원래의 변수에 접근할 수가 없다.

멤버

fun main() {
  var sohee = Person(Cheon, Sohee)
  // 속성 초기화
  sohee.hobby = "스케이트보드 타기"
  // 메서드 출력
  sohee.stateHobby()
}

class Person(firstName: String = "John", lastName: String = "Doe"){
  // 변수 = 프로퍼티
  var age : Int? = null
  var hobby : String = "watch Nexflix"
  var firstName : String? = null

  // 함수 = 메서드
  fun stateHobby(){
    // 객체의 프로퍼티에 파라미터값을 저장한다.
    this.firstName = firstName
    println("$firstName\'s hobby is $hobby")
  }

  // 2번째 생성자
  constructor(firstName: String, lastName: String, age: Int)
    : this(firstName, lastName){
      // 나이 재정의
      this.age = age
    }
}

늦은 초기화

변수를 만들면 항상 초기화를 해주어야 하는데 lateinit을 앞에 붙어주면 나중에 초기화 예정을 의미한다.

fun main(){
  var myCar = Car()
  myCar.owner // 오류가 발생
}
class Car(){
  lateinit var owner : String
  init {
    // 이니셜라이저를 하게되면 더이상 오류가 발생하지 않는다.
    this.owner = "Frank"
  }
}

lateinit을 사용한 변수를 호출하게 되면 오류가 발생한다.
Uninitalized Property Access Exception이 발생한다.
프로퍼티 소유주가 초기화되지 않아서 비어있는 데이터가 접근했기 때문이다.

해결하기 위해서는 이니셜라이저를 init안에 넣어줘야 한다.

세터 & 게터

세터는 변수를 생성할 때 클래스 내에서 속성을 어떻게 지정할 건가이다.
게터는 변수를 생성하고 객체에서 속성을 호출하였을 때 발생한다.

fun main(){
  var myCar = Car()
  println("brand is: ${myCar.myBrand}") // bwm
  myCar.maxSpeed = -5 // 에러가 발생하게 된다.
}

class Car(){
  val myBrand: String = "BMW"
  
  // myBrand에 접근하게 되면 myBrand의 소문자를 받게 된다.
  get() {
    return field.toLowerCase()
  }

  var maxSpeed: Int = 250
    // 변수가 생성이 하단의 코드가 자동으로 생성된다.
    // get() = field
    // 여기서 value는 변수를 뜻한다.
    set(value){
      field = if(value > 0) value else throw IllegalArgumentException("최고 속도를 0 이상으로 설정하세요")
    }

  var myModel : String = "M5"
    // 다른곳에서 지정하려면 불가능하다.
    private set
  
  init(){
    this.myModel = "M3"
  }
}

private는 해당 클래스 내에서만 사용할 수 있도록 하는 접두사?이다.

데이터 클래스

클래스 생성자 앞에 data를 작성하면 데이터 클래스를 만들 수 있다.
반드시 주 생성자에 매개변수 하나는 꼭 있어야 한다.
매개변수는 var또는 val로 넣어야 한다.
데이터 클래스는 추상, 오픈, 봉인, 내부 클래스일 수 없다.

data class User(val id: Long, var name: String)

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

  val name = user1.name
  println(name) // Sohee

  // val로 정해져 있어서 재 정의가 불가하다.
  // user1.id = 2

  val user2 = User(1, "Sohee")

  println(user1.equals(user2)) // ture

  // 데이터 클래스는 toString을 내장하고 있다.
  println("User Details: $user1")

  // 데이터 클래스는 copy를 내장하고 있다.
  // 매개변수에 따로 변경할 값을 넣으면 그 값으로 바뀌게 된다.
  val updatedUser = user1.copy(name="Cheon Sohee")

  println(updatedUser) // User(id=1, name="Cheon Sohee")

  // 컴포넌트(속성) 별로 호출할 수 있다.
  println(updatedUser.component1()) // 1
  println(updatedUser.component2()) // Cheon Sohee

  // 구조분해 할당
  val (id, name) = updatedUser
}

상속

부모 클래스의 프로퍼티와 메소드를 모두 이어 받아 사용할 수 있다.
코드 재사용성이 높아진다.

  // Main Class of 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
    }

    fun drive(distance: Double){
      println("Drove for $distance KM")
    }
  }

  // Sub Class of Car
  // 상속받은 매개변수도 입력해줘야 한다.
  class ElectricCar(name: String, brand: String, batteryLife: Double) : Car(name, brand){
    override var range = batteryLife * 6
    var chargerType = "Type1"

    override fun drive(distance: Double){
      println("Drove for $distance KM on electricity")
    }

    fun 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(200.0)
    // 매개변수의 갯수에 따라 다른 결과를 출력할 수 있다.
    myECar.drive(200.0)
    myECar.drive()
  }

open: 상속을 받을려면 부모클래스가 오픈키워드를 가지고 있어야한다.

  • 또한 변수나 함수도 오버라이딩 하기 위해서는 open 키워드를 가지고 있어야한다.

sealed: 해당 키워드를 가지고 있으면 상속할 수 없다.

  • 해당 인자를 가지고 있으면 클래스가 객체로 인식된다.

인터페이스

인터페이스는 클래스가 서명할 수도 있는 계약서입니다.
모든 프로퍼니와 함수를 구현해줄 수 있지만, 꼭 할 필요는 없다.
클래스 내에서 사용하는 프로퍼티와 메서드를 미리 지정하는 타입이다.

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 = return "driving the interface drive" 
  override fun drive(): String {
    return "driving the interface drive"
  }
  
  open fun drive(distance: Double){
    println("Drove for $distance KM")
  }
}

fun main(){
  var audiA3 = Car(200.0, "A3", "Audi")

  // 따로 함수를 생성하지 않았지만 interface에 있는 값이 나온다.
  audiA3.brake()
}

구체적인 클래스를 제작하지 않고 구현하고 싶을 때 사용한다.

추상

키워드 abstract을 사용하지 않으면 추상 클래스가 아니다.
추상 클래스와 메서드는 따로 구현하지 않는다.
abstract fun run() 구현하지 않고 추후 상속 받을 때 오버라이딩 한다.

추상클래스는 객체를 만들 수 없다.
오로지 상속을 통해 객체를 만들 수 있다.

타입 캐스팅

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

listOf(): 이걸 사용하면 리스트를 만들 수 있다.

Smart Cast: 자동으로 타입을 할당한다.

val obj1: Any = "I have a dream"
if (obj1 !is String) {
  println("Not a String")
} else {
  println("Found a String of length ${obj1.length}")
}

Explicit: as 키워드를 사용해서 타입 캐스팅을 한다.

val obj1: Any = "I have a dream"

val str1: String = obj1 as String
println(str1.length)

val obj2: Any = 1337
val str2: String = obj2 as String
println(str2) // Error

// str3을 nullable한 타입으로 지정하여 String이 아니면 null이 들어오도록 설정한다.
val obj3: Any = 1337
val str3: String? = obj3 as? String 
println(str3)