코딩하는 개굴이

[Kotlin IN ACTION] 클래스, 객체, 인터페이스 본문

안드로이드/KOTLIN

[Kotlin IN ACTION] 클래스, 객체, 인터페이스

개굴이모자 2021. 4. 4. 01:30
반응형

Kotlin IN ACTION 4강 : 클래스, 객체, 인터페이스

본 내용은 Kotlin IN ACTION (드미트리 제메로프, 스베트라나 이사코바 지음 / 에이콘 출판사) 책을 기반으로 작성되었습니다.

클래스 계층 정의

코틀린 인터페이스

코틀린 인터페이스 정의 및 구현하는 방법은 자바 8과 유사하다.
코틀린 인터페이스 안에는 추상 메소드와 구현이 있는 메소드도 정의할 수 있다. (자바 8의 디폴트 메소드와 유사)

  • 코틀린에서 인터페이스는 interface 키워드를 사용하여 정의한다.
  • 인터페이스를 구현하는 모든 비추상 클래스(구체적 클래스)들은 추상 메소드를 구현해야 한다.
    • 자바에서는 클래스의 확장에서는 extends를, 인터페이스 구현은 implements 키워드를 사용하지만, 코틀린은 클래스 네임 뒤 : 을 붙이고 인터페이스와 클래스 이름을 적는 것으로 구현할 수 있다.
    • 자바와 동일하게 코틀린도 인터페이스의 구현은 제한이 없지만 클래스의 확장은 오직 1개만 가능하다.
    • override 시, override 변경자를 꼭 사용해야 한다.
      • 실수로 네이밍이 같은 경우 등 상위 클래스의 메소드를 의도치 않게 오버라이드 하지 않도록 하기 위함
    • 인터페이스 메소드는 디폴트 구현을 제공할 수 있다.
      • 자바8에서는 인터페이스 메소드의 디폴트 구현 시, 메소드 앞에 default 를 붙여야 했다.
      • 코틀린은 구현하려는 본문을 메소드 시그니처 뒤에 추가하면 된다.
      • 디폴트 메소드가 존재할 경우, 상속 시 필수적으로 구현할 필요가 없다.
      • 만일 동시에 구현하려는 인터페이스에 같은 이름의 디폴트 메소드가 존재할 경우, 별도 처리가 없다면 컴파일 오류가 발생한다.
        • 어느쪽의 디폴트 메소드도 자동으로 선택되지 않기 때문에, 두 구현을 모두 대체하는 오버라이딩 메소드를 직접 구현해야 한다.
        • 상위 타입의 이름을 꺽쇠 사이에 넣고 super 를 지정한다면 어떤 상위 타입의 멤버 메소드의 호출을 하는지 명시할 수 있다.
interface Clickable {
    fun click()
    fun showText() = println("I'm Clickable") //구현하려는 본문을 뒤에 추가하여 디폴트 메소드를 구현할 수 있다.
}

interface Focusable {
    fun focused()
    fun showText() = println("I'm Focusable")
}

class Button : Clickable, Focusable { //interface 의 구현에는 제한이 없다.
    override fun click() = println("Clicked") // 구현하려는 인터페이스들의 추상 메소드는 구현해야 한다.
    override fun focused() = println("Focused")
    override fun showText() { //상위 인터페이스 둘다 디폴트 구현이 있는 showText라는 메소드가 존재하므로, 직접 해당 디폴트들을 대체할 메소드를 작성해야 한다.
        super<Clickable>.showText()
        super<Focusable>.showText() //둘다 해당하는 overriding 이기 때문에, 어떤 상위 타입의 메소드를 지정해 주어야 한다.
    }
}

open, final, abstract 변경자

자바에서는 상속을 금지하는 키워드인 final 을 명시하지 않을 경우, 모든 클래스를 다른 클래스가 상속할 수 있다.

이는 편리할 수 있지만 문제가 생길 수도 있다.

  • 취약한 기반 클래스 문제(fragile base class): 하위 클래스가 기반 클래스에 대해서 가졌던 가정이 기반 클래스가 변경되면서 깨져버린 경우
    • 하위 클래스에서 오버라이드 하게 의도된 클래스, 메소드가 아닐 경우 모두 final 로 만들어 상속을 금지하는 것이 좋다.

