생성자(Constructor) 개념과 하는일
생성자(Constructor)는 인스턴스가 생성될때 호출 되는 인스턴스 초기화 메서드이다. 주로 인스턴스 변수의 초기화 작업에 사용되며, 인스턴스 생성시에 실행되어야 할 작업을 위해서도 사용된다.
생성자도 메서드이기 때문에, 구조도 메서드랑 거의 비슷하지만 리턴값이 없다. 따라서 void 키워드도 메서드명 앞에 붙지 않는다. 그리고 생성자 이름은 클래스 이름과 동일해야한다.
class Coffee {
// Constructor (생성자)
Coffee() {
}
public static void main(String[] args) {
Coffee coffee = new Coffee();
System.out.println(coffee);
}
}
생성자는 단순히 인스턴스 변수들의 초기화에 사용되는 메서드이다. 실제 인스턴스 생성은 new 연산자에 의해서 동적으로 메모리에 할당된다.
아래의 Coffee클래스의 인스턴스를 생성하는 코드의 수행 과정을 살펴보자.
- 연산자 new에 의해서 힙(heap)영역에 Coffee 클래스의 인스턴스가 생성된다.
- 생성자 Coffee()가 호출되어 수행된다.
- 연산자 new의 결과로 인스턴스 주소가 반환되어, 대입연산자(=)에 의해 참조 변수 coffee에 저장된다.
Coffee coffee = new Coffee(); // Coffee()가 생성자(메서드)를 호출한 것
또한 생성자는 메서드이기 때문에, 오버로딩(Overloading)도 가능하다. 아래의 코드에서는 생성자가 두 개 이기 때문에, 각 생성자에 맞게 인스턴스를 생성과 초기화가 가능하다.
class Coffee {
String beanName;
Coffee() {
}
Coffee(String b) {
beanName = b;
}
public static void main(String[] args) {
Coffee coffee1 = new Coffee();
Coffee coffee2 = new Coffee("Arabica");
System.out.println(coffee2.beanName);
}
}
기본 생성자 (Default Constructor)
사실 모든 클래스에는 반드시 하나의 생성자가 정의되어 있어야한다. 물론 컴파일러가 기본 생성자(default constructor)를 생성 해주어서, 생성자 정의를 안하여도 됬던 것이다. 그러므로 아래의 코드에서 생성자를 new Coffee();에서 생성자인 Coffee()가 호출되어도 수행이 되는 것이다.
class Coffee { }
public static void main(String[] args) {
Coffee coffee = new Coffee();
System.out.println(coffee);
}
컴파일 시, 소스 파일(.java)의 클래스에 생성자가 정의되지 않아도, 컴파일러가 자동으로 아래와 같은 내용의 기본 생성자를 추가하여 컴파일한다. 컴파일러가 자동으로 추가하는 기본 생성자는 매개변수와 내용이 없다. 만일 클래스의 접근 제어자(Access Modifier)가 public인 경우에는, 기본 생성자도 'public 생성자이름() {}'으로 추가된다.
Coffee() { } // ClassName() {}
하지만 클래스의 내에 이미 생성자가 정의되어있다면, 기본 생성자는 추가될 필요가 없다는 뜻이므로 추가 되지 않는다. 아래의 코드에서는 기본 생성자로 초기화하여, 컴파일 에러가 발생한다.
class Coffee {
String beanName;
Coffee(String b) {
beanName = b;
}
public static void main(String[] args) {
Coffee coffee = new Coffee(); // compile error: no arguments
System.out.println(coffee);
}
}
매개변수가 있는 생성자
생성자는 원하는 값으로 초기화 해주기 위해 쓰는 것이다. 그러므로 일반적으로 원하는 값을 매개변수 값으로 넘겨, 인스턴스 변수를 초기화 시켜주는데 사용한다. 물론, 기본 생성자로 인스턴스를 초기화 해준 뒤에, 인스터스 변수를 일일히 초기화 해줄 수도 있다. 하지만 불편할 것이다.
생성자 Coffee()을 호출했을 때는, 추가적으로 일일히 인스턴스 변수의 값들을 초기화해주었다. 생성자 Coffee(String b, int a)을 호출했을 때는 한번에 원하는 값으로 초기화하였다. 훨씬 더 간결하고 초기화라는 의미에도 걸맞는 코드 처럼 보인다.
class Coffee {
String beanName;
int density;
Coffee() { }
Coffee(String b, int a) {
beanName = b;
density = a;
}
public static void main(String[] args) {
Coffee coffee1 = new Coffee();
coffee1.beanName = "Arabica";
coffee1.density = 2;
Coffee coffee2 = new Coffee("Arabica", 2);
}
}
생성자에서 다른 생성자 호출과 this()와 this
생성자간의 호출
같은 클래스 내의 메서드들 간에 서로 호출할 수 있는 것처럼, 생성자 간에도 서로 호출이 가능하다. 생성자에서 다른 생성자를 호출할때, 기존의 클래스 이름 대신 this()로 호출한다. 여기서 this는 클래스 자신이고 this()는 생성자이며, 매개변수 개수와 타입을 통해서 클래스 내의 생성자를 호출한다.
class Rectangle {
int width;
int height;
// 기본 생성자
Rectangle() {
this(1, 1); // 아래의 생성자를 호출하여, 기본 너비와 높이로 초기화
}
// 너비와 높이를 받는 생성자
Rectangle(int w, int h) {
width = w;
height = h;
}
}
위의 코드에서 생성자의 매개변수는 인스턴스 변수의 앞글자만 따서 변수명을 지었다. 인스턴스 변수와 생성자의 매개변수는 같은 변수명을 쓸수 없었기 때문이다. 이러면 생성자 호출시, 매개변수에 어떤 값을 넘겨야 하는지 헷갈린다. 그리하여 같은 변수명을 쓸수있도록 this로 구분하였다.
class Rectangle {
int width;
int height;
// 기본 생성자
Rectangle() {
this(1, 1);
}
// 너비와 높이를 받는 생성자
Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
}
this는 인스턴스를 자신을 가리키는 참조변수이다. this를 선언하지 않았는데 참조변수가 사용할 수 있는 것은, 참조변수 this가 지역변수(local variable)로 숨겨진 채로 존재하기 때문이다. 하지만 클래스 메서드인 static method는 인스턴스를 생성하지 않고도 호출될수 있다. 따라서 클래스 메서드의 호출로 생성된 스택 프레임의 local variable array에는 this 참조 변수 값은 저장하지 않도록 동작한다.
만일, 생성자에서 다른 생성자를 호출할때, 다른 생성자를 호출하는 코드가 생성자의 첫줄이 아니면 컴파일 에러가 발생한다. 다른 생성자를 첫 줄에서만 호출이 가능하도록 만들어진 이유가 있다. 생성자 내에서 초기화 이후 다른 생성자를 호출하게 되면, 그전의 초기화는 무용지물이 되기 때문이다.
// 기본 생성자
Rectangle() {
height = 3;
this(1, 1); // Call to 'this()' must be first statement in constructor body
}
// 너비와 높이를 받는 생성자
Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
생성자에서 다른 생성자 호출의 장점
다음 코드와 같이 생성자를 활용하면, 다양한 초기화 옵션이 가능하게 해준다. 기본 생성자로 초기화 할때는 설정한 초기값으로 세팅할 수 있고, 매개변수를 활용하여 요구에 맞는 초기화 방법을 선택할 수 있다.
class Rectangle {
int width;
int height;
// 기본 생성자
Rectangle() {
this(1, 1); // 기본 너비와 높이로 초기화
}
// 너비와 높이를 받는 생성자
Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
// 정사각형을 만드는 생성자
Rectangle(int sideLength) {
this(sideLength, sideLength); // 너비와 높이를 동일하게 초기화
}
}
클래스의 인스턴스 변수의 추가 혹은 삭제나 변수명이 변경이 된다면, 생성자의 매개변수도 추가 혹은 삭제나 변수명을 변경해줘야한다. 대게 클래스의 인스턴스 생성자 호출이 프로젝트의 여러곳에서 많이 발생하므로, 그러한 생성자 호출 코드를 모두 변경 해줘야한다. 아래의 예시코드를 작성하였다.
다음 Coffee클래스의 인스터스 변수는 beanName만 있다. 여기서 농도(density) 인스턴스 변수를 추가해보겠다.
public class Coffee {
String beanName;
Coffee(String beanName) {
this.beanName = beanName;
}
public static void main(String[] args) {
Coffee coffee = new Coffee("Arabica");
}
}
위의 코드에서 인스턴스 변수 density을 추가한 코드이다. 인스턴스 변수를 추가하면서 Coffee(String beanName, int density) {} 생성자도 추가하였다. 기존의 프로그램내에서 new Coffee("Arabica")으로 인스턴스를 생성하고 초기화한 코드들이 있을것이다. 그 코드들은 density을 초기화 하지 못한다. 초기화를 해줄려면, new Coffee("Arabica", 0)와 같이 수정해야할 것이다. 여간 번거로운게 아니다.
public class Coffee {
String beanName;
int density;
Coffee(String beanName) {
this.beanName = beanName;
}
Coffee(String beanName, int density) {
this.beanName = beanName;
this.density = density;
}
}
하지만 위의 this.beanName = beanName 코드에서 this()을 통해 this(beanName, 0)로 변경한다면, 기존의 new Coffee("Arabica")로 호출한 코드를 변경하지 않고 density를 초기화가 가능하다. 수정이 필요한 경우에도 보다 적은 코드만을 변경하면 유지보수가 쉬워진다. 실제 프로그램은 변경사항이 많으므로 유용하게 쓸수있을 것이다.
class Coffee {
String beanName;
int density;
Coffee(String beanName) {
this(beanName, 0);
}
Coffee(String beanName, int density) {
this.beanName = beanName;
this.density = density;
}
}
물론, 아래의 코드처럼 this()을 통해 다른 생성자를 호출 하는 거 대신에, 인스턴스 변수에 직접 값을 대입해도 된다. 그렇지만 변수의 개수가 많아지면, 코드양이 많아지고 치기 귀찮아진다. 더군다나 가시성도 안좋다.
Coffee(String beanName) {
this(beanName, 0);
}
Coffee(String beanName) {
this.beanName = beanName;
this.density = 0;
}
생성자를 이용한 인스턴스의 복사
개발하다보면 사용중인 인스턴스의 복사가 필요한 경우가 있다. Java API의 많은 클래스에서도 인스턴스의 복사를 위한 생성자를 정의해놓고 있다.
인스턴스 복사란, 현재 인스턴스와 동일한 상태의 인스턴스를 만드는 것이다. 동일한 상태의 인스턴스란, 인스턴스 변수의 값이 같다는 것이다. 인스턴스 변수를 제외한 클래스 변수와 인스턴스 메서드, 클래스 메서드는 모두 동일하기 때문이다. 그러므로 인스턴스 변수 값을 복사하면 된다.
class Coffee {
String beanName;
int density;
Coffee() {
this("Arabica", 0);
}
Coffee(String beanName, int density) {
this.beanName = beanName;
this.density = density;
}
public static void main(String[] args) {
Coffee coffee1 = new Coffee("Arabica", 1);
Coffee coffee2 = new Coffee(coffee1.beaname, coffe1.density);
}
}
위의 예제에는 인스턴스 변수의 개수가 적어서 인스턴스에 참조하여 변수값을 각각 가져오는 것이 불편하지 않다.히자만 인스턴스 변수가 많을 경우에는 생성자를 이용하여 인스턴스의 복사를 하면 훨씬 편하다.
class Coffee {
String beanName;
int density;
Coffee(String beanName, int density) {
this.beanName = beanName;
this.density = density;
}
// 인스턴스의 복사를 위한 생성자
Coffee(Coffee coffee) {
this(coffee.beanName, coffee.density);
}
public static void main(String[] args) {
Coffee coffee1 = new Coffee("Arabica", 1);
Coffee coffee2 = new Coffee(coffee1);
}
}
챰고 자료
- 자바의 정석 (남궁성 지음)
'Java > Java Language' 카테고리의 다른 글
[Java] 상속 (Inheritance) (0) | 2024.06.18 |
---|---|
[Java] 변수의 초기화 (0) | 2024.06.13 |
[Java] 오버로딩 (Overloading) (0) | 2024.06.13 |
[Java] 메서드 (Method) (0) | 2024.06.12 |
[Java] 메서드의 종류와 호출 과정 및 디스패치 (0) | 2024.06.12 |