Java 자료형·문자열·객체 초기화
Java의 기본형과 참조형, 필드의 기본값과 지역 변수의 초기화, 문자열의 불변성과 비교를 구분한다. 생성자 호출과 정적·인스턴스 초기화의 순서를 짧은 코드에서 추적한다.
Java의 기본형과 참조형, 필드의 기본값과 지역 변수의 초기화, 문자열의 불변성과 비교를 구분한다. 생성자 호출과 정적·인스턴스 초기화의 순서를 짧은 코드에서 추적한다.
그림으로 확인하기

Java 자료형의 큰 구분
Java는 정적 타입 언어다. 모든 변수와 식에는 컴파일 시점에 확인되는 형이 있으며, 형은 저장 가능한 값과 사용할 수 있는 연산을 제한한다.
기본형 8개
| 분류 | 형 | 크기·범위의 핵심 | 대표 리터럴 |
|---|---|---|---|
| 정수형 | byte | 8비트, -128~127 | 명시적 접미사 없음 |
| 정수형 | short | 16비트, -32,768~32,767 | 명시적 접미사 없음 |
| 정수형 | int | 32비트, -2³¹~2³¹-1 | 10, 0xFF, 0b1010 |
| 정수형 | long | 64비트, -2⁶³~2⁶³-1 | 10L |
| 문자형 | char | 16비트 부호 없는 UTF-16 코드 단위, 0~65,535 | 'A', '\u0041' |
| 실수형 | float | 32비트 IEEE 754 binary32 | 3.14F |
| 실수형 | double | 64비트 IEEE 754 binary64 | 3.14, 3.14D |
| 논리형 | boolean | true, false만 가짐 | true, false |
char는 16비트 UTF-16 코드 단위다. 따라서 모든 유니코드 문자가 항상 char 하나에 들어가는 것은 아니다. 보조 문자 영역의 문자는 서로게이트 쌍인 두 char로 표현될 수 있다.
boolean은 정수형이 아니다. C처럼 0을 false, 0이 아닌 값을 true로 자동 변환하지 않는다.
int count = 0;
boolean ok = count != 0; // 명시적으로 비교해야 한다.
리터럴의 기본형과 접미사
정수 리터럴은 특별한 접미사가 없으면 먼저 int로 해석 가능한지 확인한다. int 범위를 넘는 값을 long으로 쓰려면 보통 L 접미사를 붙인다.
long population = 3_000_000_000L;
실수 리터럴은 접미사가 없으면 double이다. float 변수에 직접 넣으려면 F 또는 f를 붙이거나 명시적으로 변환해야 한다.
double rate = 3.14;
float ratio = 3.14F;
소문자 l은 숫자 1과 혼동되므로 long에는 대문자 L을 쓰는 것이 좋다.
확대 변환과 축소 변환
작은 범위의 수치형 값을 더 넓은 범위의 형으로 옮기는 확대 기본형 변환은 많은 대입 문맥에서 자동으로 허용된다.
int i = 100;
long l = i;
double d = l;
반대로 더 좁은 형으로 옮기는 축소 기본형 변환은 정보 손실 가능성이 있으므로 일반적으로 명시적 캐스팅이 필요하다.
int value = 130;
byte narrowed = (byte) value; // 상위 비트가 버려져 -126
확대 변환도 항상 수학적 값을 완벽하게 보존하는 것은 아니다. 매우 큰 long을 float나 double로 바꾸면 정밀도가 줄 수 있다. 따라서 “확대 변환은 무조건 정보 손실이 없다”는 선지는 틀릴 수 있다.
수치 승격과 연산 결과형
byte, short, char끼리 산술 연산을 해도 피연산자는 보통 int로 승격된다.
byte a = 10;
byte b = 20;
int sum = a + b;
// byte result = a + b; // 컴파일 오류: a + b의 형은 int
복합 대입 연산자는 오른쪽 계산 결과를 왼쪽 변수형으로 암시적으로 변환하는 동작을 포함한다.
byte n = 10;
n += 20; // n = (byte) (n + 20)과 유사한 변환 포함
따라서 n = n + 20은 별도 캐스팅 없이 컴파일되지 않을 수 있지만 n += 20은 컴파일된다. 다만 범위를 넘으면 값이 잘릴 수 있으므로 안전한 연산이라는 뜻은 아니다.
정수 연산의 오버플로는 기본적으로 예외를 던지지 않고 해당 정수형의 비트 폭에 맞춰 결과가 순환한다. 반면 정수를 0으로 나누면 ArithmeticException이 발생한다. 부동소수점 0으로 나누기는 IEEE 754 규칙에 따라 무한대나 NaN이 될 수 있다.
참조형·객체·null
참조형의 종류
Java 언어 명세의 참조형에는 클래스형, 인터페이스형, 타입 변수, 배열형이 포함된다. 필기 기본 문제에서는 주로 클래스형과 배열형을 다루며, 문자열 String과 모든 배열은 참조형이다.
String name = "Kim";
int[] scores = {90, 80, 70};
위 코드에서 name에는 문자열 객체 자체가 들어가는 것이 아니라 그 객체를 가리키는 참조값이 저장된다. scores에도 배열 객체에 접근하는 참조값이 저장된다.
대입은 참조값을 복사한다
StringBuilder first = new StringBuilder("A");
StringBuilder second = first;
second.append("B");
second = first는 객체를 복제하지 않는다. 두 변수에 같은 참조값이 들어가므로 두 변수가 같은 StringBuilder 객체를 가리킨다. 따라서 second.append("B") 뒤에는 first.toString()도 "AB"가 된다.
객체의 독립 복사본이 필요하면 해당 클래스가 제공하는 복사 생성자, 팩터리 메서드, clone 규약, 직접 작성한 복사 로직 등을 검토해야 한다. 단순 참조 대입은 얕은 참조 공유일 뿐이다.
Java의 매개변수 전달은 항상 값 전달
메서드를 호출하면 각 매개변수 변수에는 대응하는 인자의 값이 복사된다.
- 기본형 인자: 기본값 자체가 복사된다.
- 참조형 인자: 객체를 가리키는 참조값이 복사된다.
따라서 메서드가 공유 객체의 필드를 바꾸면 호출자도 변경된 객체를 보지만, 매개변수 변수에 새 객체를 대입해도 호출자의 참조 변수는 바뀌지 않는다.
class Box {
int value;
}
static void change(Box box) {
box.value = 20; // 호출자와 공유하는 객체 변경
box = new Box(); // 매개변수 변수만 새 참조값으로 재대입
box.value = 30;
}
“객체가 참조로 전달된다”라고 외우면 매개변수 재대입도 호출자 변수에 반영된다고 오해하기 쉽다. 정확한 표현은 객체 참조값이 값으로 전달된다이다.
null의 의미
null은 어떤 객체도 가리키지 않는 특별한 참조값이다. 기본형 변수에는 넣을 수 없고 참조형 변수에만 넣을 수 있다.
String text = null;
// int number = null; // 컴파일 오류
널 참조로 인스턴스 필드나 메서드에 접근하면 일반적으로 NullPointerException이 발생한다.
String text = null;
// int length = text.length(); // 실행 시 NullPointerException
null을 비교할 때는 == null 또는 != null을 사용한다. equals를 호출하려면 호출 대상이 널이 아닌지 확인해야 한다.
변수의 기본값과 반드시 초기화해야 하는 지역 변수
기본값을 받는 대상
다음 대상은 생성될 때 기본값을 받는다.
- 클래스 변수, 즉
static필드 - 인스턴스 필드
- 배열의 각 요소
| 형 | 기본값 |
|---|---|
byte, short, int, long | 0에 해당하는 값 |
float, double | 양의 0.0 |
char | 널 문자 \u0000 |
boolean | false |
| 모든 참조형 | null |
class Defaults {
int count; // 0
boolean active; // false
String name; // null
}
int[] numbers = new int[3]; // 모든 요소가 0
String[] names = new String[2]; // 모든 요소가 null
지역 변수에는 자동 기본값이 없다
메서드나 생성자 블록 안에서 선언한 지역 변수는 사용 전에 명시적으로 값이 정해졌음을 컴파일러가 확인할 수 있어야 한다.
int local;
// System.out.println(local); // 컴파일 오류: 초기화되지 않은 지역 변수
조건문 안에서만 값이 대입되는 경우에도 모든 실행 경로에서 값이 정해지는지 확인한다.
int result;
if (condition) {
result = 1;
} else {
result = 2;
}
System.out.println(result); // 모든 경로에서 대입되므로 가능
“Java의 변수는 모두 자동으로 0이나 null로 초기화된다”는 선지는 틀리다. 기본값 규칙은 필드와 배열 요소에 적용되고, 일반 지역 변수에는 적용되지 않는다.
String의 불변성·인턴·비교
String은 불변 객체다
String 객체는 만들어진 뒤 내부 문자열 값이 바뀌지 않는다. 문자열 메서드가 반환하는 값은 기존 객체를 변경한 결과가 아니라 새 문자열 참조일 수 있다.
String text = "java";
text.toUpperCase();
System.out.println(text); // java
text = text.toUpperCase();
System.out.println(text); // JAVA
첫 번째 toUpperCase()의 반환값을 저장하지 않았으므로 text가 가리키는 문자열은 그대로다. 두 번째 문장은 text 변수에 다른 문자열 참조를 다시 대입한 것이다. 변수의 재대입과 객체의 변경을 구분해야 한다.
문자열을 반복해서 수정해야 한다면 가변 객체인 StringBuilder를 사용할 수 있다. 다만 StringBuilder는 String과 다른 클래스이며 같은 불변성 규칙을 적용하면 안 된다.
문자열 리터럴과 인턴
동일한 문자열 리터럴과 문자열 값을 만드는 컴파일 시간 상수식은 인턴되어 같은 String 인스턴스를 공유한다.
String a = "java";
String b = "ja" + "va"; // 컴파일 시간 상수식
System.out.println(a == b); // true
반면 new String(...)은 새 객체를 만든다.
String a = "java";
String c = new String("java");
System.out.println(a == c); // false
System.out.println(a.equals(c)); // true
실행 중에 계산되는 문자열 연결은 상수식이 아니다.
String part = "ja";
String d = part + "va";
System.out.println(a == d); // false
System.out.println(a.equals(d)); // true
시험 문제에서는 먼저 식이 컴파일 시간 상수식인지, 실행 중 계산식인지 구분한다. 그러나 실제 문자열 내용 비교에는 객체 공유 여부를 추측하지 말고 equals를 사용한다.
==, equals, compareTo
| 방법 | 비교 기준 | 대표 결과 |
|---|---|---|
a == b | 두 참조값이 같은 객체를 가리키는가 | 참조 동일성 |
a.equals(b) | 문자열의 내용이 같은가 | 내용 동등성 |
a.compareTo(b) | 사전식 순서가 같은가·앞서는가·뒤지는가 | 음수·0·양수 |
equals를 호출하는 기준 객체가 null이면 예외가 발생한다.
String input = null;
boolean same = "Y".equals(input); // false, 예외 없음
상수 문자열을 왼쪽에 두는 방식은 널 입력을 비교할 때 사용할 수 있다. 여러 객체형을 일반적으로 비교할 때는 Objects.equals(a, b)도 널 안전 비교를 제공한다.
박싱과 언박싱의 기본
Integer 같은 래퍼 클래스는 기본형 값을 객체로 다룰 때 사용한다. 기본형을 래퍼로 바꾸는 것은 박싱, 래퍼에서 기본형 값을 얻는 것은 언박싱이다. 예를 들어 Integer n = 3;과 int x = n;에는 자동 변환이 적용된다. null인 래퍼를 기본형으로 언박싱하면 예외가 발생한다. 래퍼 객체의 내용 비교를 객체 참조의 == 결과에 의존해서는 안 된다.
클래스·객체·참조·생성자
네 용어의 관계
- 클래스(class): 객체가 가질 필드와 메서드, 생성 규칙을 선언한 형이다.
- 객체(object): 실행 중에 생성된 클래스의 인스턴스다.
- 참조(reference): 객체나 배열에 접근하기 위한 값이다.
- 참조 변수: 참조값을 저장하는 변수다.
Person person = new Person();
Person은 클래스형, new Person()은 새 객체를 만들고 그 참조값을 결과로 내는 식, person은 그 참조값을 보관하는 변수다.
생성자의 문법과 성질
생성자는 클래스 인스턴스를 초기화하는 과정에서 실행되는 특별한 선언이다.
class Person {
String name;
Person(String name) {
this.name = name;
}
}
생성자의 핵심 성질은 다음과 같다.
- 이름은 클래스의 단순 이름과 같아야 한다.
- 반환형을 적지 않는다.
void도 반환형이므로 쓰면 생성자가 아니라 메서드가 된다. - 생성자는 상속되지 않으며 오버라이딩 대상이 아니다.
- 매개변수 목록이 다르면 여러 생성자를 오버로딩할 수 있다.
- 생성자는 직접
return 값;을 사용할 수 없다. - 생성자에는
abstract,static,final,native,strictfp,synchronized를 붙일 수 없다.
class Sample {
void Sample() { } // 생성자가 아니라 반환형 void인 메서드
}
기본 생성자와 무인자 생성자
클래스에 생성자 선언이 하나도 없을 때 컴파일러는 기본 생성자(default constructor) 를 암시적으로 선언한다. 일반적인 최상위 클래스에서 이 생성자는 매개변수가 없고 상위 클래스의 접근 가능한 무인자 생성자를 호출한다.
class Point {
int x;
int y;
}
위 클래스에는 컴파일러가 무인자 기본 생성자를 넣는다. 그러나 생성자를 하나라도 직접 선언하면 기본 생성자는 더 이상 자동으로 생기지 않는다.
class Point {
Point(int x) { }
}
// new Point(); // 컴파일 오류: 사용자가 무인자 생성자를 선언하지 않음
사용자가 직접 Point() { }를 선언한 것은 무인자 생성자이지만, 컴파일러가 자동 생성한 기본 생성자는 아니다. 두 표현을 같은 말로 쓰지 않는다.
this, this(...), super(...)
this는 현재 생성 중이거나 메서드를 실행 중인 객체를 가리킨다.this(...)는 같은 클래스의 다른 생성자를 호출한다.super(...)는 직접 상위 클래스의 생성자를 호출한다.
class Person {
String name;
int age;
Person(String name) {
this(name, 0);
}
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
한 생성자 본문에는 명시적 생성자 호출이 최대 하나만 들어간다. this(...) 호출은 결국 어떤 생성자에서 상위 클래스 생성자 호출로 이어져야 하며, 순환해서 자기 자신을 다시 호출하면 컴파일 오류다.
명시적 생성자 호출이 없으면 Object를 제외한 생성자에는 일반적으로 super() 호출이 암시적으로 포함된다. 상위 클래스에 접근 가능한 무인자 생성자가 없다면 하위 생성자가 적절한 super(인자)를 명시해야 한다.
클래스 초기화와 객체 초기화 순서
클래스 초기화: static 영역
클래스 초기화는 그 클래스가 선언한 정적 필드 초기화식과 정적 초기화 블록을 실행하는 과정이다.
class Counter {
static int first = print("field");
static { print("block"); }
static int print(String text) {
System.out.println(text);
return 0;
}
}
정적 필드 초기화식과 정적 초기화 블록은 소스에 적힌 순서대로 실행된다. 일반적으로 다음과 같은 첫 능동 사용 직전에 클래스가 초기화된다.
- 그 클래스의 객체를 처음 생성할 때
- 그 클래스가 선언한 정적 메서드를 처음 호출할 때
- 그 클래스가 선언한 정적 필드에 처음 대입할 때
- 컴파일 시간 상수가 아닌 정적 필드를 처음 읽을 때
상위 클래스가 아직 초기화되지 않았다면 상위 클래스가 하위 클래스보다 먼저 초기화된다. 클래스 초기화가 정상적으로 끝난 뒤에는 같은 클래스에서 다시 실행되지 않는다.
단, 컴파일 시간 상수인 static final 필드를 읽는 것만으로는 그 선언 클래스의 초기화가 발생하지 않을 수 있다. “클래스 이름이 등장하면 무조건 정적 블록이 실행된다”는 식으로 판단하면 안 된다.
객체 생성의 큰 흐름
아래 예제처럼 this(...)·super(...) 호출 앞에 별도 실행문을 두지 않는 생성자 형식의 흐름은 다음과 같다.
- 필요하면 해당 클래스와 상위 클래스를 초기화한다.
- 새 객체의 저장 공간을 확보한다.
- 그 객체가 가진 모든 인스턴스 필드에 기본값을 넣는다.
- 생성자 인자식을 왼쪽에서 오른쪽으로 평가한다.
- 선택된 생성자를 호출하고
this(...)·super(...)생성자 체인을 처리한다. - 상위 클래스 생성자 처리가 끝난 뒤 현재 클래스의 인스턴스 필드 초기화식과 인스턴스 초기화 블록을 소스 순서대로 실행한다.
- 현재 생성자 본문의 나머지 문장을 실행한다.
- 완성된 객체의 참조값을
new식의 결과로 돌려준다.
모든 인스턴스 필드가 기본값을 먼저 받는다는 점이 중요하다. 명시적 필드 초기화식은 그 기본값을 나중에 덮어쓴다.
상속이 있을 때의 인스턴스 초기화
하위 클래스 객체 하나에는 상위 클래스에서 선언한 인스턴스 필드도 함께 포함된다. 생성자 체인은 상위 클래스 부분을 먼저 완성한 뒤 하위 클래스 부분으로 돌아온다.
아래 예제의 생성자 형식에서 순서는 다음과 같다.
- 객체 전체의 인스턴스 필드 기본값 설정
- 최상위 쪽 상위 클래스의 인스턴스 필드 초기화식·초기화 블록
- 그 상위 클래스 생성자 본문
- 다음 하위 클래스의 인스턴스 필드 초기화식·초기화 블록
- 그 하위 클래스 생성자 본문
- 가장 아래 하위 클래스까지 반복
각 클래스 안에서는 인스턴스 필드 초기화식과 인스턴스 초기화 블록이 서로 섞인 채 소스에 나타난 순서로 실행된다.
this(...)로 같은 클래스의 다른 생성자에 위임한 경우, 해당 클래스의 필드 초기화식과 인스턴스 초기화 블록은 생성자마다 반복 실행되는 것이 아니라 그 객체 생성 과정에서 한 번만 실행된다.
배열 길이와 향상된 for문
Java 배열의 length는 배열 객체의 원소 수이며 메서드가 아닌 필드 형태로 사용한다. 원소 수가 0인 배열도 유효한 객체이지만 접근 가능한 인덱스는 없다. 배열 길이는 생성 후 바뀌지 않는다.
2차원 배열은 배열을 참조하는 배열이다. 각 행 배열의 길이가 다를 수 있다. 바깥 배열의 length는 행 참조의 수이고 a[i].length는 해당 행의 원소 수이다. 초기화되지 않은 행 참조가 null이면 그 행의 length를 읽을 때 예외가 발생한다.
for (int v : values)는 배열 원소값을 순서대로 반복 변수 v에 전달한다. 같은 값이 중복돼도 각 원소를 방문한다. v에 새 정수를 대입하는 것은 원래 배열 요소를 대입하는 것이 아니다. 참조형 원소를 순회할 때는 참조값이 복사되므로 그 참조를 통해 공유 객체 내부를 바꿀 수 있지만, 반복 변수에 다른 객체를 대입해도 배열 요소 자체가 교체되지는 않는다.
문자열 인덱스와 부분 문자열
String의 인덱스는 0부터 시작하며 UTF-16 코드 단위 위치를 기준으로 한다. s.charAt(i)는 위치 i의 char 값을 반환하고 유효 범위는 0 <= i < s.length()이다. 반환형이 길이 1의 String인 것은 아니다. 숫자 문자 '0'부터 '9'의 코드값은 연속하므로 해당 범위의 문자 c에 대해 c - '0'으로 대응하는 정수를 얻을 수 있다.
substring(begin)은 begin부터 끝까지, substring(begin, end)는 begin을 포함하고 end는 제외한 구간의 String을 반환한다. 0 <= begin <= end <= length를 만족해야 한다. begin과 end가 같으면 빈 문자열이며, substring(length)도 빈 문자열이다. 잘못된 범위는 인덱스 관련 예외를 일으킨다. 반환된 문자열의 인덱스는 다시 0부터 시작한다.
원래 String은 불변이므로 반환값을 다른 변수에 저장하거나 원래 변수에 재대입하는 동작과 구분한다. substring의 반환값을 버렸다고 원래 문자열이 잘라지지는 않는다.
정밀도와 정수 승격의 짧은 예
Java의 float는 유효한 이진 정밀도가 24비트이다. 정수는 절댓값이 2^24 이하인 범위에서 float로 정확히 표현할 수 있지만, 더 큰 모든 정수를 빠짐없이 표현하지는 못한다. 예를 들어 16,777,217을 float로 변환하면 16,777,216으로 반올림된다. 확대 변환이 허용된다는 것과 정밀도가 보존된다는 것은 다르다.
char끼리 산술 연산을 하면 정수 승격을 거쳐 보통 int 결과가 된다. 숫자 계산을 먼저 하는지 문자열 연결이 먼저 시작되는지는 피연산자 형과 왼쪽 결합에 따라 확인한다.