본문 바로가기
Kotlin

Kotlin10. class와 OOP

by 히예네 2023. 8. 6.
728x90
반응형

1. 객체지향

C는 절차지향 , 함수를 쓰는 순서가 중요하다.

어느정도 프로그램이 복잡해지면 , 변수나 함수들이 점점 늘어나게된다. 

이렇게 많아진 함수나 변수는 정리되지 않으며 나중에 코드를 보기 힘들어진다.

그 방법 중 하나로, 대규모 프로젝트를 만들때 주류가 되는 개념이 「객체지향」이다. 

객체라는 덩어리로 프로그램을 구축한다. 

 

객체는???

변수나 함수를 덩어리로 만들어서, 항상 그 내용을 뱃속에 보존하고 이용할 수 있게 만든 것이다. 

객체는 함수와 변수를 같이 갖고있다.

이 객체가 갖고 있는 능력을 class라 부른다.

이미 존재하는 class(Random,, 등)가 존재하고, 내가 class를 만들수도 있다. 

fun main() {
    var p = Person()
    p.name = "cake"
    p.mail = "cake@gmail.com"
    println(p.say())
}

class Person {
   var name = "아테나"
   var mail = "thena@a.a"
    
   fun say() = "name : $name , mail : $mail"
}

class 안에는 변수와 한수가 들어갈수있다. 

class안에 정의되어있는 var, val는 프로퍼티라 부르고, 함수는 메소드라한다. 

 

class는 저 자체로 바로쓰는게 아니다!! class는 객체를 만들어서 쓰는것이다.

    var p = Person() //인스턴스를 작성했다라고도 한다.
    p.name = "cake"
    p.mail = "cake@gmail.com"
    //프로퍼티 재정의
println(p.say())
//메소드 재정의

그런데 이렇게 매번 프로퍼티에 값을 주고, 메소드를 부르는것은 너무 귀찮다. 주생성자(primary constructor)를 이용하여 더 간단하게 만들어보자. (더 길어보이긴 하는데 확장이 되는걸 이해해야함) 

fun main() {
    println(Person().say()) //디폴트값
    
    val me = Person("아테나", "아테나@gma.co")
    println(me.say())
}

class Person(name:String="default",mail:String="default") {
//name과 mail 값을 주생성자로 던진다. 
   var name : String //참조변수
   var mail : String //참조변수
    
   init{
       this.name = name
       this.mail = mail
   }//init 메소드
    
   fun say() = "name : $name , mail : $mail"
}

//output
name : default , mail : default
name : 아테나 , mail : 아테나@gma.co

init메소드

던지는 파라미터를 단순히 프로퍼티에 던지는게 아니라, 좀 더 복잡한 처리가 이용되는경우

class에 초기화처리를 가져와서 처리할 수 있습니다. 거기서 이용되는게 init이다. 

파라미터나 리턴값이 필요없다. init안에 class 파라미터로 준비되어있는 값을 초기화해주는 방식이다.

 

init과 this를 사용하는거 이해하기

       this.name = name
       this.mail = mail

파라미터값을 프로퍼티에 대입한다. 그런데 프로퍼티(var name)와 주생성자의 (name ) 파라미터 이름이 같기 때문에 

name을 정확하게 인식할 수 없다. 그럴때 this를 쓴다.

this는 적성한 인스턴스자신을 나타내는 특별한 값이다. 

this.name이라고 하면 작성한 인스턴스 자신의 name 프로퍼티(var name)를 가르키는것이다. 

name프로퍼티에 name파라미터를 저장하는것!! 

 

※ init을 쓰지않고 바로 대입해서 초기화 할 수도 있다.

class Person(name:String="default",mail:String="default") {
   var name = name
   var mail = name 
    
   fun say() = "name : $name , mail : $mail"
}

프로퍼티에 직접 파라미터값을 대입하는형태 . init은 프로퍼티에값을 대입하는거뿐만 아니라 다양한 초기화처리를 대응해줌. 이런 대응이 필요없고 간단한 작업이라면 직접 대입해도 됨. 

 

보조생성자 (second constructor)

주생성자는 던지는 파라미터값이 정해져있으면 편리하지만.. 여러 variation이 존재하는경우 쓰기가 힘들다..

(예를들어 Person 클래스에 name과 mail을 던져 객체를 만들고싶다 or 배열로 묶어서 던지고싶다 )

그럴때 이용하는게 보조생성자이다. 

보조생성자를 쓸때는 주생성자는 반드시 지우고 쓴다!!!(class의 파라미터와 init)

 

fun main() {

    val me = Person("아테나", "아테나@gma.co")
    println(me.say())
    
    val data = mapOf<String,String>(
    "name" to "cake",
    "mail" to "candy@ca.d")
    val me2 = Person(data)
    println(me2.say())
}

class Person{
   var name : String //참조변수
   var mail : String //참조변수
    
    constructor(name :String = "no",mail:String="no"){
        this.name= name
        this.mail = mail
    }
    
    constructor(arr:Map<String,String>){
        this.name = arr["name"]?: "no name" //?:는 엘비스 연산자 
        this.mail = arr["mail"]?: "no mail"
    }
    
