티스토리 뷰

* 본 게시물은 HeadFirst 교재를 공부 목적으로 정리한 글 입니다.

9. 생성자와 가비지 컬렉션

<객체의 삶과 죽음>

객체가 어떤 식으로 만들어지는지, 객체가 살아있는 동안 어떻게 살아가는지 그리고 객체를 어떻게 효율적으로 관리하고 버리는지 알아 볼 것입니다.

> 힙, 스택, 영역, 생성자, 상위 생성자, 널 레퍼런스 같은 것..

스택과 힙

우리는 자바를 쓸 때 두 가지 메모리 공간을 다룬다. 하나는 객체가 사는 곳(),다른 하나는 메소드 호출과 지역 변수가 살아가는 곳(스택). JVM이 시작되면 JVM이 돌아가고 있는 운영체제로부터 메모리를 받아서 그 메모리에서 자바 프로그램을 실행 시킴.

인스턴스 변수(instance variable)와 지역 변수(local variable)은 어디 살까?

*인스턴스 변수 : 클래스 내,메소드 밖에서 선언된 변수.그 변수가 속한 객체 안에서 산다.

public class Duck{
   int size;    //인스턴스 변수
}


*지역변수 : 메소드 안에서 선언한 것. 메소드 매개변수도 지역 변수에 포함. 메소드가 스택에 들어있는 동안만 살아 있다. (해당 메소드 스택 프레임 안에 들어있음)

public void foo(int x){
   int i = x + 3;
   boolean b = true;
}  //매개변수 x와 변수 i,b는 모두 ‘지역변수’


메소드는 스택에 차곡차곡 쌓임.

메소드를 호출하면 그 메소드는 호출 스택 맨 위에 올라감. 실제로 스택에 들어가는 것은 스택 프레임(frame)인테, 거기에는 실행하는 코드, 모든 지역 변수의 값을 포함한 메소드의 상태가 들어있음. 스택 맨 위에 있는 메소드는 항상 그 스택에서 현재 실행 중인 메소드다. 메소드가 끝날때 까지 스택에 머무름. 메소드가 실행되고 나면 스택에서 그 스택 프레임이 제거 됨.

스택에 들어있는 객체 레퍼런스

지역 변수로 들어있는 객체는?

지역 변수가 객체에 대한 레퍼런스인 경우에는 변수(레퍼런스, 즉 리모컨)만 스택에 들어감.

객체 자체는 여전히 힙 안에 있음.

public class StackRef{
   public void foof(){
       barf();
   }
   public void barf(){
       Duck d = new Duck(24);
   }
}


지역 변수가 스택에서 산다면 인스턴스 변수는 어디서 사는가?

new CellPhone() 같은 명령을 내리면 자바에서는 힙에 그 CellPhone 객체를 위한 공간을 만들어야 함. 그 객체의 모든 인스턴스 변수를 저장하는 데 충분한 공간을 확보함. 즉, 인스턴스 변수는 힙에 그 변수가 속하는 객체 안에서 산다.


객체의 인스턴스 변수 값은 그 객체 안에서 사는데, 그 인스턴스 변수가 모두 원시 변수라면 자바에서는 그 원시 유형을 바탕으로 공간을 만듬. ex) int > 32비트, long > 64비트

하지만 인스턴스 변수가 객체라면? CellPhone에 Antenna 유형의 레퍼런스 변수가 있다면?

레퍼런스 변수의 값은 객체 전체가 아닌 그 객체에 대한 리모콘. 그러므로 Antenna의 리모컨(레퍼런스 변수)이 들어갈 만한 공간만 확보하면 됨.


그러면 그 Antenna 객체는 언제 힙에 자리를 잡을까?

private Antenna ant;


새로운 Antenna 객체를 대입하기 전까지는 힙에 실제 Antenna객체가 만들어지지 않음.

private Antenna ant = new Antenna();


힙에는 CellPhone객체 안에 Antenna레퍼런스 변수가 들어있고, Antenna객체는 따로 생성.

객체생성

Duck myDuck = new Duck();

> Duck()이라는 이름을 가진 메소드를 호출 하는 것 ? X

Duck 생성자를 호출하는 것. new 라는 키워드를 사용했을 때 실행 할 코드 들어있음.

즉, 어떤 클래스 유형의 인스턴스를 만들 때 실행 할 코드가 들어있음.

생성자를 호출할 때는 반드시 new라는 키워드를 쓰고 그 뒤에 클래스명을 적어줘야 함. JVM에서는 클래스를 찾아서 그 클래스에 들어있는 생성자를 호출 함.

컴파일러에서 만드는 기본 생성자

public Duck(){

}
//메소드와 다르게 return유형이 없음.

새로운 Duck을 만드는 방법

생성자의 가장 중요한 특징은 객체가 레퍼런스에 대입되기전에 실행 됨.

new 가운데 끼어들 수 있는 기회를 제공!

public class Duck{
   public Duck(){
       System.out.println("Quack");
   }
}

public class UseADuck{
   public static void main(String[] args){
       Duck d = new Duck();
   }
}
>> Quack

