Kotlin을 배워보자 #2-클래스, 위임, companion object, object, lambda

이영준·2022년 6월 7일
0

코틀린 문법

목록 보기
2/4

📌클래스

🔑클래스 생성

class Human constructor(val name : String = "Anonymous"){
    //val name = name
    fun eatingCake(){
        println("This is Yummy")
    }
}

fun main(){
    val human = Human("youngjun")
    val stranger = Human()
    human.eatingCake()
    println("Human name is ${human.name}")
    println("Human name is ${stranger.name}")
}

생성자를 포함한 Human 클래스 이다. 위 코드에서 constructor은 생략해도 무방하다. 생성자로 name을 주고, 입력안할시 기본값을 anonymous로 설정했다.

val name : String = "Anonymous"에서 val을 붙인 것은 name이라는 프로퍼티를 따로 설정하지 않고 생성자의 name을 val name 프로퍼티로 그대로 쓴다는 의미이다.

자바로 따지면
private String name = "Anonymous";한 것과 같은 의미가 된다.

🔑클래스 멤버변수 지정

'위의 val name : String = "Anonymous"에서 val을 붙인 것은 name이라는 프로퍼티를 따로 설정하지 않고 생성자의 name을 val name 프로퍼티로 그대로 쓴다는 의미이다.' 부분을 처음 쓸 때는 그런가보다 하고 제대로 이해하지 못했었는데, 즉 val을 붙이지 않은 경우에는 주 생성자의 지역변수가 되기 때문에 클래스의 메서드들이 name에 접근할 수 없다.

var name : String
  init{
  this.name = name}
  

과 같이 init 내에서 멤버변수로 따로 할당해줘야 될텐데, var 이나 val 로 주 생성자의 매개변수로 쓰면 자동으로 클래스의 멤버변수로 할당이 되어 따로 init 함수 내에서 선언해주지 않아도 클래스의 메서드에서 사용할 수 있다.

🔑주생성자 본문 구성 - init

주 생성자의 일부로 init 내의 구문이 class 생성과 함께 호출된다.

fun main() {
    var mydate1 = MyDate3(2002)
    println(mydate1) //MyDate(year=2002, month=0, day=0)
    var mydate2 = MyDate3(2002, 2)
    println(mydate2) //MyDate(year=2002, month=2, day=0)
}

class MyDate3 (year:Int) { // 주생성자
    var year:Int = 0
    var month:Int = 0
    var day:Int = 0
    init{
        println("init 호출")
        this.year = year
    }
    constructor(year:Int, month:Int) : this(year){ //부생성자
        println("부생성자 호출")
        this.month = month
    }
    override fun toString(): String {
        return "MyDate(year=$year, month=$month, day=$day)"
    }
}

init 호출
MyDate(year=2002, month=0, day=0)
init 호출
부생성자 호출
MyDate(year=2002, month=2, day=0)

🔑생성자 오버로딩(부 생성자)

class Human constructor(val name : String = "Anonymous"){
    constructor(name : String, age : Int) : this(name){
        println("My name is ${name}, ${age} years old")
    }
    init{
        println("New Human has been born")
    }

    //val name = name
    fun eatingCake(){
        println("This is Yummy")
    }
}
    constructor(name : String, age : Int) : this(name){
        println("My name is ${name}, ${age} years old")
    }

기본 생성자 외에 생성자를 오버로딩 하기 위해서는 위와 같이 constructor 코드를 추가적으로 작성한다. 이 대 this(name)처럼 주 생성자의 변수값을 받아와야 한다.

이 때 코드를 실행하면, constructor가 위에 있음에도 불구하고 init 내부 블록의 코드가 가장 먼저 실행된다.

🔑생성자에 default 주기

class MyDate4 (var year:Int=2002, var month:Int, var day:Int) { // 주생성자
    override fun toString(): String {
        return "MyDate(year=$year, month=$month, day=$day)"
    }
}

default value를 주면 자동으로 해당 프로퍼티가 초기화된다.

🔑클래스 상속

open class Human constructor(val name : String = "Anonymous"){...}

class Korean : Human(){}

