Theme:

Python이나 JavaScript에서 Java로 넘어오면 처음 느끼는 게 "왜 이렇게 타입을 일일이 써야 하지?"다. 변수 하나 만들 때마다 int, String, double을 붙여야 하고, 타입이 안 맞으면 컴파일부터 막힌다. 이게 불편하기만 한 건지, 아니면 이유가 있는 건지 — 자바의 타입 시스템을 처음부터 정리해보자.

변수 선언

Java에서 변수를 만들려면 타입을 먼저 써야 한다.

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

Python이라면 age = 25로 끝이지만, Java는 int age = 25로 타입을 명시한다. 이걸 **정적 타이핑(Static Typing)**이라고 한다.

왜 이렇게 엄격한가요?

정적 타이핑의 장점은 명확하다.

  • 컴파일 시점에 오류를 잡는다: 런타임에 터지는 것보다 훨씬 안전하다
  • IDE 자동 완성이 강력해진다: 타입을 알면 어떤 메서드를 쓸 수 있는지 IDE가 바로 알려줌
  • 대규모 코드베이스에서 유리하다: 팀 프로젝트에서 타입이 곧 문서 역할

단점은 코드가 길어진다는 것이지만, Java 10부터 var 키워드로 다소 완화됐다.

JAVA
var age = 25;            // 컴파일러가 int로 추론
var name = "Java";       // String으로 추론
var list = new ArrayList<String>(); // ArrayList<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 이 네 개가 주력이다.

JAVA
int count = 100;         // 일반적인 정수
long id = 123456789L;    // 큰 숫자 (L 접미사 필수)
double rate = 3.14;      // 소수점
boolean done = false;    // 참/거짓

byte, short는 메모리를 아껴야 하는 특수한 상황에서만 쓰고, floatdouble이 더 정확해서 잘 안 쓴다. char도 문자열(String)로 대체하는 경우가 많다.

리터럴(Literal) 표기

JAVA
int decimal = 100;          // 10진수
int hex = 0xFF;             // 16진수
int binary = 0b1010;        // 2진수
int million = 1_000_000;    // 언더스코어로 가독성 향상

long big = 100L;            // long은 L 접미사
float f = 3.14f;            // float은 f 접미사
double d = 3.14;            // double은 기본

char c = 'A';               // 작은따옴표
char unicode = '\uAC00';    // 유니코드 (가)

String s = "Hello";         // 큰따옴표 — 기본 타입이 아님!

참조 타입 (Reference Types)

기본 8타입을 제외한 나머지는 전부 참조 타입이다.

JAVA
String name = "Java";          // String은 참조 타입
int[] numbers = {1, 2, 3};     // 배열도 참조 타입
List<String> list = new ArrayList<>(); // 컬렉션도 참조 타입

기본 타입 vs 참조 타입의 차이

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

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

이걸 그림으로 보면 이해가 쉽다.

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

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

null

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

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

null인 변수에 메서드를 호출하면 **NullPointerException(NPE)**이 터진다. Java에서 가장 흔한 런타임 에러다.

형변환 (Type Casting)

자동 형변환 (묵시적 / Widening)

작은 타입에서 큰 타입으로는 자동 변환된다.

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

변환 방향:

PLAINTEXT
byte → short → int → long → float → double
              char ↗

강제 형변환 (명시적 / Narrowing)

큰 타입에서 작은 타입으로는 명시적으로 캐스팅해야 한다.

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 = 123 + "";   // 간편하지만 가독성 논란
String s3 = Integer.toString(123);

래퍼 클래스 (Wrapper Class)

기본 타입은 객체가 아니다. 그래서 컬렉션에 넣거나 null을 표현할 수 없다.

JAVA
// List<int> list = new ArrayList<>(); // 컴파일 에러!
List<Integer> list = new ArrayList<>(); // Integer(래퍼)로 감싸야 함
기본 타입래퍼 클래스
intInteger
longLong
doubleDouble
booleanBoolean
charCharacter
byteByte
shortShort
floatFloat

오토박싱 / 언박싱

Java 5부터 기본 타입 ↔ 래퍼 클래스 변환이 자동으로 된다.

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

List<Integer> list = new ArrayList<>();
list.add(42);          // 오토박싱
int value = list.get(0); // 언박싱

편리하지만 주의할 점이 있다.

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 — 값 비교는 equals()

==는 참조(주소) 비교이고, equals()가 값 비교다. 래퍼 클래스 비교는 항상 equals()를 쓰는 게 안전하다.

연산자

산술 연산자

JAVA
int a = 10, b = 3;

System.out.println(a + b);  // 13
System.out.println(a - b);  // 7
System.out.println(a * b);  // 30
System.out.println(a / b);  // 3 — 정수 나눗셈 (소수점 버림!)
System.out.println(a % b);  // 1 — 나머지

정수끼리 나누면 소수점이 버려진다는 게 초보자가 가장 많이 실수하는 부분이다.

JAVA
int a = 10, b = 3;
System.out.println(a / b);          // 3
System.out.println((double) a / b); // 3.333... — 하나를 double로 바꿔야 함

비교 / 논리 연산자

JAVA
// 비교 연산자
System.out.println(10 > 5);   // true
System.out.println(10 == 10); // true
System.out.println(10 != 5);  // true

// 논리 연산자
System.out.println(true && false); // false (AND)
System.out.println(true || false); // true  (OR)
System.out.println(!true);        // false  (NOT)

단축 평가 (Short-circuit Evaluation)

&&||는 왼쪽 결과만으로 확정되면 오른쪽을 실행하지 않는다.

JAVA
String name = null;

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

이 패턴은 NPE를 방지할 때 자주 쓰인다.

증감 연산자

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

int j = 5;
System.out.println(++j); // 6 — 증가 후 출력

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

상수 (final)

값이 변하면 안 되는 변수는 final을 붙인다.

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

final String GREETING = "Hello";
// GREETING = "Hi"; // 컴파일 에러!

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

타입 추론 (var)

Java 10부터 지역 변수에 var를 쓸 수 있다.

JAVA
var count = 10;                      // int
var name = "Java";                   // String
var list = new ArrayList<String>();  // ArrayList<String>
var map = Map.of("a", 1, "b", 2);   // Map<String, Integer>

쓸 수 있는 곳: 지역 변수, for-each 루프 변수

JAVA
for (var entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

쓸 수 없는 곳: 필드, 메서드 매개변수, 반환 타입

JAVA
// var field = 10;           // 클래스 필드에서는 불가
// public var getCount() {}  // 반환 타입에서도 불가

var를 쓸지 말지는 오른쪽에서 타입이 명확한지로 판단하면 된다.

JAVA
var count = 10;                       // 명확 → var OK
var result = someService.process();   // 타입 불명확 → 타입 명시가 나음

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

정리

  • Java는 정적 타이핑 — 변수 선언 시 타입을 명시하고, 컴파일 시점에 타입 체크
  • 기본 타입 8개: int, long, double, boolean + byte, short, float, char
  • 참조 타입: 기본 타입을 제외한 모든 것 (String, 배열, 컬렉션 등)
  • 형변환: 작은 → 큰은 자동, 큰 → 작은은 명시적 캐스팅 필요
  • 래퍼 클래스: 기본 타입을 객체로 감싸는 클래스 (intInteger)
  • == vs equals(): 참조 타입 비교는 equals()를 쓰자

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

댓글 로딩 중...