메서드의 값 전달
인자에서 매개변수로 복사되는 값과 필드 변경·매개변수 재대입의 차이를 실행으로 확인합니다.
메서드에 객체를 전달하면 객체 자체가 이동할까요, 참조가 전달될까요?
Java를 설명할 때 흔히 “기본형은 값 전달, 객체는 참조 전달”이라고 줄여 말하지만 이 표현은 호출자가 가진 변수를 메서드가 직접 바꿀 수 있다는 오해를 만듭니다.
자바는 메서드를 호출할 때도 변수의 값을 복사합니다.
기본형 인자는 숫자나 불리언 값을 복사하고, 참조형 인자는 참조값을 복사합니다.
매개변수는 호출된 메서드 안에서 새로 생기는 지역 변수입니다.
이 원칙 하나로 필드 변경은 호출자에게 보이지만 매개변수 재대입은 보이지 않는 이유를 설명할 수 있습니다.
기본형 인수 전달
다음 프로그램의 결과를 먼저 예측해 보세요.
public class PrimitiveParameter {
public static void main(String[] args) {
int age = 30;
increase(age);
System.out.println("main=" + age);
}
static void increase(int value) {
value += 10;
System.out.println("method=" + value);
}
}method=40
main=30increase(age)를 호출하는 순간 age의 값 30이 매개변수 value에 복사됩니다.
메서드 안의 value += 10은 지역 변수 value만 40으로 바꿉니다.
호출자의 age와 연결되어 있지 않습니다.
호출자의 값을 바꾸려면 결과를 반환하고 다시 대입해야 합니다.
age = increased(age);
static int increased(int value) {
return value + 10;
}반환값도 메서드에서 호출자 쪽으로 복사되는 값입니다.
참조형 인수 전달
이번에는 Member를 전달합니다.
public class ReferenceParameter {
public static void main(String[] args) {
Member member = new Member();
member.email = "method@example.com";
member.age = 30;
member.active = false;
activate(member);
System.out.println(member.email);
System.out.println(member.active);
}
static void activate(Member target) {
target.active = true;
}
}method@example.com
true호출 전 member에 x101이 들어 있다면 activate의 target에도 x101이 복사됩니다.
두 변수는 서로 다른 지역 변수지만 같은 인스턴스에 도달합니다.
target.active = true는 target 변수를 수정한 것이 아니라 x101 객체의 필드를 수정했으므로 호출 뒤 member.active로 읽어도 true입니다.
매개변수 재대입
참조값이 복사된다는 사실을 확인하기 가장 좋은 반례는 메서드 안에서 매개변수를 새 객체로 바꾸는 것입니다.
public class ReassignParameter {
public static void main(String[] args) {
Member member = new Member();
member.email = "original@example.com";
replace(member);
System.out.println(member.email);
}
static void replace(Member target) {
target = new Member();
target.email = "temporary@example.com";
}
}original@example.comreplace를 시작할 때는 member와 target이 모두 x101을 가리킵니다.
target = new Member()가 새 객체 x202를 만들고 지역 변수 target에 새 참조를 넣습니다.
호출자의 member 변수에는 대입하지 않았으므로 여전히 x101입니다.
메서드가 끝나면 지역 변수 target은 사라지고, 다른 참조가 없다면 x202는 더 이상 도달할 수 없습니다.
“객체를 메서드에 넘기면 메서드가 호출자의 변수를 바꿀 수 있다”는 설명은 정확하지 않습니다.
메서드는 공유 객체의 필드를 바꿀 수 있지만 호출자의 지역 변수 자체를 다른 참조로 교체할 수는 없습니다.
초기화 메서드
객체를 여러 개 만들다 보면 필드 대입 코드가 반복됩니다.
Member first = new Member();
first.email = "class@example.com";
first.age = 40;
first.active = false;
Member second = new Member();
second.email = "member2@example.com";
second.age = 35;
second.active = false;참조형 매개변수가 공유 객체를 바꾼다는 점을 이용해 초기화 메서드를 만들 수 있습니다.
public class MemberInitializer {
public static void main(String[] args) {
Member first = new Member();
init(first, "class@example.com", 40, false);
Member second = new Member();
init(second, "member2@example.com", 35, false);
print(first);
print(second);
}
static void init(
Member target,
String email,
int age,
boolean active
) {
target.email = email;
target.age = age;
target.active = active;
}
static void print(Member member) {
System.out.printf(
"%s / %d세 / %s%n",
member.email, member.age, member.active
);
}
}init은 새 객체를 만들지 않습니다.
호출자가 만든 객체의 참조를 받아 필드를 채웁니다.
이 방식은 중복을 줄이지만 호출 전에 빈 객체를 만들어야 하고, 초기화를 빼먹은 객체도 여전히 존재할 수 있습니다.
생성자가 필요한 이유가 여기서 나타나지만, 그 개선은 다음 장의 객체 생명주기에서 다룹니다.
팩터리 메서드
현재 문법만으로도 객체 생성과 필드 초기화를 한 메서드에 모을 수 있습니다.
public class MemberFactory {
public static void main(String[] args) {
Member member = create("factory@example.com", 45, false);
print(member);
}
static Member create(
String email,
int age,
boolean active
) {
Member created = new Member();
created.email = email;
created.age = age;
created.active = active;
return created;
}
static void print(Member member) {
System.out.printf(
"%s / %d세 / %s%n",
member.email, member.age, member.active
);
}
}create 안의 지역 변수 created는 메서드가 끝나면 사라집니다.
하지만 반환문이 그 변수의 참조값을 호출자에게 복사합니다.
호출자의 member가 같은 객체를 가리키므로 객체는 계속 도달 가능한 상태입니다.
지역 변수의 수명과 객체의 수명은 같지 않습니다.
객체는 특정 지역 변수에 소속되는 것이 아니라 도달 가능한 참조가 있는 동안 사용될 수 있습니다.
변경 메서드의 이름
참조형 매개변수를 받는 메서드는 전달된 객체를 읽기만 할 수도 있고 필드를 바꿀 수도 있습니다.
static void print(Member member) { ... } // 읽기
static void activate(Member member) { ... } // 변경컴파일러가 이 차이를 메서드 시그니처만으로 강제하지는 않습니다.
따라서 이름, 문서, 객체 설계로 변경 여부를 드러내야 합니다.
print가 몰래 active = true로 바꾼다면 호출자는 출력만 요청했다고 생각했다가 예상하지 못한 상태 변경을 만납니다.
현재의 Member는 필드 접근 제한도 없고 객체가 스스로 행동하지도 않는 단순 데이터 묶음입니다.
이후 접근 제어와 캡슐화를 배우면 변경 책임을 객체 안으로 옮길 수 있습니다.
지금은 호출 경계에서 어떤 값이 복사되는지 확실히 보는 것이 우선입니다.
연습 문제
다음 두 메서드를 구현하고 차이를 설명하세요.
int nextAge(int current)는 다음 나이를 반환합니다.void celebrateBirthday(Member member)는 전달된 회원의 나이를 1 증가시킵니다.- 현재 나이가 120이면 두 메서드 모두
IllegalStateException을 던집니다. replace(Member member)에서 매개변수에 새 객체를 대입해도 호출자 변수가 바뀌지 않는 실행 사례를 추가합니다.
해설 보기
public class BirthdayParameter {
public static void main(String[] args) {
int age = 30;
age = nextAge(age);
System.out.println("number=" + age);
Member member = new Member();
member.email = "method@example.com";
member.age = 30;
celebrateBirthday(member);
System.out.println("member=" + member.age);
replace(member);
System.out.println("after replace=" + member.email);
}
static int nextAge(int current) {
requireBirthdayAllowed(current);
return current + 1;
}
static void celebrateBirthday(Member member) {
requireBirthdayAllowed(member.age);
member.age++;
}
static void replace(Member member) {
member = new Member();
member.email = "replacement@example.com";
}
static void requireBirthdayAllowed(int currentAge) {
if (currentAge >= 120) {
throw new IllegalStateException("maximum age");
}
}
}number=31
member=31
after replace=method@example.com숫자 버전은 계산 결과를 반환하므로 호출자가 다시 대입해야 합니다.
객체 버전은 복사된 참조로 공유 객체의 필드를 바꿉니다.
replace는 지역 매개변수만 새 참조로 바꾸므로 호출자의 member.email은 그대로입니다.
메서드 호출도 대입과 똑같이 값을 복사합니다.
이 규칙을 알면 필드 변경, 재대입, 반환 참조를 하나의 흐름으로 설명할 수 있습니다.
다음 절에서는 참조값이 아예 없음을 나타내는 null과 점 연산이 만날 때 왜 실행이 중단되는지 조사합니다.