개인 광고 영역

2017. 03. 08 생성자 설명을 수정

kotlin의 constructor 정리해보려고 합니다.

우선 일반적인 java에서의 생성자는 아래와 같이 className(매개 변수) 형태로 정의 가능합니다.

생성자를 여러 개 두더라도 override를 이용하여 이를 해결할 수 있습니다.

public class Sample {

	private String name;
	private int age;
	private String birthday;

	public Sample(String name) {
		this.name = name;
	}

	public Sample(String name, int age) {
		this(name);
		this.age = age;
	}

	public Sample(String name, int age, String birthday) {
		this(name, age);
		this.birthday = birthday;
	}
}


Kotlin constructor

코틀린 클래스에서 언급하였던 constructor에 대해서 정리하면 아래와 같습니다.

  • constructor는 생성자의 역할을 할 수 있다
  • 그렇지만 java에서 설명하는 생성자와는 다르다

아래와 같이 java 스타일 그대로를 생성자 정의로 사용해보겠습니다.

class Sample constructor(val name: String) {

	constructor(name: String, age: Int): this(name)

	constructor(name: String, age: Int, birthday: String): this(name, age)
}

java처럼 그대로 읽으면 constructor(val name: String)이 존재하고, 그다음 constructor(name: String, age: Int): this(name)에서는 첫 번째 constructor에 name만을 넘겨주고 자기를 정의합니다.

우선 좀 더 코틀린 스럽게 정리하면 아래와 같이 정의 가능하겠습니다.

default 생성자 정의를 사용하면 코틀린 스럽죠.

class Sample(val name: String, val age: Int = 0, val birthday: String = "") {
	// Do nothing
}

java와의 다른 점을 정리하면

  • default 변수 선언을 통해 3가지 생성자를 한 번에 정의 가능


constructor 정리

kotlin만 사용하는 경우에는 default 정의만으로 모든 것을 해결할 수 있습니다.

하지만 모든 것이 kotlin에서만 사용하는 것도 아니고, java와 혼용도 가능합니다.

하지만 아직 저기에서 정의한 코드가 어떠한 결과를 가지는지 내가 원하는 결과를 가질 수 있는지도 확인해보지 않았습니다.

kotlin constructor가 무엇을 뜻하는지를 파악해야 합니다.

  • constructor(생성자)처럼 사용이 가능, 하지만 java처럼 초기화되는 것은 아님
  • init {} block은 무엇을 하는 녀석일까?

여기에서는 init이 중요해 보이지만 먼저 java를 다시 보겠습니다.

public class Sample {
	private String name;
	private int age;

	public Sample(String name) {
		System.out.println("name " + name);
		this.name = name;
	}

	public Sample(String name, int age) {
		this(name);
		System.out.println("name " + name + ", age " + age);
		this.age = age;
	}
}

위의 코드의 결과를 확인해보죠.

1 번째 new Sample("ABC")을 초기화 하는 경우

name ABC

2번째 new Sample("ABC", 20)을 초기화 하는 경우

name ABC
name ABC, age 20


kotlin 코드를 좀 더 살펴보자

java 스타일로 작성한 kotlin의 생성자를 아래와 같이 정의하고 출력해보겠습니다.

class Sample {

	init {
		println("Sample init")
	}

    constructor(name: String) {
        println("name $name")
    }

	constructor(name: String, age: Int): this(name) {
        println("name $name, age $age")
    }

	constructor(name: String, age: Int, birthday: String): this(name, age) {
        println("name $name, age $age birthday $birthday")
    }
}

위의 코드를 생성자를 통해 실행하고, 결과가 어떻게 나오는지 보죠.

1 번째 Sample(“ABC”) 생성자 초기화 시

// init block
Sample init
// constructor(name: String)
name ABC

2 번째 Sample(“ABC”, 20) 생성자 초기화 시

// init block
Sample init
// constructor(name: String)
name ABC
// constructor(name: String, age: Int)
name ABC, age 20

3 번째 Sample(“ABC”, 20, “2008-01-01”) 생성자 초기화 시

// init block
Sample init
// constructor(name: String)
name ABC
// constructor(name: String, age: Int)
name ABC, age 20
// constructor(name: String, age: Int, birthday: String)
name ABC, age 20, 2008-01-01

음? init??이 생성자와 무슨 연관일까요?


init {}

Class and Inheritance 문서에는 init에 대해서 아래와 같이 설명합니다.

The primary constructor cannot contain any code. Initialization code can be placed in initializer blocks, which are prefixed with the init keyword:

기본 constructor에서는 코드가 포함될 수 없으며, 초기화 코드는 init block으로 시작해야 한다는 정의입니다.

그리고 최상위 constructor에서만 val/var 변수를 가질 수 있습니다.(전역 변수 역할)

하위 constructor에서는 val/var을 정의하면 오류가 발생합니다.

여기서 다시 보는 class 원형은 아래와 같습니다.

class ClassName constructor() {
  init {
		// Do nothing
	}
}


primary/secondary constructor

init을 확인하기 위해서는 아래와 같은 코드가 작성될 수 있습니다.

