Java의 기본 자료형에는 byte, short, int, long, char, float, double, boolean이 존재한다. 각각의 자료형은 다음과 같이 분류할 수 있다.
그 외 기본 자료형을 제외한 모든 자료형은 참조 자료형이라 한다. new를 사용해서 초기화한다. class, array 등이 이에 해당한다.
정수형 변수는 2진수로 표현할 때 LSB와 MSB를 통해 표기가 가능하다. LSB란 Least Significant Bit의 약자로, 가장 낮은 bit를 의미한다. 이 LSB 값을 통해 해당 변수가 홀수인지, 짝수인지 손쉽게 알아낼 수 있다. MSB란 Most Significant Bit의 약자로, 가장 최상위의 bit를 의미한다. 해당 비트를 통해서는 변수가 양수인지, 음수인지를 판별할 수 있다.
[참조] https://blog.naver.com/ansdbtls4067/220886567257
반면, 소수형 변수는 정수형 변수에 비해 2진수에서의 표현이 어렵다. IEEE 754의 부동 소수점을 표현하는 표준을 살펴보면 소수는 다음과 같이 표현된다.
(-1)*s : 소수의 부호부
c : 양의 정수로 표현된 기수부
b : 밑수(IEEE 754에서는 2 또는 10), 2일 경우 2진수, 10일 경우 10진수
q : 지수부
따라서 2진수로 소수를 저장하기 위한 float와 double은 위의 부호부, 기수부, 지수부를 저장할 공간을 필요로 한다. float와 double은 각각 32 byte, 64 byte의 저장 공간을 갖는다.
이때, float 변수로 지수부가 8비트를 넘는 수를 할당하거나 기수부가 23비트를 넘는 수를 할당하고자 한다면 저장할 수 있는 공간을 넘어서게 되어 해당 값의 정확성을 보장할 수 없게 된다. double도 마찬가지로 지수부가 11비트를 넘거나 기수부가 52비트를 넘는 수를 할당하게 되면 값의 정확성을 보장할 수 없게 된다.
실제로 아래의 코드를 따라 수행해보면 다음과 같은 결과를 얻을 수 있다.
public class CheckFloatDouble {
public static void main(String[] args) {
float f1 = 0.1f;
double d1 = 0.1;
System.out.format("The value of float 0.1 is %.23f\n", f1);
System.out.format("The value of double 0.1 is %.23f\n", d1);
}
}
23자리의 기수부를 저장할 수 있는 double은 정확히 0.1의 값을 출력하는 반면, 23자리의 기수부를 저장할 수 없는 float는 0.1이란 값을 정확히 보장해주지 못하고 있는 것을 알 수 있다.
따라서 정수형 변수와 달리 소수형 변수는 정밀도 문제에 대해 더욱 신경써주어야 한다.
call by value
call by value는 원본 데이터는 따로 보존되어 있고 함수의 호출 등이 발생했을 때 원본 데이터의 값만 copy해가는 경우에 해당한다. 따라서 함수가 종료된 후 원본 데이터의 값을 출력해보면 함수를 통해 변경된 값이 아닌 원래의 값을 출력한다.
call by reference
call by reference는 원본 데이터의 값을 직접 변경하는 경우에 해당한다. 따라서 call by value와 달리 호출 후 원본 데이터의 값을 출력해보면 함수를 통해 변경된 값을 출력한다.
Java가 call by value인지, call by reference인지 알기 위해서는 JVM의 동작 원리에 대해 파악하고 있어야 한다.
[참조] https://jackjeong.tistory.com/37
public class CallByValue {
public static void main(String[] args) {
int i1 = 1;
String s1 = "abc";
changeValue(i1, s1);
System.out.println(i1);
System.out.println(s1);
}
static void changeValue(int i, String s) {
i = 2;
s = "def";
System.out.println(i);
System.out.println(s);
}
}
Java의 기본 자료형은 전부 JVM의 stack 메모리 영역에 저장된다. 따라서 main 함수가 시작되며 String[] args 변수부터 차례로 stack에 변수들이 쌓이게 된다. 이후, changeValue 함수가 종료되면 함수의 지역변수인 i와 s는 stack에서 지워지게 된다. 기본 자료형을 매개변수로 넘겼을 때 원본 데이터인 i1, s1이 변경되지 않았으므로 call by value가 발생했음을 알 수 있다.
먼저, parameter로 사용될 Student class를 생성한다. 생성자와 name parameter에 대한 getter만 구현하였다.
public class Student {
String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
다음으로, Student 객체를 parameter로 받아 객체의 instance parameter를 변경하는 함수를 구현하고 해당 함수의 전후로 Student 객체의 값 변화에 대한 로그를 출력하게 했다. 그 결과 student 객체의 원본 값이 불변하고 있다는 사실을 알 수 있었다.
public class CallByValue2 {
public static void main(String[] args) {
Student std = new Student("hjkim");
System.out.println("std : " + std);
System.out.println("std.name : " + std.name);
changeValue2(std);
System.out.println("std : " + std);
System.out.println("std.name : " + std.name);
}
static void changeValue2(Student student) {
System.out.println("student : " + student);
System.out.println("student.name : " + student.name);
}
}
원본 데이터가 변하지 않았던 이유는 무엇일까? 해당 예제에서는 언뜻 보면 changeValue2의 파라미터로 객체의 주소를 넘겨주고 있어 call by reference인 것처럼 보인다. 하지만 이 주소는 객체의 실제 주소를 넘겨주고 있는 것이 아니라 그 변수가 가리키고 있는 heap 영역의 객체를 가르키는 새로운 지역변수를 생성한 것이다. 따라서 실제 주소값을 넘겨주고 있지 않아 원본 데이터가 보존되고, 이는 곧 call by value로 동작한다는 것을 알 수 있었다.
또 한 가지 알고 넘어갈 점은 Java의 참조 자료형은 기본 자료형과 달리 JVM의 heap 메모리 영역에 저장된다는 것이다. heap은 stack과 달리 process 전역에서 공유하는 공간이다. 참조 자료형에서 'new'라는 예약어를 사용하면 heap 영역에 해당 instance가 저장된다.
정리하면, Java는 call by value로 작동하며 C와는 다르게 메모리에 직접적으로 접근하여 데이터를 수정하는 것을 막고 있다.
[참조] https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value