자바와는 달리 코틀린은 위 철학을 따라, 클래스와 메소드가 기본적으로 final 이다.

  • 어떤 상속을 허용하기 위해서는 해당 클래스, 메소드, 프로퍼티 앞에 open 변경자를 붙여야 한다.
  • open 이었던 것을 상속하면 해당 클래스, 메소드는 기본적으로 open이다.
    • 본인이 상속한 것에 대해서 하위 클래스에서 오버라이드 하지 못하게 할 경우, 오버라이드 하는 메소드 앞에 final 을 명시해야 한다.
  • abstract 로 선언한 추상 클래스는 인스턴스화 할 수 없다.
    • 추상 멤버 및 클래스는 오버라이드 해야만하기 때문에 open 이 된다.
    • 추상 멤버는 기본적으로 open 이기 때문에, 추상 멤버 앞에 open 변경자를 명시할 필요 없다.
    • 추상 클래스 하위의 비추상 함수는 기본적으로 final이다.
abstract class abClass { // 추상 클래스의 인스턴스를 만들 수는 없다.
    abstract fun abFun() // 이 추상 함수는 구현이 없기 때문에 하위 클래스에서 반드시 오버라이드 해야한다.

    open fun opFun() { ... } 
    fun normalFun() { ... } 
    // 위 두 함수는 추상 클래스에 속하였어도 비추상 함수이다. 추상 클래스 아래 비추상 함수는 기본적으로 final 이지만, open 으로 오버라이드를 허용할 수 있다.
}
  • 상속 제어 변경자
    • final : 기본 변경자로, 오버라이드 불가
    • open : open 을 명시해야 오버라이드 가능
    • abstract : 추상 클래스의 멤버에만 붙일 수 있으며, 구현이 있으면 안된다. 하위 클래스에서 반드시 오버라이드 해야함
    • override : 상위 클래스나 인스턴스 멤버를 오버라이드 하는 경우는 기본적으로 open 이기 때문에 하위 클래스의 오버라이드를 막기 위해서는 final 을 명시해야한다.

가시성 변경자

가시성 변경자 (visibility modifier)는 코드의 선언에 대해 클래스 외부 접근을 제어하여 클래스에 의존하는 외부 코드를 깨지 않고 클래스 내부 구현을 변경 가능하다.

  • 코틀린의 기본 가시성은 package-private인 자바와 달리 public이다.
  • 코틀린에는 package-private이 없다.
    • internal 가시성을 도입하여 같은 모듈 내에서만 볼 수 있도록 한다.
    • internal 가시성은 컴파일 되면 바이트 코드상에서 public이 되므로, 코틀린에서는 접근 불가한 대상이 자바에서 접근할 수 있는 경우가 생길 수 있다.
  • 코틀린 가시성 변경자
    • public : 기본 가시성으로, 모든 곳에서 볼 수 있다.
    • internal : 같은 모듈 안에서만 볼 수 있다.
    • protected : 하위 클래스 안에서만 볼 수 있다.
    • private : 같은 클래스 안에서만 볼 수 있다.

내부 클래스와 중첩 클래스

  • 중첩 클래스
    • 아무런 변경자가 붙지 않으면 기본이 자바의 static 중첩 클래스와 동일하다.
    • Kotlin : class A
    • JAVA : static class A
  • 내부 클래스
    • 중첩 클래스와 달리 바깥 클래스에 대한 참조를 포함하게 하고 싶다면, inner 변경자를 붙여 내부 클래스로 만든다.
    • Kotlin : inner class A
    • JAVA : class A
    • 내부 클래스에서 바깥 클래스 Outer 의 참조에 접근 시, this@Outer를 라고 사용한다.

봉인된 클래스

클래스 계층을 정의 시, 계층의 확장을 풀어주는 경우 예상치 못한 상속 등이 있을 수 있기 때문에 제한을 둘 수 있다.

  • sealed 클래스
    • 상위 클래스에 sealed 변경자를 붙일 경우 상위 클래스를 상속한 하위 클래스는 반드시 상위 클래스 안에 중첩 되어있어야 한다.
    • 정해놓은 특정 클래스들의 상속만 허용한다.
    • sealed 로 표시된 클래스는 본인 안에 중첩된 하위 클래스의 상속을 허용하기 때문에 자동으로 open 이다.
