Java는 call by value 일까?

Java에서 모든 것은 pass-by-value입니다. 참조가 아닌 값에 의해 호출됩니다.

stackoverflow에서 찾은 예제로 설명드리겠습니다.

예제1.

  • myCat은 Cat이 아닙니다. Cat에 대한 포인터입니다. 즉, 주소값을 갖고 있습니다.
    1
    2
    Cat myCat = new Cat("Rover");
    foo(myCat);
    따라서 만들어진 Cat 객체의 주소가 foo 메소드에 넘겨지게 됩니다.

Cat 객체의 주소를 42라고 가정하겠습니다. 42가 메소드에 넘겨지겠죠?
foo 메소드가 다음과 같다면,

1
2
3
4
5
public void foo(Cat someCat) {
someCat.setName("Max"); // 1.
someCat = new Cat("Fifi"); // 2.
someCat.setName("Rowlf"); // 3.
}

someCat 은 주소값 42를 갖게 됩니다.

  • line 1.
    • someCat은 42라는 주소에 해당하는 Cat을 가리킵니다.
    • Max로 이름을 바꿉니다. (someCat의 이름은 Max가 됩니다.)
  • line 2.
    • 이름이 Fifi인 새로운 Cat 객체가 생성됩니다.
    • 주소는 74라고 가정하겠습니다.
  • line 3.
    • someCat은 74라는 주소에 해당하는 Cat을 가리킵니다.
    • Rowlf로 이름을 바꿉니다. (someCat의 이름은 Rowlf가 됩니다.)

이제, 메소드가 끝났습니다. myCat은 변경되었을까요?

정답은 “변경되지 않았다.” 입니다.
myCat은 여전히 주소값 42를 갖고 있습니다. 하지만 주목해야 할 점은 “이름은 Max로 변경되었다.” 라는 것입니다.

C 언어를 공부하셨던 분들이라면 C와 유사하지만 다르다 라는 것을 느끼셨을 겁니다. C와 마찬가지로 주소값이 전달되지만, C와 다르게 포인터가 가리키는 곳을 변경할 수 없습니다.

위 코드를 다시 살펴보겠습니다.

  • 참조형 타입은 메소드로 주소값이 전달됩니다.
    메소드가 리턴되고 Max라고 이름이 바뀌었기 때문입니다.
    1
    2
    3
    public void foo(Cat someCat) {
    someCat.setName("Max");
    }
  • 포인터가 가리키는 곳을 변경할 수는 없습니다.
    변경이 가능했다면 myCat은 Rowlf라는 이름일텐데 Max이기 때문입니다.
    여기선 new 연산자를 통해 다른 주소값을 가진 다른 인스턴스가 생성된 것이고 메소드와 같은 라이프 사이클을 갖습니다. 즉, 메소드 리턴과 동시에 사라지게 됩니다.
    1
    2
    3
    4
    public void foo(Cat someCat) {
    someCat = new Cat("Fifi");
    someCat.setName("Rowlf");
    }
    이게 왜 값에 의한 호출인지 아직 명확하지 않아 보입니다.

다음 예시를 보시면 명확하게 이해가 되실 겁니다.

예제2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {

public static void main(String[] args) {
Foo f = new Foo("f");
changeReference(f); // It won't change the reference!
modifyReference(f); // It will modify the object that the reference variable "f" refers to!
}

public static void changeReference(Foo a) {
Foo b = new Foo("b");
a = b;
}

public static void modifyReference(Foo c) {
c.setAttribute("c");
}

}

2-1.

1
Foo f = new Foo("f");

위에서 설명했던 대로 f는 주소값을 가지고 타입이 Foo이고 속성이 f인 객체를 가리킵니다.

2-2.

1
public static void chanageReference(Foo a)

a는 Foo 타입으로 선언되어 있고 null로 초기화 되어 있습니다.

2-3.

1
changeReference(f);

f를 인자로 넘겨주어 a는 f와 동일한 주소값을 갖게 됩니다. 동일한 주소값이니 똑같은 곳을 가리키고 있는 것을 확인할 수 있습니다.

2-4.

1
Foo b = new Foo("b");

b는 주소값을 가지고 타입이 Foo이고 속성이 b인 객체를 가리킵니다.

2-5.

1
a = b;

b의 주소값을 a에 할당하게 되어 a는 b가 가리키는 곳을 가리키게 됩니다.

2-6.

1
2
3
modifyReference(Foo c) { }

modifyReference(f);

f를 인자로 넘겨주어 c는 f의 주소값을 받게 됩니다. 당연히 f가 가리키는 곳을 가리키겠죠?

2-7.

1
c.setAttribute("c");

c가 가리키고 있는 객체의 속성을 c로 변경합니다. c와 f는 동일한 것을 가리키기에 f가 가리키는 객체의 속성은 c가 되는 것입니다.

call by reference 였다면 f의 주소값을 c나 a가 받았겠죠? 그렇다면 위에 a = b가 되었을 때 f도 바뀌었을 것입니다. 하지만 call by value이기 때문에 f, c, a, b 모두 각각 따로 존재하고 값만 전달 받고 있습니다.

Java에서는 모두 값을 전달하는 pass-by-value라는 것을 기억하시면서 이번 글을 마칩니다.

[Reference]