Kotlin에서는 클래스를 상속받기 위해서 같은 파일 내에 있어도 부모 클래스를open으로 지정해주고, class Korean : Human()이라고 상속 받을 클래스를 콜론을 사용하여 명시해준다. (자바와 달리 extends가 아닌 : 로 상속받는다.)

//  상속 가능한 클래스를 위해 open 사용
open class Human (var name: String="홍길동", var age: Int) { // 주생성자
    fun play() = println("name: $name")
    fun sing(vol: Int) = println("Sing age: $age")
}

//  주 생성자를 사용하는 상속
class Woman (name: String, age: Int) : Human(name, age) {
    fun singHitone() = println("Happy Song!") // 새로 추가된 메서드
}

//  부 생성자를 사용하는 상속
class Man : Human {
    val race: String
    constructor(name: String, age: Int, race: String) : super(name, age) {
        this.race = race// 새로 추가된 프로퍼티
    }
}

🔑메서드 오버라이딩

open class Human constructor(val name : String = "Anonymous"){
    constructor(name : String, age : Int) : this(name){
        println("My name is ${name}, ${age} years old")
    }
    init{
        println("New Human has been born")
    }

    //val name = name
    fun eatingCake(){
        println("This is Yummy")
    }

    open fun singASong(){
        println("Lalala")
    }

}

class Korean : Human(){

    override fun singASong(){
        super.singASong()
        println("라라라")
    }
}
fun main(){
    val me = Korean()
    me.singASong()
}

상속받은 Korean 클래스에서 부모 클래스의 singASong 메서드를 바꿔 사용하려면 부모의 singASong을 open으로 해주고,
override fun SingASong()으로 새로 함수를 작성한다.

기본적으로 method는 final method이기 때문에 open 키워드를 꼭 사용해야 오버라이딩을 할 수 있다.

결과
New Human has been born
Lalala
라라라

자바와 마찬가지로 코틀린 역시 가장 마지막에 오버라이딩 된 메서드가 실행된다.

open class Human62 {
    fun play() { println("Human62.play()") }
    open fun sing() { println("Human62.sing()") }
    open fun sing2() { println("Human62.sing2()") }
}

open class Animal62 : Human62(){
    override fun sing(){ println("Animal62.sing()") }
    final override fun sing2(){ println("Animal62.sing2()") }
}

open class Animal622 : Animal62(){
    override fun sing(){ println("Animal622.sing()") }
}

fun main() {
    var animal = Animal622()
    animal.play()  //Human62.play()
    animal.sing()  //Animal622.sing()
    animal.sing2()  //Animal62.sing2()
}

결과:
Human62.play()
Animal622.sing()
Animal62.sing2()

🔑 (+참고) super

super은 부모 메서드를 자식 클래스가 출력하고 싶을 때 사용하는 것으로 부모를 가리킨다.
korean 클래스에서 부모인 Human의 singASong도 사용하고 싶은 경우에는 super메서드를 사용한다.

📌프로퍼티

자바의 필드와 같다. 하지만 그와 달리 기본적인 접근 메서드(getter, setter)를 가지고 있다.

아래와 같이 주생성자에 매개변수를 주고 프로퍼티에 매개변수 값을 넣는 것으로 프로퍼티를 표현하거나,
바로 주생성자 안에 val, var 로 프로퍼티를 선언할 수 있다.

이 때, val로 선언한 프로퍼티는 바꿀 수 없는 값이므로 setter를 쓸 수 없다.(setter가 생성되지 않는다.)

class User(_id: Int, _name: String, _age: Int) {
    // 프로퍼티들    
    val id: Int = _id // 불변 (읽기 전용)
    var name: String = _name // 변경 가능
    var age: Int = _age // 변경 가능
}

class User1(val id:Int, var name:String, var age:Int) { // 프로퍼티들
}

🔑Getter, setter 직접 지정 (field)

var 프로퍼티
get() {게터 본문}
set(value) {세터 본문}

val 프로퍼티 (세터 설정 불가)
get() {게터 본문}