sealed class Expr { //기반 클래스를 sealed 하여 모든 하위 클래스를 나열한다.
    class Num(val value : Int) : Expr()
    class Sum(val left : Expr, val right : Expr) : Expr()
}

fun eval (e: Expr) : Int = 
    when (e) { // 만일 sealed 가 아니라면 다른 클래스가 상속할 가능성이 있기 때문에 예외처리가 필수적이나, sealed 되어 하위 클래스가 Num, Sum외에 나올 가능성이 없기 때문에, 예외처리가 필요없다.
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }

생성자와 프로퍼티를 갖는 클래스 선언

자바에서는 생성자를 하나 이상 선언할 수 있었다. 코틀린도 마찬가지이나, 주생성자, 부생성자를 구분한다.

  • 주 생성자 (primary constructor): 클래스를 초기화할 때 쓰는 생성자로, 클래스 본문 밖에서 정의한다.
  • 부 생성자 (secondary constructor): 클래스 본문 안에서 정의하는 생성자

주 생성자와 초기화 블록

class People(val name: String)

  • 클래스 이름 뒤에 오는 괄호로 들어오는 코드인(val name: String)를 주 생성자 라고 한다.
    • 주 생성자의 목적
      • 생성자 파라미터를 지정
      • 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의
  • constructor 키워드를 이용해서 주,부 생성자를 정의한다.
  • 주 생성자는 별도의 코드를 포함할 수 없으므로, init 키워드로 시작하는 초기화 블록을 이용한다.
    • 초기화 블록은 클래스의 객체가 만들어질 때(인스턴스화 될 때) 실행될 코드가 들어간다.
  • 생성자 파라미터의 구분
    • 프로퍼티와 생성자 파라미터를 구분하기 위한 방법
    • _name : 밑줄로 구분한다.
    • this.name : 자바처럼 this 를 쓴다.
  • val 키워드를 사용해 생성자 생성 시, 굳이 아래 코드처럼 해주지 않아도 자동으로 생성된다.
    • class People constructor(val _name: String) 으로 생성 시, val _name 의 프로퍼티를 자동 생성 해 준다.
  • 생성자 파라미터에 대한 디폴트 값을 지정할 수 있다.
    • class People constructor(val _name: String = "Doraemon")
    • 모든 생성자 파라미터에 디폴트 값을 지정 시, 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어 준다.
  • new 키워드 없이 생성자를 직접 호출하여 인스턴스를 생성할 수 있다.
    • val ppl = People("Nobita")
  • 부모 클래스 초기화 시, 부모 클래스 이름 뒤 괄호를 치는 것으로 부모 클래스의 생성자를 호출할 수 있다.
  • 클래스 정의 시, 별도 생성자를 지정하지 않을 경우, 컴파일러가 자동으로 디폴트 생성자를 만든다.
  • 자바에서처럼 해당 클래스를 클래스의 외부에서 인스턴스화 하지 못하도록 막기 위해서 모든 생성자를 private 으로 만들어 singleton 인 클래스를 생성할 수 있으나, 해당 방법 외에도 싱글턴 객체 자체를 선언할 수 있다. (아래 기입되어있음)
open class People constructor(_name: String) { // 생성자 파라미터 name 을 보유하는 주 생성자 선언
//만일 (val _name: String)으로 선언했다면, 밑의 프로퍼티 선언을 대신해 준다.
    val name: String //프로퍼티

    init {
        name = _name //초기화 블록
    }
}

class FuturePeople(name: String) : People(name) {...} //부모 클래스의 상속 시, 부모 클래스의 생성자는 People()으로 호출할 수 있으며, 파라미터는 괄호 안에 넘긴다.

부 생성자

자바에서 오버로드한 생성자가 필요한 상황이 여럿 있는데, 이는 코틀린의 디폴트 파라미터 값과 이름 붙인 인자 문법을 이용해 대부분 해결할 수 있다. (부 생성자를 이런 경우에 여럿 만들지 않도록 주의해야 한다.)

