자바는 항상 Call-By-Value(Pass-By-Value)로만 동작한다고 한다.

분명 자바를 처음 배울때는 Call-By-Value와 Reference-By-Value를 모두 사용한다고 배웠던거 같은데 어떻게 된건지 알아보겠다.

(Call-By-Value와 Pass-By-Value에서 Call과 Value는 거의 동일하게 사용한다고 하니 Call-By-Value로 통일하여 작성하기로 한다.)


일단 처음 Call-By-Value와 Reference-By-Value를 학습할 때 배우는 swap 예제를 살펴본다.

 

아래는 Call-By-Value의 예시로 작성된 코드이다.

a,b 변수를 swap 함수의 인자로 건네줘서 값을 바꿨지만 a, b 변수의 값은 그대로이다. 즉, a, b의 값만 복사(Call-By-Value)하여 x, y 변수가 지역변수로 새로 생성됐기 때문에 x와 y를 스왑하던, 값을 바꾸던 a, b 변수에는 영향이 없는 것이다. 

public class CallByTest {
    @Test
    public void callbyvalue(){
        int a = 10;
        int b = 20;

        System.out.println("swap() 호출 전 : a = " + a + ", b = " + b); // a = 10, b = 20
        swap(a, b);
        System.out.println("swap() 호출 후 : a = " + a + ", b = " + b); // a = 10, b = 20
    }

    public void swap(int x, int y){
        int temp = x;
        x = y;
        y = temp;
    }
}

 

그럼 Call-By-Reference 예제로 배웠던 swap 예제코드를 살펴본다.

swap 호출전후로 인자로 넘겼던 객체의 상태값이 바꼈으므로 Call-By-Reference의 예시로 알았던 코드이다.

public class CallByTest {
    @Test
    public void CallByReference(){
        Game a = new Game(10);
        Game b = new Game(20);

        System.out.println("swap() 호출 전 : a.star = " + a.star + ", b.star = " + b.star); // a.star = 10, b.star = 20
        swap(a, b);
        System.out.println("swap() 호출 후 : a.star = " + a.star + ", b.star = " + b.star); // a.star = 20, b.star = 10
    }

    public void swap(Game x, Game y){
        int temp = x.star;
        x.star = y.star;
        y.star = temp;
    }

    class Game {
        int star;
        public Game(int star) {
            this.star = star;
        }
    }
}

 

하지만, 진정한 Call-By-Reference는 참조하는 객체의 상태값을 변경하는게 아니라 실제 x,y 변수가 참조하는 객체를 변경하여 외부의 a 변수가 참조하는 객체를 바꿀수 있는 경우를 말한다.

자바는 Call-By-Value만 지원하므로 아래 코드에서는 a가 참조하는 객체가 realCallByReference() 함수에서 변경한 객체(star 값이 20인)로 변경되지 않고 여전히 10이 출력되는 걸 볼 수있다.

public class CallByTest {
    @Test
    public void test(){
        Game a = new Game(10);

        realCallByReference(a);

        System.out.println("a가 참조하는 객체는 바뀌지 않음 : " + a.star); // 10
    }

    public void realCallByReference(Game x){
        x = new Game(20);
    }
}

1. 함수 내에서 지역변수 x가 새로 생성되며 함수인자인 a와 같은 객체를 참조함.

    > Call-By-Value에 의해 a가 참조하는 객체에 대한 위치값만 전달해줬음.

2. realCallByReference 함수내에서 x가 참조하는 객체를 신규로 생성한 객체로 변경함.

    > a와 같은 객체를 참조하다가 새로운 객체로 변경되었으며 a에는 전혀 영향없음.

3. realCallByReference 함수 종료후 출력결과를 보면 a는 그대로 기존 객체를 참조하고 있음.

 


정리해보면

자바는 Call-By-Value만을 지원한다. 

처음 학습시 Call-By-Reference의 예제로 알고있던 swap코드는 참조하는 객체의 상태값을 바꾸는 것이지 참조하는 객체 자체를 바꾸는 것이 아니므로 진정한 Call-By-Reference의 예제는 아니다.

 

참고 StackOverflow

stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value

 

Is Java "pass-by-reference" or "pass-by-value"?

I always thought Java uses pass-by-reference. However, I've seen a couple of blog posts (for example, this blog) that claim that it isn't (the blog post says that Java uses pass-by-value). I don't ...

stackoverflow.com

 


 

 

 

자바에서 코드를 작성하며 String을 비교하는 경우는 매우 많습니다.

보통 int나 char 타입의 경우 '==' 비교를 많이 사용합니다만, String의 경우 대부분 == 비교를 사용하면 안됩니다.

 

== 과 .equals() 의 차이

  • == 비교는 참조하는 객체가 동일한지를 비교합니다. 
  • .equals() 비교는 값 자체가 동일한지를 비교합니다.

String 비교시 대부분의 경우는 값 자체가 동일한지를 비교하므로 == 비교가 아닌 .equals() 비교를 해야합니다.

 

간단한 테스트 코드를 보겠습니다.

// 값에 대한 비교이므로 동일
new String("test").equals("test") // true 

// new String()으로 새로운 객체를 생성하여 같은 객체끼리 동일한지 비교
// 서로 참조하는 객체가 다르므로 false
new String("test") == "test" // false 
new String("test") == new String("test") // false 

// "test" 스트링 리터럴은 컴파일러에 의해 interned 되므로
// 같은 객체끼리 비교하는 것이 됨
"test" == "test" // true 

// 스트링 리터럴은 컴파일러에 의해 조립되어("te" + "st" = "test") interend 되므로 
// 같은 객체끼리 비교하는 것이 됨
"test" == "te" + "st" // true