   fun say() = "name : $name , mail : $mail"
}

//output
name : 아테나 , mail : 아테나@gma.co
name : cake , mail : candy@ca.d

init과 class에서는 () 가 없었지만- , 보조생성자에는 () 안에 파라미터를 넣어준다. 

 

※주생성자에서.. 함수 이름 뒤에 constructor를 쓰는게 기본이지만 생략 가능함

class Person constructor (name:String="default",mail:String="default")

2. 프로퍼티

class에서 필요한 데이터를 프로퍼티에 저장할 수 도 있지만, 값취득이나 값 변경에 쓸때는 어떤 처리가 필요하다.

 

get , set 함수

프로퍼티 뒤에 get, set함수를 작성한다. 값을 변경할때 get, set함수를 이용하여 가져온다.

반드시 2개다 쓸 필요는 없구 필요한거를 본인이 판단해서 쓰면된다. 

fun main() {
    val me = Person("아테나","dd@dma.co")
    me.content = "ok"
    me.say()
    
    me.content = "banananana,orangeeee"
	me.say()    
}

class Person {
    var name : String
    var mail : String
    var content : String
    	get() = "내 이름은 $name 이고 ,메일은 $mail 입니다."
    	set(value :  String){
            val arr = value.split(",")
            // split은 특정 구분자를 이용해서 배열로 토해낸다.  
            
            if(arr.size>=2){
                name = if (arr[0] == "") "no name" else arr[0]
                mail = if (arr[1] == "") "no mail" else arr[1]
            }
        }
        
     constructor(name : String = "aa", mail : String = "bb"){
         this.name = name
         this.mail = mail
     }
     
     fun say(){
         println(this.content)
     }
}
//output
내 이름은 아테나 이고 ,메일은 dd@dma.co 입니다.
내 이름은 banananana 이고 ,메일은 orangeeee 입니다.

 

필드

프로퍼티에 따라서는, get/set에 특정처리를 통해 값을 설정하고 싶을수도있다.

(예를들면 Person안에 age 프로퍼티를 가져온다고 하자. age가 음수가 되지 않게 get/set을 처리할수도 있다. )

    var age : Int
    	get() = if(age<0) 0 else age
    	set(value) { if (value < 0) age = 0 else age = value }

이러면 에러이다!!!! age가 0미만인면 계속 age를 뱉어낸다. 이걸 방지하기 위해 field를 사용한다. 

    var age : Int
    	get() = if( field<0 ) 0 else field
    	set(value) { if (value < 0) field = 0 else field = value }

age 값을 get/set 안에서 본인 (age)를 조작하게되면 이런문제가 생기니 이걸 피하기 위해 field를 쓴다.

field는 get/set안에서 그 프로퍼티 자신(var age)을 말하는것이다. 

 

 

3. 오버로드

Person에는 say()라는 메소드가 존재한다.

다양한 출력을 하고싶다면? 오버로드하며 된다.

코틀린에서는 같은 이름의 메소드를 복수정의하는것이 가능하다. 

파라미터나 리턴값이 다르면 , 이름이 같아도 별도 메소드로 쓰는것이 가능함.

fun main() {
    val me = Person("아테나","dd@dma.co")
    me.say()
    me.say("*")
    me.say("value=[","!]")   
}

class Person {
    var name : String
    var mail : String

     constructor(name : String = "aa", mail : String = "bb"){
         this.name = name
         this.mail = mail
     }
     
     fun say(){
         println("name : $name , mail : $mail ")
     }
     
     fun say(c : String){
         println(" $c $name $c $c $mail $c  " )
     }
     
     fun say(sc : String , ec : String){
         println("$sc $name $ec $sc $mail $ec  ")
     }
}

//output
name : 아테나 , mail : dd@dma.co 
 * 아테나 * * dd@dma.co *  
value=[ 아테나 !] value=[ dd@dma.co !]

 

4. class 상속

복잡한 class를 만들때 이미 만들어져있는 비슷한 class가 있다면 활용하는게 더 효율이 좋다.

처음부터 만드는게 아니라 이미 있던 class를 활용하는개념이 상속이다. 

class앞에 open이 붙어야 상속할때 이용 할 수있다. 

fun main() {
    val me = Person("아테나","dd@dma.co")
    me.say()
    
    val me2 = NewPerson("koko","aa@d.com",45)
    me2.say()
}

open class Person {
    var name : String
    var mail : String

     constructor(name : String = "aa", mail : String = "bb"){
         this.name = name
         this.mail = mail
     }
     
     open fun say(){
         println("name : $name , mail : $mail ")
     }
     
}   
  class NewPerson : Person{
      var age : Int
      
      constructor(name : String = "cc", mail : String = "dd", age : Int = 0){
          this.name = name
          this.mail = mail
          this.age = age
      }
      
      override fun say(){
          println("Name : $name $age $mail ")
      }
  }
  
  //output
  name : 아테나 , mail : dd@dma.co 
Name : koko 45 aa@d.com

Person class는 super class

NewPerson class는 subclass

 

