본문 바로가기
Java/Java Story

[Java] Java는 Call by value? Call by reference?

by 잭피 2020. 9. 18.

 

안녕하세요~ 잭코딩입니다!

 

이번 내용에서는 Call by value와 Call by reference를 살펴보고 

Java는 어떠한 방식인지 예제를 통해 알아볼까요?


자바에서 메서드(함수)를 정의한 후 필요한 변수 or 객체를 인자 값으로 받아온다 

이때, 인자 값을 어떤 식으로 받아 올 것인지에 대한 방식이다

Call by value (값에 의한 호출)

'값'을 넘겨주는 호출 방식

Call by reference (참조에 의한 호출)

'참조값(주소 혹은 포인터)을 넘겨주는 호출 방식'

1. 자바의 기본형(Primitive Type)의 경우는 Call by value이다 

(기본형 : boolean, char, byte, short, int, long, float, double)

public class Main {
    public static void main(String[] args) {
        int jack = 1;
        int coding = 1;
        changeJackCoding(jack, coding);
        System.out.println("jack : " + jack);
        System.out.println("coding : " + coding);
    }

    static void changeJackCoding(int a, int b) {
        a = a + 1;
        b = b + 1;
        System.out.println("a : " + a);
        System.out.println("b : " + b);
    }
}

-------- 결과 --------
a : 2
b : 2
jack : 1
coding : 1

결과를 보면 변수 jack과 coding은 changeJackCoding 함수를 거쳤는데 그대로 값이 1,1이다 

매개변수로 받은 a, b의 값에 1씩 더하니 a와 b는 값이 2이다 

즉, 값만 넘겨받았다

왜 이런 걸까?

우선 자바의 기본 자료형은 스택 영역에 저장된다는 것을 이해해야 한다

(애플리케이션이 동작할 때 프로그램이 메모리에 올라가는데, JVM에서는 스레드 1개당 독립적인 스택 공간이 할당된다)

한번 그림으로 위 코드를 나타내 보자

 

JVM 스택영역

 

main 함수가 실행되면서 String [] args 매개변수부터 스택에 쌓인다

그다음 선언한 jack, coding을 생성하고, changeJackCoding의 인자 값으로 넘긴다

이때 넘어온 인자 값은 jack, coding 변수가 아닌 변수에 담긴 값 1만 복사합니다

그 후, 매개변수 a, b가 스택에 쌓입니다

changeJackCoding 함수가 종료되면 a, b는 스택 영역에 지워집니다

 

JVM 스택영역

 

그래서 결국 jack과 coding은 1을 출력합니다

코드와 스택의 그림을 보면서 자바의 기본형(Primitive Type)은 왜 call by value인지 알아보았다

 

그럼 자바의 참조형(Reference Type)은 어떤지 살펴보자

2. 자바의 참조형(ReferenceType)의 경우도 Call by value이다 

객체의 '주소값'을 매개변수로 전달하니 call by reference가 아니냐는 의문을 가질 수 있지만,

정확하게 말하면 '주소값'이 아니라, '주소를 가리키는 참조값'이다

또한, 주소값 자체를 '복사 없이' 인자로 전달하는 게 아니라 자기 자신이 갖고 있는 값을 복사해서 전달한다

결국 기본형 변수나 참조형 변수 모두 자기 자신이 갖고 있는 값을 복사해서 전달하기 때문에 Call by value이다

아래의 2가지 예제를 통해 자세히 알아보자

(참조형 : 기본형 8가지를 제외한 나머지 타입)

public class JackCoding {
    String name;
 // getter, setter, constructor 생략..   
}

먼저 JackCoding이라는 클래스를 만들고 코드를 적어보자

public class Main {
    public static void main(String[] args) {
        JackCoding jack = new JackCoding("jack");
        System.out.println("jack: " + jack);
        System.out.println("jack.name: " + jack.getName());
        changeJackCoding(jack);
        System.out.println("jack: " + jack);
        System.out.println("jack.name: " + jack.getName());
    }

