Java 8 주요 특징
Java 8(2014년 3월 Oracle 공식 릴리스)은 Java 생태계의 중대한 전환점입니다. 함수형 프로그래밍 패러다임을 도입하여 코드의 간결성과 가독성을 크게 향상시켰습니다.
Lambda 표현식
Lambda는 익명 클래스의 축약형 문법으로, 메서드를 값처럼 전달할 수 있게 합니다. 화살표 연산자 ->를 기준으로 좌측에는 매개변수, 우측에는 실행 본문을 배치합니다.
public class LambdaDemo {
public static void main(String[] args) {
// 기존 익명 클래스 방식
Runnable legacy = new Runnable() {
@Override
public void run() {
System.out.println("전통적 방식");
}
};
// Lambda 변환
Runnable modern = () -> System.out.println("현대적 방식");
// 매개변수가 있는 경우
Comparator<String> lengthCompare = (a, b) -> a.length() - b.length();
new Thread(modern).start();
new Thread(() -> System.out.println("즉석 생성")).start();
}
}
함수형 인터페이스
추상 메서드를 단 하나만 보유한 인터페이스를 의미하며, @FunctionalInterface 어노테이션으로 검증할 수 있습니다. Java 8에서 제공하는 대표적인 내장 함수형 인터페이스는 다음과 같습니다.
| 인터페이스 | 입력 | 반환 | 메서드 |
|---|---|---|---|
| Consumer<T> | T | void | accept(T t) |
| Supplier<T> | 없음 | T | T get() |
| Function<T,R> | T | R | R apply(T t) |
| Predicate<T> | T | boolean | boolean test(T t) |
public class FunctionalInterfaceDemo {
public static void main(String[] args) {
// Consumer: 소비 패턴
executePayment(amount -> System.out.println("결제 완료: " + amount + "원"), 50000);
// Supplier: 생성 패턴
int[] luckyNumbers = generateRandoms(() -> (int)(Math.random() * 45) + 1, 6);
System.out.println(Arrays.toString(luckyNumbers));
// Function: 변환 패턴
String processed = transform(String::toUpperCase, "hello world");
// Predicate: 필터 패턴
List<String> candidates = Arrays.asList("Kim", "Lee", "Park", "Choi");
List<String> selected = filterByCondition(s -> s.length() >= 4, candidates);
}
static void executePayment(Consumer<Integer> payment, int amount) {
payment.accept(amount);
}
static int[] generateRandoms(Supplier<Integer> generator, int quantity) {
return IntStream.range(0, quantity).map(i -> generator.get()).toArray();
}
static String transform(Function<String, String> converter, String target) {
return converter.apply(target);
}
static List<String> filterByCondition(Predicate<String> condition, List<String> items) {
return items.stream().filter(condition).collect(Collectors.toList());
}
}
메서드 참조
Lambda 본문이 단순 메서드 호출일 경우 더 간결한 문법으로 대체 가능합니다.
public class MethodReferenceDemo {
public static void main(String[] args) {
// 객체::인스턴스메서드
Consumer<String> printer = System.out::println;
// 클래스::정적메서드
Comparator<Integer> numericOrder = Integer::compare;
// 클래스::인스턴스메서드
Function<Employee, String> nameExtractor = Employee::getName;
// 클래스::new (생성자 참조)
Supplier<Employee> factory = Employee::new;
}
}
Stream API
컬렉션 데이터 처리를 선언적으로 표현하는 API입니다. 원본 데이터를 변경하지 않고 파이프라인을 구성하여 연산을 수행합니다.
Stream 생성
public class StreamCreation {
public static void main(String[] args) {
List<Product> inventory = Arrays.asList(
new Product("노트북", 1200000),
new Product("모니터", 350000),
new Product("키보드", 80000)
);
// 컬렉션 기반
Stream<Product> fromCollection = inventory.stream();
// 배열 기반
Stream<String> fromArray = Arrays.stream(new String[]{"A", "B", "C"});
// 직접 생성
Stream<Integer> direct = Stream.of(10, 20, 30);
// 무한 스트림
Stream<Integer> sequence = Stream.iterate(0, n -> n + 2).limit(10);
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
// 기본형 특화
IntStream prices = IntStream.rangeClosed(1000, 5000);
}
}
중간 연산
public class StreamIntermediate {
public static void main(String[] args) {
List<Order> orders = initializeData();
// 필터링 + 중복 제거 + 정렬 + 제한
orders.stream()
.filter(o -> o.getAmount() > 100000)
.distinct()
.sorted(Comparator.comparing(Order::getDate))
.skip(2)
.limit(5)
.forEach(System.out::println);
// 매핑: 객체에서 특정 속성 추출
List<String> customerNames = orders.stream()
.map(Order::getCustomerName)
.collect(Collectors.toList());
// 병렬 처리
orders.parallelStream()
.map(o -> heavyCalculation(o))
.collect(Collectors.toList());
}
}
최종 연산
public class StreamTerminal {
public static void main(String[] args) {
List<Sale> records = loadSalesData();
// 집계
long totalCount = records.stream().count();
Optional<Sale> cheapest = records.stream().min(Comparator.comparing(Sale::getPrice));
Optional<Sale> priciest = records.stream().max(Comparator.comparing(Sale::getPrice));
// 리듀스
int totalRevenue = records.stream()
.map(Sale::getPrice)
.reduce(0, Integer::sum);
// 컬렉팅
Map<String, List<Sale>> groupedByRegion = records.stream()
.collect(Collectors.groupingBy(Sale::getRegion));
Set<String> uniqueProducts = records.stream()
.map(Sale::getProduct)
.collect(Collectors.toSet());
}
}
새로운 날짜/시간 API
기존 Date, Calendar의 설계 결함과 스레드 안전성 문제를 해결한 java.time 패키지를 제공합니다. 모든 객체는 불변(immutable)입니다.
핵심 클래스
public class ModernDateTime {
public static void main(String[] args) {
// 현재 시점
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime current = LocalDateTime.now();
// 직접 생성
LocalDate birthDate = LocalDate.of(1995, 5, 23);
LocalTime wakeUp = LocalTime.of(7, 30);
// 조작 (기존 객체 불변, 새 객체 반환)
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate withYearChanged = today.withYear(2025);
// 특정 속성 추출
DayOfWeek dayOfWeek = today.getDayOfWeek();
int year = today.getYear();
}
}
포맷팅과 파싱
public class DateFormatting {
public static void main(String[] args) {
// 패턴 정의
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분");
// 포맷팅
String formatted = LocalDateTime.now().format(formatter);
// 파싱
LocalDateTime parsed = LocalDateTime.parse("2024년 12월 25일 14시 30분", formatter);
}
}
Instant와 ZoneId
public class InstantAndZone {
public static void main(String[] args) {
// UTC 기준 시간戳
Instant timestamp = Instant.now();
long epochMillis = timestamp.toEpochMilli();
// 시区 정보
ZoneId seoul = ZoneId.of("Asia/Seoul");
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
// 타임존 변환
ZonedDateTime inKorea = timestamp.atZone(seoul);
ZonedDateTime inJapan = timestamp.atZone(tokyo);
// 레거시 호환
Date legacyDate = new Date();
Instant converted = legacyDate.toInstant();
LocalDateTime modern = LocalDateTime.ofInstant(converted, seoul);
// 역변환
Instant backToInstant = modern.atZone(seoul).toInstant();
Date recovered = Date.from(backToInstant);
}
}
스레드 안전한 포맷터
public class ThreadSafeFormatter {
private static final DateTimeFormatter PATTERN =
DateTimeFormatter.ofPattern("yyyyMMdd");
public static void main(String[] args) throws Exception {
ExecutorService workers = Executors.newFixedThreadPool(10);
Callable<LocalDate> parser = () ->
LocalDate.parse("20241225", PATTERN);
List<Future<LocalDate>> futures = new ArrayList<>();
for (int i = 0; i < 100; i++) {
futures.add(workers.submit(parser));
}
for (Future<LocalDate> future : futures) {
System.out.println(future.get());
}
workers.shutdown();
}
}