객체 상태 초기화

생성자를 이용하여 중요한 Duck의 상태를 초기화 하는 방법

객체의 상태를 초기화 하는 작업은 대부분 생성자에서 처리함. 생성자는 상속되지 않음.

초기화 코드를 집어넣기에 가장 좋은 장소는 생성자.

public class Duck{
   int size;
   public Duck(int duckSize){ //int 매개변수를 추가
        System.out.println("Quack");
        size = duckSize;
        System.out.println("size is " + size);
   }
}

public class UseADuck{
   public static void main (String[] args){
       Duck d = new Duck(42);
   }
}
>> size is 42


*생성자 오버로딩

public class Duck2{
   int size;
   public Duck2(){
       //기본값을 지정
      size = 27;
   }
   public Duck2(int duckSize){
       //duckSize 매개변수를 사용
       size = duckSize;
   }
}


크기를 알고 있는 상태에서 Duck을 만들 때

> Duck2 d = new Duck2(15);

크기를 모르는 상태에서 Duck을 만들 때

> Duck2 d2 = new Duck2();

>> 클래스에 두 개 이상의 생성자가 있다는 것은 오버로드된 생성자가 있다는 것.

생성자 오버로딩과 기본 생성자

인자가 없는 생성자는 컴파일러에서 ‘항상’ 자동으로 만들어주지 않나요?

> 컴파일러에서는 생성자가 전혀 없는 경우에만 생성자를 자동으로 만들어 줌.

인자를 받아들이는 생성자를 만들었는데, 기본 생성자도 만들고 싶다면 직접 만들어야 함.

클래스에 생성자가 두 개 이상 있으면 각 생성자의 인자 목록은 반드시 서로 달라야 함.


‘생성자 오버로딩’을 이용하면 한 클래스에 두 개 이상의 생성자를 만들 수 있음.

이 때 각 생성자의 인자 목록이 서로 다르지 않으면 컴파일 되지 않음.

매개변수 명을 다르게 해도 안됨. 중요한 것은 변수의 유형(int,Dog등)과 순서!

순서가 다르다면 똑같은 유형의 인자들을 가지는 생성자를 만들 수 있음.

Public class Mushroom{
   public Mushroom(int size){ } //매개변수가 int인 생성자
   public Mushroom(){ }  //기본 생성자
   public Mushroom(boolean isMagic){ }  //매개변수가 boolean
   public Mushroom(boolean isMagic, int size){ } //아래랑 같은데 순서다름
   public Mushroom(int size, boolean isMagic){ }
}

생성자 오버로딩

1. 생성자는 누군가가 어떤 클래스 유형에 대해 new를 쓸 때 실행되는 코드.

> Duck d = new Duck();


2. 생성자명은 반드시 클래스명과 같아야 하면 리턴 유형은 없음.

> public Duck(int size){ }


3. 클래스를 만들 때 생성자를 만들지 않으면 컴파일러에서 자동으로 추가. 기본 생성자는 언제나 인자가 없는 생성자

public Duck(){ }


4. 인자 목록만 다르면 한 클래스에 생성자 여러 개를 만들 수 있음. 오버로드된 생성자.

public Duck(){ }
public Duck(int size){ }
public Duck(String name){ }
public Duck(String name, int size){ }

객체의 상위클래스 부분을 위한 공간

모든 객체에는 그 객체에서 선언한 인스턴스 변수뿐만 아니라 상위클래스에서 받아온 것도 모두 들어있다.(최소한 Object 클래스는 들어있음)


따라서 어떤 객체가 만들어지면 그 객체에는 상속 트리 전체에 걸쳐 축적된 그 객체에 들어있는 모든 인스턴스 변수에 대한 공간이 부여 됨.

새로 만들어지는 객체에 각 상위클래스를 나타내는 알맹이 같은 층이 들어있는걸로 이해.

힙에 객체가 하나밖에 없음. Snowboard객체. 하지만 그 객체 자체의 Snowboard부분Object부분이 모두 들어있음. 두 클래스에 들어있는 모든 인스턴스 변수가 그 안에 들어감.


<객체의 일생에서 상위클래스 생성자의 역할>

새로운 객체를 만들 때 객체의 상속 트리에 들어있는 모든 생성자가 실행되어야 함.

추상 클래스에도 생성자는 있음. 추상 클래스에 대해 new키워드를 사용할 수는 없지만 추상 클래스도 상위클래스기 때문에 구상 클래스의 인스턴스를 만들면 그 생성자가 실행 됨.

상위클래스의 생성자가 실행되면 그 객체의 상위클래스 부분이 구축 됨. 객체가 제 모양을 갖추려면 그 객체의 상위클래스 부분도 제 모양을 갖춰야 하는데, 바로 그런 이유로 인해 상위클래스의 생성자도 실행되어야 함. 상속 트리에 들어있는 모든 클래스의 인스턴스 변수가 선언되고 초기화되어야 함.


