자바 역사를 통틀어 가장 큰 변화가 자바 8에서 일어났고, 우리는 자바의 크고 작은 변화 덕분에 프로그램을 더 쉽게 구현할 수 있게 되었다.
다음은 사과 목록을 무게순으로 정렬하는 고전적 코드입니다.
Collections.sort(inventory, new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
inventory.sort(comparing(Apple::getWeight));
자바 8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용이라는 두 가지 요구사항을 기반으로 합니다.
스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초한다.
스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 합니다.
→ 다중 프로세싱 코어에서 synchronized를 사용하면 (다중 처리 코어에서는 코드가 순차적으로 실행되어야 하므로 병렬이라는 목적을 무력화시키면서) 생각보다 훨씬 더 비싼 대가를 치러야 할 수 있다.
자바 8에서 함수를 새로운 값의 형식으로 추가했습니다. (이급 시민을 일급 시민으로 바꿀 수 있는 기능)
프로그래밍 언어의 핵심은 값을 바꾸는 것입니다.
디렉터리에서 모든 숨겨진 파일을 필터링한다고 가정하자. 다행히 File 클래스는 이미 isHidden 메서드를 제공한다.
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
}
File 클래스에는 이미 isHidden 메서드가 있는데 왜 굳이 FileFilter로 isHidden을 복잡하게 감싼 다음에 FileFilter를 인스턴스화해야 할까?
자바 8
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
자바 8의 메서드 참조 :: ('이 메서드를 값으로 사용하라'는 의미)를 이용해서 listFiles에 직접 전달할 수 있다.
직접 메서드를 정의할 수도 있지만, 이용할 수 있는 편리한 클래스나 메서드가 없을 때 새로운 람다 문법을 이용하면 더 간결하게 코드를 구현할 수 있습니다.
프레디케이트(predicate)란?
- 인수로 값을 받아 true나 false를 반환하는 함수
- 메서드가 p라는 이름의 프레디케이트 파라미터로 전달됨.
public static boolean isGreenApple(Apple apple) {
return GREEN.equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public interface Predicate<T> {
boolean test(T t)
}
static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory) {
if(p.test(apple)) {
result.add(apple);
}
}
return result;
}
// 다음처럼 메서드 호출 가능
filterApples(inventory, Apple::isGreenApple);
filterApples(inventory, Apple::isHeavyApple);
재사용성이 없는 메서드는 익명 함수 또는 람다로 해결할 수 있습니다.
하지만 람다가 몇 줄 이상으로 길어진다면, 익명 람다 보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직합니다. 코드의 명확성이 우선시되어야 합니다.
리스트에서 고가의 트랜잭션(거래)만 필터링한 다음에 통화로 결과를 그룹화해야 한다고 가정하자.
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for(Transaction transaction: transactions) {
if(transaction.getPrice() > 1000) {
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
if(transactionsForCurrency == null) {
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency, transactionsForCurrency);
}
transactionForCurrency.add(transaction);
}
}
import static java.util.stream.Collectors.groupingBy;
Map<Currency, List<Transaction>> transactionByCurrencies =
transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000)
.collect(groupingBy(Transaction::getCurrency));
컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점을 두는 반면 스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다. 스트림은 스트림 내의 요소를 쉽게 병렬로 처리할 수 있는 환경을 제공한다는 것이 핵심이다.
자바의 변화 과정 속 개발자들이 겪는 어려움 중 하나는 기존 인터페이스의 변경이었습니다. 인터페이스를 업데이트하려면 해당 인터페이스를 구현하는 모든 클래스도 업데이트해야 하므로 불가능에 가까웠습니다. 이 문제를 해결하고자 나온 것이 디폴트 메서드입니다.
어떻게 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있을까?
그런데 고민해야 할 문제가 한 가지 있습니다. 여러 인터페이스에 다중 디폴트 메서드가 존재할 수 있다는 것은 결국 다중 상속이 허용된다는 의미일까?
NullPointer 예외를 피할 수 있도록 도와주는 Optional 클래스의 등장