자바는 값마다 타입을 못 박는다. 파이썬이나 자바스크립트는 변수에 아무 값이나 담고 나중에 종류를 바꿔도 실행 전까지 조용하지만, 자바는 변수를 만들 때 어떤 종류의 값이 들어올지 타입을 먼저 정하고 어긋나는 값을 컴파일 단계에서 막는다. int age = 30;으로 선언한 변수에 age = "서른";을 대입하면 돌려 보기도 전에 거부한다. 초반엔 답답해도 프로젝트가 커질수록 실행 없이 상당수 실수를 걸러 주는 안전망이 된다.
![[Java 02] 변수와 기본 타입](https://img.thenullpage.com/posts/6446/6446_2_00cd59.webp)
타입은 원시 타입과 참조 타입으로 갈린다. 원시 타입(primitive)은 값 자체를 변수에 직접 담고, 참조 타입(reference)은 값이 놓인 위치인 참조를 담는다. 숫자, 참거짓, 문자 하나는 원시고 문자열, 배열, 우리가 만드는 모든 객체는 참조다. 원시는 여덟 개뿐이다. 정수는 byte(1), short(2), int(4), long(8바이트), 실수는 float(4), double(8바이트), 여기에 문자 하나의 char(2바이트)와 참거짓의 boolean이 더해진다. 특별한 이유가 없으면 정수는 int, 실수는 double을 쓰고, 21억을 넘길 것 같으면 long으로 간다.
int count = 100;
long population = 8_000_000_000L;
double pi = 3.14159;
char grade = 'A';
boolean passed = true;
리터럴에는 접미사 규칙이 붙는다. 정수 리터럴은 기본이 int라 그 범위를 넘는 값을 long에 넣을 때 끝에 L을 붙이고, 실수 리터럴은 기본이 double이라 float에 넣을 땐 f를 붙인다. 큰 수는 8_000_000_000L처럼 밑줄로 자리를 끊어 읽기 쉽게 써도 되며, 밑줄은 컴파일 때 무시되어 값에 영향이 없다.
원시는 값이 복사되고 참조는 주소가 복사된다. 원시 타입은 다른 변수에 넣으면 값이 통째로 복사되어 서로 남남이 되지만, 참조 타입은 주소만 복사되어 두 변수가 같은 객체를 가리킨다. 그래서 한쪽으로 객체 내부를 바꾸면 다른 쪽에서도 바뀌어 있다.
int x = 1;
int y = x; // 값 복사
y = 99; // x는 여전히 1
int[] a = {1, 2, 3};
int[] b = a; // 주소 복사, 같은 배열
b[0] = 99; // a[0]도 99가 된다
원시 타입에는 짝이 되는 래퍼 클래스가 있다. int에는 Integer, double에는 Double처럼 원시를 객체로 감싼 래퍼가 있다. 컬렉션(List, Map)은 객체만 담아 List<int>는 안 되고 List<Integer>여야 하며, 값 없음을 null로 표현할 때도 래퍼가 필요하다. 원시 값과 래퍼 사이 변환은 오토박싱, 언박싱으로 자동 처리되지만 대가가 있다. 박싱은 힙에 객체를 만들어 대량 반복 시 느려지고, null인 래퍼를 언박싱하면 그 순간 NullPointerException(널 참조 예외)이 터진다. 그래서 누적 변수는 래퍼가 아니라 원시 long으로 두어야 한다.
래퍼는 반드시 equals로 비교한다. 자바는 자주 쓰는 작은 정수 -128부터 127까지를 미리 캐시해 둔다. 이 범위의 Integer는 같은 객체라 ==가 우연히 참이 되지만, 128부터는 매번 새 객체라 값이 같아도 ==가 거짓이 된다.
Integer a = 127, b = 127;
System.out.println(a == b); // true (캐시)
Integer c = 128, d = 128;
System.out.println(c == d); // false
System.out.println(c.equals(d)); // true (정답)
참조 타입 비교는 항상 equals가 원칙이다. ==는 같은 객체인지를, .equals()는 값이 같은지를 묻는다. 문자열도 "abc" == new String("abc")는 거짓이고 .equals가 참이라, 값 비교는 무조건 equals로 못을 박는 게 안전하다. 원시끼리는 반대로 ==가 값 비교라 그대로 쓰면 된다.
var는 타입을 지우는 게 아니라 추론한다. JDK 10부터 지역 변수에 한해 var list = new ArrayList<String>();처럼 쓰면 오른쪽을 보고 컴파일러가 타입을 정한다. 자바스크립트의 var와 이름만 같을 뿐 여전히 정적 타입이라 한 번 정해지면 다른 종류를 못 넣는다. 지역 변수 전용이라 필드, 매개변수, 반환 타입엔 못 쓰고, var x;나 var x = null;처럼 추론할 오른쪽이 없으면 쓸 수 없다.
형변환에는 자동과 수동이 있다. 작은 그릇의 값을 큰 그릇에 담는 건 손실이 없어 자동으로 되지만, 큰 값을 작은 그릇에 담으면 값이 잘릴 수 있어 명시적 캐스팅을 적어야 한다. 특히 int 최댓값 약 21억에 1을 더하면 예외가 아니라 최솟값으로 한 바퀴 도는 오버플로가 조용히 일어난다. 넘칠 것 같으면 long이나 BigInteger를 쓴다.
int i = 100;
long big = i; // 자동 확대
double d = 3.99;
int t = (int) d; // 축소, 소수점 버림 → 3
long huge = 10_000_000_000L;
int overflow = (int) huge; // 범위 초과, 값 망가짐
실수 계산에는 오차가 깔려 있다. double과 float는 실수를 2진 부동소수로 근사해서, 십진수로 딱 떨어지는 값도 정확히 담지 못한다. 0.1 + 0.2는 0.3이 아니라 0.30000000000000004가 나온다. 화면 표시엔 대개 괜찮지만 돈 계산에선 사고가 되므로, 금액처럼 한 푼도 틀리면 안 되는 값은 BigDecimal로 다룬다.
System.out.println(0.1 + 0.2); // 0.30000000000000004
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3 (문자열로 만들어야 정확)
final은 재대입을 막는다. 변수 앞에 final을 붙이면 한 번 값을 넣은 뒤 다시 대입할 수 없어 상수를 만들 때 쓰며, 상수 이름은 관례상 MAX_SIZE처럼 대문자와 밑줄로 적는다. 단 참조 타입에 붙이면 참조만 못 바꿀 뿐 객체 내부는 얼리지 않는다. final int[] arr = {1,2,3};에서 arr = other;는 막히지만 arr[0] = 99;는 여전히 된다.
정수 나눗셈은 소수점을 버린다. int끼리 나누면 결과도 int라 소수점 아래가 버려져 5 / 2는 2.5가 아니라 2다. 실수 결과가 필요하면 한쪽을 실수로 만들어 5 / 2.0처럼 쓴다. %는 나머지를 주어 짝수 판별(n % 2 == 0)이나 순환 인덱스에 요긴하다.
System.out.println(5 / 2); // 2
System.out.println(5 / 2.0); // 2.5
System.out.println(7 % 3); // 1
char는 숫자이기도 하다. char는 문자 하나를 담지만 속으로는 유니코드 번호를 가진 정수라, char c = 'A'; 다음 int n = c;는 65가 되고 (char)(c + 1)은 'B'가 된다. 작은따옴표는 문자 하나 char, 큰따옴표는 문자열 String이라 'A'와 "A"는 타입부터 다르다.
다음 편에서는 연산자와 제어문, 문자열을 다룬다. 값의 타입을 봤으니 다음은 그 값으로 계산하고 흐름을 나누는 이야기다. 산술과 비교 연산자, if와 반복문, 함정 많은 String의 불변성과 StringBuilder, 값을 돌려주는 switch 표현식을 코드로 뜯어본다. 이번 편의 원시와 참조 구분은 문자열 비교에서 곧바로 다시 등장한다.