class User2(_id: Int, _name: String, _age: Int) {
    // field는 원래 있던값, value는 새로 입력된 값
    val id: Int = _id
        get() = field  //val은 getter만 가능함.
    var name: String = _name
        get() = field
        set(value) {
            if (value.length > 10) {
                println("이름이 너무 깁니다.")
            } else {
                field = value
            }
        }
    var age: Int = _age
        get() = field
        set(value) {
            println("현재값 : $field")
            if (value < 0 || value > 150) {
                println("나이를 확인해 주세요.")
            } else {
                field = value
            }
        }
}

field 란 자바의 this.필드값처럼 기존에 저장하고 있는 프로퍼티를 담는 변수이다.

📌위임

  • 일의 책임 및 처리를 다른 클래스 또는 메서드에게 넘긴다는 의미
  • 한 객체가 기능 일부를 다른 객체로 넘겨주어, 첫 번째 객체 대신 수행하도록 하는 일
  • 다른 클래스의 기능을 사용하되 그 기능을 변경하지 않으려면 상속 대신 위임
  • 위임을 활용하면 한 객체의 변경이 다른 객체에 미치는 영향이 적어짐

🔑By 를 통한 클래스 위임

interface Animal {
    fun eat() { println("Animal.eat()") }
}
class Cat : Animal {
    override fun eat(){ println("Cat eatting...")}  // overriding 워킹함.
}
val cat = Cat()
class Robot : Animal by cat
// Animal의 정의된 Cat의 모든 멤버를 Robot에 위임함
//cat은 Animal 자료형의 private 멤버로 Robot 클래스 내에 저장
//Animal에 대한 명시적인 참조를 사용하지 않고도 eat()을 호출

fun main() {
    var robot = Robot();
    robot.eat()
    println(robot::class.java)
}

위 코드에서 Cat클래스가 open이 아니기 때문에 상속받아 메서드를 사용하는 것이 원래는 불가능하다. 하지만 위임을 통해 구현체를 받아와서 메서드를 받아올 수 있다.

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}

🔑Observable 함수로 위임

  • 콜백처럼 프로퍼티의 내용이 변경될 때 수행할 작업을 정의할 수 있다.
class Observable{
    var name :String by Delegates.observable("처음"){
            property, oldValue, newValue ->
        println("$oldValue -> $newValue")
    }
}
fun main() {
    var ob = Observable()
    ob.name = "두번째"
    ob.name = "세번째"
    /* 결과
    처음 -> 두번째
    두번째 -> 세번째
     */
}

처음 -> 두번째
두번째 -> 세번째

🔑vetoable

  • 프로퍼티의 내용을 변경할 때 수행할 작업도 정의하고, 조건부로 변경을 하도록 할 수 있다.
  • vetoable 마지막 줄이 true이면 할당, false면 거부
class Vetoble{

    var age:Int by Delegates.vetoable(22) {
            property, oldValue, newValue ->
        println("property:${property.name}, $oldValue -> $newValue , result : ${oldValue > newValue}")
        oldValue > newValue
    }
}

fun main() {
    var v = Vetoble()
    println(v.age) //22
    v.age = 20
    println(v.age) //20
    v.age = 23
    println(v.age) //20
}

22
property:age, 22 -> 20 , result : true
20
property:age, 20 -> 23 , result : false
20

📌Companion Object

	정적 메소드나 변수를 사용할 때 사용하는 것으로 
    자바나 C++의 static과 유사하여 클래스 멤버임을 지정하기 위해 사용할 수 있다. 
    클래스의 인스턴스와 상관없이 호출해야 하지만 class의 내부 정보에 접근할수 있는 함수가 필요할때
    companion object를 class 내부에 선언한다. 
    

쉽게 말해 컴패니언 오브젝트는 클래스의 객체를 선언하지 않고 클래스명으로 곧바로 멤버에 접근하기 위해서 사용한다.

예를 들어

package com.example.myapplication

class Book private constructor(val Id : Int, val name : String){

    companion object BookFactory : IdProvider{

        override fun getId(): Int {
            return 444
        }

        val myBook = "new book"
        fun create() = Book(getId(), myBook)
    }
}