class Sample constructor() {
  init {
		println("Sample init")
	}

	constructor(name: String): this() {
		println("Sample constructor")
	}
}

위의 class는 primary/secondary를 함께 정의한 경우입니다.

primary와 secondary는 아래와 같은 코드를 말하는데

  • primary constructor : constructor()
  • secondary constructor : constructor(name: String)

primary는 class 정의와 동시에 constructor를 정의하는 경우를 말하고, secondary는 class { 이후에 정의하는 경우에는 모두 secondary constructor입니다.

아래의 코드는 secondary constructor 만을 가지는 경우입니다.

class Sample {
  init {
		println("Sample init")
	}

	constructor(name: String): this() {
		println("Sample constructor")
	}
}

위의 코드를 출력해보면 아래와 같이 확인 가능합니다.

Sample init
Sample constructor


init의 호출 시점은

kotlin 스타일로 코드를 좀 더 이쁘게 작성하기 위해서는 init을 잘 알아두면 도움이 됩니다.

2 번째 Sample(“ABC”, 20, “2008-01-01”) 생성자 초기화 시에 아래와 같이 호출이 되었는데요.

// init block
Sample init
// constructor(name: String)
name ABC
// constructor(name: String, age: Int)
name ABC, age 20
// constructor(name: String, age: Int, birthday: String)
name ABC, age 20, 2008-01-01

이를 코드 순서 그대로 나열해보겠습니다.

  • constructor(name, age, birthday) // 호출
  • this(name, age) // 호출
  • this(name) // 호출
  • init {} // 출력
  • this(name) { // 동작 } // 출력
  • this(name, age) { // 동작 } // 출력
  • constructor(name, age, birthday) { // 동작 } // 출력

위와 같은 순서로 호출이 발생하였습니다.

constructor(name, age, birthday)을 출력하는 경위 위와 같습니다.

init {}은 항상 호출되며 secondary constructor만 정의한 경우에도 init {}이 가장 먼저 호출됩니다.


java 스타일로 멤버 변수 초기화

java처럼 멤버 변수를 초기화해보죠.

var name, age, birthday를 각각 생성하고, 이를 초기화하면 아래와 같습니다.

class Sample {

    var name: String = ""
    var age: Int = 0
    var birthday: String = ""

    constructor(name: String) {
        this.name = name
    }

	constructor(name: String, age: Int): this(name) {
        this.age = age
    }

	constructor(name: String, age: Int, birthday: String): this(name, age) {
        this.birthday = birthday
    }
}

java에서 보았던 코드 스타일을 그대로 가져왔습니다. default는 java에서는 동작하지 않습니다.


constructor에서 val? var? 사용하기

constructor에서는 최상위 생성자에서만 var/val이 사용 가능합니다.

init {}에 정의되어 있던 내용으로도 알 수 있지만 생성자의 역할만 할 뿐 실제 생성자로서의 동작은 하면 안 되는 constructor이기 때문입니다.

그래서 정의할 때 최 상위의 constructor에서만 var/val 정의가 가능합니다.

그렇지 않으면 var 정의하고 this.name = name의 형태 코드가 생기셔야 합니다.

이제 default 값으로 정의했던 코드를 다시 한번 보겠습니다.

class Sample constructor(val name: String, val age: Int = 0, val birthday: String = "")

또는 constructor 생략

class Sample(val name: String, val age: Int = 0, val birthday: String = "")

위와 같이 정의할 수 있습니다.

위와 같이 한다면 java에서는 override 할 수 없겠죠.


kotlin constructor와 init 사용하기

constructor와 init을 모두 합치고, java와 함께 사용하려면 다음과 같은 방법으로 constructor 정의해주는 게 좋습니다.

최상위 생성자에는 val name: String, val age: Int, val birthday: String를 추가합니다.

그리고 나머지 생성자는 변숫값 받아오는 역할만을 하도록 정의합니다.

그리고 init {}에서 필요한 행동을 추가로 해주시면 되겠습니다.

class Sample(val name: String, val age: Int = 0, val birthday: String = "2000-01-01") {

	constructor(name: String, age: Int): this(name, age, "2000-01-01")

	constructor(name: String): this(name, 0)

    init {
        println("name $name age $age birthday $birthday")
    }
}

위와 같이 정의하고, 출력하면 아래와 같은 결과를 가져올 수 있습니다.

name a age 0 birthday 2000-01-01
name b age 10 birthday 2000-01-01
name c age 20 birthday 2017-01-01


마무리

kotlin의 class에는 최상위에 생략 가능한 constructor()이 포함되어 있습니다.

그래서 init {}이 항상 처음 호출되는 것입니다.

그러다 보니 원하는 결괏값을 가지려면 java 스타일로 생성자를 정의해야 하거나, 마지막에 추천하는 방식을 사용하는 게 좋습니다.

결국 java에서 사용하지 않는다면 default만 잘 정의해도 문제는 없죠.


kotlin 관련 글 더 보기


개인 광고 영역

Tae-hwan

Android, Kotlin .. Create a content development.