클래스에는 객체가 가져야 할 구성 멤버가 선언된다.
: 객체의 데이터가 저장되는 곳
선언 형태는 변수(variable)과 비슷하지만, 필드를 변수라고 부르지 않는다.
필드는 생성자와 메소드 전체에서 사용되며, 객체가 소멸되지 않는 한 객체와 함께 존재한다.
필드는 클래스 내부 어디서든 선언할 수 있지만, 생성자와 메소드 내부에서는 선언될 수 없다.
만약, 생성자와 메소드 내부에서 선언한다면 필드가 아니라 모두 로컬 변수이다.
타입 필드 [= 초기값];
이러한 방식으로 선언한다.
타입 : 기본타입(byte, short, int, long, float, double, boolean), 참조타입(배열, 클래스, 인터페이스) 가능
필드의 초기값 : 선언 시 가능, 생략 가능
만약 필드의 초기값을 생략한다면 객체 생성 시 자동으로 기본 초기값으로 설정
: 객체 생성 시 초기화 역할 담당
[public] 클래스();
생성자는 메소드와 비슷한 형태이나, 리턴 타입이 없고 클래스 이름과 동일하다.
생성자 내부에는 필드에 초기값을 저장하거나, 메소드를 호출해 객체에 대한 준비를 한다.
필드가 변수와, 메소드가 함수와 비슷하다면 생성자는 좀 생소하다.
생성자는 new 연산자와 같이 사용되어 클래스로 객체를 생성할 때 호출되어 객체의 초기화를 담당한다.
여기서 핵심은 객체를 초기화한다는 것이다.
즉, 필드를 초기화하거나, 메소드를 호출해서 객체를 사용할 준비를 한다는 것이다.
생성자를 실행시키지 않고는 클래스로부터 객체를 만들 수 없다.
만약, 열심히 클래스를 만들었는데 생성자를 깜빡했다면?
다행히, 컴파일러는 기본 생성자(Default Constructor)를 바이트 코드에 추가시킨다.
그렇기 때문에 생성자를 까먹고 선언하지 않아도 객체를 생성시킬 수 있다.
그럼 여기서 드는 의문은 그럼 생성자를 왜 만드는가?
이에 대한 답은 클래스로부터 각각의 객체를 만들 때 미리 생성자의 매개변수에 해당하는 값들을 외부에서 입력해 만들 수 있는 편리함이 있다.
public class Person {
//field
String nation = "대한민국";
}
이와 같은 Person이라는 클래스를 만들고 이에 해당하는 객체를 만든다면 모든 사람의 국적은 "대한민국"으로 만들어진다.
하지만 만약 내가 대한민국, 미국, 스위스의 국적을 가진 사람 객체를 만들고 싶다면 어떻게 해야할까?
바로 그럴때 생성자를 활용하면 된다.
public class Person {
//field
String nation ;
//constructor
public Person (String n) {
nation = n;
}
}
이처럼 생성자를 만들고 매개 변수 n값으로 국적을 입력 받아 객체를 생성하면 된다.
그럼, 실행 클래스 코드를 살펴보자.
public class PersonExample {
public static void main(String[] args) {
Person p1 = new Person("Korea");
Person p2 = new Person("USA");
Person p3 = new Person("Swiss");
}
}
내가 Person 클래스에서 생성자 매개 변수 이름을 n으로 정했다.
(관례적으로 가독성을 위해 필드와 매개 변수는 동일한 이름을 사용한다.)
만약 n이 아니라, nation이라면?
생성자 내부 코드는
public Person(String nation) {
nation = nation;
}
그럼 두 nation 중 어느 것이 매개 변수일까?
프로그래밍 언어를 공부한 사람이라면 =을 기준으로 오른쪽이라는 걸 알테지만, 초등학생이 본다면? 당연히 모를 것이다.
하지만 실제론 필드와 매개 변수 이름이 동일하기 때문에 생성자 내부에서 해당 필드에 접근할 수 없다.
왜냐하면 동일한 이름의 매개 변수가 사용 우선순위가 높기 때문이다.
그럼 어떻게 동일한 이름의 매개 변수를 사용하냐면 바로 필드 앞에 this.를 붙이면 된다.
public Person(String nation) {
this.nation = nation;
}
: 매개 변수를 달리하는 생성자를 여러 개 선언하는 것
자바는 다양한 객체를 손 쉽게 선언할 수 있도록 생성자 오버로딩을 제공한다.
ex)
public class Car {
Car() { ...}
Car(String model) {...}
Car(String model, String color) {...}
Car(String model, String color, int maxSpeed) {...}
}
주의할 점은 매개 변수의 타입, 매개 변수의 개수, 선언된 순서가 똑같을 경우 매개 변수 이름만 바꾸는 것은 생성자 오버로딩이라고 볼 수 없다.
ex)
Car(String model, String color) {...}
Car(String color, String model) {...} // 오버로딩 아님
생성자가 오버로딩되어 있을 경우, new 연산자로 생성자를 호출할 때 제공되는 매개값의 타입과 수에 의해 호출될 생성자가 결정된다.
//Execution Class
public class CarExample {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car("자가용");
Car car3 = new Car("자가용", "red");
}
}
만약 여러개의 API(라이브러리 클래스)를 작성해야한다면, 만약 그 코드 내부의 생성자 오버로딩을 많이 해야한다면?
당연히 중복되는 코드는 많이 발생한다.
당연히 사람이라면 귀찮은 것이 당연하다.
이에 대한 해결책으로 다른 생성자를 호출할 수 있는 this()가 있다.
필드 초기화 내용은 한 생성자에만 집중적으로 작성하고 나머지 생성자는 초기화 내용을 가지고 있는 생성자를 호출하는 방법으로 개선한다.
this()는 반드시 생성자의 첫줄에만 허용된다.
// 중복된 코드가 있는 생성자 오버로딩 예시
Car(String model) {
this.model = model;
this.color = "silver";
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 = color;
this.maxSpeed = maxSpeed;
}
이러한 코드를 아래와 같이 바꿀 수 있다.
public class Car {
//field
String company = "hyundai";
String model;
String color;
int maxSpeed;
//constructor
Car() {
}
Car(String model) {
this(model, "silver", 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;
}
}
: 객체의 동작에 해당
메소드가 하는 일은 필드를 읽고 수정하는 역할, 다른 객체를 생성하여 다양한 기능을 수행, 객체 간의 데이터 전달, 외부로부터 매개값을 받을 수 있으며, 실행 후 어떤 값을 리턴할 수 있다.
만약 매개 변수의 수를 모를 경우 배열 타입으로 선언하는 것이 해결책이다.
int sum(int[] values) { }
그럼 이 메소드를 호출한다면
int [] values = {1, 2, 3};
int result = sum(values);
int result = sum(new int[] {1,2,3,4,5});
위 코드에서 불편한 점이 없는가?
바로 배열은 참조 타입이기 때문에 미리 배열을 생성한 후 호출해야한다.
그래서 배열을 생성하지 않고 값의 리스트만 넘겨주는 방법이 있다.
바로 ... 을 사용하는 것이다.
int sum(int ... values) { }
그럼 호출 시에는
int result = sum(1,2,3);
라고만 작성하면 된다.