ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin] 클래스 선언/생성자/ 생성자 상속
    Kotlin/Constructor 2021. 7. 10. 20:34

    클래스 선언

    class 이름 헤더{  //클래스 선언
    	바디
    }

     

    클래스의 헤더나 바디는 모두 optional이 될 수 있다.

    class 이름 헤더 //바디가 없는 경우
    class 이름 //헤더와 바디 모두 없는 경우

    생성자

     

    생성자는 하나의 주생성자(primary constructor)와 여러개의 부생성자(secondary constructor)를 가질 수 있다.

    주생성자, 부생성자는 constructor키워드를 사용한다.

     

    1. 주생성자 (primary constructor)

    클래스헤더 부분에는 주생성자를 파라메터와 함께 명시하는 곳이다. 파라메터도 optional이다. 

    class Person constructor(personName: String, age: Int){
    }

     

    주생성자에 anotation, visibility modifier가 없으면 constructor키워드는 생략이 가능하다.

    class Person (personName: String, age: Int){ //constructor키워드 생략
    }

    있으면 constructor 앞부분에 어노테이션, 제한자 키워드를 사용한다.

    class Person public @Inject constructor(name: String) { /*...*/ }

     

     


     

    코틀린 공식 사이트를 보면 주생성자는 파라메터만 정의하는 부분이다.  property초기화 관련부분은 init{}블록에서 할 수 있다고 나와있다.  정말 불친절하다. 그렇다면 init{}블록은 뭘까?

     

    init{}블록은 기본적으로 주생성자에 전달된 인자를 가지고 property를 초기화 할 수 있는 부분이다.

    클래스 선언부에 초기화 로직을 넣을 수 없으므로 init{}에서 초기화하는 것이다.

    init{}블록에서는 자바의 생성자 처럼 this키워드를 사용할 수 있다.

     

     

    in java

    class Person{
    	
    	private String name;
    	private int age;
    	
    	public Person(String personName, int personAge) {
    		name=personName;
    		age=personAge;
    	}
    }
    
    public class Test {
    	public static void main(String[] args) {
    		 Person person=new Person("hyun",1000);
    
    	}
    }

     

    in kotlin

    class Person(personName: String, personAge: Int) {
        var name: String
        var age: Int
    
        init {
            name = personName //initialization block
            age = personAge
        }
    }
    
    fun main() {
        var person:Person=Person("hyun",1000)
    
    }

    보이는가 constructor(personName:String, personAge:Int) 주생성자 선언부분에는 코드 초기화 부분을 넣을 수 없어 그냥 클래스 바디에 init{}블록에 주생성자 관련한 초기화 로직을 넣는 것이다.

     

    자바의 new Person에서 new 키워드만 빼면 인스턴스 생성이 가능하다.

     

    property초기화 과정을 순서대로 자세히 보자.

     

    인스턴스가 생성이 되면서 Person클래스의 property(자바의 멤버변수들)은 자바와 비슷하게 각 자료형의 기본값으로 초기화가 된다. name은 null, age는 0으로 초기화가 된다. 

     

    명시적 초기화가 진행된다( 여기서 명시적 초기화는 안했으니 진행되지 않는다)

     

    그다음 init블록의 코드가 수행되어 name은 "hyun", age는 1000이 된다. 

     

    인스턴스 초기화 작업이 완료되었다.

     

    위의 코드는 그냥 억지로 자바에 맞춰 짠 코드이고 이제 좀더 코틀린식으로 쓰는 법을 알아보자


    코틀린에서는 자바와는 다르게 멤버 명시적 초기화 부분에서 주생성자의 파라메터를 사용할 수 있다.

     

    in kotlin

    class Person(personName: String, personAge: Int) {
        var name: String = personName //property initializer
        var age: Int = personAge
    	init{
        
        	}
    }
    
    fun main() {
        var person:Person=Person("hyun",1000)
    
    }

    밑의 자바 코드와 비교해보면 뭔 말인지 알 것이다.

    in java 

    class Person{
    	
    	// private String name= personName; //자바에서는 불가능
    	// private int age = personAge;
    	
    	public Person(String personName, int personAge) {
    	}
    }

     

    그렇지만 주생성자의 parameter는 클래스 바디 전체구간에서 사용할 수 있는 것이 아니라 

    명시적 초기화 부분, init{}블록 부분에서만 사용할 수 있다. 

    class Person(personName: String, personAge: Int) {
        var name: String = personName //property initializer
        //name=personNmae 여기는 property나 주생성자 파라메터 인식안됨! 자바에서도 불가능했다.
        var age: Int = personAge
    	init{
        	
        	}
    }
    
    fun main() {
        var person:Person=Person("hyun",1000)
    
    }

     


    다음으로는 자바와는 다르게 코틀린에서는 컴파일 문제가 발생하는 경우를 봐보자

     

    in java

    class Person {
    
    	String name;
    	int age;
    
    	Person() {}
    }
    
    public class Test {
    	public static void main(String[] args) {
    		Person person = new Person();
    	}
    }

    자바에서는 명시적 초기화 또는 심지어 생성자를 없애도 기본값으로 초기화되어 컴파일 문제가 없다

    그러나 코틀린에서는 non-null타입 프로퍼티는 초기화가 필수이다.

    (lateinit 키워드를 통해 인스턴스가 생성된 후에 값을 늦게 초기화 할 수 있지만 lateinit를 사용하지 않으면 초기화를 꼭 해줘야한다)

     

    in kotlin

    class Person() {
        var name: String //컴파일 에러
        var age: Int //컴파일 에러
    
        init {
    
        }
    }
    
    fun main() {
        var person: Person = Person()
    
    }

    컴파일 에러가 발생한다. 변수 선언 옆에 리터럴또는 주생성자 파라메터 변수로 초기화 하거나

    init{}블록에서 초기화를 인위적으로 한번 해줘야 한다.

     

    해결1

    class Person() {
        var name: String ="" //인위적으로 한번 초기화 해줘야함
        var age: Int=0
    
        init {
    
        }
    }
    
    fun main() {
        var person: Person = Person()
    
    }

     

    해결2

    class Person() {
    
        var name: String
        var age: Int
    
        init {
            name=""
            age=0
        }
    }
    
    fun main() {
        var person: Person = Person()
    
    }

     

    해결3, 해결4

    class Person(name: String, age: Int) {
    
        var name: String //=name 해결3
        var age: Int //=age 해결3
    
        init {
            this.name = name //해결4
            this.age = age //해결4
        }
    }
    
    fun main() {
        var person: Person = Person("", 0)
    
    }

     

    lateInit을 사용하지 않은 프로퍼티는 인위적으로 초기화가 없으면 에러가 발생한다는 것을 주의하자. (아마 null에 관대하지 않은 코틀린이라 강제하는 것 같다. property가 참조변수 타입이면 null로 자동 초기화되니까 NPE를 미연에 방지하기 위해)


    여태 init블록에서 대신 초기화를 진행한다고 하였는데

    코틀린에서는 주생성자에 매개변수 대신 property를 바로 지정하여 명시적 초기화하는 노고없이 바로 초기화가 가능하다.

     

    기존의 코드

    class Person(personName: String, personAge: Int) {
        var name: String
        var age: Int
    
        init {
            name = personName 
            age = personAge
        }
    }
    
    fun main(){
        var person: Person= Person("hyun", 1000)
        println(person.name) //hyun
        println(person.age) //1000
        
    }

     

    매개변수를 property로 지정한 경우

    class Person(var name: String, var age: Int) { //파라메터 부분이 property로 변경(var또는 val 추가)
      
    }
    
    fun main(){
        var person: Person= Person("hyun", 1000)
        println(person.name) //hyun
        println(person.age) //1000
        
    }

    파라메터에 매개변수 대신 property를 지정하면 

    파라메터가 넘어갈때 자동으로 property값이 파라메터 값으로 초기화된다. 

    이렇게 자동으로 초기화되는 과정을 통해 바디 내부의 명시적 초기화부분이나 init{}블록에서 초기화를 생략할 수 있는 장점이 있다.

     


    매개변수에 default값을 지정해 인스턴스 생성시 생성자의 인자에 값을 지정하지 않으면 매개변수 값이 default값으로 설정된다. 

     

    파라메터에 default값을 설정한 경우

    class Person(name: String ="kim", age: Int =30) {
        var name=name //명시적 초기화
        var age=age
    }
    
    
    fun main() {
        var person: Person = Person() //name, age둘다 명시x -> 파라메터가 default값으로 설정됨
        var person2: Person = Person(age = 50) //순서가 다를경우 property이름= 값으로
        //age만 값이 설정되고, 나머지 프로퍼티는 default값으로 설정된다.
        
        println(person.name) //kim
        println(person.age) //30
        println(person2.name) //"kim"
        println(person2.age) //50
    }

     

     

    default값을 지정하면서도 매개변수에 property를 정의하면 property값이 default값으로 초기화 된다.

    파라메터에 매개변수 대신 property를 정의하면서 추가적으로 default값을 설정한 경우

    class Person(var name: String ="kim", var age: Int =30) {
    }
    
    fun main() {
        var person: Person = Person()
        println(person.name) //kim
        println(person.age) //30
    }

    property name, age가 각각의 default값으로 초기화됨

    (내용 추가)

    프로퍼티는 인위적으로 초기화를 한번해줘야하는데

    주생성자에 property를 선언하면서 default값을 설정하면 위에서 말한 인위적 초기화 작업이 없어도 컴파일 에러가 발생하지 않았다. 

     

    class Person(var name: String ="kim", var age: Int =30) {
    
    }
    
    fun main() {
        var person: Person = Person("hyun") //name만 명시 -> age는 default값 30
        println(person.name) //hyun
        println(person.age) //30 (default값)
    }

     

    내부적으로 어떻게 이러한 동작되는 가능할것인가는 뒤에서 조금 건드려 볼 것이다. 

     

    (내용 추가)

    init 블록은 여러개가 존재할 수 있다. 

    class Test() {
    
        var i: Int = 1 //명시적 초기화1
        var i2: Int = 2 //명시적 초기화2
    
        init { //init 블록1
            i = 100
            i2 = 200
        }
    
        var i3: Int = 3 //명시적 초기화3
    
        init { //init 블록2
            i3 = 300
        }
    }
    
    fun main() {
        val test: Test = Test()
    }

    자료형 기본값 자동 초기화- 명시적 초기화1,2 - init 블록1 - 명시적 초기화3 - init블록2 의 순서로 진행된다.

    (자동 초기화- 명시적1,2,3 - init 블록1- init블록2의 순서가 아니다)


    부생성자

     

    이제 부생성자에 대하여 알아보자. 

    부생성자는 constructor키워드를 생략할 수 없다.

     

    주생성자의 로직을 직접 넣을 수 없어 init블록을 사용하였지만

    부생성자는 자바의 생성자처럼 똑같이 하면된다.

    어려운 부분은 부생성자의 선언부 부분이다.

     

    부생성자는 주생성자를 상속받으므로서 생성을 주생성자에게 직간접적으로 위임해야 한다. 라고 코틀린 공식 사이트에 써있다. 뭔말일까?? 

    =주생성자를 무조건 최초에 호출해야한다 이다.

    이는 클래스에서 주생성자가 존재하는 경우 부생성자를 호출시키면 주생성자가 무조건 먼저 호출되어야 한다는 말이다.

     

    주생성자에게 생성작업을 직간접적으로 위임시키지 않아 컴파일 에러가 발생한다.

    (부생성자안에서 주생성자가 호출되는 곳이 없다.)

     

     

    부생성자 선언시 : this(_name)를 통해 주생성자를 상속받아 부생성자가 수행되기 전에

    주생성자에게 생성작업을 위임시켜 컴파일 문제가 없어졌다. 

    (자바로 치면 한 생성자에서 조상 생성자가 먼저 실행되도록 가장 첫줄에 super(...)를 호출하는 것이라 보면된다)

     

    부생성자와 주생성자가 둘다 있다. 인스턴스 초기화 과정을 봐보자.

     

    Person(100,"hyun") 로 인스턴스를 생성하면 인자에 맞는 주생성자 또는 부생성자가 호출된다.

    지금은 인자에 맞는 것이 부생성자니 constructor(name:String,age:Int)가 호출된다.

     

    부생성자의 괄호부분의 생성작업이 진행되기 전에 주생성자에게 위임시킨 작업 this(_name:String)생성자 작업이 먼저 진행된다. 이때 부생성자의 _name이 주생성자의 인자로 넘어간다.

    (마치 자바의 부모 클래스부터 초기화 시키는 super(...)와 비슷하다)

     

    ( 기본값 자동 초기화- ( 명시적초기화,  init블록들의 순서상 )이 수행된다)

    인스턴스가 생성되고 proeprty자료형에 해당하는 기본값 name=null, age=0로 초기화가 진행된다.

    명시적 초기화로 name="hyun" , age=0로 초기화가 된다.

     

    //age는 init{}블록전까지 무조건 인위적으로 초기화가 되어있어야 하므로 쓰레기 값 0으로 인위적으로 한번 초기화해줬다.

    (주생성자는 자주쓰일것 같은 생성자이다. 주생성자에서 모든 프로퍼티를 인위적으로 한번 초기화하지 않은 채로

    부생성자를 통해 인스턴스를 생성했다면 초기화되지 않은 프로퍼티가 있을 수 있기 때문에 주생성자작업쪽인 init{}전까지 모든 프로퍼티가 인위적으로 초기화가 되어있어야 한다.)

     

    init{}블록은 없으니 수행안됨

    주생성자에 위임시킨 작업이 끝났다. 

     

    부생성자의 {} 내용으로 돌아와서  this.age= age가 수행되어

     age=100으로 초기화된다.

     

    부생성자까지 포함되어있을 때 초기화 과정을 간단하게 살펴봤다.

    예제의 코드는 name이 주생성자에서 의미있는 값으로 초기화되고 age는 부생성자에서 의미있는 값으로 초기화된다.

     

     

    주생성자 위임에 대해 더 알아보자.

    주생성자가 constructor(_name : String)이다

    부생성자에서 주생성자에게 생성을 위임시킨다고 : this(_name)을 했지만 컴파일 에러가 발생하였다.

     

    주생성자의 _name에 해당하는 String 변수를 부생성자에서 주생성자쪽으로 넘겨주지 않았기 때문이다. 

    모든 부생성자는 주생성자에게 _name초기화를 위임시켜야하는데 부생성자에서는 String형 관련 파라메터를 하나라도 주생성자에게 넘겨주지 않고있다. 부생성자에서 String파라메터 하나라도 선언하지 않았고 주생성자에게 String형인 _name초기화를 어떻게 위임시킬 수 있는가. 

     

    그렇기 때문에 주생성자가 존재한다면 부생성자에서는 주생성자에 포함된 변수 자료형 매개변수들을 모두 선언해야한다.

     

    부생성자에서 String을 선언해서 주생성자에게 넘겨주면 해결할 수 있다.

    class Person(_name: String){
    	var name: Sring= _name
        var age: Int= 0
        
        constructor(_age: Int, _name: String): this(_name){
        	this.age= age
        }
    }

    주생성자의 인자가 여럿 일때

    class Person( test1: String, test2: Int) {
    	//가능  
        constructor( test1: String , test2: Int, test3: Int) : this( test1 , test2) {
        }
    	
        //에러
        //중복된 생성자. 생성자 오버로딩 만족x (주생성자와 부생성자 인자형태가 같다)
        constructor( test1: String , test2: Int) : this (test1, test){
        	
        //에러
        //주생성자에게 위임하기 위해서는 주생성자에 포함된 인자를 모두 넘겨야 한다. 
        //test2: Int 인자를 넘기지 않았다.
        constructor( test1: String ) : this( test1 ){
        }
        
        
    }

     

     

    상속받은 주생성자에게 Int형이라는 값만 넘겨주면 되기 떄문에

    부생성자에서 변수의 이름은 age대신 다른 것을 사용해도 된다.

    age대신 value로 사용했다.


    부생성자에서도 default값을 설정할 수 있다.

    default값을 설정할 수 있지만, 부생성자의 매개변수에는 주생성자처럼 property를 선언할 수 없다.

     

    기본값으로 받은 400은 주생성자로 넘어가 var age:Int=400이 되고 

    기본값으로 받은 "hyun"은 부생성자의 this.name=name으로 된다. 

     


     

    부생성자도 대충 알아봤으니 이제 복잡한 내용으로 들어가보자.

     

    주생성자 부생성자 모두 없을 경우 파라메터가 없는 주생성자를 만들어준다고 했다.

    class Person(){}

    class Person{} 의 차이는 클래스 내에 생성자가 어떻게 존재하는지에 따라 다르다.

     

    일단 밑의 코드를 봐보면 컴파일 에러가 발생한다.

    class Person()은  class Person constructor()로 파라메터가 없는 주생성자가 있는 것이다.

    그렇기 때문에 부생성자를 생성할 때 마다 선언부에 : 주생성자 를 해줘야 한다.

    거기다가 주생성자가 인자가 있으면 인자가 있는 만큼 부생성자에서 선언을 해주고 넘겨줘야한다.

     

    다음은 주생성자가 없는 경우이다. 

    주생성자가 없는 상태이다. 부생성자도 없었으면 클래스에 생성자가 하나도 없어서

    자동으로 Person()생성자가 만들어지겠다. 

    그러나 지금 보면 부생성자가 하나라도 있기 때문에 constructor()주생성자를 자동으로 만들어주지 않는다.

    그렇기 떄문에 주생성자는 없는 것이고 주생성자가 없기 때문에 부생성자를 정의할때마다 : 주생성자 라는 위임작업이 없어진다.  그말은 곧 자바식으로 자유롭게 생성자를 꾸릴 수 있다는 것이다.

     

    그렇기 때문에 이러한 경우에는 this로 위임시키면 에러가 발생한다.

    위에서 설명한 것 처럼 부생성자가 하나 있으니 컴파일러는 파라메터가 없는 주생성자를 만들어주지 않는다

    그렇기 때문에 없는 생성자를 부르게 되는 것이다. 

     

    과연 주생성자가 없는게 맞는걸까 ? 검증을 위해 Adnroid studio에서 해결해주는 자동 문제 해결을 눌러보자.

    Android studio툴에서 자동으로 해결해주는 버튼을 클릭해보면 이렇게 해결해주었다.

    파라메터가 없는 부생성자를 만들어 줬다.

    파라메터가 없는 생성자는 정의되 있지않은 생성자니 만들어준 것이다.

     

    내가 장시간 동안 삽질했던 내용이라 예제를 더 설명한다.

    이전 예제에서는 주생성자가 없고 보조생성자가 하나 있어 파라메터가 없는 주생성자를 만들어 주지 않았다.

    그러나 이번 예제는 주생성자 보조생성자 모두 없어 컴파일하면서 자동으로 빈 파라메터의 주생성자를 추가시켜준다.

     

    그렇기 때문에 class Person() {}  , class Person {} 의 차이를 확인하기 위해서는 보조생성자가 있는지 없는지를 확인해야 한다.


    다음은 아까 못다룬 매개변수에 default값을 설정하는 것이 어떤식인지 찔끔 건드려본다 ㅠㅠ

     

    주생성자, 부생성자 인자의 중복없이 어떻게 생성자 오버로딩 조건이 만족하는 것일까?

    이게 참 신기방기하게 재밌는 구조로 이루어져있다.

    Person인스턴스 생성시 인자를 생략하면 주생성자로 넘어간다.

    저렇게 인자없이 넘기면 인자(String,age)인 주생성자로 넘어가는데 그러면 인자가 없는 부생성자를 만들면 저 default값 설정되는 주생성자는 호출이 어떻게 되나 하고 파라메터가 없는 부생성자를 만들어본다.

     

    음... 주생성자 위임이 빠졌네 하고 : this(...)를 추가하고 주생성자에 값을 넘겨줘야하니 어쩔수없이 

    이런식으로 된다....

    결국 인자가 없는 부생성자는 절대로 못만드는 것이다.

     

    바로 위의 부생성자 설명에서 부생성자는 주생성자가 가지고 있는 인자 개수보다 많게 설정하거나 

    같은 개수로 설정하면 생성자 중복을 피하기 위해 주생성자의 인자 선언 순서와 다른 조합으로 선언되야 한다고 했다.

    바로 이러한 이유로 default값을 설정할 수 있는 것이다.


    상속관계에 있는 클래스라면 자손 클래스에서 생성자 선언을 어떻게 할까?

    일단 class Child :Parent { } 이런식으로 자손 클래스 헤더 옆에 : 조상생성자인것은 알겠는데....

     

    부생성자에서 주생성자에게 위임... 자손에서 조상에게의 위임을 위해 코틀린은 좀 복잡하다고 생각됐다. 그러나 코틀린 사이트에서 보니 나름의 규칙이 존재했다. 또한 생성자의 위임과정을 조금 이해해보면 왜 이런 규칙을 지키도록 했는지가 이해가 될 것이다.

     

    If the derived class has a primary constructor, the base class can (and must) be initialized in that primary constructor according to its parameters.

    자손 클래스에 주생성자가 있으면 조상 클래스는 무조건(can이 아니라 must이다) 자손의 주생성자를 통해 초기화 시켜줘야한다.

    이말은 즉슨 선언부에서 자손의 주생성자를 통해 조상의 존재하는 주, 부 생성자들 하나로 파라메터를 넘겨라 이말이겠다.  조상의 부생성자도 가능한 이유는 조상의 부생성자에 어차피 조상 주생성자 위임과정이 존재하기 때문이다.

     

    -자손 주생성자: 조상 주생성자

    open class Base(p: Int)
    
    class Derived(p: Int) : Base(p)

    위임 처리

    자손 주생성자-> 조상 주생성자

     

    -자손 주생성자: 조상 부생성자

    open class Base(p: Int) { //주생성자
        constructor(p: Int, p2: Int) : this(p) //부생성자
    }
    
    class Derived(p: Int, p2: Int) : Base(p, p2) //자손 주생성자: 조상 부생성자

     위임 처리

    자손 주생성자-> 조상 부생성자-> 조상 주생성자

     

     

     

    자손의 생성자가 하나라도 없기 때문에 빈 생성자인 constructor() 주생성자가 만들어 진다. 그렇기 때문에 주생성자가 있는 경우이고 무조건 선언부에서 조상 주,부생성자 하나를 골라 위임을 시켜야한다.

    주생성자가 없어서 조상생성자에게 위임이 불가능하다.

    이러한 경우는 밑의 규칙대로 진행해야한다.


    If the derived class has no primary constructor, then each secondary constructor has to initialize the base type using the super keyword or it has to delegate to another constructor which does. Note that in this case different secondary constructors can call different constructors of the base type:

    자손 클래스에 주생성자가 없으면

    자손 클래스의 각각의 부생성자 모두에서 super로(조상 클래스의 존재하는 생성자중 하나를 선택해서) 위임시켜야 한다.

     

    class MyView : View {
        constructor(ctx: Context) : super(ctx)
    
        constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
    }

     MyVIew에 주생성자가 없는 경우이다. 부생성자를 통해 MyView 인스턴스를 만든다고하자. 부생성자에서 주생성자로의 위임이 없다. 주생성자에서 조상 생성자의 위임도 불가능하다. 중

     

    주생성자가 없는경우 위임이 중간에 끊기기 때문에 선언된 모든 부생성자에 직접 조상생성자들(주,부생성자 아무거나하나)중 하나에게 위임을 시켜야한다.


    자손의 주생성자가 있는 경우

    클래스 선언부에서 주생성자: 조상주,부생성자

     

    조상(주){

    부1

    부2

    }

     

    자손(주): (주){ //case1

    }

    자손(주): (부1){ //case2

    }

    자손(주): (부2) //case3


    자손의 주생성자가 없는 경우

    자손의 모든 부생성자가 조상의 주,부생성자중 하나를 통해 위임시켜야한다.

     

    조상(주){

    부1

    부2

    }

     

    자손{

     부1 : 주 or 부1 or 부2

     부2 : 주 or 부1 or 부2

    }

    댓글

Designed by Tistory.