Kotlin companion object vs Kotlin object in class vs Java static
DoDoBest
·2024. 3. 22. 16:21
목차
- object 키워드는 무엇인가
- object class는 무엇인가
- companion object는 무엇인가
- Java에서의 Kotlin Companion object
- Companion object에서 const val는 어떻게 변환될까
- class 내의 object class와 companion object는 어떤 차이가 있는가
- Java static block
- @JvmStatic
- @JvmField
- 자바에서 Kotlin Companion Object와 같은 클래스 만들어보기
1. Kotlin에서의 object 키워드
object 키워드를 이용해서 클래스를 정의하면, 정의와 동시에 인스턴스(객체)를 생성합니다. object 키워드를 사용하는 상황은 아래와 같습니다. 이 중에서 객체 식을 제외한 나머지 2가지에 대해 알아보겠습니다.
- 객체 선언(object declaration)을 통한 싱글턴 정의
- 동반 객체(companion object) 사용
- 객체 식(object expression)
2. object class(객체 선언)
자바에서는 보통 클래스의 생성자를 private으로 제한하고, 정적인 필드에 그 클래스의 유일한 객체를 저장하는 싱글턴 패턴을 통해 구현합니다.
코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원합니다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언입니다.
객체 선언은 object 키워드로 시작합니다. 객체 선언 안에 property, 메소드, 초기화 블록 등이 들어갈 수 있습니다. 하지만 생성자는(주 생성자와 부 생성자 모두) 객체 선언에 쓸 수 없습니다.
object class는 클래스나 인터페이스를 상속할 수 있습니다.
interface Person {
fun eat()
}
object King: Person {
private var name: String
init {
name = Server.getClientName() // 이름을 반환하는 함수가 있다고 가정
}
override fun eat() {
println("밥을 먹었습니다.")
}
fun changeName(newName: String) {
name = newName
}
}
클래스 안에 object class를 선언할 수도 있습니다. 모든 클래스는 동일한 object class에 해당하는 인스턴스를 가지게 됩니다.
data class Person(val name: String) {
object NameComparator: Comparator<Person> {
private var count = 0
override fun compare(o1: Person, o2: Person): Int {
println("이름을 ${++count}번 비교했습니다.")
return o1.name.compareTo(o2.name)
}
}
}
fun main() {
val persons = listOf(Person("John"), Person("James"))
println(persons.sortedWith(Person.NameComparator))
val persons2 = listOf(Person("김민수"), Person("홍길동"))
println(persons2.sortedWith(Person.NameComparator))
}
/* 실행 결과
이름을 1번 비교했습니다.
[Person(name=James), Person(name=John)]
이름을 2번 비교했습니다.
[Person(name=김민수), Person(name=홍길동)]
*/
Object class는 자바 코드로 어떻게 변환될까요?
1. object class는 static final class로 변환됐습니다.
2. 생성자는 private로 선언되었으며, 싱글턴 객체는 object class 내부에 static final 변수인 INSTANCE에 public으로 저장되었습니다.
자동으로 생성되는 함수(toString, equals, hashCode, copy)를 제외하기 위해 data 키워드를 없앴습니다.
public final class Person {
@NotNull
private final String name;
@NotNull
public final String getName() {
return this.name;
}
public Person(@NotNull String name) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
}
@Metadata(...)
public static final class NameComparator implements Comparator {
private static int count;
@NotNull
public static final NameComparator INSTANCE;
public int compare(@NotNull Person o1, @NotNull Person o2) {
Intrinsics.checkNotNullParameter(o1, "o1");
Intrinsics.checkNotNullParameter(o2, "o2");
StringBuilder var10000 = (new StringBuilder()).append("이름을 ");
++count;
String var3 = var10000.append(count).append("번 비교했습니다.").toString();
System.out.println(var3);
return o1.getName().compareTo(o2.getName());
}
public int compare(Object var1, Object var2) {
return this.compare((Person)var1, (Person)var2);
}
private NameComparator() {
}
static {
NameComparator var0 = new NameComparator();
INSTANCE = var0;
}
}
}
3. Companion Object
코틀린 언어는 자바 static 키워드를 지원하지 않습니다. 대신 코틀린에서는 패키지 수준의 최상위 함수와 object class를 활용할 수 있습니다.
최상위 함수는 private으로 표시된 클래스 비공개 멤버에 접근할 수 없습니다.
companion object는 변수나 함수가 인스턴스에 종속되는 것이 아니라 class에 종속되도록 하고 싶을 때 사용합니다.
클래스 안에 정의된 객체 중 하나에 companion 키워드를 붙임으로써 동반 객체(Companion object)를 만들 수 있습니다. companion object는 객체의 이름을 지정하지 않으면 Companion으로 설정되며, 외부 클래스의 이름만을 사용해 접근할 수 있습니다.
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
fun main() {
A.bar() // Companion object called
A.Companion.bar()
}
companion object는 외부 클래스의 private 생성자도 호출할 수 있습니다. 이를 이용해 아래와 같이 팩토리 패턴을 구현할 수 있습니다. 동일한 객체를 반환하는 것을 보여주기 위해 name이라는 파라미터를 입력하도록 설정했습니다.
class User private constructor(private val name: String) {
fun sayWhoIAm() {
println("I'm $name")
}
companion object {
@Volatile
private var instance: User? = null
fun getInstance(name: String): User {
if (instance != null) {
return instance!!
}
return synchronized(this) {
if (instance == null) { // double check
instance = User(name)
}
instance!!
}
}
}
}
fun main() {
val user = User.getInstance("민수")
val user2 = User.getInstance("철수")
user.sayWhoIAm() // I'm 민수
user2.sayWhoIAm() // I'm 민수
}
Companion object는 클래스이기 때문에 인터페이스를 구현하거나 다른 클래스를 상속할 수 있습니다.
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
open class SomeThing() {
open fun funcFromSomeThing() {
println("Im SomeThing")
}
}
class Person() {
companion object : JSONFactory<Person>, SomeThing() {
override fun fromJSON(jsonText: String): Person {
TODO()
}
override fun funcFromSomeThing() {
println("I'm Person")
}
}
}
Companion object에서 외부 클래스에 있는 변수나 메서드에 접근할 수 없습니다.
class A {
val name: String = "A"
fun hello() {
println("Hello")
}
companion object {
private val companionVariable = "Hey!"
fun bar() {
// hello() // Unresolved reference: hello
// println(name) // Unresolved reference: name
println(companionVariable)
}
}
}
4. Java에서의 Kotlin Companion object
Companion object에 이름을 붙이지 않았다면 자바에서 Companion이라는 이름으로 접근할 수 있습니다.
class Foo {
companion object {
fun bar() {
println("Bar!")
}
}
}
Foo.Companion.bar();
이것이 어떻게 가능한 걸까요? Companion object가 디컴파일 된 코드를 살펴봅시다.
Companion object는 Foo 클래스 내부에 static 클래스로 선언되었습니다.
Foo 클래스는 Companion Object 객체를 static 변수 Companion으로 가지고 있습니다.
@Metadata(...)
public final class Foo {
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
@Metadata(...)
public static final class Companion {
public final void bar() {
String var1 = "Bar!";
System.out.println(var1);
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
5. Companion object에서 const val는 어떻게 변환될까
Companion object는 클래스가 로딩될 때 초기화 됩니다. 아래 코드를 실행하면 어떤 결과가 출력될까요?
class Foo {
companion object {
const val NUMBER = 3
const val NAME = "민수"
init {
println("init is called!")
}
}
}
fun main() {
println(Foo.NUMBER)
println(Foo.NAME)
}
3과 민수가 출력되며, companion object의 init 블록은 초기화 되지 않습니다.
컴파일러는 const 키워드가 붙은 값을 호출하는 곳에 대입하기 때문에, 위 코드에서 Foo 객체는 호출되지 않습니다.
자바로 디컴파일된 코드는 아래와 같습니다.
또한 const가 붙은 값은 companion object가 아닌 Foo 클래스의 static 멤버 변수로 선언됩니다.
따라서 단순히 NAME과 NUMBER를 호출하는 것만으로는 Foo 객체에 접근하지 않기에, init 블록은 호출되지 않습니다.
init 블록을 호출하고자 할 경우, 아래와 같이 의미는 없으나, Foo 객체를 생성하거나 Companion object 객체를 호출해야 합니다.
// Kotlin
fun main() {
Foo.Companion
Foo()
}
// Java
public class Main {
public static void main(String[] args) {
Foo.Companion companion = Foo.Companion;
Foo foo = Foo();
}
}
6. object class in class vs companion object
class 내부에 있는 object class와 companion object의 차이는 다음과 같습니다.
- 해당 인스턴스의 static 변수 위치
- init block의 위치(Java에서는 static block으로 변환)
object 클래스의 init block은 object 클래스 내부에 있으며, 인스턴스는 object 클래스의 내부에 static INSTANCE 변수로 있습니다.
companion object 클래스의 init block은 외부 클래스 내부에 있으며, 인스턴스는 외부 클래스의 static Companion 변수로 있습니다.
class ObjectClass {
object Object {
val name = "Hello"
fun say() {
println("Hi!")
}
init {
println("Init is called")
}
}
}
class CompanionObjectClass {
companion object {
val name = "Hello"
fun say() {
println("Hi!")
}
init {
println("Init is called")
}
}
}
자바에서는 다음과 같이 접근할 수 있습니다.
public class Main {
public static void main(String[] args) {
Object objectClass = ObjectClass.Object.INSTANCE;
CompanionObjectClass.Companion companionClass = CompanionObjectClass.Companion;
}
}
7. Java static block
Java static block에 있는 코드는 class가 메모리에 최초로 올라가는 시점에 한 번만 호출됩니다.
8. @JvmStatic
Java에서 Kotlin과 같이 Companion 클래스 이름을 붙이지 않고 Companion 클래스의 변수나 함수에 접근하려면, Kotlin에서 해당 변수나 함수에 @JvmStatic 애노테이션을 붙여줘야 합니다.
// Kotlin
class Foo {
companion object {
@JvmStatic
fun bar() {
println("Bar!")
}
}
}
// Java
public class Main {
public static void main(String[] args) {
Foo.bar();
}
}
@JvmStatic을 붙이지 않았을 때 디컴파일된 코드를 확인해보면 다음과 같습니다. Foo 클래스에는 Companion 변수만 있습니다.
@JvmStatic을 붙이면 Foo 클래스에 bar 함수를 호출할 수 있는 static 함수가 생성됩니다.
변수의 경우 JvmStatic 애노테이션을 붙이지 않으면 다음과 같이 변환됩니다.
변수가 외부 클래스의 private static 변수로 선언되었으며, Companion의 getter, setter non-static 함수를 통해 접근할 수 있습니다.
class Foo {
companion object {
var name = "Bar!"
}
}
@JvmStatic을 붙이면 Companion 뿐만 아니라, Foo 클래스의 getter, setter static 함수를 통해 바로 접근할 수 있도록 변경됩니다.
9. @JvmField
이전 Companion object 예시에서, 변수에 접근하려면 getter, setter 함수를 통해서만 가능했습니다.
@JvmField 애노테이션을 붙이면 getter, setter 함수 없이 변수에 직접 접근할 수 있습니다.
자바로 디컴파일된 코드를 확인해보면 Companion object에 있던 getter, setter 함수가 사라졌으며, 변수의 접근자가 private에서 public으로 변경되었다.
JvmField는 함수에 적용할 수 없습니다.
10. 자바에서 Kotlin Companion Object와 같은 클래스 만들어보기
Kotlin Companion object가 자바로 변환된 것과 같은 클래스를 자바에서 만들어 보면 다음과 같습니다.
import org.jetbrains.annotations.NotNull;
public final class JavaFoo {
public static final Companion Companion = new Companion();
private static String name = "Bar!";
public static final class Companion {
public final void bar() {
System.out.println("Bar!");
}
public final String getName() {
return JavaFoo.name;
}
public final void setName(@NotNull String newName) {
JavaFoo.name = newName;
}
}
}
위 코드에서 bar함수와 name에 JvmStatic을 붙이면 아래와 같이 변경됩니다.
public final class JavaFoo {
public static final Companion Companion = new Companion();
private static String name = "Bar!";
public static void bar() {
Companion.bar();
}
public static String getName() {
return name;
}
public static void setName(String newName) {
name = newName;
}
public static final class Companion {
public final void bar() {
System.out.println("Bar!");
}
public final String getName() {
return JavaFoo.name;
}
public final void setName(String newName) {
JavaFoo.name = newName;
}
}
}
위 코드에서 name에 JvmStatic 대신 JvmField를 붙이면 아래와 같이 변경됩니다.
public final class JavaFoo {
public static final Companion Companion = new Companion();
public static String name = "Bar!";
public static void setName(String newName) {
name = newName;
}
public static final class Companion {
public final void bar() {
System.out.println("Bar!");
}
}
}
참고자료
Kotlin In Action 4.4 object 키워드
'학습' 카테고리의 다른 글
Android 이미지 읽기 권한 다루기 (0) | 2024.04.02 |
---|---|
ViewModelProvider, ViewModelStore (0) | 2024.03.29 |
EditText가 입력된 text를 복원하는 과정 (0) | 2024.03.20 |
자바 Static, Kotlin Companion, 그리고 Annotation의 Function (0) | 2024.03.16 |
for .. in 은 무엇일까 (0) | 2024.03.08 |