orElse 의 인자는 값이 있어도 실행된다는 것을 카운터로 확인합니다.
import java.util.Optional;
public class OrElseTiming {
static int calls = 0;
static String loadDefault(String tag) {
calls++;
System.out.println(" loadDefault(" + tag + ") 실행됨");
return "default-" + tag;
}
public static void main(String[] args) {
Optional<String> present = Optional.of("kim");
Optional<String> absent = Optional.empty();
System.out.println("값 있음 + orElse:");
System.out.println(" → " + present.orElse(loadDefault("A"))); // 값이 있어도 실행
System.out.println("값 있음 + orElseGet:");
System.out.println(" → " + present.orElseGet(() -> loadDefault("B"))); // 실행 안 됨
System.out.println("값 없음 + orElseGet:");
System.out.println(" → " + absent.orElseGet(() -> loadDefault("C"))); // 실행됨
System.out.println("총 loadDefault 호출 = " + calls);
try {
absent.orElseThrow(() -> new IllegalStateException("사용자 없음"));
} catch (IllegalStateException e) {
System.out.println("orElseThrow: " + e.getMessage());
}
}
}
// 출력:
// 값 있음 + orElse:
// loadDefault(A) 실행됨
// → kim
// 값 있음 + orElseGet:
// → kim
// 값 없음 + orElseGet:
// loadDefault(C) 실행됨
// → default-C
// 총 loadDefault 호출 = 2
// orElseThrow: 사용자 없음Optional<User> findById 를 받아 map / filter / orElseThrow 로 비즈니스 로직을 체이닝합니다. if-null 이 하나도 없습니다.
import java.util.*;
public class RepositoryChain {
record User(long id, String name, String email, boolean active) {}
static class UserRepository {
private final Map<Long, User> db = Map.of(
1L, new User(1, "kim", "kim@x.com", true),
2L, new User(2, "lee", null, true),
3L, new User(3, "park", "park@x.com", false));
Optional<User> findById(long id) { return Optional.ofNullable(db.get(id)); }
}
static class UserService {
private final UserRepository repo = new UserRepository();
// 활성 사용자의 이메일 도메인. 없거나 비활성이거나 이메일 없으면 "unknown"
String emailDomain(long id) {
return repo.findById(id)
.filter(User::active)
.map(User::email) // email 이 null 이면 여기서 empty
.map(e -> e.substring(e.indexOf('@') + 1))
.orElse("unknown");
}
User requireActive(long id) {
return repo.findById(id)
.filter(User::active)
.orElseThrow(() -> new NoSuchElementException("활성 사용자 없음: id=" + id));
}
void greet(long id) {
repo.findById(id).ifPresentOrElse(
u -> System.out.println("안녕하세요, " + u.name()),
() -> System.out.println("id=" + id + " 사용자를 찾을 수 없습니다"));
}
}
public static void main(String[] args) {
UserService svc = new UserService();
for (long id : new long[]{1, 2, 3, 9}) System.out.println(id + " → " + svc.emailDomain(id));
svc.greet(1);
svc.greet(9);
try { svc.requireActive(3); } catch (NoSuchElementException e) { System.out.println(e.getMessage()); }
}
}
// 출력:
// 1 → x.com
// 2 → unknown
// 3 → unknown
// 9 → unknown
// 안녕하세요, kim
// id=9 사용자를 찾을 수 없습니다
// 활성 사용자 없음: id=3or() 와 Objects.requireNonNullElse"시스템 프로퍼티 → 환경 변수 → 설정 파일 → 기본값" 순서로 값을 찾는 전형적인 설정 로더입니다.
import java.util.*;
public class ConfigFallback {
static final Map<String, String> FILE_CONFIG = Map.of("db.host", "file-host", "db.port", "5432");
static Optional<String> fromSystemProperty(String key) { return Optional.ofNullable(System.getProperty(key)); }
static Optional<String> fromEnv(String key) { return Optional.ofNullable(System.getenv(key.toUpperCase().replace('.', '_'))); }
static Optional<String> fromFile(String key) { return Optional.ofNullable(FILE_CONFIG.get(key)); }
static String resolve(String key, String defaultValue) {
return fromSystemProperty(key)
.or(() -> fromEnv(key)) // 비어 있을 때만 다음 소스 시도 (지연 평가)
.or(() -> fromFile(key))
.orElse(defaultValue);
}
static int resolveInt(String key, int defaultValue) {
return fromSystemProperty(key).or(() -> fromFile(key))
.map(String::strip)
.filter(s -> s.matches("\\d+")) // 숫자가 아니면 empty → 기본값
.map(Integer::parseInt)
.orElse(defaultValue);
}
public static void main(String[] args) {
System.setProperty("db.host", "prop-host");
System.out.println("db.host = " + resolve("db.host", "localhost")); // 프로퍼티 우선
System.out.println("db.port = " + resolveInt("db.port", 3306)); // 파일
System.out.println("db.user = " + resolve("db.user", "app")); // 기본값
System.setProperty("db.port", "abc");
System.out.println("db.port(bad) = " + resolveInt("db.port", 3306)); // 검증 실패 → 기본값
// Optional 이 과한 곳: 단순 null 기본값은 Objects 로
String nick = null;
System.out.println(Objects.requireNonNullElse(nick, "anonymous"));
}
}
// 출력:
// db.host = prop-host
// db.port = 5432
// db.user = app
// db.port(bad) = 3306
// anonymous외부 API 응답처럼 어느 단계든 null 일 수 있는 중첩 구조를 map 체인으로 안전하게 파고들고, Optional::stream 으로 여러 결과에서 빈 값을 걸러냅니다.
import java.util.*;
import java.util.stream.*;
public class NestedDto {
record Geo(Double lat, Double lng) {}
record Address(String city, Geo geo) {}
record Company(String name, Address address) {}
record Employee(String name, Company company) {}
static String companyCity(Employee e) {
return Optional.ofNullable(e)
.map(Employee::company)
.map(Company::address)
.map(Address::city) // 어느 단계든 null 이면 여기까지 empty 로 흘러감
.orElse("(no city)");
}
static OptionalDouble latitude(Employee e) {
return Optional.ofNullable(e)
.map(Employee::company).map(Company::address).map(Address::geo).map(Geo::lat)
.map(OptionalDouble::of).orElse(OptionalDouble.empty());
}
public static void main(String[] args) {
List<Employee> staff = Arrays.asList(
new Employee("kim", new Company("ACME", new Address("Seoul", new Geo(37.5, 127.0)))),
new Employee("lee", new Company("Beta", null)),
new Employee("park", null),
null,
new Employee("choi", new Company("Gamma", new Address("Busan", null))));
for (Employee e : staff) System.out.println((e == null ? "null" : e.name()) + " → " + companyCity(e));
// Optional::stream 으로 있는 값만 모으기
List<String> cities = staff.stream()
.map(e -> Optional.ofNullable(e).map(Employee::company).map(Company::address).map(Address::city))
.flatMap(Optional::stream)
.toList();
System.out.println("cities = " + cities);
// 스트림 최종 연산의 Optional
OptionalDouble avgLat = staff.stream().map(NestedDto::latitude)
.filter(OptionalDouble::isPresent).mapToDouble(OptionalDouble::getAsDouble).average();
System.out.println("avgLat = " + avgLat.orElse(Double.NaN));
}
}
// 출력:
// kim → Seoul
// lee → (no city)
// park → (no city)
// null → (no city)
// choi → Busan
// cities = [Seoul, Busan]
// avgLat = 37.5"없음"의 이유가 여러 가지일 때 Optional 대신 sealed 타입을 쓰고, switch 완전성 검사와 case null 을 활용합니다. record 생성자의 requireNonNull 도 함께 봅니다.
import java.util.*;
public class SealedLookup {
record Account(String id, long balance) {
Account { Objects.requireNonNull(id, "id 는 필수"); } // 생성 시점에 null 차단
}
sealed interface Lookup permits Found, NotFound, Frozen {}
record Found(Account account) implements Lookup {}
record NotFound(String id) implements Lookup {}
record Frozen(String id, String reason) implements Lookup {}
static final Map<String, Account> DB = Map.of("A1", new Account("A1", 500), "A2", new Account("A2", 0));
static final Set<String> FROZEN = Set.of("A2");
static Lookup find(String id) {
if (FROZEN.contains(id)) return new Frozen(id, "의심 거래");
Account a = DB.get(id);
return a == null ? new NotFound(id) : new Found(a);
}
static String describe(Object o) { // nullable 값 분기: case null
return switch (o) {
case null -> "null 입력";
case Found f when f.account().balance() > 0 -> "잔액 " + f.account().balance();
case Found f -> "잔액 없음";
case NotFound n -> "계좌 없음: " + n.id();
case Frozen z -> "동결(" + z.reason() + "): " + z.id();
default -> "알 수 없음: " + o;
};
}
public static void main(String[] args) {
for (String id : new String[]{"A1", "A2", "A9"}) System.out.println(describe(find(id)));
System.out.println(describe(null));
try { new Account(null, 1); } catch (NullPointerException e) { System.out.println("NPE: " + e.getMessage()); }
}
}
// 출력:
// 잔액 500
// 동결(의심 거래): A2
// 계좌 없음: A9
// null 입력
// NPE: id 는 필수