Python이나 JavaScript에서는 age = 25로 끝이지만, Java에서는 int age = 25라고 타입을 명시해야 한다. 타입이 안 맞으면 컴파일 자체가 안 된다. 이 엄격함이 불편하기만 한 건지, 아니면 이유가 있는 건지 — 자바의 타입 시스템을 처음부터 정리해보자.

변수 선언과 정적 타이핑

Java에서 변수를 만들려면 반드시 타입 을 먼저 써야 해요. 이걸 정적 타이핑(Static Typing) 이라고 합니다.

JAVA
int age = 25;           // 정수
double height = 175.5;  // 실수
String name = "Java";   // 문자열
boolean active = true;  // 참/거짓

정적 타이핑이 엄격한 데는 이유가 있어요. 타입을 컴파일 시점에 확정하기 때문에, 런타임에 터질 버그를 미리 잡을 수 있습니다. IDE도 타입 정보를 이용해서 자동 완성과 리팩토링을 지원해요. 대규모 코드베이스에서는 타입 자체가 문서 역할을 하기 때문에 팀원이 코드를 파악하기 쉬워집니다.

단점은 코드가 길어진다는 건데, Java 10부터 var 키워드가 도입되어 다소 완화됐습니다.

JAVA
var age = 25;            // 컴파일러가 int로 추론
var name = "Java";       // String으로 추론

var를 써도 내부적으로는 타입이 확정돼요. 동적 타이핑이 되는 건 아닙니다.

기본 타입 8가지 (Primitive Types)

Java에는 8개의 기본 타입이 있습니다. 이 8개가 전부이고, 나머지는 모두 참조 타입이에요.

분류타입크기범위 / 설명
정수byte1바이트-128 ~ 127
short2바이트-32,768 ~ 32,767
int4바이트약 ±21억
long8바이트매우 큰 정수
실수float4바이트소수점 약 7자리
double8바이트소수점 약 15자리
문자char2바이트유니코드 한 글자
논리boolean-true / false

실무에서는 대부분 int, long, double, boolean 이 네 개로 충분해요. byte/short는 메모리를 극도로 아껴야 하는 상황에서만 쓰고, floatdouble보다 정밀도가 낮아서 잘 안 씁니다.

리터럴 표기

숫자 리터럴에는 접미사와 표기법이 있어요.

JAVA
long big = 100L;            // long은 L 접미사 필수
float f = 3.14f;            // float은 f 접미사 필수
double d = 3.14;            // double은 기본
int million = 1_000_000;    // 언더스코어로 가독성 향상

longL을 빼먹으면 컴파일러가 int로 해석해요. 값이 int 범위를 넘으면 컴파일 에러가 나기 때문에 L 접미사를 잊으면 안 됩니다.

참조 타입 (Reference Types)

기본 8타입을 제외한 ** 나머지는 전부 참조 타입 **입니다. String, 배열, 컬렉션 모두 참조 타입에 해당해요.

기본 타입 vs 참조 타입 — 핵심 차이

기본 타입은 ** 값 자체 **를 변수에 저장합니다. 참조 타입은 ** 객체의 주소(레퍼런스)**를 저장해요. 이 차이 때문에 동작이 완전히 달라집니다.

JAVA
// 기본 타입: 값이 복사됨
int a = 10;
int b = a;
b = 20;
System.out.println(a); // 10 — a는 변하지 않음

값이 독립적으로 복사되기 때문에 b를 바꿔도 a에 영향이 없어요.

JAVA
// 참조 타입: 주소가 복사됨
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1;
arr2[0] = 99;
System.out.println(arr1[0]); // 99 — arr1도 바뀜!

arr2 = arr1은 같은 배열을 가리키는 주소를 복사한 거예요. 그래서 arr2를 통해 배열을 바꾸면 arr1으로 접근해도 바뀐 값이 보입니다.

PLAINTEXT
기본 타입:   a → [10]    b → [20]     ← 각각 독립된 값

참조 타입:   arr1 → ┐
                     ├→ [99, 2, 3]   ← 같은 배열을 가리킴
             arr2 → ┘

null과 NullPointerException

참조 타입 변수는 아무 객체도 가리키지 않을 수 있어요. 그 상태가 null입니다.

JAVA
String name = null;
System.out.println(name.length()); // NullPointerException!

null인 변수에 메서드를 호출하면 NullPointerException(NPE) 이 터져요. Java에서 가장 흔한 런타임 에러이므로, 참조 타입 변수를 사용하기 전에 null 여부를 확인하는 습관이 중요합니다.

형변환 (Type Casting)

자동 형변환 — 작은 타입에서 큰 타입으로

작은 타입의 값을 큰 타입에 넣으면 데이터 손실이 없기 때문에 자동으로 변환됩니다.

JAVA
int i = 100;
long l = i;       // int → long 자동 변환
double d = l;     // long → double 자동 변환

변환 방향: byte → short → int → long → float → double (char는 int로 합류)

강제 형변환 — 큰 타입에서 작은 타입으로

반대 방향은 데이터 손실이 생길 수 있기 때문에 명시적으로 캐스팅해야 해요.

JAVA
double d = 3.99;
int i = (int) d;     // 3 — 소수점 버림 (반올림 아님!)

long big = 300;
byte small = (byte) big; // 44 — 오버플로우!

소수점이 잘리거나, 범위를 넘어서 엉뚱한 값이 나올 수 있어요. 강제 형변환을 쓸 때는 데이터 손실 여부를 반드시 확인 해야 합니다.

문자열 ↔ 숫자 변환