    static void changeJackCoding(JackCoding param) {
        param = new JackCoding("coding");
        System.out.println("param: " + param);
        System.out.println("param.name: " + param.getName());
    }
}
------------------- 결과 -------------------
jack: com.company.JackCoding@6cd8737
jack.name: jack
param: com.company.JackCoding@22f71333
param.name: coding
jack: com.company.JackCoding@6cd8737
jack.name: jack

먼저 name이 jack인 JackCoding 인스턴스를 생성한다

참조값은 JackCoding@6cd8737이고, 이름은 jack이다

 

그리고 changeJackCoding 메서드 매개변수로 위의 인스턴스를 넘겨준다

그리고 name이 coding인 JackCoding 인스턴스를 생성해서 param에 넣어준다

참조값은 JackCoding@22f71333이고, 이름은 coding이다

 

하지만, 최종적으로 마지막 두 라인을 보면

인스턴스 jack은 여전히 참조값이 JackCoding@6cd8737이고, 이름은 jack이다

 

왜 바뀌지 않았을까?

우선 참조형의 경우 값이 힙 영역에 저장되는 것을 이해해야 한다

(힙은 스택과 달리 프로세스 전역에서 공유하는 공간이고, 모든 객체들은 힙 영역에 저장된다)

그림을 보며 이해해보자

 

 

먼저 JackCoding 객체의 인스턴스 jack을 생성하면, 참조 변수와 참조값이 Stack에 생성되고,

실제 인스턴스 값은 Heap 영역에 생성된다

그리고 changeJackCoding() 메서드가 실행되면서

새로 coding이라는 이름을 가진 JackCoding 객체의 새로운 인스턴스를 생성한다

그리고 param에 넣는다

(만약 새로운 인스턴스를 생성하지 않았더라면, jack의 참조값이 param에 저장된다 이 경우는 아래 2번째 예제에서 볼 수 있다)

참조 변수 param에는 참조값이 저장되고 값은 Heap 영역에 저장된다

따라서 메서드를 빠져나왔을 때, jack 참조 변수는 기존에 자신이 보던 객체를 참조하고 있는 것이다

그렇다면 메서드에 참조값을 넘겨서 값을 변경할 수는 없을까?

있다

위의 예제를 변형하여 메서드 내에서 객체를 새로 생성하지 말고 참조값을 이용해보자

public class Main {

    public static void main(String[] args) {
        JackCoding jack = new JackCoding("jack");
        System.out.println("jack: " + jack);
        System.out.println("jack.name: " + jack.getName());
        changeJackCoding(jack);
        System.out.println("jack: " + jack);
        System.out.println("jack.name: " + jack.getName());
    }

    static void changeJackCoding(JackCoding param) {
        System.out.println("param: " + param);
        System.out.println("param.name: " + param.getName());
        param.setName("coding");
    }
}

--------- 결과 ---------
jack: com.company.JackCoding@6cd8737
jack.name: jack
param: com.company.JackCoding@6cd8737
param.name: jack
jack: com.company.JackCoding@6cd8737
jack.name: coding

위와 다른 점은 changeJackCoding() 메서드에서 새로운 인스턴스를 생성하지 않고,

기존에 받은 객체의 값을 변경하였다

어떻게 동작하는지 그림을 통해 이해해보자 

 

 

먼저 jack 인스턴스를 생성하면, 참조 변수 jack과 참조값 6cd8737이 스택 영역에 들어간다

그리고 객체 값은 힙 영역에 저장된다

changeJackCoding() 메서드가 실행되면 매개변수로 들어간 참조값 6cd8737이 복사되고

참조 변수 param과 참조값 6cd8737이 스택 영역에 들어간다 

결국 참조값이 같으므로 두 참조 변수는 Heap 영역의 같은 값을 바라보고 있다

따라서 아래처럼 param의 값을 변경하면 jack도 같은 값을 바라보고 있으니 변경되는 것이다

 

결국 자바는 기본형이든 참조형이든 모두 Call by value 방식이다

메서드의 매개변수는 모두 값이 복사되어 stack에 쌓인다

 

댓글