이 글은 Java의 data type와 call by value를 정리한 글이다. data type에 따라 function parameter로 넘기고 값을 조작했을 때 어떻게 바뀌는지, 그리고 특히 예외적으로 작동하는 string의 동작 방식에 대해 정리했다.
C++의 reference
C++에서는 reference라는 변수형을 지정할 수 있다. C++에서 reference는 pointer처럼 동작하지만 변수처럼 사용할 수 있는, 변수의 주소값을 저장하는 변수이다.
#include <iostream>
using namespace std;
int main(){
int number = 10;
int &reference = number;
cout<<"before"<<endl;
cout<<number<<endl; // 10
cout<<reference<<endl; // 10
reference = 15;
cout<<"after"<<endl;
cout<<number<<endl; // 15
cout<<reference<<endl; // 15
return 0;
}
위 코드를 예를 들어... number라는 변수를 reference라는 변수에 reference로 저장한다. 이후에 reference의 값을 바꾸어도 reference 변수는 number 변수의 주소값을 가지고 있기 때문에 number 변수의 값도 수정하게 된다.
Java data types
java의 변수 종류는 크게 primitive type과 reference type 2가지가 있다.
Primitive type
primitive type은 stack에 저장되는 데이터들이다. NULL이 될 수 없으며 NULL이 되기 위해서는 wrapper class를 따로 사용해 주어야 한다. 변수 선언 시 stack에 올라간다.
기본적인 자료형이 이에 들어가며 종류는 아래와 같다.
- 정수형
- byte
- char
- short
- int
- long
- 실수형
- float
- double
- 논리형
- boolean
변수 값 바꿔보기
public class App {
public static void main(String[] args) throws Exception {
int number = 10;
int ref = number;
System.out.println("before");
System.out.println(number); // 10
System.out.println(ref); // 10
ref = 15;
System.out.println("after");
System.out.println(number); // 10
System.out.println(ref); // 15
}
}
primitive type의 경우 변수마다 직접 값을 저장하고 있기 때문에 ref를 15로 바꾸어도 number는 변하지 않는다.
Reference type
reference type은 주소값을 가리키는 데이터들이다. primitive type이 아닌 모든 데이터들이며, reference type은 java.lang.Object를 상속받으며, NULL일 수 있다.
종류는 아래와 같다.
- Class
- Array
- Annotations
- Interface
- Enumeration
변수 값 바꿔보기
public class App {
public static void main(String[] args) throws Exception {
int[] numbers = new int[1];
numbers[0] = 10;
int[] ref = numbers;
System.out.println("before");
System.out.println(numbers[0]); // 10
System.out.println(ref[0]); // 10
ref[0] = 15;
System.out.println("after");
System.out.println(numbers[0]); // 15
System.out.println(ref[0]); // 15
}
}
reference type의 경우 변수의 주소를 저장하고 있다. numbers 변수는 heap에 할당된 int array의 주소를 가지고 있으며, ref 변수는 numbers 변수가 가리키는 값을 저장한다. 따라서 ref 변수로 접근해 array 값을 바꾸면 numbers 변수가 참조하는 array 값이 바뀌게 된다.
Reference type의 비교
primitive type에서 ==나 !=를 사용하면 해당 변수 안에 있는 값을 비교한다. reference type도 동일하다. 다만 reference type은 변수가 가리키는 object를 가지고 있는 것이 아니라 주소값을 가지고 있다. 즉 reference type의 ==나 !=는 두 reference type이 같은 object를 가리키는지 여부를 리턴해 준다.
Reference type, String
요상한 동작 방식
java에서 오직 string만이 다르게 동작한다. string은 reference type이지만 primitive type처럼 동작한다. (정확히는 선언 방법에 따라 다르다.) 이는 string literal이 immutable이기 때문이다. 아래 예시를 보자.
public class App {
public static void main(String[] args) throws Exception {
String str = "string";
String ref = str;
System.out.println("before : ");
System.out.println(str); // string
System.out.println(ref); // string
System.out.println(str == ref); // true
System.out.println(str.equals(ref)); // true
ref = "reference";
System.out.println("after : ");
System.out.println(str); // string
System.out.println(ref); // now it is reference
System.out.println(str == ref); // false
System.out.println(str.equals(ref)); // false
}
}
위 코드에서 class와 같이 reference type인 경우 ref="reference"로 바꾼다면 str도 바뀌어야 할 것 같다. 그러나 실제로는 string이 출력된다.
변수들이 가리키는 주소를 보자. 위 예시에서 ref="reference"를 하기 전까지는 str과 ref는 같은 메모리를 가리키고 있다. (str == ref라는 것에서 같은 주소값임을 확인) 그러나 ref="reference"를 하면 주소값이 다르게 바뀐다. 다른 객체를 가리키게 되는 것이다.
그 이유 : immutable이기 때문
String ref = new String("reference");
String ref = "reference";
String이 reference type임을 감안한다면 위 줄과 같이 new 연산자를 사용해서 새로운 string을 할당해 주어야 한다. 그러나 Java에서 아래와 같이 변수를 선언해도 선언된다. 위 두 줄은 동일하게 작동하는 것 같지만 string의 세부 구현 때문에 조금 다르게 동작한다.
모든 객체의 할당이 heap에 저장되는 class나 array 등과 다르게 string은 어떻게 선언하고 할당하느냐에 따라 메모리에 올라가는 방식이 다르다.
// 1. string 객체 생성
String str1 = new String("a");
String str1 = new String("a");
// ...
// "a"라는 string이 계속 생성됨
// 2. string literal로 string 생성
String str1 = "a";
String str2 = "a";
// ...
// 같은 string을 가리킴
// 3. string literal로 새로운 string 생성
String str2 = "b"; // string constant pool에 새로운 string을 생성하고 str2에 reference type으로 넣음
1.처럼 new String()을 사용할 경우 새로운 String 객체가 생성되기 때문에 heap에 올라간다.
2.처럼 = "reference"와 같이 string literal을 사용할 경우 string constant pool이라는 곳에서 intern() method로 해당 string이 있는지 검사한다. string constant pool에는 지금까지 할당한 모든 string의 정보들이 들어있다. string constant pool에 이미 할당되어 있는 string과 같은 내용의 string을 string literal로 선언한다면 해당 string을 참조해서 값을 가져온다. 없다면 새로 값을 만들고 해당 주소를 가져온다.
3.처럼 새로운 string을 literal로 생성한다면 string constant pool에 새로운 string을 생성하고 해당 주소를 가져온다.
이렇듯 string은 primitive type처럼 동작하게 되지만 실체는 reference type이다. 이는 string이 일부 값만을 골라 수정할 수 없는 immutable이기 때문에 string의 값을 바꾸려면 새로운 할당을 해 주어야 하기 때문이다. 새로운 할당 시 객체를 생성하든, string literal로 생성하든 다른 주소값을 가리키기 때문에 다른 값을 가리키는 변수가 된다.
마지막으로 확인해 보자.
public class App {
public static void main(String[] args) throws Exception {
System.out.println("----- Object -----");
String objectString1 = new String("object");
String objectString2 = new String("object");
System.out.println(objectString1 == objectString2); // false
System.out.println(objectString1.equals(objectString2)); // true
System.out.println("----- literal -----");
String literalString1 = "literal";
String literalString2 = "literal";
System.out.println(literalString1 == literalString2); // true
System.out.println(literalString1.equals(literalString2)); // true
System.out.println("----- Object string change value -----");
String objectStringReference = objectString1;
// objectStringReference[0] = "a"; // error : string is immutable
System.out.println("----- new literal -----");
objectString1 = "literal";
System.out.println(objectString1 == literalString1); // true
System.out.println(objectString1.equals(literalString1)); // true
}
}
Pass by value
Java는 모든 parameter를 pass by value로 사용하고 있다.
- primitive type을 parameter로 넘기면 해당 값을 복사해서 넘긴다.
- reference type도 동일하다. 다만 [reference type 안에 있는 값]을 복사하는 게 아니라, [reference type이 가리키는 주소]를 복사해서 보낸다.
primitive type의 경우
public class App {
private static void process(int integer){
System.out.println("----- in function -----");
System.out.println("before processing : " + integer);
integer = 10;
System.out.println("after processing : " + integer);
System.out.println("----- leaving function -----");
}
public static void main(String[] args) throws Exception {
int someInteger = 1;
System.out.println("value before function call : " + someInteger);
process(someInteger);
System.out.println("value after function call : " + someInteger);
}
}
primitive type의 경우 변수에 값을 저장하며 parameter로 넘길 때 해당 값을 복사해서 넘긴다. 따라서 function call 내부에서 값을 바꾸더라도 외부에는 영향을 주지 않는다.
reference type의 경우
public class App {
private static void process(int[] integerArray){
System.out.println("----- in function -----");
System.out.println("before processing : " + integerArray[0] + ", " + integerArray[1]); // 1, 2
integerArray[0] = 10;
integerArray[1] = 20;
System.out.println("after processing : " + integerArray[0] + ", " + integerArray[1]); // 10, 20
System.out.println("----- leaving function -----");
}
public static void main(String[] args) throws Exception {
int[] integerArray = new int[2];
integerArray[0] = 1;
integerArray[1] = 2;
System.out.println("value before function call : " + integerArray[0] + ", " + integerArray[1]); // 1, 2
process(integerArray);
System.out.println("value after function call : " + integerArray[0] + ", " + integerArray[1]); // 10, 20
}
}
integer array의 경우 reference type이며 call by value의 방법에 따라 parameter로 넘길 때 integerArray의 주소값을 복사해서 넘긴다. 따라서 function call 내부에서 값을 바꾸면 해당 메모리를 참조하는 모든 reference type variable에 영향을 미치게 된다.
public class App {
private static void process(String[] stringArray){
System.out.println("----- in function -----");
System.out.println("before processing : " + stringArray[0]); // value
stringArray[0] = "changed";
System.out.println("after processing : " + stringArray[0]); // changed
System.out.println("----- leaving function -----");
}
public static void main(String[] args) throws Exception {
String[] stringArray = new String[1];
stringArray[0] = "value";
System.out.println("value before function call : " + stringArray[0]); // value
process(stringArray);
System.out.println("value after function call : " + stringArray[0]); // changed
}
}
String array의 경우도 동일하다. 다만 sring의 경우 reference type이고, literal로 stringArray[0]에 값을 할당했기 때문에 stringArray[0]이 가리키는 주소값이 달라졌을 것이다.
string의 경우
public class App {
private static void processLiteral(String string){
System.out.println("----- in function -----");
System.out.println("before processing : " + string); // origin string
string = "changed string";
System.out.println("after processing : " + string); // changed string
System.out.println("----- leaving function -----");
}
private static void processObject(String string){
System.out.println("----- in function -----");
System.out.println("before processing : " + string); // origin string
string = new String("changed string");
System.out.println("after processing : " + string); // changed string
System.out.println("----- leaving function -----");
}
public static void main(String[] args) throws Exception {
String someString = "origin string";
System.out.println("value before function call : " + someString); // origin string
processLiteral(someString);
System.out.println("value after function call : " + someString); // origin string
System.out.println();
System.out.println("value before function call : " + someString); // origin string
processObject(someString);
System.out.println("value after function call : " + someString); // origin string
}
}
위에서 설명했듯 string은 reference type이지만 primitive처럼 작동한다.
'Development > Java' 카테고리의 다른 글
[Java] Polymorphism - static polymorphism, dynamic polymorphism, casting (0) | 2023.02.23 |
---|---|
[Java] Inheritance - access modifier, super, overriding (0) | 2023.02.23 |
[Java] static, final (0) | 2023.02.23 |
[Java] Class - constructor, access modifier, identity & equality & hashCode (2) | 2023.02.22 |
[Java] HttpURLConnection으로 HTTP 통신하기 (0) | 2022.09.13 |