클래스에는 객체가 가져야 할 구성 멤버가 선언된다. 구성 멤버에는 필드(Field), 생성자(Constructor), 메소드(Method)가 있다. 이 구성 멤버들은 생략되거나 복수 개가 작성될 수 있다.
필드
필드(Field)는 객체의 고유 데이터, 객체가 가져야 할 부품, 객체의 현재 상태 데이터를 저장하는 곳이다. 자동차 객체를 예로 들어 보면 제작회사, 모델, 색깔, 최고 속도는 고유 데이터에 해당하고, 현재 속도, 엔진 회전 수는 상태 데이터에 해당한다. 그리고 차체, 타이어는 부품에 해당한다. 따라서 자동차 클래스를 설계할 때 이 정보들은 필드로 선언되어야 한다.
필드 선언
필드 선언은 클래스 중괄호 { } 블록 어디서든 존재할 수 있다. 생성자 선언과 메소드 선언의 앞과 뒤 어떤 곳 에서도 필드 선언이 가능하다. 하지만 생성자와 메소드 중괄호 블록 내부에는 선언될 수 없다. 생성자와 메소드 중괄호 블록 내부에 선언된 것은 모두 로컬 변수가 된다. 필드 선언은 변수의 선언 형태와 비슷하다. 그래서 일부 사람들은 클래스 멤버 변수라고 부르기도 하는데, 필드라는 용어를 그대로 사용하는 것을 추천한다.
타입 필드 [ = 초기값 ];
타입은 필드에 저장할 데이터의 종류를 결정한다. 타입에는 기본타입과 참조타입이 모두 올 수 있다. 필드의 초기값은 필드 선언시 주어질 수도 있고, 생략될 수도 있다. 다음은 올바르게 필드를 선언한 예를 보여준다.
String company = "현대자동차";
String model = "그랜저";
int maxSpeed = 300;
int productionYear;
int currentSpeed;
boolean engineStart;
초기값이 지정되지 않은 필드들은 객체 생성 시 자동으로 기본 초기값으로 설정된다. 정수 타입 필드는 0, 실수 타입 필드는 0.0 그리고 boolean 필드는 false로 초기화되고, 참조타입은 null로 초기화된다.
필드 사용
필드를 사용한다는 것은 필드값을 읽고, 변경하는 작업을 말한다. 클래스 내부의 생성자나 메소드에서 사용할 경우 단순히 필드 이름으로 읽고 변경하면 되지만, 클래스 외부에서 사용할 경우 우선적으로 클래스로부터 객체를 생성한 뒤 필드를 사용해야 한다. 그 이유는 필드는 객체에 소속된 데이터이므로 객체가 존재하지 않으면 필드도 존재하지 않기 때문이다.
// Car.java - Car 클래스 필드 선언
public class Car {
// 필드
String company = "현대자동차";
String model = "그랜저";
String color = "검정";
int maxSpeed = 350;
int speed;
}
// CarExample.java - 외부 클래스에서 Car 필드값 읽기와 변경
public class CarExample {
public static void main(String[] args) {
// 객체 생성
Car myCar = new Car();
// 필드값 읽기
System.out.println("제작회사 : " + myCar.company);
System.out.println("모델명 : " + myCar.model);
System.out.println("색깔 : " + myCar.color);
System.out.println("최고속도 : " + myCar.maxSpeed);
System.out.println("현재속도 : " + myCar.speed);
// 필드값 변경
myCar.speed = 60;
System.out.println("수정된 속도 : " + myCar.speed);
}
}
Car 클래스는 speed 필드 선언 시 초기값을 주지 않았다. 그러나 출력해보면 기본값이 0이 들어있는 것을 볼 수 있다.
생성자
생성자(Constructor)는 new 연산자와 같이 사용되어 클래스로부터 객체를 생성할 때 호출되어 객체의 초기화를 담당한다. 객체 초기화란 필드를 초기화하거나, 메소드를 호출해서 객체를 사용할 준비를 하는 것을 말한다. 생성자를 실행시키지 않고는 클래스로부터 객체를 만들 수 없다. new 연산자에 의해 생성자가 성공적으로 실행되면 힙(heap) 영역에 객체가 생성되고 객체의 주소가 리턴된다. 리턴된 객체의 주소는 클래스 타입 변수에 저장되어 객체에 접근할 때 이용된다. 만약 생성자가 성공적으로 실행되지 않고 예외(에러)가 발생했다면 객체는 생성되지 않는다.
기본 생성자
모든 클래스는 생성자가 반드시 존재하며, 하나 이상을 가질 수 있다. 우리가 클래스 내부에 생성자 선언을 생략했다면 컴파일러는 다음과 같이 중괄호 { } 블록 내용이 비어있는 기본 생성자(Default Constructor)를 바이트 코드에 자동 추가시킨다.
클래스가 public class로 선언되면 기본 생성자에서도 public이 붙지만, 클래스가 public 없이 class롬나 선언되면 기본 생성자에도 public이 붙지 않는다. public에 대한 의미는 나중에 알아보기로 하고 지금은 그냥 넘어가도록 하자.
클래스에 명시적으로 선언한 생성자가 한 개라도 있으면, 컴파일러는 기본 생성자를 추가하지 않는다. 명시적으로 생성자를 선언하는 이유는 객체를 다양하게 초기화하기 위해서이다.
생성자 선언
기본 생성자 대신 우리가 생성자를 명시적으로 선언하려면 다음과 같은 형태로 작성하면 된다.
클래스( 매개변수선언, ... ){
// 객체의 초기화 코드
}
생성자는 메소드와 비슷한 모양을 가지고 있으나, 리턴 타입이 없고 클래스 이름과 동일하다. 생성자 블록 내부에는 객체 초기화 코드가 작성되는데, 일반적으로 필드에 초기값을 저장하거나 메소드를 호출하여 객체 사용 전에 필요한 준비를 한다. 매개 변수 선언은 생략할 수도 있고, 여러 개를 선언해도 좋다. 매개 변수는 new 연산자로 생성자를 호출할 때 외부의 값을 생성자 블록 내부로 전달하는 역할을 한다. 예를 들어 다음과 같이 Car 생성자를 호출할 때 세 개의 값을 제공한다고 보자.
Car myCar = new Car("그랜저", "검정", 300);
우 개의 매개값은 String 타입이고 마지막 매개값은 int 타입인 것을 볼 수 있다. 세 매개값을 생성자가 받기 위해서는 다음과 같이 생성자를 선언해야 한다.
public class Car {
// 생성자
Car(String model, String color, int maxSpeed) { ... }
}
클래스에 명시적으로 선언되어 있을 경우에는 반드시 선언된 생성자를 호출해서 객체를 생성해야만 한다. 다음 예제를 보면 Car 클래스에 생성자 선언이 있기 때문에 기본 생성자(Car())를 호출해서 객체를 생성할 수 없고 Car(String color, int cc)를 호출해서 객체를 생성해야 한다.
// Car.java - 생성자 선언
public class Car {
// 생성자
Car(String color, int cc) {
}
}
// CarExample - 생성자를 호출해서 객체 생성
public class CarExample {
public Static void main(String[] args) {
Car myCar = new Car("검정", 3000);
// Car myCar = new Car(); (x) 기본생성자를 호출할 수 없다.
}
}
필드 초기화
클래스로부터 객체가 생성될 때 필드는 기본 초기값으로 자동 설정된다. 만약 다른 값으로 초기화를 하고 싶다면 두 가지 방법이 있다. 하나는 필드를 선언할 때 초기값을 주는 방법이고, 또 다른 하나는 생성자에서 초기값을 주는 방법이다. 필드를 선언할 때 초기값을 주게 됨녀 동일한 클래스로부터 생성되는 객체들은 모두 같은 데이터를 갖게 된다. 물론 객체 생성 후 변경할 수 있지만, 객체 생성 시점에는 필드의 값이 모두 같다. 예를 들어 다음과 같이 Korean 클래스에 nation 필드를 선언하면서 "대한민국"으로 초기값을 준 경우, Korean 클래스로부터 k1과 k2 객체를 생성하면 k1과 k2 객체의 nation 필드에는 모두 "대한민국"이 저장되어 있다.
public class Korean {
String nation = "대한민국";
String name;
String ssn;
}
...
Korean k1 = new Korean();
Korean k2 = new Korean();
하지만 객체 생성 시점에 외부에서 제공되는 다양한 값들로 초기화되어야 한다면 생성자에서 초기화를 해야한다. 위 코드에서 name과 ssn 필드 값은 클래스를 작성할 때 초기값을 줄 수 없고 객체 생성 시점에 다양한 값을 가져야 한다. 따라서 생성자의 매개값으로 이 값들을 받아 초기화하는 것이 맞다.
public class Korean {
// 필드
String nation = "대한민국";
String name;
String ssn;
// 생성자
public Korean(String n, String s) {
name = n;
ssn = s;
}
}
아래 코드에서 "박자바", "김자바"는 매개 변수 n을 통해 전달되고, "011225-1234567", "112233-9876543" 은 매개변수 s를 통해 전달된다. 이 값들은 각각 name 필드와 ssn 필드의 초기값으로 사용된다.
Korean k1 = new Korean("박자바", "011225-1234567");
Korean k2 = new Korean("김자바", "112233-9876543");
Korean 생성자의 매개 변수 이름은 각각 n과 s를 사용했다. 매개 변수의 이름이 너무 짧으면 코드의 가독성이 좋지 않기 때문에 가능하면 초기화시킬 필드 이름과 비슷하거나 동일한 이름을 사용할 것을 권한다. 관례적으로 필드와 동일한 이름을 갖는 매개 변수를 사용한다. 이 경우 필드와 매개 변수의 이름이 동일하기 때문에 생성자 내부에서 해당 필드에 접근할 수 없다. 왜냐하면 동일한 이름의 매개 변수가 사용 우선순위가 높기 때문이다. 해결 방법은 필드 앞에 "this."을 붙이면 된다. this는 객체 자신의 참조인데, 우리가 우리 자신을 "나"라고 하듯이 객체가 객체 자신을 this라고 한다. "this.필드"는 this라는 참조 변수로 필드를 사용하는 것과 동일하다. this를 이용하여 Korean 생성자를 수정하면 다음과 같다.
public Korean(String name, String ssn) {
this.name = name;
this.ssn = ssn;
}
객체의 필드는 하나가 아니라 여러 개가 있고, 이 필드들을 모두 생성자에서 초기화한다면 생성자의 매개 변수의 수는 객체의 필드 수만큼 선언되어야 한다. 그러나 실제로는 중요한 몇 개 필드만 매개 변수를 통해 초기화되고 나머지 필드들은 필드 선언시에 초기화하거나 생성자 내부에서 임의의 값 또는 계산된 값으로 초기화한다. 아니면 객체 생성 후에 필드 값을 별도로 저장하기도 한다.
생성자 오버로딩
외부에서 제공되는 다양한 데이터들을 이용해서 객체를 초기화하려면 생성자도 다양화될 필요가 있다. Car 객체를 생성할 때 외부에서 제공되는 데이터가 없다면 기본 생성자로 Car 객체를 생성해야하고, 외부에서 model 데이터가 제공되거나 model과 color가 제공될 경우에도 Car 객체를 생성할 수 있어야 한다. 생성자가 하나뿐이라면 이러한 요구 조건을 수용할 수 없다. 그래서 자바는 다양한 방법으로 객체를 생성할 수 있도록 생성자 오버로딩(Overloading)을 제공한다. 생성자 오버로딩이란 매개 변수를 달리하는 생성자를 여러 개 선언하는 것을 말한다.
다음은 Car 클래스에서 생성자를 오버로딩한 예를 보여준다.
public class Car {
Car() { ... }
Car(String model) { ... }
Car(String model, String color) { ... }
Car(String model, String color, int maxSpeed) { ... }
}
생성자 오버로딩 시 주의할 점은 매개 변수의 타입과 개수 그리고 선언된 순서가 똑같을 경우 매개 변수 이름만 바꾸는 것은 생성자 오버로딩이라고 볼 수 없다. 다음과 같은 경우에 해당된다.
Car(String model, String color) { ... }
Car(String color, String model) { ... } // 오버로딩이 아님
생성자가 오버로딩 되어 있을 경우, new 연산자로 생성자를 호출할 때 제공되는 매개값의 타입과 수에 의해 호출될 생성자가 결정한다. 다음은 다양한 방법으로 Car 객체를 생성한다.
Car car1 = new Car();
Car car2 = new Car("그랜저");
Car car3 = new Car("그랜저", "흰색");
Car car4 = new Car("그랜저", "흰색", 300);
new Car()는 기본 생성자로 객체를 생성하고, new Car("그랜저")는 Car(String model) 생성자로 객체를 생성한다. new Car("그랜저", "흰색")는 Car(String model, String color) 생성자로 객체를 생성하고 new Car("그랜저", "흰색", 300)는 Car(String model, String color, int maxSpeed) 생성자로 객체를 생성한다.
다른 생성자 호출(this())
생성자 오버로딩이 많아질 경우 생성자 간의 중복된 코드가 발생할 수 있다. 매개 변수의 수만 달리하고 필드 초기화 내용이 비슷한 생성자에서 이러한 현상을 많이 볼 수 있다. 이 경우에는 필드 초기화 내용은 한 생성자에만 집중적으로 작성하고 나머지 생성자는 초기화 내용을 가지고 있는 생성자를 호출하는 방법으로 개선할 수 있다. 생성자에서 다른 생성자를 호출할 때에는 다음과 같이 this() 코드를 사용한다.
클래스( [매개변수선언, ... ] ) {
this( 매개변수, ..., 값, ...); // 클래스의 다른 생성자 호출
실행문;
}
this()는 자신의 다른 생성자를 호출하는 코드로 반드시 생성자의 첫줄에서만 허용된다. this()의 매개값은 호출되는 생성자의 매개 변수 타입에 맞게 제공해야 한다. this() 다음에는 추가적인 실행문들이 올 수 있다. 이 말은 호출되는 생성자의 실행이 끝나면 원래 생성자로 돌아와서 다음 실행문을 진행한다는 뜻이다.
Car(String model) {
this.model = model;
this.color = "은색";
this.maxSpeed = 250;
}
Car(String model, String color) {
this.model = model;
this.color = "color";
this.maxSpeed = 250;
}
Car(String model, String color, int maxSpeed) {
this.model = model;
this.color = "은색";
this.maxSpeed = maxSpeed;
}
앞의 코드를 보면 세개의 생성자 내용이 비슷하므로 앞에 두 개의 생성자에서 this()를 사용해서 마지막 생성자인 Car(String model, String color, int maxSpeed)를 호출하도록 수정하면 중복 코드를 최소화할 수 있다.
public class Car {
// 필드
String company = "현대자동차";
String model;
String color;
int maxSpeed;
// 생성자
Car(){
}
Car(String model) {
this(model, "은색", 250); // 호출
}
Car(String model, String color) {
this(model, color, 250); // 호출
}
Car(String model, String color, int maxSpeed) {
this.model = model;
this.color = color;
this.maxSpeed = maxSpeed;
}
}
'Java 길찾기 > 이것이 자바다' 카테고리의 다른 글
[Java] 인스턴스 멤버와 this (0) | 2022.01.10 |
---|---|
[Java] 클래스의 구성 멤버 (2) (2) | 2022.01.07 |
[Java] 클래스 (0) | 2022.01.05 |
[Java] 참조 타입 (3) (0) | 2022.01.04 |
[Java] 참조 타입 (2) (0) | 2022.01.03 |