Java 변수와 타입 | 기본 타입, 참조 타입, 형변환
이 글의 핵심
Integer 두 개를 ==로 비교했는데 어떤 값에서는 true, 어떤 값에서는 false가 나오는 현상처럼 타입을 모르면 설명하기 어려운 버그가 생깁니다. 기본 타입과 참조 타입이 메모리에서 다르게 다뤄지는 이유, 명시적 형변환에서 값이 잘리는 함정, 오토박싱 비용을 짚어 타입 선택 기준을 세우게 합니다.
들어가며
Java는 정적 타입 언어(변수마다 타입을 미리 적어 두며, 소스를 바이트코드로 바꿀 때 검사하는 방식)이므로, 변수에는 컴파일러가 검사할 수 있는 타입(명찰)을 붙입니다. 기본형과 참조형을 구분해 두면 이후 컬렉션·객체와 연결하기 쉽습니다.
Java의 타입은 크게 두 갈래입니다. int, double 같은 기본 타입 8개는 변수 안에 값 자체가 들어 있고, 그 외의 모든 것(String, 배열, 직접 만든 클래스)은 참조 타입이라서 변수 안에는 힙에 있는 객체를 가리키는 참조가 들어 있습니다. 이 차이가 ==의 의미, null 가능 여부, 메서드에 넘겼을 때의 동작, 컬렉션에 넣을 수 있는지까지 결정합니다. Java가 “모든 것이 객체”인 언어가 아닌 이유는 성능입니다. 숫자 하나하나를 객체로 만들면 객체 헤더(보통 12~16바이트)와 GC 부담이 붙기 때문에, 자주 쓰는 숫자와 불리언은 기본 타입으로 남겨 두었습니다. 대신 이 이중 구조 때문에 오토박싱이라는 편의 기능과 그에 따른 함정이 생겼고, 이 글 후반에서 그 부분을 자세히 다룹니다.
기본 타입 (Primitive Types)
정수 타입
// byte: -128 ~ 127
byte b = 127;
// short: -32,768 ~ 32,767
short s = 32767;
// int: -2,147,483,648 ~ 2,147,483,647 (기본)
int i = 2147483647;
// long: -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
long l = 9223372036854775807L; // L 접미사 필수
// 언더스코어 사용 가능 (가독성)
int million = 1_000_000;
long billion = 1_000_000_000L;
정수 타입 선택 가이드: 일반 비즈니스 로직에서는 int가 기본입니다. 파일 크기·타임스탬프처럼 범위가 큰 값은 long, 메모리가 극도로 제한된 바이너리 포맷이나 대량 배열에서는 byte/short를 고려합니다. L 접미사를 빼먹으면 리터럴이 int로 처리되어 범위를 넘을 때 컴파일 에러(integer number too large)가 납니다. 소문자 l도 허용되지만 숫자 1과 구분이 어려워 대문자 L을 쓰는 것이 관례입니다.
더 위험한 것은 연산 중 오버플로입니다. Java의 정수 연산은 범위를 넘어도 예외 없이 조용히 반대쪽 끝으로 넘어갑니다. Integer.MAX_VALUE + 1은 -2147483648이 됩니다. 또 long ms = 24 * 60 * 60 * 1000 * 30;처럼 결과를 long에 담아도, 오른쪽 계산은 모두 int끼리 먼저 이뤄지므로 long에 대입되기 전에 이미 오버플로가 일어나 음수가 됩니다. 첫 번째 피연산자를 24L로 바꿔야 전체가 long으로 계산됩니다. 오버플로를 반드시 감지해야 하는 금액 계산 같은 곳에서는 Math.addExact, Math.multiplyExact를 쓰면 넘칠 때 ArithmeticException을 던져 줍니다.
참고로 byte와 short는 연산하는 순간 int로 승격됩니다. 그래서 byte a = 1, b = 2; byte c = a + b;는 incompatible types: possible lossy conversion from int to byte 에러가 납니다. byte를 쓴다고 계산이 빨라지는 것도 아니므로, 파일 포맷이나 네트워크 버퍼처럼 바이트 단위 데이터를 다룰 때가 아니면 int를 쓰는 것이 자연스럽습니다.
실수 타입
// float: 32비트 부동소수점
float f = 3.14f; // f 접미사 필수
// double: 64비트 부동소수점 (기본)
double d = 3.14159;
double scientific = 1.23e-4; // 0.000123
부동소수점 주의: float와 double은 이진 부동소수점이라 0.1 + 0.2 같은 식이 기대와 다르게 보일 수 있습니다. 그래도 지리·그래픽·통계 전처리 등에서는 여전히 널리 사용되며, 정확한 십진 연산이 필요하면 BigDecimal로 전환하는 것이 안전합니다.
실제로 System.out.println(0.1 + 0.2);를 실행하면 0.30000000000000004가 출력됩니다. 0.1은 이진수로 정확히 표현할 수 없는 무한 소수라 가장 가까운 근삿값이 저장되기 때문입니다. 그래서 부동소수점 값은 ==로 비교하면 안 되고, Math.abs(a - b) < 1e-9처럼 허용 오차로 비교해야 합니다. 금액 계산에 double을 쓰면 1원 단위 오차가 누적되어 정산이 맞지 않는 문제가 생기는데, 이때 BigDecimal을 쓸 때도 new BigDecimal(0.1)이 아니라 new BigDecimal("0.1")이나 BigDecimal.valueOf(0.1)을 써야 합니다. double 생성자는 이미 오차가 섞인 값을 그대로 받아서 0.1000000000000000055511151231257827...이 됩니다.
float의 f 접미사가 필수인 이유는 Java에서 소수 리터럴의 기본 타입이 double이기 때문입니다. float f = 3.14;는 64비트 값을 32비트에 넣는 축소 변환이라 possible lossy conversion from double to float 에러가 납니다. float는 유효 자릿수가 약 7자리라 정밀도가 금방 부족해지므로, 메모리를 아껴야 하는 대량 배열(그래픽 버텍스 등)이 아니라면 double이 기본 선택입니다.
문자 타입
char c = 'A';
char korean = '가';
char unicode = '\u0041'; // 'A'
// char는 16비트 유니코드
char는 정확히 말하면 “유니코드 문자 하나”가 아니라 UTF-16 코드 유닛 하나입니다. 한글 ‘가’는 16비트 안에 들어가지만, 이모지(😀)나 일부 한자처럼 코드 포인트가 U+FFFF를 넘는 문자는 char 두 개(서로게이트 쌍)로 표현됩니다. 그래서 이모지 한 글자를 char 리터럴로 쓰면 컴파일 에러가 나고, "😀".length()는 1이 아니라 2입니다. 사용자 입력 글자 수를 세거나 문자열을 자를 때 length()와 substring()을 그대로 쓰면 이모지가 반으로 잘려 깨진 문자가 나올 수 있습니다. 문자 단위로 정확히 다뤄야 한다면 codePointCount()나 codePoints() 스트림을 씁니다. 또 char는 숫자 타입이기도 해서 'A' + 1은 문자 ‘B’가 아니라 정수 66입니다.
불리언 타입
boolean flag = true;
boolean isActive = false;
// 조건식 결과
boolean isAdult = age >= 18;
boolean과 널: 기본 타입 boolean은 null을 가질 수 없지만, 래퍼 Boolean은 null이 가능해 NPE(NullPointerException) 위험이 생깁니다. API 설계 시 “세 가지 상태(참/거짓/미정)”가 필요하면 Optional<Boolean>·별도 enum을 검토하세요.
참조 타입 (Reference Types)
String
// 리터럴 (String Pool)
String name1 = "홍길동";
String name2 = "홍길동";
System.out.println(name1 == name2); // true (같은 객체)
// new 키워드
String name3 = new String("홍길동");
System.out.println(name1 == name3); // false (다른 객체)
System.out.println(name1.equals(name3)); // true (값 비교)
== 와 equals: 리터럴로 만든 동일 문자열은 풀에서 재사용될 수 있어 ==가 true로 나올 수 있지만, 일반적인 비교는 항상 equals가 안전합니다. 특히 사용자 입력·DB에서 읽은 문자열은 ==에 의존하면 버그로 이어지기 쉽습니다.
String 풀(String Pool)은 JVM이 소스 코드의 문자열 리터럴을 한 번만 만들어 공유하는 저장소입니다. name1과 name2는 같은 리터럴이라 같은 객체를 가리키고, new String(...)은 풀과 별개로 새 객체를 강제로 만듭니다. 같은 원리로 "홍" + "길동"처럼 컴파일 시점에 결정되는 상수 연결도 풀의 객체가 되지만, 변수를 섞은 first + last는 실행 중에 새 객체가 만들어집니다. 그래서 “테스트에서는 ==가 잘 됐는데 실제 입력에서는 안 된다”는 현상이 나옵니다. 제가 Java를 처음 배울 때 흔히 봤던 실수도 이것인데, 테스트 코드에는 리터럴을 쓰니 ==가 우연히 통과하고, 실제로는 Scanner로 읽은 문자열이라 항상 false가 됩니다.
equals를 쓸 때도 순서가 중요합니다. input.equals("yes")는 input이 null이면 NullPointerException이 나지만, "yes".equals(input)은 그냥 false를 반환합니다. 둘 다 null일 수 있다면 Objects.equals(a, b)가 안전합니다. 또 String은 불변(immutable) 객체라서 name.toUpperCase()는 name을 바꾸지 않고 새 문자열을 반환합니다. 결과를 변수에 다시 받지 않으면 아무 일도 일어나지 않은 것처럼 보입니다.
배열
// 선언 및 초기화
int[] numbers = {1, 2, 3, 4, 5};
int[] arr = new int[5];
// 접근
System.out.println(numbers[0]); // 1
numbers[0] = 10;
// 길이
System.out.println(numbers.length); // 5
// 다차원 배열
int[][] matrix = {
{1, 2, 3},
{4, 5, 6}
};
System.out.println(matrix[0][1]); // 2
배열 실무 노트: Java 배열은 크기가 고정이라, 동적 증가가 잦은 경우 ArrayList 등 컬렉션이 낫습니다. 다차원 배열은 “배열의 배열”이라 행마다 길이가 달라도 되지만, 직사각형이 아닌 구조는 루프 작성 시 주의가 필요합니다.
new int[5]로 만든 배열은 모든 원소가 기본값(숫자는 0, boolean은 false, 참조 타입은 null)으로 채워집니다. 지역 변수는 초기화하지 않으면 컴파일 에러(variable might not have been initialized)가 나지만, 배열 원소와 필드는 자동으로 기본값을 갖는다는 점이 다릅니다. String[] names = new String[3]; 직후 names[0].length()를 호출하면 NullPointerException이 나는 이유가 이것입니다.
배열은 참조 타입이므로 int[] copy = numbers;는 복사가 아니라 같은 배열을 가리키는 참조를 하나 더 만드는 것입니다. copy[0] = 99를 하면 numbers[0]도 99가 됩니다. 실제 복사본이 필요하면 numbers.clone()이나 Arrays.copyOf(numbers, numbers.length)를 씁니다. 다차원 배열의 clone()은 바깥 배열만 복사하고 안쪽 행은 공유하는 얕은 복사라는 점도 주의해야 합니다. 범위를 벗어난 인덱스는 C처럼 조용히 넘어가지 않고 ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5로 즉시 실패합니다. 배열을 출력할 때 System.out.println(numbers)는 [I@1b6d3586 같은 타입과 해시 코드만 보여 주므로 Arrays.toString(numbers)를 써야 합니다.
형변환 (Type Casting)
자동 형변환 (Widening)
// 작은 타입 → 큰 타입 (자동)
byte b = 10;
int i = b; // OK
long l = i; // OK
float f = l; // OK
double d = f; // OK
// 순서: byte → short → int → long → float → double
자동 형변환은 “값의 범위가 더 넓은 쪽”으로만 일어나기 때문에 컴파일러가 허락해 줍니다. 그런데 범위가 넓다고 해서 정밀도까지 보장되는 것은 아닙니다. long(64비트 정수)에서 float(유효 자릿수 약 7자리)로, int에서 float로 가는 변환은 자동으로 허용되지만 큰 값에서는 뒷자리가 사라집니다. 예를 들어 int big = 123_456_789; float f = big;의 f를 다시 int로 바꾸면 123456792가 됩니다. 컴파일러가 경고도 하지 않으므로, 큰 정수를 부동소수점으로 옮길 일이 있다면 double(유효 자릿수 약 15~16자리)을 쓰는 편이 안전합니다.
char는 이 순서에서 별도 경로에 있습니다. char는 int로 자동 변환되지만, byte나 short와 char 사이에는 자동 변환이 없습니다. char는 부호 없는 16비트이고 short는 부호 있는 16비트라 서로의 범위를 온전히 담지 못하기 때문입니다.
명시적 형변환 (Narrowing)
// 큰 타입 → 작은 타입 (명시적)
double d = 3.14;
int i = (int) d; // 3 (소수점 버림)
long l = 1000L;
int i2 = (int) l;
// 주의: 데이터 손실 가능
int big = 130;
byte small = (byte) big; // -126 (오버플로우)
(int) 3.14가 3이 되는 것은 반올림이 아니라 0 방향으로 버림이기 때문입니다. 그래서 (int) -3.7은 -4가 아니라 -3입니다. 반올림이 필요하면 Math.round(), 내림은 Math.floor()를 써야 합니다.
(byte) 130이 -126이 되는 과정은 비트로 보면 명확합니다. 130은 이진수로 1000 0010인데, byte로 자르면 하위 8비트만 남고 맨 앞 비트가 부호 비트로 해석되어 -126이 됩니다. 정수끼리의 축소 변환은 이렇게 상위 비트를 그냥 잘라 버리고 예외를 던지지 않습니다. 반면 double을 int로 바꿀 때 범위를 넘으면 잘리지 않고 Integer.MAX_VALUE나 MIN_VALUE로 고정되며, NaN은 0이 됩니다. 규칙이 타입마다 다르므로, 외부 입력값을 축소 변환해야 한다면 먼저 범위를 확인하거나 범위를 넘을 때 예외를 던지는 Math.toIntExact(long)을 쓰는 것이 좋습니다.
래퍼 클래스 (Wrapper Classes)
기본 타입 vs 래퍼 클래스
| 기본 타입 | 래퍼 클래스 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
래퍼 클래스가 필요한 이유는 Java의 제네릭과 컬렉션이 객체만 다룰 수 있기 때문입니다. List<int>는 쓸 수 없고 List<Integer>를 써야 합니다. 또 래퍼는 null을 가질 수 있어서 “값이 없음”을 표현할 수 있는데, 이것이 장점이자 NPE의 원인이 됩니다. DB의 NULL 허용 컬럼을 JPA 엔티티에 매핑할 때 int 대신 Integer를 쓰는 것이 대표적인 예입니다. int로 매핑하면 NULL을 읽을 때 0이 되거나 예외가 나서, “값이 0”과 “값이 없음”을 구분할 수 없습니다.
오토박싱/언박싱
Java 5부터 기본 타입과 래퍼 클래스 간 자동 변환이 지원됩니다:
// 오토박싱 (Auto-boxing): 기본 타입 → 래퍼 클래스
Integer obj = 10;
// 내부 동작: Integer obj = Integer.valueOf(10);
// 기본 타입 int를 자동으로 Integer 객체로 변환
// 언박싱 (Unboxing): 래퍼 클래스 → 기본 타입
int primitive = obj;
// 내부 동작: int primitive = obj.intValue();
// Integer 객체를 자동으로 int로 변환
// 자동 변환 예시
Integer a = 10; // 오토박싱
Integer b = 20; // 오토박싱
Integer sum = a + b; // 언박싱 → 연산 → 오토박싱
// 내부 동작:
// 1. a.intValue() + b.intValue() (언박싱)
// 2. 10 + 20 = 30 (int 연산)
// 3. Integer.valueOf(30) (오토박싱)
// 컬렉션에서 자동 변환
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // 오토박싱: int → Integer
int first = numbers.get(0); // 언박싱: Integer → int
// 주의: null 언박싱 시 NullPointerException
Integer nullValue = null;
// int x = nullValue; // NullPointerException!
// null을 기본 타입으로 변환할 수 없음
// 안전한 처리
Integer value = getValue(); // null 가능성
int result = (value != null) ? value : 0; // null 체크
언박싱 NPE가 특히 찾기 어려운 이유는 코드에 역참조가 보이지 않기 때문입니다. int x = map.get("key");처럼 평범한 대입문에서 NullPointerException이 나는데, 코드만 보면 메서드 호출이 없어 보입니다. 컴파일러가 몰래 넣은 .intValue() 호출에서 터진 것입니다. 최근 JDK(14 이상)는 “Helpful NullPointerExceptions” 기능으로 Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null처럼 원인을 알려 주므로, 이 메시지를 보면 언박싱을 의심하면 됩니다. Map이라면 map.getOrDefault("key", 0)으로 이 문제를 미리 피할 수 있습니다.
삼항 연산자도 조심해야 합니다. Integer r = flag ? count : null;은 안전해 보이지만, count가 int라면 삼항 연산식 전체가 int로 결정되어 null을 언박싱하려다 NPE가 날 수 있습니다. 두 피연산자의 타입을 모두 Integer로 맞추면 해결됩니다.
오토박싱 성능 주의:
// ❌ 나쁜 예: 반복문에서 오토박싱
Integer sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i; // 매 반복마다 언박싱 + 박싱 발생
}
// 1000번의 불필요한 객체 생성
// ✅ 좋은 예: 기본 타입 사용
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i; // 기본 타입 연산 (빠름)
}
Integer result = sum; // 마지막에 한 번만 박싱
Integer는 불변 객체라서 sum += i는 기존 객체의 값을 바꾸는 것이 아니라 Integer.valueOf(sum.intValue() + i)로 새 객체를 만들어 참조를 바꿉니다. -128~127 범위는 캐시된 객체를 재사용하지만 그 이상은 매번 새로 할당되므로, 이 루프는 수백 개의 짧게 사는 객체를 만듭니다. 1000번 정도는 JIT 최적화(탈출 분석)로 체감되지 않을 수 있지만, 수백만 번 도는 집계 루프나 List<Integer>로 대량 데이터를 다루는 코드에서는 GC 부담과 캐시 효율 저하가 눈에 띄게 드러납니다. 숫자 스트림을 쓸 때 Stream<Integer> 대신 IntStream을 쓰는 것도 같은 이유입니다.
래퍼 클래스 메서드
// 문자열 → 숫자
int num = Integer.parseInt("123");
double d = Double.parseDouble("3.14");
// 숫자 → 문자열
String str = Integer.toString(123);
String str2 = String.valueOf(123);
// 비교
Integer a = 100;
Integer b = 100;
System.out.println(a.equals(b)); // true
// 최대/최소값
System.out.println(Integer.MAX_VALUE); // 2147483647
System.out.println(Integer.MIN_VALUE); // -2147483648
parseInt와 valueOf는 비슷해 보이지만 반환 타입이 다릅니다. Integer.parseInt("123")은 기본 타입 int를, Integer.valueOf("123")은 Integer 객체를 반환합니다. 계산에 쓸 값이라면 parseInt가 불필요한 박싱을 피합니다. 둘 다 앞뒤 공백을 허용하지 않아서 Integer.parseInt(" 123")은 NumberFormatException: For input string: " 123"이 나므로 사용자 입력은 trim() 후 파싱합니다.
비교 예제에서 a == b가 아니라 a.equals(b)를 쓴 것도 의도적입니다. 100은 캐시 범위라 ==도 true지만, 1000으로 바꾸면 ==는 false가 됩니다(아래 FAQ 참고). Integer와 Long을 equals로 비교하면 값이 같아도 타입이 달라 false라는 함정도 있습니다. Long.valueOf(1).equals(1)은 1이 Integer로 박싱되어 false가 됩니다.
var (Java 10+)
// 타입 추론
var name = "홍길동"; // String
var age = 25; // int
var price = 19.99; // double
var list = new ArrayList<String>();
// 주의: 초기화 필수
// var x; // 에러!
var는 동적 타입이 아닙니다. 컴파일러가 오른쪽 식을 보고 타입을 한 번 결정해 고정하므로, var age = 25; 다음에 age = "스물다섯";을 대입하면 컴파일 에러가 납니다. 바이트코드에는 추론된 실제 타입이 기록되므로 성능 차이도 없습니다.
쓸 때 주의할 점은 몇 가지입니다. var 는 지역 변수에만 쓸 수 있고 필드, 메서드 매개변수, 반환 타입에는 쓸 수 없습니다. var x = null;은 타입을 추론할 수 없어 에러이고, var list = new ArrayList<>();처럼 다이아몬드 연산자와 함께 쓰면 ArrayList<Object>로 추론되어 타입 안전성이 사라집니다. 또 var n = 10;은 int, var price = 19.99;는 double로 추론되므로 long이나 float가 필요하면 10L, 19.99f처럼 리터럴로 표시해야 합니다. 오른쪽만 보고 타입이 분명할 때(new, 팩토리 메서드) var를 쓰고, var result = service.process();처럼 타입이 드러나지 않는 곳에서는 명시하는 것이 읽는 사람을 배려하는 방법입니다.
타입 변환과 배열 처리 예제
타입 변환
public class TypeConversion {
public static void main(String[] args) {
// 문자열 → 숫자
String input = "123";
int num = Integer.parseInt(input);
// 안전한 변환
try {
int result = Integer.parseInt("abc");
} catch (NumberFormatException e) {
System.out.println("변환 실패");
}
// 숫자 → 문자열
int value = 123;
String str = String.valueOf(value);
String str2 = Integer.toString(value);
}
}
NumberFormatException은 언체크 예외라서 try-catch를 쓰지 않아도 컴파일이 됩니다. 그래서 사용자 입력이나 설정 파일 값을 parseInt로 바로 변환하는 코드는 잘못된 입력 하나로 프로그램 전체가 종료될 수 있습니다. 예제처럼 변환 실패를 잡되, 실무에서는 catch 블록에서 메시지만 출력하고 넘어가기보다 기본값을 쓸지, 사용자에게 다시 입력받을지, 에러로 올릴지를 명확히 결정해야 합니다. 숫자 → 문자열 변환은 String.valueOf와 Integer.toString 모두 결과가 같고, "" + value도 동작하지만 의도가 덜 분명합니다. String.valueOf(Object)에 null을 넘기면 예외 대신 문자열 "null"이 반환된다는 점은 알아 둘 만합니다.
배열 처리
public class ArrayExample {
public static void main(String[] args) {
int[] scores = {85, 92, 78, 95, 88};
// 합계
int sum = 0;
for (int score : scores) {
sum += score;
}
// 평균
double average = (double) sum / scores.length;
System.out.println("평균: " + average);
// 최대값
int max = scores[0];
for (int score : scores) {
if (score > max) {
max = score;
}
}
System.out.println("최대값: " + max);
}
}
평균 계산의 (double) sum / scores.length에서 캐스팅 위치가 중요합니다. sum / scores.length는 int끼리의 나눗셈이라 소수점이 먼저 버려지고(438 / 5 = 87), 그 결과를 (double)로 바꿔도 87.0밖에 되지 않습니다. sum을 먼저 double로 바꿔야 나눗셈 자체가 실수 연산이 되어 87.6이 나옵니다. (double) (sum / scores.length)처럼 괄호를 잘못 치면 같은 버그가 됩니다.
최댓값을 scores[0]으로 시작한 것도 이유가 있습니다. int max = 0;으로 시작하면 모든 점수가 음수일 때 틀린 결과가 나옵니다. 대신 이 코드는 배열이 비어 있으면 scores[0]에서 ArrayIndexOutOfBoundsException이 나므로, 빈 배열이 들어올 수 있다면 먼저 길이를 확인해야 합니다. Arrays.stream(scores).max()는 빈 배열에서 OptionalInt.empty()를 반환해 이 경우를 타입으로 드러내 줍니다.
변수와 타입 요약
핵심 요약
- 기본 타입: byte, short, int, long, float, double, char, boolean
- 참조 타입: String, 배열, 객체
- 형변환: 자동 (Widening), 명시적 (Narrowing)
- 래퍼 클래스: Integer, Double 등
- 오토박싱: 자동 변환
다음 단계
같이 보면 좋은 글
- C++ 변수와 자료형
- Kotlin 변수와 타입
- Swift 변수와 타입 | var, let, 옵셔널
- C++ numeric_limits
- Java 클래스와 객체 | OOP, 상속, 인터페이스
- Java 컬렉션 | ArrayList, HashMap, Set
자주 묻는 질문 (FAQ)
Q. Integer 두 개를 ==로 비교하면 왜 결과가 달라질 수 있나요?
A. Integer는 객체라서 ==는 값이 아니라 같은 객체인지를 비교합니다. 오토박싱은 내부적으로 Integer.valueOf()를 쓰는데, -128~127 범위는 캐시된 객체를 재사용하므로 100 == 100은 true가 나오지만 그 범위를 넘는 값은 false가 나올 수 있습니다. 래퍼 타입의 값 비교는 항상 equals()를 쓰고, null인 래퍼를 언박싱하면 NullPointerException이 난다는 점도 함께 기억해야 합니다.