본문 바로가기

Kotlin

[Kotlin] 3. Object

# Object

3가지 쓰임새에 대해 알아보자

  1. singleton
  2. companion object
  3. anonymous object(무명객체)

# 1) Object 키워드 -  단일 인스턴스 - singleton

  • object 키워드로 obect declaration(객체 선언, object 선언)을 통해 싱글턴을 간단히 사용할 수 있다
  • 클래스 선언 + 클래스 인스턴스 생성(단일) 을 한번에 해결해 준다
  • 클래스 안에 객체 선언도 가능 -> 그래도 단일 인스턴스
  • 인터페이스,클래스 상속도 가능
  • thread-safe

+ 번외 팁 : 인터페이스 구현이나, 클래스 상속 모두 ':'으로 표현하기에 구분이 헷갈릴 수 있지만..! 명칭 뒤에 '()' 가 붙으면 클래스고 없으면 인터페이스다.  아래 예제에서 Comparotor<T>는 인터페이스!

 

'object' 객체선언을 통한 Singleton 예제

data class Student(val name: String, val age: Int) {

    //**************object declaration (객체 선언, singleton) **************
    object AgeComparator : Comparator<Student> {
        override fun compare(student1: Student?, student2: Student?): Int {
            return student1!!.age.compareTo(student2!!.age)
        }
    }
}

fun main() {
    val studentTina = Student("Tina", 17)
    val studentJully = Student("July", 40)
    val studentScott = Student("Scott", 31)
    var list = listOf(studentTina, studentJully, studentScott)

    println("[Before sort]$list") // Tina, July, Scott 순

    list = list.sortedWith(Student.AgeComparator) //singleton 객체 넘김

    println("[Before sort]$list") // Tina, Scott, July  순 (오름차순)
}

JAVA로 컴파일시

   public final class Student{
         
         ...
            
   
   public static final class AgeComparator implements Comparator {
      @NotNull
      public static final Student.AgeComparator INSTANCE;

      public int compare(@Nullable Student student1, @Nullable Student student2) {
         Intrinsics.checkNotNull(student1);
         int var10000 = student1.getAge();
         Intrinsics.checkNotNull(student2);
         return Intrinsics.compare(var10000, student2.getAge());
      }

      // $FF: synthetic method
      // $FF: bridge method
  
      public int compare(Object var1, Object var2) {
         return this.compare((Student)var1, (Student)var2);
      }

      private AgeComparator() {
      }

      static {
         Student.AgeComparator var0 = new Student.AgeComparator();
         INSTANCE = var0;
      }
   }
}

위 JAVA로 컴파일된 부분을 확인하면 싱글턴 형태로 인스턴스 명이 'INSTANCE'로 고정 돼 있는걸 확인 할 수 있다 

따라서 Java에서 Kotlin 객체 선언된 싱글턴 사용할 땐

Student.AgeComparator.INSTANCE.compare(student1, student2);

 

 

# 2) Object 키워드 -  Companion Object

  • 어떤 클래스 내부 객체에 'companion' 변경자를 붙이면 Companion Object가 된다
  • Java에서 static 메소드/필드 사용하는 것처럼 쓰면서, 클래스의 인스턴스와 무관하게, 클래스 내부 멤버 접근 필요할 경우 Companion Object를 사용한다  -> 즉 Java에서의 정적 메소드와 정적 필드를 대신한다 

 

  • static 처럼 사용한다면 그럼 그냥 최상위 함수를 사용하면 안되는가?
  • -> Companion Object는 자신을 둘러싼 클래스의 private 멤버를 사용 할 수 있다 (최상위 함수는 클래스의 private 멤버에 접근 불가)
  • -> 정적 메소드/필드와 같은 동작이 필요할 때, 최상위 함수를 우선으로 사용 하되, 커버가 안될 때 companion object를 사용하도록 한다

 

  • 이름을 따로 지정 안할 시, Companion Object 이름은 'Companion' 이다 (이름 생략 가능)
  • 사용하려면 {바깥클래스}.{Companion Object 이름}.{필드 or 메소드}  
  • Companion Object는 최상위 선언은 불가 (클래스와 동반자니까!)
  • 1 Companion Object per 1 Class (클래스와 동반자니까!)
  • 인터페이스,클래스 상속 가능
  • 확장 함수도 사용 가능
  • 자신을 둘러싼 클래스의 private 생성자를 사용 할 수 있으므로-> Factory 메소드 에 유용

Companion Object

class Student(val name: String, val age: Int) {
    companion object {
        fun getSchool() = "ML School"
    }
}