※ 사실 Person도 Any라는 class를 상속받음 

toString 등 내가 정의안해도 이미 사용이 가능한 메소드들이 있다. 암묵적으로 Any class를 상속 받았기때문에 가능한것이다.

Any는 모든 class의 super class이다. 

 

 

주생성자가 있는 class를 상속받을때는 

주생성자 안에 있는 파라미터도 신경써서 작성해야한다. 

fun main() {
    val me = Person("아테나","dd@dma.co")
    me.say()
    
    val me2 = NewPerson("koko","aa@d.com",45)
    me2.say()
}

open class Person(name : String, mail : String) {
    var name : String
    var mail : String
    
    init{
        this.name = name
        this.mail = mail
    }

     open fun say(){
         println("name : $name , mail : $mail ")
     }
     
}  

 class NewPerson(name : String, mail : String, age : Int): Person(name, mail){
 //Person(name, mail) 파라미터도 정확하게 기재해줘야한다. 
 
 
      var age : Int

       init{
        this.name = name
        this.mail = mail
        this.age = age
      }
       
      override fun say(){
          println("Name : $name $age $mail ")
      }
  }

 

메소드 오버라이드

say()라는 똑같은 메소드가 존재한다. 

super class에 있는 메소드를 subclass에 데려올때 subclass 측에 준비되어있는 메소드를 호출하게 되는데 

이걸 오버라이드라고 한다. 

 

덮어쓰는게 아니다! 올라탄것이라 볼수있다. 밑에 기존 메소드가 깔려있는것임. 

없애고 새로 쓴 게 아니라 위에 있는것이다 . 

상속받은 기능이 마음에 안들때 수정하고싶다.

메소드 앞에도 open이 써져있어야 오버라이드 가능하다. 그리고 subclass의 메소드 앞에 override를 써주면 된다.  

 

※오버로딩과 오버라이드는 다른것이다.

오버로딩 :  이름이 같은 함수를 파라미터/리턴값등을 다르게 해서 사용

오버라이드 : 상속받은 메소드가 맘에 안들때 , 그 위에 다른 메소드를 태운다. 

 

5. 캐스트 (다형성) 

상속관계에 있는 class에서는

class를 캐스트해서 별도의 class로써 다루는게 가능하다. (캐스트는 형추론)

NewPerson은 Person을 상속받아 만들었다. 그러니까 NewPerson은 Person으로 캐스트하여 이용가능하다. 

as를 이용한다.

 

중요!

 

업캐스트와 다운캐스트를 이해해야한다. 

  1. Upcasting (업캐스팅):
    • Upcasting은 부모 클래스의 참조 변수가 자식 클래스의 객체를 참조하는 것을 의미합니다.
    • 부모 클래스로의 업캐스팅은 항상 안전합니다. 왜냐하면 부모 클래스는 자식 클래스보다 더 일반적인 형태를 나타내며, 자식 클래스의 특정 기능이나 속성은 부모 클래스 참조로는 접근할 수 없습니다.
    • 예를 들어, 부모 클래스인 "동물(Animal)"과 그 자식 클래스인 "고양이(Cat)"가 있다면, "동물" 타입의 참조 변수로 "고양이" 객체를 참조하는 것이 업캐스팅입니다.
  2. Downcasting (다운캐스팅):
    • Downcasting은 부모 클래스의 참조 변수가 자식 클래스의 객체를 참조할 때 다시 원래 자식 클래스 타입으로 변환하는 것을 의미합니다.
    • 다운캐스팅은 업캐스팅과 달리 안전하지 않을 수 있습니다. 부모 클래스로부터 받은 참조 변수를 자식 클래스로 강제 변환할 때, 참조하려는 객체가 실제로 해당 자식 클래스의 인스턴스인지 확인해야 합니다. 그렇지 않으면 런타임 에러인 "ClassCastException"이 발생할 수 있습니다.
    • 예를 들어, "동물" 타입의 참조 변수로 "고양이" 객체를 참조한 뒤, 이를 다시 "고양이" 타입으로 다운캐스팅하려 할 때, 참조하려는 객체가 실제로 "고양이" 클래스의 인스턴스인지 확인해야 합니다.

 

6.접근제한자

(1) Top level 요소

함수나 클래스 등 소스코드에 직접 기술한다.

public 어디서든 이용가능
internal 같은 모듈에서만 이용가능
private 그 파일 안에서만 이용 가능

(2) class 내 요소

클래스 내 프로퍼티나 메소드등에서 사용한다. 

public 외부에서 이용가능
internal 같은 모듈에서만 이용가능
protected 같은 클래스 및 서브클래스에서만 사용가능
private 같은 클래스에서 이용가능

 

728x90
반응형

'Kotlin' 카테고리의 다른 글

Kotlin12. class의 다른 확장 - Delegation, observable...  (0) 2023.08.13
Kotlin11. interface, abstract, Singleton  (0) 2023.08.13
Kotlin9. 재귀함수, 꼬리재귀  (0) 2023.08.01
Kotlin8. 람다식 , 고차함수  (0) 2023.08.01
Kotlin7. 함수  (0) 2023.07.31