그럼에도 프레임워크 클래스의 확장을 위해 여러 방법으로 인스턴스 초기화가 가능하도록 다양한 생성자들을 지원해야 하는 경우 등 생성자가 여럿 필요한 경우에 부 생성자를 사용한다.

  • 부 생성자는 주 생성자를 선언하지 않고, constructor 키워드로 시작한다.
  • super() 키워드를 이용해 자신에 대응하는 상위 클래스의 생성자를 호출할 수 있다.
  • 부 생성자가 필요한 이유
    • 자바 상호 운용성
    • 클래스 인스턴스 생성 시, 파라미터 목록이 디폴트 파라미터 및 이름 붙인 인자 문법으로 해결되지 않는 등, 생성 방법이 여럿 존재해야 하는 경우
  • 객체 생성을 위임한다.
    • 같은 클래스의 다른 생성자에게 생성을 위임할 수 있다.
    • 클래스에 주 생성자가 없는 경우 부 생성자는 반드시 상위 클래스를 초기화하거나, 다른 생성자에게 생성을 위임해야 한다.
      • 객체 생성 위임은 결국에는 상위 클래스 생성자를 호출해야 한다.
class ButtonSample: View {
    constructor(context: Context): this(context, SAMPLE_STYLE){ 
        //ButtonSample 클래스의 다른 생성자에게 객체 생성을 위임한다.
        ...
    }

    constructor(context: Context, attr: AttributeSet): super(context, attr){ 
        //상위 클래스의 context를 받는 생성자를 호출한다.
        ...
    }
 }

인터페이스에 선언된 프로퍼티 구현

코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.

추상 프로퍼티 선언이 들어있는 인터페이스는 이를 구현하는 하위 클래스에서 해당 추상 프로퍼티 값을 얻을 수 있는 방법을 제공해야 한다.

  • 하위 클래스에서 추상 프로퍼티 구현 방법

    • 주 생성자 안에 추상 프로퍼티를 직접 선언

      • override 를 이용하여 추상 프로퍼티를 구현하고 있음을 표시해야 한다.
      • class SampleUser(override val name: String): User
    • 커스텀 getter로 추상 프로퍼티를 설정한다.

      • 필드에 값을 직접 저장하지 않고, getter 를 이용해서 매번 계산하여 값을 설정

        class SampleUser(val email: String): User {
          override val name: String
              get() = email.substringBefore('@') 
              //getter를 이용하여 매번 get 으로 부를 때 마다 
              //이메일 주소에서 name을 계산해 반환할 수 있도록 한다.
        }
    • 초기화 식으로 추상 프로퍼티 값을 초기화한다.

      • 위의 getter를 이용해 매번 값을 계산하는 것과는 달리 객체를 초기화하는 단계에 한번만 세팅.

        class SampleUser(val email: String): User {
          override val name = setNameByEmail(email) //추상 프로퍼티를 초기화한다.
        }
    • 뒷받침하는 필드를 사용

      • 뒤에서 설명 추가
  • 인터페이스의 getter, setter 프로퍼티 선언

    • 인터페이스에 추상 프로퍼티 뿐 아닌, getter, setter 의 프로퍼티를 선언할 수 있으며, 이들은 구현하는 클래스에서 오버라이드하지 않고 상속할 수 있다.

      interface User {
        val email: String
        val name: String
            get() = email.substringBefore('@') //오버라이드 하지 않아도 된다.
      }

getter, setter와 뒷받침하는 필드

프로퍼티는 위에서 언급한 대로 값을 저장하는 프로퍼티와 커스텀 접근자에서 매번 값을 계산하는 프로퍼티가 있는데, 이 두 유형을 조합하여 어떤 값을 저장하되, 그 값을 변경/읽을 때마다 정해진 로직을 실행하는 유형의 프로퍼티를 만드는 방법이 있다.

  • 뒷받침하는 필드
    • 값을 저장하는 동시에 로직을 실행할 수 있도록 하기 위해 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근한다.
    • field라는 식별자를 이용해 뒷받침하는 필드에 접근할 수 있다.
    • getter에서는 field 값만 읽고, setter에서는 field 값만 읽거나 쓸 수 있다.
      • 본 프로퍼티를 대신해 접근한다.
    • 직접 접근하는 것과의 차이
      • 클래스의 프로퍼티를 사용하는 쪽에는 차이가 없다.
      • 컴파일러는 디폴트 접근자 구현, getter, setter 정의 등 모든 경우에서 getter 와 setter에서 field 를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해준다.
        • field를 쓰면 컴파일러가 따로 생성하지 않는다.