JAVA
// 문자열 → 숫자
int num = Integer.parseInt("123");
double d = Double.parseDouble("3.14");

// 숫자 → 문자열
String s1 = String.valueOf(123);
String s2 = Integer.toString(123);

래퍼 클래스와 오토박싱

기본 타입은 객체가 아닙니다. 그래서 컬렉션(List, Map)에 직접 넣을 수 없어요.

JAVA
// List<int> list = new ArrayList<>(); // 컴파일 에러!
List<Integer> list = new ArrayList<>(); // Integer(래퍼)로 감싸야 함

이 문제를 해결하기 위해 각 기본 타입에 대응하는 래퍼 클래스 가 존재합니다 (int → Integer, long → Long, double → Double 등).

Java 5부터는 기본 타입과 래퍼 클래스 사이의 변환이 자동으로 이루어져요.

JAVA
Integer a = 10;        // 오토박싱: int → Integer
int b = a;             // 언박싱: Integer → int

편리하지만 주의할 점이 있어요. == 연산자는 래퍼 클래스에서 참조(주소) 비교 를 합니다.

JAVA
Integer x = 127;
Integer y = 127;
System.out.println(x == y);  // true — 캐시 범위 (-128~127)

Integer a = 128;
Integer b = 128;
System.out.println(a == b);  // false — 캐시 밖이라 다른 객체!
System.out.println(a.equals(b)); // true — 값 비교

Java는 -128~127 범위의 Integer를 캐싱해서 재사용하기 때문에, 이 범위 안에서는 ==true를 반환해요. 하지만 범위 밖에서는 서로 다른 객체가 생성되므로 ==false가 됩니다. 래퍼 클래스 비교는 항상 equals()를 쓰는 게 안전해요.

연산자

산술 연산자 — 정수 나눗셈 주의

JAVA
int a = 10, b = 3;
System.out.println(a / b);  // 3 — 소수점 버림!

정수끼리 나누면 소수점이 버려져요. 이건 초보자가 가장 많이 실수하는 부분입니다. 소수점 결과가 필요하면 피연산자 중 하나를 double로 바꿔야 해요.

JAVA
System.out.println((double) a / b); // 3.333...

단축 평가 (Short-circuit Evaluation)

&&||는 왼쪽 결과만으로 전체 결과가 확정되면 오른쪽을 실행하지 않아요. 이 특성을 이용하면 NPE를 방지할 수 있습니다.

JAVA
String name = null;

// name이 null이면 &&의 오른쪽은 실행 안 됨 → NPE 방지
if (name != null && name.length() > 0) {
    System.out.println(name);
}

namenull일 때 name.length()를 호출하면 NPE가 터지지만, && 왼쪽이 false이면 오른쪽을 아예 평가하지 않기 때문에 안전해요.

증감 연산자

JAVA
int i = 5;
System.out.println(i++); // 5 — 출력 후 증가 (후위)
System.out.println(++i); // 7 — 증가 후 출력 (전위)

i++(후위)와 ++i(전위)의 차이를 알아두되, 복잡한 표현식에서 섞어 쓰면 가독성이 떨어지므로 피하는 게 좋습니다.

상수 (final)

값이 변하면 안 되는 변수는 final을 붙여요. 한번 초기화되면 재할당이 불가능합니다.

JAVA
final int MAX_SIZE = 100;
// MAX_SIZE = 200; // 컴파일 에러!

관례적으로 상수는 ** 대문자 + 언더스코어 **로 씁니다 (MAX_SIZE, DEFAULT_VALUE).

주의할 점

1. 정수 나눗셈에서 소수점이 사라진다

10 / 33.333...이 아니라 3이 되는 건 직관에 반하는 동작이에요. 피연산자가 모두 정수이면 결과도 정수가 되기 때문입니다. ** 비율 계산, 평균 계산에서 이 실수가 자주 발생해요.** 피연산자 중 하나를 double로 캐스팅하면 됩니다.

2. Integer 캐시 범위를 벗어난 == 비교

위에서 다뤘지만, Integer 비교에서 ==를 쓰면 -128~127 범위에서만 올바르게 동작해요. 이 범위 밖에서는 같은 값이라도 false가 반환됩니다. 프로덕션에서 금액, ID 같은 큰 숫자를 ==로 비교하면 버그가 발생해요.

3. null 래퍼 클래스의 언박싱

JAVA
Integer value = null;
int result = value; // NullPointerException!

null인 래퍼 객체를 기본 타입으로 언박싱하면 NPE가 터져요. DB에서 null이 올 수 있는 컬럼을 Integer로 받은 뒤 int로 변환할 때 이 문제가 자주 발생합니다.

TIP 이 글의 코드 예제를 직접 실행해보고 싶다면 Java 기본기 핸드북을 확인해보세요.

정리

항목설명
정적 타이핑변수 선언 시 타입을 명시. 컴파일 시점에 타입 체크
기본 타입 8개int, long, double, boolean + byte, short, float, char
참조 타입기본 타입 제외 모든 것 (String, 배열, 컬렉션). 주소를 저장
형변환작은 → 큰은 자동, 큰 → 작은은 명시적 캐스팅 (데이터 손실 주의)
래퍼 클래스기본 타입의 객체 버전 (int → Integer). 오토박싱/언박싱
== vs equals()참조 타입 비교는 반드시 equals() 사용
varJava 10+ 지역 변수 타입 추론. 내부적으로는 타입 확정

다음 글에서는 제어문과 반복문을 다뤄요. if, switch, for, while — 코드의 흐름을 다루는 기본기를 정리할 예정입니다.

댓글 로딩 중...