새로 만들어지는 Hippo 객체는 Animal객체기도 하고 Object 객체기도 함. Hippo를 만들고 싶다면 그 안에 Animal과 Object도 만들어야 함. 이런 모든 과정은 생성자 연쇄(constructor chaining)라는 과정을 통해 이뤄짐.

객체 생성

1. 다른 클래스에 있는 코드에서 new Hippo()를 호출하면 Hippo() 생성자가 스택 맨 위의 스택 프레임에 들어감.

2. Hippo()에서 상위클래스 생성자를 호출하면 Animal()생성자가 스택 맨 위에 올라감.

3. Animal에서 상위클래스 생성자를 호출하면 Object가 Animal의 상위클래스기 때문에 Object()생성자가 스택 맨 위로 올라감.

4. Object()가 종료되면 그 스택 프레임이 스택에서 제거 됨. 그러면 다시 Animal() 생성자로 돌아가서 Animal에서 그 상위클래스 생성자를 호출 한 바로 아래줄에서 실행이 계속 됨.


<상위클래스 생성자는 어떻게 호출할까?>

유일한 방법은 super()를 호출하는 것.

public class Duck extends Animal{
   int size;
   public Duck(int newSize){
       super();
       size = newSize;
   }
}


생성자에서 super()를 호출하면 상위클래스 생성자가 스택 맨 위에 올라감.

이런 식으로 반복하다 보면 처음에 호출한 생성자가 스택 맨 위에 남게 되고 호출이 종료 됨.


우리가 직접 super()를 호출하지 않으면 컴파일러가 알아서 처리해줌.

객체 라이프사이클

객체의 상위클래스 부분은 하위클래스 부분이 구축되기 전에 완전히 제 모습을 갖춰야 함.

하위 클래스 객체는 상위클래스로부터 상속 받은 것을 필요로 할 수 있기 때문에 상속받을 대상이 미리 만들어져야 하는 것이 당연. 하위 클래스 생성자가 종료되기 전에 상위클래스 생성자가 반드시 종료되어야 함.

super()를 호출하는 선언문은 모든 생성자의 첫번째 선언문이어야 함.

public Boop(){
   //super();을 써줘도 됨.
}
public Boop(int i){
   //super();을 써줘도 됨.
   size = i;
}
public Boop(int i){
   size =i;
   super():   //super()를 직접호출할 때 그 앞에 다른 선언문이 있으면 안 됨!
}


<인자가 있는 상위클래스 생성자>

super()를 호출할 때 뭔가를 전달할 수 있을까? 가능함. 그게 불가능하다면 ‘인자가 있는 생성자’가 있는 클래스를 확장하는 것이 아예 불가능.

public abstract class Animal{
   private String name;    //모든 동물에 이름이 있음
   
   public String getName(){
       return name;
   }

   public Animal(String theName){
       name = theName;
   }
}


public class Hippo extends Animal{
   public Hippo(String name)}    //Hippo생성자에서도 이름을 받아들임
       super(name);
   }
}


public class MakeHippo{
   public static void main(String[] args){
       Hippo h = new Hippo("Buffy");
       System.out.println(h.getName());
   }
}
>>Buffy

오버로드된 생성자 호출 방법

모든 생성자에는 super() 또는 this()를 호출하는 선언문이 들어갈 수 있지만 둘 다 쓸 수는 없음. 같은 클래스에 있는 다른 생성자를 호출할 때는 this()를 사용하면 됨. this()는 생성자에서만 호출할 수 있으며 반드시 그 생성자의 첫번째 선언문이어야만 함. 생성자에서는 super()나 this()를 호출할 수 있는데 둘을 동시에 쓸 수는 없음.

생성자에 중복된 코드가 들어있으면 관리하기가 까다로움 그래서 생성자 코드의 대부분을 오버로드된 생성자 하나에 몰아놓는 것이 좋음.

밑에 코드에선 Mini(Color c){} 생성자에 코드를 몰아 넣어서 기본생성자 안에 this()를 사용함. 매개변수가 Color  type 생성자를 호출하고, 파라미터로 기본값 red를 전달.

코드의 중복을 피할 수 있어서 좋음.

class Mini extends Car{
   Color color;
   public Mini(){
       this(Color.Red);   //this를 사용하여 c를 매개변수로 받는 생성자 호출
   }
   public Mini(Color c){  //실제로 처리하는 생성자
       super("Mini");
       color = c;
   }
   public Mini(int size){
       this(Color.Red); //this와 super을 같이 쓰면 안됨.
       super(size);
   }
}

객체의 일생

1. 지역 변수

   > 그 변수를 선언한 메소드 안에서만 살 수 있음.

2. 인스턴스 변수

   > 객체가 살아있는 동안 계속 살 수 있음.


객체의 레퍼런스를 제거하는 세 가지 방법

  1. 레퍼런스가 영원히 영역을 벗어남. ex)메소드 내에서 선언되면 메소드 종료 시

  2. 레퍼런스에 다른 객체 대입 (기존 객체를 더 이상 참조하지 않음)

  3. 레퍼런스를 직접 null로 선언


댓글