class User(val name: String) {
    var address: String = "UNKNOWN"
        set(value: String) {
            println(""" "$name" 's Address changed from 
            "$field" -> "$value".""".trimIndent()) //뒷받침하는 필드인 field 값을 읽어 온다.
            field = value //뒷받침하는 필드 field의 값을 바꾼다.
        }
}

접근자의 가시성

접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같으나, get이나 set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.

...
var counter: Int = 0 
    private set
...

by를 사용한 클래스의 위임

객체 지향 시스템을 설계 시, 시스템을 취약하게 만드는 원인은 상속 시, 하위 클래스가 상위 클래스의 메소드를 오버라이드하면 하위클래스는 상위의 세부 구현 사항에 의존하게 된다.

이후, 상위 클래스의 구현이 바뀌는 등의 변화가 생길 경우 하위 클래스가 상위 클래스에 대해 가지는 가정이 깨져 코드의 정상 작동이 불가할 수 있다.

이를 해결하기 위해 코틀린에서는 final 키워드를 이용해 기본적으로 상속을 허용하지 않고있다.

그러나, 상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때가 있다.
이때 데코레이터(Decorator) 패턴을 사용한다.

  • 데코레이터 패턴
    • 상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스인 데코레이터를 지정한다.
      • ex) collection -> Decorator 클래스 : array, hash 등..
    • 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 하고, 기존 클래스를 데코레이터 내부에 필드로 유지한다.
    • 새로 정의해야 하는 기능은 데코레이터의 메소드에 새로 정의
    • 기존 기능이 필요한 부분은 데코레이터의 메소드가 기존 클래스의 메소드에게 요청을 전달(forwarding)
    • 자바에서는 구현에 준비 코드가 많이 필요하지만, 코틀린의 경우 이러한 위임을 기능으로 제공한다.
  • by 키워드
    • 인터페이스를 구현 시, by 키워드를 이용해 해당 인터페이스에 대한 구현을 다른 객체에서 위임중이라고 명시한다.
    • class SampleSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()): MutableCollection<T> by innerSet {...} //MutableCollection의 구현을 innerSet에게 위임한다.

object 키워드 : 클래스 선언과 인스턴스 생성

객체 지향 설계 시, 인스턴스가 하나만 필요한 클래스가 유용한 경우가 많은데, 자바에서는 클래스의 생성자를 private로 제한하고 정적인 필드에 그 클래스의 유일한 객체를 저장하는 싱글턴 패턴(singleton pattern)을 사용한다.

코틀린은 언어에서 객체 선언 기능으로 싱글턴을 지원한다.

  • 객체 선언
    • object 키워드를 이용해서 클래스의 인스턴스를 만들어 변수에 저장하는 모든 작업을 한번에 끝내준다.
    • 객체 선언은 클래스 선언과 단일 인스턴스의 선언을 합친 선언
    • 생성자는 객체 선언에 쓸 수 없다.
    • 중첩 객체로 클래스 안에서 객체 선언을 할 수 있다.
      • 예를 들어, 특정 클래스의 인스턴스를 비교하는 싱글턴이 필요해서 해당 클래스에서만 쓰인다면 중첩 객체를 사용한다.
    • 코틀린에서의 객체 선언은 유일한 인스턴스에 대한 정적인 필드가 있는 자바 클래스로 컴파일되며, 이때 인스턴스 필드의 이름은 일괄 INSTANCE이다.
      • 자바 코드에서 코틀린의 싱글턴 객체를 사용하려면 정적인 INSTANCE 필드를 통하면 된다.
object NameComparator: Comparator<Person> {
        override fun compare(p1: Person, p2: Person) : Int = p1.name.compareTo(p2.name)
} //객체 선언 기능을 사용

...

