ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin] object
    Kotlin/Class and objects 2021. 12. 13. 18:54

    object expression과 object declaration를 이해하기 전에 알고있어야할 개념

    [Java/Class] - [Java] 내부 클래스

    [Java/Class] - [Java] 익명 클래스

    [Kotlin/Class and objects] - [Kotlin] Nested Class

     

    자바 내부 클래스, 익명 클래스와 코틀린의 내부 클래스를 이해해야 object 표현식, 선언식의 깊은 이해가 가능하다.

    object의 내용을 다뤄보기 전에 필요한 내용들을 간단하게 정리해본다 ( 자세한 것은 위의 포스팅을 참고 )

     

    • 자바와 코틀린의 내부 클래스 선언 가능 차이

     

    Java - 내부 클래스의 선언에는 제한이없다.

     

    코틀린- Inner클래스 내부에 Nested클래스 정의하는 것을 가능하지 않게 해놨다. <- 중요

    (참고- 코틀린에서는 inner 키워드를 붙이지 않으면 static 내부, inner 키워드를 붙여 non static 내부 클래스를 선언한다)


    • static 내부 클래스의 인스턴스 생성

     

    중첩 레벨이 얼마든 간에 모든 외부 클래스의 인스턴스와 관계없이 독자적으로 인스턴스를 생성 가능하다. 


    • 자바 익명 클래스의 한계

    1. 인터페이스, 클래스 통틀어 무조건 하나만 상속, 구현 가능

    2. 아무것도 상속, 구현하지 않은 클래스는 생성 불가능

    3. 익명 객체는 무조건 부모 타입 변수들에만 담길 수 있기 때문에 익명 클래스 타입으로 캐스팅이 불가능하다.

    -> 상속한 객체인데 자신의 멤버는 사용이 불가능한 아이러니가 생긴다.

    코틀린의 object expression은 익명 클래스 타입변수를 사용할 수 있다.

     

    object에 대해 알아보자.


     

    1. object expression (object 표현식)

     

    object expression

    자바의 익명 클래스의 한계를 개선한 버전 + 코틀린에서 따로 응용할 수 있는 문법 

     

    object expression으로 만들어진 객체는 이름으로 정의된 것이 아니기 때문에 anonymous objects 라고 부른다.

    코틀린의 object는 자바과 다르게 아무것도 상속받지 않을 수 있는데, 상속받은 것이 없으면 object expression은 Any를 상속받은 익명 클래스에 해당하는 객체가 된다.

    현재 local 변수에서 변수타입을 지정하지 않았는데 이러한 경우 익명 클래스 타입 변수가 된다.

    타입 자동 추론은 밑에서 살펴본다.

    Kotlin의 object expression

    java와 다르게 

    1. 바로위의 이미지처럼 아무것도 상속하지 않는 Any를 상속한 익명 클래스를 만들 수 있다

    2. 클래스 상속(하나)과 여러 인터페이스 구현이 가능하다.

    -> 익명 객체를 담은 변수를 여러 타입으로 캐스팅이 가능하다.

    3. java와 달리 익명 객체가 익명 클래스 타입변수에 담길 수 있다.

    -> 이러한 장점은 익명 클래스에 새로 정의된 변수, 메서드를 모두 자유롭게 사용할 수 있다는 것이다.

     

    open class Parent {
        open fun classMethod() {
            println("부모 메서드")
        }
    }
    
    interface Inter {
        fun interMethod() { //일반 메서드 (추상 x)
            println("인터페이스 메서드")
        }
    }
    
    interface Inter2 {
        fun interMethod2() { //일반 메서드(추상 x)
            println("인터페이스2 메서드")
        }
    }
    
    abstract class Abst{
        abstract fun abstMethod()
    }
    
    fun main() {
    //test변수 타입 익명 클래스
        val test = object : Parent(/*생성자있으면 여기다 넣음*/), Inter, Inter2 {}
        test.classMethod()
        test.interMethod()
        test.interMethod2()
        println()
    
        val test2= object : Abst() {
            override fun abstMethod() {
                println("추상 클래스 메서드 오버라이딩")
            }
        }
        test2.abstMethod()
    }

    일단 local 에서 object표현식이 변수에 대입될 때 변수 타입이 생략되면 익명 클래스 타입 변수가 되기 때문에 

    상속한 것과 새로 정의된 멤버를 모두 자유롭게 사용가능한 것을 볼 수 있다.

     

     

    코틀린에서는 오브젝트를 리턴하는 함수를 사용할 수 있다.

     

    로컬 함수의 리턴 타입이 생략되면 바로전의 로컬 프로퍼티 타입 추론처럼 익명 클래스 타입을 리턴하는 함수가 된다.

    fun main() {
    
        //익명 클래스의 익명 객체를 리턴하는 함수
        //리턴 타입 익명 클래스
        fun getObject() = object {
            val x: String = "x"
        }
        
        val obj = getObject()
        println(obj.x)
    }

    함수를 호출할 때마다 새로운 익명 객체를 생성하여 리턴한다.

    fun main() {
    
        //함수 리턴 타입 익명 클래스
        fun getObject() = object {
            val x: String = "x"
        }
    
        val obj = getObject()
        println("obj.hashCode(): ${obj.hashCode()}")
    
        val obj2 =getObject()
        println("obj2.hashCode(): ${obj2.hashCode()}")
    
    }

     

    어떤 타입으로 되는지 정확하게 알고 있어야 하는 이유를 보자.

    open class Parent {
        
        fun pMethod() {
            println("클래스 메서드")
        }
    }
    
    interface Inter {
        
        fun iMethod() {
            println("인터페이스 메서드")
        }
    }
    
    fun main() {
    
        val test = object : Parent(),Inter{}
        test.pMethod()
        test.iMethod()
    
        val test2: Inter = object : Parent(),Inter{}
        test2.iMethod()
        //test2.pMethod() 
        //에러- Inter것만 사용가능
    
        val test3: Parent = object : Parent(),Inter{}
        //test3.iMethod() 
        //에러- Parent것만 사용가능
        
    }

     

     

     

    타입을 명시하지 않는 경우를 타입 추론을 보자.

    1. 로컬인 경우 (비교적 간단)

    타입이 생략되었으면 무조건 익명 클래스 타입

    interface A
    interface B
    
    fun main() {
    
        //로컬에서는 다음과 같이됨
    
        //1. 리턴 타입 명시 안한 경우 - 익명 클래스
    
        fun getObject() = object {}
        fun getObject2() = object : A {}
        fun getObject3() = object : A, B {}
        val test = object {}
        val test2 = object : A {}
        val test3 = object : B {}
    
        //2. 리턴 타입 명시한 경우 - 해당 타입
    
        fun getObject4(): Any = object {}
        fun getObject5(): Any = object : A {}
        fun getObject6(): A = object : A, B {}
        fun getObject7(): B = object : A, B {}
    
        val test4: Any = object {}
        val test5: Any = object : A {}
        val test6: B = object : B {}
    
    }

    리턴 타입 명시 안한 경우 -> 익명 클래스 타입

    ....생략...


    2. 로컬이 아닌 경우

     

    1. inline이 아닌 private -> 익명 클래스 타입 허용 

    2. 이외의 모든 경우 -> 익명 클래스 타입 허용 x

     

    • inline이 아닌 private

    타입 생략시 모두 익명 클래스 타입


    • 이외의 모든 경우

    ( inline ) ||  (  public,  protected(멤버만, top레벨 사용불가 제한자) ,internal 제한자   )

     

     

    타입이 생략된 경우 타입 추론

     

    object의 supertype 선언이 없는 경우 -> Any

    object의 supertype 선언이 하나인 경우 -> 하나에 해당하는 그 타입

    object의 supertype 선인이 두개이상인 경우 -> 익명 클래스 타입 허용 안하므로 -> 명시적으로 타입 선언 필수

     

     

    inline

     

    또는 private 이외의 제한자

     

    테스트

    object expression의 타입에 대해서 정리해보았는데 아직 코틀린에서 의도한 타입 추론을 깊게 이해하지 못한 것 같다.


    object expression 바깥의 변수 사용가능

     

    https://hellose7.tistory.com/122

     

    [Kotlin] Closure

    람다식, 익명 함수, 로컬 함수, object 표현식(object 선언 x) 는 외부의 컨텍트의 변수에 접근 가능 https://umbum.dev/598 x + y } ``` 코틀린 람다는 자바 람다와 달리 항상 중괄호 사이에 위치한다. 반면 메.

    hellose7.tistory.com

    공식문서의 스닙셋이다. object 표현식 외부의 clickCount, enterCount 프로퍼티에 접근이 가능한 것을 볼 수있다.

    이렇게 외부 변수를 사용할 수 있는 것을 클로져라고 부르는데 해당 내용은 참고한 링크에 있다.

    외부 변수에 접근 가능한 것은 object 표현식에서만 가능하지 바로 밑에서 살펴볼 object 선언식에서는 가능하지 않다.


    2. object declaration

     

    object expression의 경우 새로운 객체를 계속 생성하였지만 object declaration의 경우 싱글톤 객체이다.

    object Test {}
    
    fun main() {
        
        val v = Test
        println("v.hashCode(): ${v.hashCode()}")
        val v2 = Test
        println("v2.hashCode(): ${v2.hashCode()}")
    }


    object expression의 익명 클래스의 익명 객체 생성 문법과 동일하게 싱글톤 객체를 꾸릴 수 있다.

    open class A {
        open fun method1() {}
    }
    
    interface B {
        fun method2()
    }
    
    interface C {
        fun method3()
    }
    
    object Test : A(), B, C {
        override fun method1() {
            super.method1()
        }
    
        override fun method2() {
            TODO("Not yet implemented")
        }
    
        override fun method3() {
            TODO("Not yet implemented")
        }
    }

    object declaration이 자바로 어떻게 구현되어 있는지 확인해보자

    ( 내부 클래스에 대한 지식이 필요하다 )

    [Kotlin/Class and objects] - [Kotlin] Inner Class

    [Java/Class] - [Java] 내부 클래스

    class C() {
    
        object Test {
            fun method() {
                println("나는 싱글톤 객체")
            }
        }
    
    }
    
    fun main() {
        val testObject = C.Test
        testObject.method()
    }

     

     

    object가 자바로 변환된 코드이다.

    • object 선언식은 static final 클래스로 변환된다.

    static 내부 클래스( Nested클래스)로 구현됨

    -> 외부 클래스의 인스턴스의 생성 유무 관계없이, 독자적으로 인스턴스를 생성할 수 있다.

    ( non static 내부 클래스로 구현되었다면 객체를 사용하기 위해 외부 클래스를 먼저 생성해야 하기 때문이다 )

     

    final 클래스로 구현됨

    -> object 선언식은 다른 것들의 super type이 될 수 없다.

     

    • 싱글톤 객체를 생성하는 코드는 static {} (클래스 초기화 블럭)에 들어가 있다

    외부에서 인스턴스 생성을 막고 자기자신에서만 인스턴스를 생성할 수 있게 private 생성자로 만든다.

    공통 목적으로 사용할 단 하나의 인스턴스를 만드는 것은 인스턴스 멤버를 사용못하니 클래스 멤버,메서드등의 부분에 구현이 되었다. -> 클래스가 처음으로 메모리에 로딩될 때 호출되는 static {} 에서 인스턴스가 생성되고 생성된 객체를 클래스 변수인 INSTANCE에 담는다. 

     

    •  클래스 변수 INSTANCE 는 오브젝트 선언식에 해당하는 객체가 담긴 변수이다. 

    이 변수를 호출하여 클래스 메모리 로딩을 트리거하여 인스턴스가 생성되도록 한다. 

    (클래스 메서드, 클래스 변수가 호출되기 위해서 클래스가 로딩되는 점을 이용한 것 같다)

     

    코틀린 C.Test -> 자바로 C.Test.INSTANCE 로 변경됨

     

    생성된 인스턴스가 INSTANCE 변수에 대입되고  INSTANCE변수가 testObject 변수에 대입된다.


    object declaration 은 다음의 경우 사용가능, 불가능하다.

    1. 로컬 선언 불가

    2, 다른 object 선언식 내부에 object 선언식 가능

    3. inner (non-static) 클래스 내부에는 object 선언식 불가

     

    •  로컬 함수 내에 선언될 수 없다. 

    함수 내부에 Local 클래스를 선언한는 것은 자바,코틀린 모두 가능하지만 

    object 선언식을 local에서 정의하면 외부 클래스없이 static 내부 클래스만 정의한 꼴이되기 때문에 불가능하다.

     

    • 다른 object 선언 내부에 선언될 수 있다.

    static 내부 클래스(non-static 내부 클래스도 마찬가지이다)는 모두 클래스 메모리 로딩(싱글톤 객체 생성로직이 있는) 에 관해서는 외부 클래스와 관계없이 독자적으로 일어나므로 문제가 되지 않는다. ( 내부 클래스 포스팅 참고 )

    <-  외부,내부가 클래스 메모리 로딩되는 순서, 같이되냐가 관계 없기 때문

     

    •  inner 클래스 내부에 선언될 수 없다.

    코틀린에서는 자바와 다르게 inner 클래스 내부에 static 내부 클래스를 정의할 수 없게 해놨다.

    그렇기 때문에 object 선언식도 static 내부 클래스이므로 정의될 수 없다.

    위의 내용은 코틀린에서 inner안에 nested를 선언하지 못하게 의도한 것이지 자바의 코드로는 아무 문제가 되지 않는다.

    class Outer4 {
    
    	class Inner {
    
    		public static class ObjectDeclaration {
    
    			public static final Outer4.Inner.ObjectDeclaration INSTANCE;
    
    			private ObjectDeclaration() {
    			}
    
    			static {
    				INSTANCE = new Outer4.Inner.ObjectDeclaration();
    				System.out.println("object 생성됨(클래스 초기화 블럭 코드)");
    			}
    			
    			public void method() {
    				System.out.println("object 메서드");
    			}
    			
    		}
    	}
    
    }
    
    public class KotlinObjectTest2 {
    
    	public static void main(String[] args) {
    		
    		Outer4.Inner.ObjectDeclaration singleInstance= Outer4.Inner.ObjectDeclaration.INSTANCE;
    		singleInstance.method();
    	}
    }


    object 선언식은 실제로 자바의 static 내부 클래스로 구현되는 것을 확인하였다.

    static 내부 클래스는 자기를 둘러싼 모든 외부 클래스들과 관계가 없기 때문에 외부클래스 인스턴스.this 같은 키워드도 사용하지 못하기 때문에 object 표현식에서는 가능했던 외부 변수의 접근은 불가능하다.

     

     

    'Kotlin > Class and objects' 카테고리의 다른 글

    [Kotlin] Nested Class  (0) 2021.12.13
    [Kotlin] object, companion object  (0) 2021.08.16

    댓글

Designed by Tistory.