기본형과 참조형 — 값이냐 주소냐
Article
변수 두 개를 만들고, 하나를 다른 하나에 복사한 다음, 원본을 바꿔보자.
int a = 1;
int b = a;
a = 99;
System.out.println(b);
b 는 얼마일까. 1 이다. b 는 a 를 복사한 뒤로 자기 갈 길을 갔다. 당연해 보인다.
이번엔 똑같은 짓을 정수가 아니라 배열로 해보자. 배열은 9강에서 제대로 다루지만, 지금은 "값 여러 개를 담는 상자"라고만 알면 된다.
int[] a = { 1, 2, 3 };
int[] b = a;
a[0] = 99;
System.out.println(b[0]);
- 99
b 는 건드린 적도 없는데 값이 바뀌었다.
똑같이 int 를 다루는데 왜 한쪽은 따라 변하고 한쪽은 안 변할까. 이번 강의가 답한다. 그리고 이 답은 앞으로 이 시리즈에서 가장 자주 되돌아올 이야기다.
여덟 개, 그리고 나머지 전부
자바의 타입은 딱 두 부류로 갈린다. 먼저 기본형. 여덟 개뿐이고, 더 늘어나지 않는다.
| 타입 | 담는 것 | 크기 | 기본값 |
|---|---|---|---|
byte | 아주 작은 정수 | 1바이트 | 0 |
short | 작은 정수 | 2바이트 | 0 |
int | 정수 — 기본으로 쓴다 | 4바이트 | 0 |
long | 큰 정수 | 8바이트 | 0L |
float | 소수 | 4바이트 | 0.0f |
double | 소수 — 기본으로 쓴다 | 8바이트 | 0.0 |
char | 글자 하나 | 2바이트 | \u0000 |
boolean | 참 또는 거짓 | 명세에 없음 | false |
boolean 의 크기만 비어 있는 게 이상할 텐데, 오타가 아니다. 자바 명세는 boolean 이 몇 바이트인지 정하지 않았다. JVM 이 알아서 하라고 넘겼다. "1비트면 충분하지 않나" 싶지만 실제로는 그보다 크게 잡는 경우가 많다.
외울 필요는 없다. 실제로 쓰는 건 int, double, boolean, long 정도다. 정수는 int, 소수는 double 을 기본으로 쓰고, 넘칠 것 같을 때만 long 으로 바꾼다.
그리고 이 여덟 개가 아닌 것은 전부 참조형이다. String 도, 배열도, 앞으로 우리가 직접 만들 클래스도 전부.
기본형
primitive type · 여덟 개뿐
- 이름이 소문자로 시작한다
intdoublebooleanchar…- 상자에 값 자체가 들어 있다
- 기본값은
0이나false null을 넣을 수 없다
참조형
reference type · 나머지 전부
- 이름이 대문자로 시작한다
Stringint[]그리고 내가 만든 클래스- 상자에 주소가 들어 있다
- 기본값은
null null을 넣을 수 있다
대문자로 시작하면 참조형이라는 것. 2강에서 클래스 이름은 대문자로 시작한다고 했던 관례가 여기서 쓸모를 얻는다.
상자 안에 무엇이 들어 있나
기본형 변수는 상자 안에 값이 그대로 들어 있다.
int a = 1; 다음 int b = a;
- a
- 1
- b
- 1
참조형은 다르다. 값이 너무 크거나 개수가 정해지지 않아서 상자 안에 다 넣을 수가 없다. 그래서 값은 저 멀리 두고, 그 값이 있는 곳의 주소만 상자에 적어둔다.
int[] a = {1,2,3}; 다음 int[] b = a;
- a
- 0x1A4F
- → [ 1, 2, 3 ]
- b
- 0x1A4F
- → 같은 곳을 가리킨다
이제 맨 처음 코드가 설명된다. a[0] = 99 는 주소를 따라가서 그곳의 첫 칸을 바꾼다. b 도 같은 곳을 가리키고 있으니 b[0] 을 보면 당연히 99 다.
복사된 것은 값이 아니라 주소였다.
null — 아무 곳도 가리키지 않는다
참조형 상자에는 주소가 들어간다고 했다. 그럼 아직 아무 주소도 안 적은 상태는 무엇인가.
그것을 null 이라고 부른다.
String name = null;
System.out.println(name);
- null
찍어보는 것까지는 괜찮다. 문제는 그 주소를 따라가려 할 때 생긴다.
String name = null;
System.out.println(name.length());
- Exception in thread "main" java.lang.NullPointerException:
- Cannot invoke "String.length()" because "<local1>" is null
- at Main.main(Main.java:4)
NullPointerException. 자바를 쓰는 동안 가장 자주 만나게 될 이름이다. 줄여서 NPE 라고 부른다.
없는 주소를 따라가려 했다는 뜻이다. 빈 종이에 적힌 주소로 찾아간 셈이다.
메시지가 꽤 친절하다. String.length() 를 부르려 했는데 대상이 null 이었다고 콕 집어준다. <local1> 은 변수 이름 자리인데, javac 로 그냥 컴파일하면 변수 이름을 .class 에 남기지 않아서 저렇게 나온다. 개발 도구에서 실행하면 대개 "name" 이라고 진짜 이름이 찍힌다.
기본형에는 null 이 없다. int age = null; 은 컴파일조차 안 된다. 값을 담는 상자에는 "주소 없음"이라는 개념 자체가 성립하지 않는다.
String 은 기본형이 아니다
String 은 워낙 자주 쓰여서 기본형처럼 보인다. 하지만 대문자로 시작한다. 참조형이다.
String greeting = "안녕";
큰따옴표로 감싼 글자를 문자열 리터럴이라고 한다. 작은따옴표는 다르다. 글자 하나짜리 기본형인 char 다.
char c = 'A'; // 작은따옴표 — 기본형 char, 글자 하나
String s = "A"; // 큰따옴표 — 참조형 String, 글자 여러 개
String 이 참조형이라는 사실은 6강에서 아주 성가신 함정을 하나 만든다. 지금 살짝만 보여준다.
String a = new String("안녕");
String b = new String("안녕");
System.out.println(a == b);
- false
눈으로 보기엔 같은 글자인데 false 다.
== 는 상자 안에 든 것끼리 비교한다. 참조형 상자에 든 것은 주소였다. 두 글자는 서로 다른 곳에 만들어졌으니 주소가 다르고, 그래서 false 다.
이 이야기는 6강에서 제대로 한다. 지금은 "== 는 상자 안을 본다" 만 챙겨두자.
리터럴에 붙는 글자들
숫자를 그냥 적으면 자바는 정수는 int, 소수는 double 로 여긴다. 그래서 이런 일이 벌어진다.
long big = 10000000000; // 에러
float f = 3.14; // 에러
- error: integer number too large
- error: incompatible types: possible lossy conversion from double to float
100억 은 int 상자에 안 들어간다. 3.14 는 double 로 취급되는데 float 상자가 더 작아서 못 담는다.
뒤에 글자를 붙여 "이건 이 타입이다"라고 알려주면 된다.
long big = 10000000000L; // L — long 이다
float f = 3.14f; // f — float 이다
L 은 소문자 l 도 되지만 숫자 1 과 헷갈리니 대문자를 쓴다.
정리하면
기본형은 상자에 값을 담고, 참조형은 상자에 주소를 담는다. 그래서 복사할 때 결과가 달라진다.
- 01
기본형은 여덟 개뿐이다
소문자로 시작한다. 실제로는 int, double, boolean, long 을 주로 쓴다.
- 02
나머지 전부가 참조형이다
대문자로 시작한다. String, 배열, 내가 만들 클래스.
- 03
복사하면 상자 안의 것이 복사된다
기본형은 값이, 참조형은 주소가 복사된다.
int[] a = { 1, 2, 3 }; int[] b = a; // 주소를 복사 a[0] = 99; // b[0] 도 99 다 - 04
참조형의 기본값은
null이다따라가려 하면 NullPointerException 이 난다. 18강에서 다시 만난다.
- 05
==는 상자 안을 비교한다참조형끼리 비교하면 주소를 비교하는 셈이 된다. 6강에서 다룬다.
다음 강의에서는 이 값들로 계산을 해본다. 5 / 2 가 2 가 되는 언어를 만나본 적 있는지 모르겠다. 자바가 그렇다. 그리고 그건 버그가 아니다.