data class Person(val name: String) {
    object NameComparator: Comparator<Person> { //클래스 안에서 중첩 객체로 객체 선언
        override fun compare(p1: Person, p2: Person) : Int = p1.name.compareTo(p2.name)
    }
}

동반 객체

코틀린은 자바의 static 키워드를 지원하지 않아 정적인 멤버가 없다. 대신, 최상위 함수와 객체 선언을 이용해 이를 대신할 수 있다.

대부분의 경우 최상위 함수를 활용할 것을 권장하나, 최상위 클래스는 private인 클래스의 비공개 멤버에 접근할 수 없다.

위처럼 클래스의 내부 비공개 정보에 접근해야 하는 함수가 필요한 경우, 클래스에 중첩된 객체 선언의 멤버 함수로 정의한다.

  • 동반 객체

    • 클래스 안에 정의된 객체에 companion이라는 키워드를 붙이면 해당 클래스의 동반객체가 된다.

    • 동반 객체의 프로퍼티나 메소드에 접근 시, 클래스의 이름에서 바로 사용할 수 있다.

    • class Sample { companion object { fun called() { println("CALLED") } } } ... Sample.called() //클래스 이름에서 바로 동반 객체의 메소드를 접근할 수 있다.

    • 동반객체는 바깥 클래스의 private 생성자의 호출이 가능하다.

      • 이러한 특성으로 클래스의 인스턴스를 생성하는 팩토리 메소드를 구현하는데 많이 사용됨.
        • 팩토리 메소드는 클래스의 하위 클래스 객체를 반환할 수도 있다.
      class User private constructor(val name: String) { //private 생성자
        companion object {
            fun newUser(email: String) = User(email.subStringBefore('@')) 
            //외부에서는 private 이기 때문에 생성이 불가하나, 
            //동반객체에서는 생성자 호출이 가능하기 때문에 생성 가능한 팩토리 메소드
        }
      }
    • 동반 객체는 클래스 안에 정의된 일반 객체이다.

      • 이름을 붙일 수 있다.
        • 클래스 이름을 이용해 동반 객체에 속한 멤버를 참조할 수 있으나, 필요 시 이름을 붙일 수 있다.
        • 이름을 지정하지 않을 경우 동반 객체의 이름은 자동으로 Companion이 된다.
      • 인터페이스를 구현할 수 있다.
        • 클래스가 상속한 인터페이스를 동반객체에서 구현할 수 있다.
      • 확장함수 및 프로퍼티를 정의하여 일반 객체처럼 사용할 수 있다.
        • 확장함수를 사용하여 마치 동반 객체 안에서 함수를 확장한 것 처럼 클래스 밖 외부에서 확장함수를 정의할 수 있다.
        • class User(val id: String, val name: String) { companion object { } // 비어있는 동반 객체를 선언한다. } ... //다른 클래스 및 모듈 fun User.Companion.fromJSON(json: String): User { ... } //클래스 밖에서 비어있는 동반객체에 대한 확장함수를 정의할 수 있다. ... val u = User.fromJSON(json) //밖에서 정의된 확장함수는 동반객체 안에서 정의된 것처럼 사용할 수 있다.

객체 식 : 무명 내부 클래스

object 키워드는 싱글턴 객체를 정의할 때도 쓰이지만, 객체 식 즉 무명 객체를 정의할 때에도 쓰인다.

  • 무명 클래스
    • 무명 클래스는 자바에서 흔히 이벤트 리스너로 쓰였던 무명 내부 클래스를 대신한다.
    • 여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스를 구현할 수 있다.
      • 자바에서는 한 인터페이스만 구현하거나 한 클래스만 확장할 수 있었다.
    • final 이 아닌 변수도 객체 식 안에서 사용할 수 있다.
      • 자바와 달리 final 이 아닌 변수를 사용할 수 있을 뿐 아니라 해당 변수의 값을 변경할 수도 있다.
fun countClicks(button: Button) {
    var count = 0 // final 이 아닌 로컬 변수

    button.setOnClickListener(object: View.OnclickListener{
        override fun onClick(v: View?) {
            count++ //로컬 변수의 값을 변경할 수 있다.
        }
    })
}
반응형
Comments