객체를 생성하고 초기화하는 방법
생성자가 필요한 이유와 생성자를 알아보긴 어떤 방식으로 객체를 생성했는지 알아보자.
package construct;
public class MemberInit {
String name;
int age;
int grade;
}
package construct;
public class MethodInitMain1 {
public static void main(String[] args) {
MemberInit member1 = new MemberInit();
member1.name = "user1";
member1.age = 15;
member1.grade = 90;
MemberInit member2 = new MemberInit();
member2.name = "user2";
member2.age = 16;
member2.grade = 80;
MemberInit[] members = {member1, member2};
for (MemberInit s : members) {
System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" +
s.grade);
}
}
}
위 코드는 가장 원초적인 방법으로 회원 객체를 생성한 후, `name`, `age`, `grade` 같은 변수에 초기값을 설정하였다. 회원 객체를 제대로 활용하기 위해선 안에 속성(멤버변수)의 값을 설정해야 한다. 그러므로 이 코드에선 객체를 선언하고 회원의 속성값을 초기설정하는 코드가 반복되게 된다.
이번에 회원값을 초기화하는 부분을 메서드 분리를 통해 리펙토링해보자.
public class MethodInitMain2 {
public static void main(String[] args) {
MemberInit member1 = new MemberInit();
initMember(member1,"user1", 15, 90);
MemberInit member2 = new MemberInit();
initMember(member2,"user2", 16, 80);
MemberInit[] members = {member1, member2};
for (MemberInit member : members) {
System.out.println("이름: " + member.name + ", 나이: " + member.age + ", 성적: " + member.grade);
}
}
static void initMember(MemberInit member, String name, int age, int grade) {
member.name = name;
member.age = age;
member.grade = grade;
}
}
`initMember()`메서드를 만들어 반복을 제거하였다. 자바는 객체지향언어를 지향한다. 그 말은 즉슨, 객체의 속성과 기능은 한 곳에 두고 사용하는 것이 더 나은 방법이다.
위 코드에 경우 `Main`클래스에 `static`메서드로 선언되어 있기 때문에 객체를 다른 클래스에서 사용 시 사용하기가 쉽지 않다. 이걸 객체지향적으로 접근하기 위해선 `MemberInit`의 속성을 초기화하는 메서드는 가지고 있으면 된다.
최종적으로 아래와 같은 형태로 선언되고 사용되게 된다.
package construct;
public class MemberInit {
String name;
int age;
int grade;
//추가
void initMember(String name, int age, int grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
}
package construct;
public class MethodInitMain3 {
public static void main(String[] args) {
MemberInit member1 = new MemberInit();
member1.initMember("user1", 15, 90);
MemberInit member2 = new MemberInit();
member2.initMember("user2", 16, 80);
MemberInit[] members = {member1, member2};
for (MemberInit s : members) {
System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" +
s.grade);
}
}
}
this
`MemberInit`클래스 코드를 다시 확인해보면 `this`라는 키워드를 사용하는 것을 알 수 있다. `initMember()`메서드를 보면 매개변수로 정의한 값들과 `MemberInit`클래스의 멤버변수의 명이 똑같은 걸 알 수 있다.
그럼 멤버변수와 메서드의 매개변수의 이름이 같으면 메서드 안에서 호출 시 구분처리를 해줘야 한다. 그때 사용하는 것이 `this`키워드이다.
기본적으로 변수의 우선순위를 확인하려면 코드 블록의 위치를 살펴보면 된다. 코드 블록이 더 안쪽에 있을수록 우선순위가 높아진다. `initMember()` 메서드의 경우, 매개변수가 메서드 내부의 코드 블록 안쪽에 위치하기 때문에 멤버 변수보다 우선순위를 가지게 됩니다.
간단히 생각하면, 멤버 변수는 클래스의 코드 블록에 속하고, 매개변수는 메서드의 코드 블록에 속한다. 따라서, 메서드 기준으로 더 가까운 매개변수가 우선되게 된다.
그럼 이제 멤버 변수에 접근하기 위해선 아까 언급한 `this`가 필요하다. 변수명 앞에 `this.명수명`형태로 사용하면 멤버 변수를 가리키게 된다. 즉, `this`는 인스턴스 자신의 참조값을 가리킨다.
this.name = name; //1. 오른쪽의 name은 매개변수에 접근
this.name = "user"; //2. name 매개변수의 값 사용
x001.name = "user"; //3. this.은 인스턴스 자신의 참조값을 뜻함, 따라서 인스턴스의 멤버 변수에
접근
this의 생략
`this`는 생략이 가능하다. 이 경우 변수를 찾을 때 가까운 지역변수(매개변수도 지역변수다.)를 먼저 찾고 없으면 그다음으로 멤버 변수를 찾는다.
주로 `this`를 생략하는 케이스는 다음과 같다.
package construct;
public class MemberThis {
String nameField;
void initMember(String nameParameter) {
nameField = nameParameter;
}
}
위 예제는 필드 이름과 매개변수의 이름이 서로 다르다. 이 경우 `nameField` 앞에 `this`가 없어도 멤버 변수에 접근한다.
- `nameField` 는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾는다. 이 경우 없으므로 멤버 변수에서 찾는
다. - `nameParameter` 는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾는다. 이 경우 매개변수가 있으므로 매
개변수를 사용한다.
this와 코딩 스타일
다음 코드와 같이 `this`가 필요 없어도 되는 코드에 `this`를 항상 사용하는 코드가 있다.
package construct;
public class MemberThis {
String nameField;
void initMember(String nameParameter) {
this.nameField = nameParameter;
}
}
`this` 생략이 가능하지만, `this`를 붙여서 사용하는 이유는 이 코드가 멤버 변수를 사용한다는 것을 눈으로 쉽게 확인할 수 있기 때문이다. 그래서 과거에는 이런 스타일을 많이 사용했다.
하지만 최근에는 IDE가 발전하게 되면서 IDE가 멤버 변수와 지역 변수를 색상으로 구분한다. 이런 점 때문에 `this`를 구분에 의미 꼭 사용할 필요는 없어졌다. 그렇기 때문에 회사 팀 스타일이나 따로 정해져 있지 않다면 본인 스타일에 맞게 사용하면 된다.
생성자의 도입
프로그래밍을 하다 보면 객체 생성과 함께 초기값을 지정해줘야 하는 경우가 많다. 하지만 위 코드를 활용하면 `initMember()`와 같은 메서드를 매번 만들어줘야 한다.
그래서 대부분의 객체 지향 언어는 객체를 생성하자마자 즉시 속성값을 초기화해 줄 수 있는 생성자라는 기능을 제공한다. 생성자는 앞서 만든 `initMember()`와 유사한 구조를 가지고 있다.
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade="
+ grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
위 코드에서 클래스명과 동일한 네이밍으로 구성된 코드블록이 생성자이다. 생성자는 메서드와 비슷하지만 다음과 같은 차이가 존재한다.
- 생성자의 이름은 클래스 이름과 같아야 한다. 따라서 첫 글자도 대문자로 시작한다.
- 생성자는 반환 타입이 없다. 비워두어야 한다.
- 나머지는 메서드와 같다.
최종적으로 다음과 같이 사용되게 된다.
public class ConstructMain1 {
public static void main(String[] args) {
MemberConstruct member1 = new MemberConstruct("user1", 15, 90);
MemberConstruct member2 = new MemberConstruct("user2", 16, 80);
MemberConstruct[] members = {member1, member2};
for (MemberConstruct member : members) {
System.out.println("이름: " + member.name + ", 나이: " + member.age + ", 성적: " + member.grade);
}
}
}
생성자 호출
생성자는 인스턴스를 생성한 다음 즉시 호출된다. 생성자를 호출하는 코드는 `new`키워드 다음에 생성자의 매개변수에 맞는 인수를 전달하여 사용된다.
new 클래스이름(생성자에 선언된 인수목록);
new MemberConstruct("user1", 15, 90);
참고로 new 키워드를 사용해서 객체를 생성할 때 마지막에 괄호( )도 포함해야 하는 이유가 바로 생성자 때문이다. 객체를 생성하면서 동시에 생성자를 호출한다는 의미를 포함된다.
생성자의 장점
1. 중복 호출 제거
생성자가 없던 시정에는 생성 직후에 어떤 작업을 수행하기 위해 다음과 같이 메서드를 직접 한번 더 호출해야 했다. 생성자를 선언하게 되면 객체 생성과 초기값 세팅을 한 번에 처리할 수 있게 되었다.
//생성자 등장 전
MemberInit member = new MemberInit();
member.initMember("user1", 15, 90);
//생성자 등장 후
MemberConstruct member = new MemberConstruct("user1", 15, 90);
2. 제약 - 생성자 호출 필수
생성자 등장 전 코드를 보면 `initMember(...)` 를 실수로 호출하지 않아도 프로그램 작동에는 문제가 없다. 하지만 동작과정에서 이름, 나이, 성적과 같은 속성이 필요하다면 시스템의 큰 문제가 될 수 있다. 아무 정보가 없는 객체가 시스템 내부에 등장하게 된다.
생성자의 진짜 장점은 객체를 생성할 때 직접 정의한 생성자가 있다면 직접 정의한 생성자를 반드시 호출해야 한다는 점이다. 참고로 생성자는 메서드 오버로딩처럼 여러 개 정의할 수 있으며, 정의된 한 경우만 호출되면 된다.
`MemberConstruct` 클래스의 경우 다음 생성자를 직접 정의했기 때문에 직접 정의한 생성자를 반드시 호출해야 한다.
MemberConstruct(String name, int age, int grade) {...}
다음과 같이 직접 정의한 생성자를 호출하지 않으면 컴파일 오류가 발생하게 된다.
MemberConstruct member3 = new MemberConstruct(); //컴파일 오류 발생
member3.name = "user1";
// 컴파일 오류 메시지
no suitable constructor found for MemberConstruct(no arguments)
결국 생성자를 사용함에 따라 필수값 입력을 보장받을 수 있게 된다.
기본 생성자
생성자 중엔 기본 생성자라는 게 있다. 매개변수가 없는 생성자를 기본 생성자라 칭하며 다음과 같은 특징을 가진다.
- 클래스에 생성자가 하나도 없다면 자바 컴파일어는 매개변수가 없고, 작동하는 코드가 없는 기본 생성자를 자동으로 만들어 준다.
- 생성자가 하나라도 있으면 자바는 기본 생성자를 만들어 주지 않는다.
package construct;
public class MemberDefault {
String name;
}
package construct;
public class MemberDefaultMain {
public static void main(String[] args) {
MemberDefault memberDefault = new MemberDefault();
}
}
위 예제를 보면 `MemberDefault` 클래스에는 생성자가 없는데도 `new MemberDefault()`라 선언해도 문제없이 동작이 된다. 그건 자바에서 자동으로 기본 생성자를 만들어주기 때문이다. 단 우리 눈에 명시적으로 보이진 않는다.
기본 생성자에 선언은 다음과 같다. 접근제어자와 클래스명으로 구성되며, 매개변수가 존재하지 않는다.
package construct;
public class MemberDefault {
String name;
//기본 생성자
public MemberDefault() {
}
}
참고로, 자바가 자동으로 만들어 주는 기본 생성자는 클래스와 같은 접근 제어자를 가지게 된다.
기본 생성자를 자동으로 만드는 이유
만약 자바에서 기본 생성자를 만들어주지 않는다면 생성자 기능이 필요하지 않은 경우에도 모든 클래스에 개발자가 직접 기본 생성자를 정의해야 한다. 생성자 기능을 사용하지 않는 경우도 많기 때문에 이런 편의 기능을 제공한다.
정리
- 생성자는 반드시 호출되어야 한다.
- 생성자가 없으면 기본 생성자가 제공된다.
- 생성자가 하나라도 있으면 기본 생성자가 제공되지 않는다. 이 경우 개발자가 정의한 생성자를 직접 호출해야 한다.
생성자 - 오버로딩과 this()
생성자의 경우 메서드와 동일하게 매개변수만 다르게 해서 여러 생성자를 만들 수 있다.
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
//추가
MemberConstruct(String name, int age) {
this.name = name;
this.age = age;
this.grade = 50;
}
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade="
+ grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
새롭게 추가된 생성자를 포함 2개의 생성자를 가지게 되면 다음과 같이 선언하여 사용할 수 있다.
MemberConstruct member1 = new MemberConstruct("user1", 15, 90);
MemberConstruct member2 = new MemberConstruct("user2", 16);
this()
두 생성자 코드를 비교해 보면 중독된 부분이 있다.
public MemberConstruct(String name, int age) {
this.name = name;
this.age = age;
this.grade = 50;
}
public MemberConstruct(String name, int age, int grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
// 이 부분이다.
this.name = name;
this.age = age;
이때 `this()`라는 기능을 사용하면 생성자 내부에서 자신의 생성자를 호출할 수 있다. `this`는 위에서 언급했듯이 인스턴스 자신의 참조값을 가르킨다.
코드를 다음과 같이 수정이 가능해진다.
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
MemberConstruct(String name, int age) {
this(name, age, 50); //변경
}
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade="
+ grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
이 코드를 확인해 보면 첫 번째 생성자 내부에서 두 번째 생성자를 호출하는 형태이다.
MemberConstruct(String name, int age) -> MemberConstruct(String name, int age, int grade)
this()` 를 사용하면 생성자 내부에서 다른 생성자를 호출할 수 있다. 이 부분을 잘 활용하면 지금과 같이 중복을 제거
할 수 있다.
단, `this()`에도 규칙이 존재한다. `this()`는 생성자 코드의 첫 줄에만 작성이 가능하다. 만약 다음과 같이 사용한다면 컴파일 오류가 발생하게 된다.
public MemberConstruct(String name, int age) {
System.out.println("go");
this(name, age, 50);
}
출처
'Language > Java' 카테고리의 다른 글
[Java] 자바의 메모리 구조와 Static (스택, 힙, 메서드 영역) (0) | 2025.01.10 |
---|---|
[Java] 접근 제어자와 캡슐화 (0) | 2025.01.10 |
[Java] null과 NullPointerException (0) | 2025.01.07 |
[Java] 변수의 초기화 (0) | 2025.01.07 |
[Java] 기본형 vs 참조형 (0) | 2025.01.04 |