interface IdProvider{
    fun getId() : Int
}

fun main(){
    val book = Book.create()
    val bookId : Int = Book.BookFactory.getId()
    println("${book.Id}, ${book.name}")
    println(bookId)
}

private constructor을 가져서 외부에서 생성할 수 없는 Book 클래스가 있다.

val book2 = Book(3,"Lee")
//위와 같이 생성자 사용 불가능

companion object를 만들고 class 밖에 interface를 선언해 compainon object내에서 (IdProvider을 상속을 받기 때문에) 재정의한다.
그리하면 따로 클래스 객체를 만들지 않고 getId 함수로 Id값을 받아오도록 할 수 있다.

interface 클래스는 하나의 클래스에서부터만 상속받을 수 있는 자식 클래스에게 다중상속 비슷한 기능을 받을 수 있도록 해준다. interface 클래스에서 위처럼 빈 함수를 추상메서드로 만들 수 있다. 자세한 내용은 추후 보충하는걸로,,

결과:
0, new book
444

📌object 클래스

오브젝트 클래스는 클래스명을 지정하지 않는 익명 클래스로 클래스 구현과 동시에 변수에 저장을 함으로써 한번만 사용하는 경우에 선언한다.

하지만 위처럼 외부에서 클래스의 멤버들에 접근을 할 수 없는데, 클래스의 타입을 지정해주지 않아 최상위 타입인 Any타입으로 저장이 되면서 any타입에 없는 메서드나 변수를 부르려고 하고 있기 때문이다. 그렇기 때문에 상위 클래스를 타입으로 지정하고, 멤버들을 상속받은 다음, 그 멤버들을 오버라이딩 한 경우에 오브젝트 클래스의 멤버들을 사용할 수 있다.

open class parent() {
    open var member= 10
    open fun member_method(){}
}

val obj = object : parent(){
    override var member= 10
    override fun member_method() : Unit{
        println("Hello World")
    }
}

fun main(){
    obj.member = 20
    obj.member_method()
}

멤버들을 상속받으려면 부모 클래스의 멤버들을 open으로 하는 것 잊지 말자!

📌object로 싱글톤 패턴 구현

싱글톤 패턴은 객체의 인스턴스를 1개만 만들어서 계속 재사용 하는 패턴이다. 코틀린은 이 싱글톤을 위해 object 키워드를 사용한다.
object CarFactory{
    val cars = mutableListOf<Car>()
    fun makeCar(horsePower: Int) : Car{
        val car = Car(horsePower)
        cars.add(car)
        return car
    }
}

data class Car(var horsePower : Int)

fun main(){
    val car1 = CarFactory.makeCar(10)
    val car2 = CarFactory.makeCar(200)

    println(car1)
    println(car2)
    println(CarFactory.cars.size.toString())
}

결과:
Car(horsePower=10)
Car(horsePower=200)
2

object는 한번만 만들어지는 클래스이다. 이 object안에 makeCar 메서드를 통하여 리스트 형식으로 car을 넣어줘 클래스를 계속 만들지 않고 하나의 클래스로 구성하여 메모리를 절약할 수 있다.

📌람다 lambda

일급객체로서의 함수의 특징

  • 모든 요소는 변수에 할당할 수 있어야 함
  • 모든 요소는 인자로 넘길 수 있어야 함
  • 모든 요소는 리턴값으로 리턴 할 수 있어야 함

lambda는 value처럼 다룰 수 있는 익명함수이다.
1. 메소드의 파라미터로 넘겨줄 수 있다.
2. return 값으로 사용할 수 있다.(코드의 마지막 줄이 return type으로 추론된다.)
3. 매개변수가 하나인 경우에 생략 가능하다.
4. 매개변수 타입을 추론할 수 있다면 생략 가능하다.

val lamdaName : Type = {argumentList -> codeBody}

fun main(){
    println(square(12))
    println(square1(12))
    println(nameAge("Lee",25))

}

val square : (Int) -> (Int) = {number -> number * number}
val square1 = {number : Int -> number * number}
val justsquare = {->println("just square")}
val nameAge = {name : String, age : Int ->
    "my name is ${name}, my age is ${age}."
}

