자바에서 변수를 하나 선언하려면 타입부터 정해야 한다. 파이썬에서는 x = 10이라고만 쓰면 끝나지만 자바는 int x = 10;처럼 타입을 변수 이름 앞에 적어야 컴파일이 된다. 이 차이는 단순한 문법 스타일이 아니라 두 언어가 값을 저장하는 방식 자체가 다르다는 신호다. 자바의 타입은 크게 두 갈래로 나뉜다. 값 자체를 변수 안에 담는 기본 타입과, 값이 저장된 위치를 가리키는 참조 타입이다. 이 구분을 건너뛰면 두 변수를 비교했는데 결과가 이상하게 나오거나, 함수에 배열을 넘겼더니 원본까지 바뀌어 있는 상황에서 계속 헤매게 된다.


기본 타입 여덟 개. 자바는 byte, short, int, long, float, double, char, boolean이라는 여덟 가지 기본 타입(primitive type)을 언어 차원에서 미리 정해 놓았다. 이 여덟 개는 클래스가 아니라 값 그 자체이고 new 없이 바로 만들어진다. int는 정수, double은 소수, boolean은 참과 거짓, char는 문자 하나를 담는다. 기본 타입 변수를 다른 변수에 대입하면 값이 그대로 복사된다.


int a = 10;
int b = a;
b = 20;
System.out.println(a); // 10
System.out.println(b); // 20


b에 20을 넣어도 a는 그대로 10이다. b = a; 라는 문장이 실행되는 순간 a 안에 있던 값 10이 복사되어 b라는 별도의 공간에 들어갔을 뿐, 두 변수는 이후로 아무 관계가 없다.


타입 변환. 크기가 작은 타입은 자동으로 큰 타입에 담긴다. int를 long에 넣거나 int를 double에 넣는 건 컴파일러가 알아서 처리해 준다.


int i = 100;
long l = i; // 자동 변환, int에서 long으로
double d = i; // 자동 변환, int에서 double로
System.out.println(l); // 100
System.out.println(d); // 100.0


반대로 큰 타입을 작은 타입에 넣으려면 값을 잃을 수 있다는 걸 알고 있다는 표시로 괄호 안에 타입 이름을 직접 적어야 한다. 이걸 형변환(cast)이라 부른다.


double price = 9.99;
int rounded = (int) price;
System.out.println(rounded); // 9


(int) price는 소수점 아래를 반올림하지 않고 그냥 잘라 버린다. 9.99가 9로 바뀌는 걸 보면 형변환이 반올림과 다르다는 걸 알 수 있다. 반올림이 필요하면 Math.round(price)처럼 별도 메서드를 써야 한다.


참조 타입. 기본 타입 여덟 개를 뺀 나머지는 전부 참조 타입(reference type)이다. String, 배열, 직접 만든 클래스가 여기 속한다. 참조 타입 변수 안에는 값이 아니라 그 값이 실제로 저장된 메모리 위치가 들어간다. 대입은 이 위치 정보를 복사하는 것이라 두 변수가 같은 실체를 가리키게 된다.


int[] arr1 = {1, 2, 3};
int[] arr2 = arr1;
arr2[0] = 99;
System.out.println(arr1[0]); // 99
System.out.println(arr2[0]); // 99


arr2 = arr1; 로 배열 자체가 복사된 게 아니라 같은 배열을 가리키는 위치 정보만 복사됐다. 그래서 arr2를 통해 값을 바꾸면 arr1로 봐도 바뀐 값이 보인다. 파이썬의 리스트도 대입할 때 참조만 넘어가므로 동작이 비슷하다. 반대로 원본과 완전히 분리된 새 배열이 필요하면 arr1.clone()처럼 복사본을 따로 만들어야 한다.


var로 타입 추론. 자바 10부터는 var 키워드로 타입 이름을 생략할 수 있다.


var count = 10;
var name = "자바";
var list = new ArrayList<String>();


