좋아요! 🎯
이번엔 enum을 이용한 클래스 설계와 ordinal() 함정, 필드 초기화 트릭을 섞어서 문제를 내고, 한 줄 한 줄 자세히 풀이해 드릴게요.
아래 코드를 보고 출력값을 유추해 보세요.
enum Rank {
BRONZE(1), SILVER(2), GOLD(3), PLATINUM(4);
private int level;
Rank(int level) {
this.level = level;
System.out.println(name() + " ordinal=" + ordinal() + ", level=" + level);
}
int getLevel() {
return level;
}
}
class User {
private final String name;
private Rank rank;
User(String name, Rank rank) {
this.name = name;
this.rank = rank;
}
void promote() {
// ordinal() 기준 업그레이드
int next = rank.ordinal() + 1;
if (next < Rank.values().length) {
rank = Rank.values()[next];
}
}
@Override
public String toString() {
return name + " is " + rank + " (level=" + rank.getLevel() + ", ordinal=" + rank.ordinal() + ")";
}
}
public class TestEnumDesign {
public static void main(String[] args) {
User u = new User("Alice", Rank.SILVER);
u.promote();
System.out.println(u);
}
}
enum Rankenum 상수(BRONZE, SILVER, ...)가 클래스가 메모리에 로드될 때 한 번 생성됨.ordinal() 호출 → 0부터 시작된 순서 반환.getLevel()은 별도로 우리가 지정한 level 반환.→ 프로그램 시작 전에 다음 출력이 일어남:
BRONZE ordinal=0, level=1
SILVER ordinal=1, level=2
GOLD ordinal=2, level=3
PLATINUM ordinal=3, level=4
✅ 이렇게 enum 상수를 정의할 때 생성자 호출 및 필드 초기화가 미리 실행됨.
main() 시작User u = new User("Alice", Rank.SILVER);
u.promote() 호출int next = rank.ordinal() + 1; // SILVER.ordinal() == 1이므로 next = 2
// Rank.values()[2] = GOLD
rank = GOLD;
System.out.println(u); 호출→ User의 toString():
name = "Alice"
rank = GOLD
rank.getLevel() = 3
rank.ordinal() = 2
→ "Alice is GOLD (level=3, ordinal=2)"
프로그램을 실행하면, 클래스 초기화 출력 + toString 출력이 나옴:
BRONZE ordinal=0, level=1
SILVER ordinal=1, level=2
GOLD ordinal=2, level=3
PLATINUM ordinal=3, level=4
Alice is GOLD (level=3, ordinal=2)
ordinal() 함정
→ ordinal()은 상수의 선언 순서를 반환(0부터).
→ 임의로 level 필드를 둬도 ordinal()은 바뀌지 않음.
필드 초기화 트릭
→ enum 상수 정의 시 생성자가 한 번 호출됨(프로그램 시작할 때 이미 호출됨).
→ 생성자 안의 출력문은 User 생성 전에 출력됨.
values() 메서드
→ 모든 상수를 배열로 반환, ordinal() 값 기준 인덱스 접근.
ordinal() 대신 필드를 쓰자실제 개발할 때 ordinal() 대신 level 필드를 쓰는 것이 안전함:
이런 문제를 연습하면서:
✅ enum 생성자 호출 타이밍
✅ ordinal() vs 고유 필드 차이
✅ values()와 ordinal() 조합 트릭
이 부분을 반드시 이해해 두면 실기 트릭 문제에서 실수하지 않아요!
맞아요, **ordinal()**은 우리가 따로 정의하지 않았는데도 enum 상수마다 자동으로 존재하는 메서드입니다. 😄
지금부터 ordinal()을 아주 자세히, 기초부터 트릭까지 확실히 알려드릴게요!
ordinal()이란?정의
ordinal()은 java.lang.Enum 클래스에서 미리 구현된 메서드로,
**열거형 상수의 선언 순서(0부터 시작하는 정수)**를 반환합니다.
enum을 정의하면 enum 상수들은 내부적으로 Enum을 상속.ordinal()은 Enum의 메서드이므로 모든 enum 상수에서 호출 가능.enum Color { RED, GREEN, BLUE }라고 하면 내부적으로 다음과 비슷하게 동작:
class Color extends Enum<Color> {
public static final Color RED = new Color("RED", 0);
public static final Color GREEN = new Color("GREEN", 1);
public static final Color BLUE = new Color("BLUE", 2);
...
}
여기서 Enum 클래스의 생성자가 두 번째 인자로 ordinal() 값을 받도록 호출되므로 각 상수는 자신만의 ordinal을 갖게 됨.
ordinal() 반환값RED.ordinal() == 0GREEN.ordinal() == 1BLUE.ordinal() == 2이렇게 0부터 시작하여 상수를 나열한 순서대로 번호가 부여돼요.
enum 상수 선언 순서 바뀌면 ordinal() 값이 바뀜.
ordinal()은 고정된 ID가 아님.enum 값 비교 용도로만 쓰자.
→ ordinal() 대신 고정된 필드(int level, String code 등)를 두는 게 안전.
ordinal() 어떻게 쓰면 좋아?✅ 단순한 내부 계산용
예를 들어 상수를 배열의 인덱스로 쓸 때 유용:
String[] descriptions = {"Fast", "Medium", "Slow"};
Speed speed = Speed.MEDIUM;
System.out.println(descriptions[speed.ordinal()]); // "Medium"
✅ switch 대신 ordinal() 활용한 배열 접근
단, 유지보수를 위해 enum 내부에 별도로 getLevel() 같은 메서드를 두는 편이 좋아요.
ordinal()은 java.lang.Enum에서 상속받는 메서드.enum 상수 어디서나 호출 가능.좋아요! 🎯
이번엔 Java의 Stream API 트릭을 짚어봅시다. Stream API는 편리하지만, 중간 연산과 최종 연산의 동작 방식, 지연 평가(lazy evaluation), 재사용 불가 같은 트릭 포인트가 많아요.
다음 코드를 보고 출력값을 유추해보세요.
import java.util.*;
import java.util.stream.*;
public class StreamTrickTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "cherry"));
Stream<String> stream = list.stream().filter(s -> {
System.out.println("Filter: " + s);
return s.contains("a");
});
long count = stream.count();
long again = stream.count(); // <- 트릭 포인트
}
}
filter)filter()는 중간 연산 → 즉, 스트림을 호출해도 아직 실행 안 됨(지연 평가).count() 같은 최종 연산을 호출하는 순간 내부적으로 모든 원소를 흘려보내며 필터를 호출.stream.count() 호출될 때 "Filter: ..." 출력s.contains("a") 조건을 만족하는 원소 수 계산 (apple, banana, apple = 3개)count()로 터미널 연산을 호출한 **Stream은 소비(consumed)**되므로again = stream.count(); 부분에서 IllegalStateException 발생이 코드의 출력과 상태:
Filter: apple
Filter: banana
Filter: apple
Filter: cherry
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
✅ Stream은 단 한 번만 소비 가능
→ 한 번 count(), collect(), forEach() 등 터미널 연산 호출 후엔 재사용 불가
✅ filter, map 같은 중간 연산은 lazy(지연 평가)
→ 터미널 연산 전까지 아무것도 출력 안 함.
✅ 한 번 소모된 Stream은 반드시 다시 생성해야 함
→ List나 Supplier<Stream> 이용
List<String> list = Arrays.asList("apple", "banana");
long c1 = list.stream().count(); // OK
long c2 = list.stream().count(); // OK: 새 스트림 생성
혹은:
Supplier<Stream<String>> supplier = () -> list.stream();
long c1 = supplier.get().count();
long c2 = supplier.get().count();
parallel() 쓰면 출력 순서 바뀜.✅ Stream은 한 번만 터미널 연산 가능
✅ 중간 연산은 터미널 연산 전까지 안 돌아감
✅ 내부 상태를 변형하는 연산은 side-effect 조심!
좋아요! 🎯
이번에는 Java Stream의 주요 메서드들을 정리 + 예제 + 동작 원리까지 자세히 알려드릴게요.
이걸 확실히 이해해 두면 스트림 트릭 문제를 풀 때 큰 도움이 돼요!
stream()Collection(List, Set 등), Arrays에서 스트림 파이프라인의 시작점을 만드는 메서드.List<String> list = Arrays.asList("a","b","c");
Stream<String> stream = list.stream();
count(), collect(), forEach() 등)이 호출되기 전까지 중간 연산은 실행 안 됨(지연 평가).-> (람다식)// (파라미터) -> {실행할 코드}
Stream<String> s = list.stream().filter(e -> e.length() > 1);
e -> e.length() > 1은 Predicate<String> 함수형 인터페이스 구현.collect()Collectors 유틸리티 메서드와 함께 씀.List<String> result = list.stream()
.filter(e -> e.length() > 1)
.collect(Collectors.toList());
filter로 조건을 걸고 → collect(Collectors.toList())가 터미널 연산 호출List로 반환.forEach()list.stream().forEach(System.out::println);
// list의 요소 하나씩 출력
peek()list.stream()
.peek(e -> System.out.println("Checking: " + e))
.filter(e -> e.startsWith("a"))
.collect(Collectors.toList());
peek() → filter() → ... 이런 식으로 요소마다 호출됨.peek() 안의 작업은 디버깅이나 로그용.parallel()list.stream().parallel().map(...).collect(...)
혹은
list.parallelStream().map(...).collect(...)
| 메서드 | 타입 | 설명 |
|---|---|---|
stream() | 시작 | 컬렉션을 스트림으로 변환 |
-> | 람다 | 함수형 인터페이스 구현 |
collect() | 터미널 | 스트림 데이터를 수집(리스트, 집합 등) |
forEach() | 터미널 | 각 요소에 작업 수행 |
peek() | 중간 | 스트림 검사 및 디버깅 |
parallel() | 중간 | 스트림을 병렬로 변환 |
✅ filter(), map(), peek() 등 중간 연산들은 터미널 연산(collect(), forEach(), count() 등)이 호출되기 전까지 실행되지 않음.
✅ 한 스트림은 한 번만 터미널 연산 가능 → 이후 재사용 불가.
✅ 성능이나 트릭 문제에서 parallel()은 순서가 보장 안 된다는 점 유의!