fun main() {
    println(Student.getSchool()) //static 메소드 처럼 사용한다
    println(Student.Companion.getSchool()) //static 메소드 처럼 사용한다
}

 

Companion Object 예시2

해당 클래스에서 다루는 TAG나, Request Code에도 많이 쓴다

기존 Java에서 'public static final String' 으로 사용 되던...

 class TestClass{
    companion object {
        private const val TAG = "TestClass"
        private const val REQUEST_CAMERA = 100
    }
        ...
}

 

Companion Object 예시3

싱글턴은 생성자에 파람값을 못넣는게 일반적이지만 꼭 필요한 경우 (아래 예시엔 context) 

아래와 같이 companion object를 이용해 싱글턴을 이용할 수 있다

Companion Object가 팩토리 패턴 구현에 적합하다는 의미와 일맥상통 한다 (private 생성자 접근가능)

class SingleTonWithContext private constructor(){
    companion object {
        @Volatile
        private var INSTANCE: SingleTonWithContext? = null

        fun getInstance(context: Context): SingleTonWithContext =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: SingleTonWithContext().also {
                    INSTANCE = it
                    INSTANCE?.setContext(context)
                }
            }
    }

    private fun setContext(context: Context) {
        //context set
    }
}

 

확장함수 사용 예제

class Student(val name: String, val age: Int) {
   companion object{}  //확장함수 사용하려면 비어있더라도 일단 선언은 해줘야 함
}

//Companion Object 확장함수
fun Student.Companion.getSchool() ="ML Shool"

fun main() {
    println(Student.getSchool())
    println(Student.Companion.getSchool())
}

 

*'Companion Object는 자신을 둘러싼 클래스의 private 멤버를 사용할 수 있다' 고 책엔 나와있지만

실제로 테스트 해보면 private 생성자만 접근 되고, Companion Object 바깥 클래스, 그러니까 둘러싼 클래스의 private 함수나 필드에 접근이 안되는데, 번역책이라 의미가 잘못 전달 된 걸까...?

 

 

Companion Object 내부 필드 선언

class TestClass {
    companion object {
        private val privateVal = "privateVal"
        val commonVal = "commonVal"
    }
    
//    fun testFun(){
//        println(privateVal) //접근 가능
//        println(commonVal) //접근 가능
//    }
}

private으로 val 설정하거나

public val 설정하거나 

Java로 컴파일 시, 모두 'private static final' 로 변경되는 걸 볼 수 있다

( 즉 Java에서의 정적 메소드와 정적 필드를 대신한다 )

 

Companion Object 내부 필드 선언 Java 변경 시

public final class TestClass {
   private static final String privateVal = "privateVal";
   @NotNull
   private static final String commonVal = "commonVal";
   @NotNull
   public static final TestClass.Companion Companion = new TestClass.Companion((DefaultConstructorMarker)null);
                       
                       ...
                       
   public static final class Companion {
      @NotNull
      public final String getCommonVal() {
         return TestClass.commonVal;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

 

 

# 3) Object 키워드 -  anonymouse object

  • 무명 객체 정의 할 때 object 키워드를 사용 할 수 있다
  • 클래스 정의 + 새 인스턴스 생성(!new!) 
  • '무명' 이므로 클래스 이름을 정하지 않음

anonymouse object 예시

fun anonymousTest(context: Context) {
    val button: Button = Button(context)
    var isClicked: Boolean //무명객체 내부에서 접근 가능

    //object 무명 객체 사용 (1)
    button.setOnClickListener(
        object : View.OnClickListener {
            override fun onClick(v: View?) {
                println("onClick!!!");
                isClicked = true
            }
        }
    )

    //object 무명 객체 사용 (2) : 변수에 대입 가능
    val listner = object : View.OnClickListener {
        override fun onClick(v: View?) {
            println("onClick!!!");
            isClicked = true
        }
    }
    button.setOnClickListener(listner)

    //abstract method가 1개인 경우, 무명 객체 보다 
    //람다식 사용이 유용하다
    button.setOnClickListener {
        println("onClick!!!")
        isClicked = true
    }
}

 

 

JAVA로 컴파일시

new OnClickListener() {
         public void onClick(@Nullable View v) {
            String var2 = "onClick!!!";
            boolean var3 = false;
            System.out.println(var2);
         }
      };

 

 

 

출처 : Kotlin In Action - 에이콘 출판사

(위 도서를 학습하고 개인 학습용으로 정리한 내용입니다)