number을 매개변수로 받아 number*number을 반환해주는 square함수를 만들어줬다.
square에서 매개변수로 들어가는 형은 여러개가 올수 있으므로 항상 ()괄호를 묶어줘야 한다. 반환값은 어차피 하나이니 꼭 괄호로 안 묶어줘도 된다.

또한 nameAge람다식에서처럼 인자를 2개 이상 받기도 한다.
justsquare처럼 매개변수가 없는 람다식은 화살표만 남겨두거나 화살표도 제거할 수 있다.

또한가지 중요한 점은 람다함수의 반환값은 본문의 마지막 줄의 실행 결과이다.

위 람다식의 반환값은 매개변수 it에 20을 더한 값이므로 println을 통하여 확인하면 람다식에 있는 프린트문과 함께 매개변수 10에 20을 더한 30이 출력된다.

결과:
hello
30

🔑람다함수의 선언과 동시에 호출

fun main(){
    println({no1 : Int, no2 : Int -> no1 + no2}(10,20))
}

람다함수 선언과 동시에 괄호로 매개변수를 작성해주면 곧바로 함수 호출도 가능하다.

결과:
30

🔑확장함수

이미 있는 클래스에 메서드를 추가할 수 있다.

fun main(){
    var a = "Lee Said"
    var b = "Kim Said"
    println(a.pizzaIsGreat())
	println(b.pizzaIsGreat())
}

val pizzaIsGreat : String.() -> String = {this + " Pizza is the best!"}

결과:
Lee Said Pizza is the best!
Kim Said Pizza is the best!

String.()라는 것은 파라미터가 따로 없다는 것이고, -> String을 반환한다. this는 확장함수를 부른 오브젝트를 뜻한다.

🔑this와 it의 사용

this는 메서드를 부른 오브젝트를 지칭하기 위해서 사용된다. it은 lamda식의 매개변수로 들어가는 값을 지칭하기 위해 사용된다. (매개변수가 한개 일때만 it 사용 가능)

fun main(){
    var c = "Lee"
    println(extendString(c,25))

}

fun extendString(name : String, age : Int) : String{
    val introduceMyself : String.(Int) -> String = {"I am ${this} and ${it} years old"}
    return name.introduceMyself(age)
}

결과:
I am Lee and 25 years old

🔑lamda의 return

fun main(){
    println(calculateGrade(98))
}

val calculateGrade : (Int) -> String = {
    when(it){
        in 0..40 -> "Fail"
        in 41..70 -> "pass"
        in 71..100 -> "perfect"
        else -> "Error"
    }
}

when을 사용하여 lamda식을 구성한 경우이다. 이 때 매개변수 it에 어떤 값이 들어와도 처리가 되도록 else 문도 꼭 써줘야 오류가 일어나지 않는다.

🔑lambda를 매개변수로 사용하기

fun main(){
    println(invokeLamda(a))
}

val a = {number : Double ->
    number == 4.2313
}

fun invokeLamda(lamda : (Double) -> Boolean) : Boolean {
    return lamda(5.234)
}

invokeLamda 함수는 lambda식을 매개변수로 받아 입력 값이 4.2313과 같은지 다른지를 반환해준다. double을 받아 boolean을 출력하는 이 lambda 매개변수를 val a로 정의하고 있다.

fun main(){
    println(invokeLamda({it > 3.22}))
}

fun invokeLamda(lamda : (Double) -> Boolean) : Boolean {
    return lamda(5.234)
}

매개변수에 바로 람다식을 쓸 수도 있다. 여기서 it은 하나의 매개변수로 전달된 값, 즉 5.234를 뜻한다.

🔑function의 마지막 매개변수가 lambda일 때

함수의 마지막 매개변수가 lambda 형일 때는

invokeLamda({it > 3.22})

가 아닌

invokeLamda {it > 3.22}

로 괄호를 삭제하여 작성해줄 수 있다.

profile
컴퓨터와 교육 그사이 어딘가

0개의 댓글