var는 동적 타입이 아니다. 컴파일러가 오른쪽 값을 보고 타입을 그 자리에서 한 번 확정하고, 그 뒤로는 int나 String을 직접 쓴 것과 똑같이 취급한다. count에 문자열을 다시 넣으려 하면 컴파일 에러가 난다.


var count = 10;
count = "문자열"; // 컴파일 에러: incompatible types


var는 초기값이 있어야만 쓸 수 있고 필드나 매개변수 자리에는 쓸 수 없다. new ArrayList<String>()처럼 오른쪽 코드만 봐도 타입이 뻔한 자리에 쓰면 중복을 줄여 준다. 반대로 타입이 한눈에 안 보이는 자리에 남발하면 코드를 읽는 사람이 타입을 추측해야 해서 오히려 가독성이 떨어진다.


오토박싱과 언박싱. 컬렉션은 기본 타입을 직접 담지 못한다. List<int>라는 문법은 존재하지 않는다. 그래서 자바는 기본 타입마다 짝이 되는 래퍼 클래스(wrapper class)를 만들어 뒀다. int의 짝은 Integer, double의 짝은 Double, boolean의 짝은 Boolean이다. 기본 타입 값을 래퍼 객체로 자동 변환하는 과정을 오토박싱, 그 반대를 언박싱이라 부른다.


List<Integer> nums = new ArrayList<>();
nums.add(10); // 오토박싱: int가 Integer로 변환되어 저장
int first = nums.get(0); // 언박싱: Integer가 int로 변환되어 반환
System.out.println(first); // 10


이 변환은 자동이라 평소엔 신경 쓸 일이 없지만 래퍼 객체끼리 ==로 비교할 때 함정이 있다.


Integer x = 100;
Integer y = 100;
System.out.println(x == y); // true


Integer p = 200;
Integer q = 200;
System.out.println(p == q); // false
System.out.println(p.equals(q)); // true


자바는 마이너스 128부터 127 사이의 Integer 값을 미리 만들어 캐시로 재사용한다. 그 범위 안에서는 x와 y가 우연히 같은 객체를 가리켜 ==가 true로 나오지만, 범위를 벗어나면 매번 새 객체가 만들어져 ==는 false다. Integer는 참조 타입이라 ==는 저장 위치를 비교하고 equals는 값을 비교한다. 범위에 따라 결과가 갈리는 비교는 신뢰할 수 없으니 래퍼 객체를 비교할 때는 항상 equals를 쓰는 편이 안전하다.


초기화 규칙. 메서드 안에서 선언한 지역변수는 초기화하지 않은 채로 쓰면 컴파일이 되지 않는다.


int x;
System.out.println(x); // 컴파일 에러: variable x might not have been initialized


반면 클래스의 필드는 선언만 해도 자동으로 기본값이 들어간다. 정수 계열은 0, boolean은 false, 참조 타입은 null이다.


class Counter {
int count;
String label;
}


Counter c = new Counter();
System.out.println(c.count); // 0
System.out.println(c.label); // null


지역변수는 컴파일러가 대신 챙겨 주지 않으니 선언과 동시에 값을 넣는 습관을 들이는 게 낫다. 필드는 기본값이 자동으로 들어가는 걸 믿고 넘어가다가, label처럼 참조 타입 필드가 null인 채로 남아 있을 때 그 값을 그대로 메서드 호출에 써 버리면 NullPointerException을 만난다.


이번 편에서 손에 쥐어야 할 감각은 세 가지다. 기본 타입은 값 자체를 담고 대입하면 값이 복사된다는 것, 참조 타입은 위치 정보를 담고 대입하면 같은 실체를 공유하게 된다는 것, 그리고 var는 타입을 대신 적어 줄 뿐 여전히 정적 타입이라는 것. 여기에 더해 래퍼 클래스를 ==로 비교하면 캐시 범위에 따라 결과가 갈린다는 함정 하나만 기억해 두면 초반에 자주 걸리는 자리는 대부분 피해 간다.