// Objects.equals()로 null을 포함하여 값 비교가 가능함
Objects.equals("test", new String("test")) // true
Objects.equals(null, "test") // false
Objects.equals(null, null) // true

string의 값 비교를 위해서는 .equals()나 Object.equals()를 사용할 수도 있고,

대소문자를 신경쓰지 않고 비교하는 String.equalsIgnoreCase() 같이 추가기능이 있는 비교메서드를 사용할 수 있습니다.

 

String intern

위 내용 중 string intern에 대해 추가설명을 드리자면,

자바는 JVM에 문자열 풀(pool)을 생성 후, 새로운 문자열이 사용되면 그 풀에 등록하여 놓습니다.

만일 그 문자열이 다시 사용되는 경우 새로 생성하는 것이 아닌 문자열 풀에서 꺼내 사용하는 것이지요. 왜냐하면 String 객체는 불변이기 때문에 동일한 객체를 계속 생성하는 것이 아니라 생성 후 공유하는 특징을 가지기 때문입니다.

 

String 객체에 대해 intern된 값을 가져올 수도 있는데요 이를위해 String 클래스의 intern()을 사용할 수 있습니다.

아래 테스트 코드를 보시죠.

 

str1과 str2는 String intern에 의해 JVM 문자열 풀의 같은 String 객체를 참조합니다. 

str1.intern() 은 JVM 문자열 풀에서 str1과 값이 동일한 String 객체를 가져옵니다. 만일 풀에 저장되어 있지 않다면 신규로 등록후 가져오게 되지요. 

str1.intern() 과 str2.intern()은 JVM 문자열 풀의 같은 객체를 가져오므로 동일합니다. 

 

str3과 str4는 new String()으로 생성된 각기 다른 String 객체를 참조합니다.

하지만 str1 ~ str4 의 intern() 호출은 결국 다 같은 JVM 문자열 풀의 같은 객체를 반환합니다.

public class StringComparison {
    @Test
    public void StringIntern(){
        String str1 = "하이";
        String str2 = "하이";

        System.out.println(str1 == str2); // true
        System.out.println(str1.equals(str2)); // true
        System.out.println(str1.intern() == str2.intern()); // true

        String str3 = new String("하이");
        String str4 = new String("하이");

        System.out.println(str3 == str4); // false
        System.out.println(str3.equals(str4)); // true
        System.out.println(str3.intern() == str4.intern()); // true
        System.out.println(str1.intern() == str4.intern()); // true
    }
}

 


 

 

자바 변수에는 생성한 오브젝트(객체)를 가리키는 참조형 타입의 변수가 있습니다.

아래 Integer 타입의 num변수를 선언 후 할당하는 경우입니다.

Integer num; // Integer 타입의 변수 num을 선언, 초기화 하지 않음
num = new Integer(10); // new 키워드를 사용해 Integer 객체를 생성후 num에게 참조시킴

첫째 줄은 Integer 타입의 num 변수를 선언했습니다. 참조형 변수를 선언만 했으므로 'null'로 초기화됩니다.

null 이란 참조형 변수가 아무것도 가리키지 않는 상태라는 의미입니다. 

두번째 줄에서 num에 세로운 Integer 객체를 생성하여 참조시켰습니다. 이로써 num은 null 에서 새로운 Integer 객체를 가리키도록(Integer 객체의 주소값이 할당) 변경되었습니다.

 

NullPointerException(아래부터는 줄여서 NPE라고 하겠습니다.)은 첫 번째 줄처럼 선언만 하고 아무것도 참조하지 않는 null 상태인 변수에서 내용을 참조하려 할 때 발생합니다.

 

내용을 참조한다는 것은 점(.)으로 객체 변수에 접근하는 것을 말합니다.

아래 코드는 null인 변수 k에 점(.)으로 indexOf() 함수를 호출하려 할 때 NPE가 발생합니다.

아무것도 가리키지 않는 상태인데 내부 내용에 접근하려 하니 오류가 나는 것이지요.

public void NPE_method1(){
    String k = null;
    k.indexOf(1);
}

 

컴파일러가 해당 참조변수는 아직 초기화 되지 않았다는 메시지를 표출하기도 하지만 아래와 같은 코드에서는 null이 넘어올지 초기화된 참조변수가 넘어올지 알 수 없습니다. 만일 doSomething(null); 로 호출하게 되면 NPE 오류가 나게됩니다. 

public void doSomething(SomeObject obj) {
   obj.myMethod();
}

 

이런 경우에는 어떻게 하여야 할까요?

제일 좋은 방법은 메서드 시작전에 obj가 초기화 되었는지 검증하는 것입니다.

doSomething 함수의 내부로직이 obj가 초기화 되었다는 것을 전제로 작성되었다면, 첫 줄에 null 값에 대한 검증 후 만일 null이 넘어온 경우 처리할 수 없다는 명시적인 메시지와 함께 종료하는 것이죠.

import java.util.Objects;

public class Test {
    public static void main(String[] args) {
        new Test().doSomething(null);
    }

    public void doSomething(Object obj){
        Objects.requireNonNull(obj, "obj는 null값이 아니여야 합니다.");
    }
}
Exception in thread "main" java.lang.NullPointerException: obj는 null값이 아니여야 합니다.

 

doSomething 내에서 obj에 null값을 허용하며 분기하여 처리할 수도 있습니다. 다음 코드를 참조해주세요.

public void doSomething(Object obj){
    if(obj == null){
        // obj가 null일 때의 로직
    }else {
        // obj를 이용한 로직
    }
}

 

 


 

 

+ Recent posts