자바 역사를 통틀어 가장 큰 변화가 자바 8에서 일어났다. 예를 들어 다음은 사과 목록을 무게순으로 정렬하는 고전적 코드다.
Collections.sort(inventory, new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight);
}
});
자바 8을 이용하면 자연어에 더 가깝게 간단한 방식으로 코드를 구현할 수 있다.
inventory.sort(comparing(Apple::getWeight));
거의 모든 자바 애플리케이션은 Collection
을 만들고 활용한다. 하지만 Collection
으로 모든 문제가 해결되는 것은 아니다.
아래의 예제 코드를 보도록 하자.
//리스트에서 고가의 거래만 필터링을 한다음 통화로 결과를 그룹화하는 코드이다.
Map<Currency, List<Transaction>> transactionsByCurrencies = new HasMap<>();
for (Transaction transaction : transactions){
if (transaction.getPrice() > 1000){
Currency currency = transaction.getCurrenct();
List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
if(transactionsForCurrenc == null){
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction);
}
}
스트림 API를 이용하면 다음처럴 코드를 작성할 수 있다.
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000)
.collect(groupingBy(Transaction::getCurrency));
java.util.stream
패키지에 스트림 API가 추가되었다. 스트림 패키지에 정의된 Stream는 T 형식으로 구성된 일련의 항목을 의미한다.동작 파라미터화
를 구현할 수 있다.😮 디폴트 메서드의 탄생배경
//inventory에서 Apple의 무게가 150이 넘는 Apple객체만 필터링하는 예제
List<Apple> heavyApples1 = inventory.stream()
.filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
자바 8 이전에는 List<T>
가stream
메서드를 지원하지 않는다는 것이 문제다.
따라서 위 예제는 컴파일 할 수 없는 코드다. 가장 간단한 해결책은 직접 인터페이스를 만들어 Collection
인터페이스에 stream
메서드를 추가하고 ArrayList
클래스에서 메서드를 구현하는 것이다.
하지만 이 방법은 사용자에게 너무 큰 고통을 안겨준다. 이미 Collection
API의 인터페이스를 구현하는 많은 Collection
프레임워크가 존재한다. 인터페이스에 새로운 메서드를 추가한다면 인터페이스를 구현하는 모든 클래스는 새로 추가된 메서드를 구현해야 한다.
🙄여기서 우리는 딜레마에 빠진다. 어떻게 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있을까?
자바 8에서는 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공한다. 메서드 본문은 클래스 구현이 아니라 인터페이스의 일부로 포함된다. 그래서 이를 디폴트 메서드 default method
라 부른다.
아래의 코드는 List
인터페이스에 추가된 default method
이다.
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
일급 객체는 아래 3개의 조건을 충족하는 객체를 1급객체라고 정의할 수 있다.
// name을 가져오는 메서드가 하나 존재한다.
public String getName(){
//TODO
}
// 조건1. 변수와 데이터에 할당 할 수 있어야 한다.
var name = getName;// Error # 변수에 메서드를 할당할 수 없다.
// 조건2. 객체의 인자로 넘길 수 있어야한다.
findByName(getName); // Error # 메서드의 인자로 메서드를 넘길 수 없다.
// 조건3. 객체의 리턴값으로 리턴 할 수 있어야 한다.
public doSomething() {
return getName; //Error #return 값으로 메서드를 넘길 수 없다
}
//결론 JAVA에서 메소드는 일급객체가 아니다.
만약 런타임 시점에 메서드를 전달할 수 있다면, 즉 메서드를 일급 객체로 만들면 프로그래밍에 유용하게 활용할 수 있다. 따라서 자바 8 설계자들은 이급 객체를 일급 객체로 바꿀수 있는 기능을 추가했다.
메서드를 일급객체로 사용하면 프로그래머가 활용할 수 있는 도구가 다양해지면서 프로그래밍이 수월해진다는 사실을 이미 실험을 통해 확인했다.
그래서 자바 8의 설계자들은 메서드를 값으로 취급할 수 있게, 프로그래머들이 더 쉽게 프로그램을 구현할 수 있는 환경이 제공되도록 자바8을 설계하기로 결정했다.
메소드 참조 (method reference)
디렉터리에서 모든 숨겨진 파일을 필터링한다고 가정하자.
우선 주어진 파일이 숨겨져 있는지 여부를 알려주는 메서드를 구현해야 한다.
다행히 File클래스는 이미 isHidden 메서드를 제공한다.
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isHidden(); // <- 숨겨진 파일 필터링!
}
});
그런데 완성한 코드가 마음에 들지 않는다. 단 세 행의 코드지만 각 행이 무슨 작업을 하는지 투명하지 않다.
File
클래스에는 이미 isHidden
이라는 메서드가 있는데 왜 굳이 FileFilter
로 isHidden
을 복잡하게 감싼다음에 FileFilter
를 인스턴스화 해야할까? 자바 8이 나타나기 전까지는 달리 방법이 없었기 때문이다.
자바 8에서는 아래와 같이 코드를 구현할 수 있다.
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
이미 isHidden이라는 함수는 준비되어 있으므로 자바 8의 `메서드 참조 ::` 를 이용해서 listFiles에 직접 전달할 수 있다.
람다 : 익명함수
자바 8에서는 메서드를 일급값으로 취급할 뿐 아니라 람다를 포함하여 함수도 값으로 취급할 수 있다.
//Apple 클래스와 getColor 메서드가 있고, Apples 리스트를 포함하는 변수 inventory가 있다.
//이때 모든 녹색 사과를 선택해서 리스트를 반환하는 프로그램을 구현하려한다.
public static List<Apple> filterGreenApples (List < Apple > inventory) {
List<Apple> result = new ArrayList<>(); // 반환되는 result는 List로, 처음에는 비어있지만
//점점 녹색사과로 채워진다.
for (Apple apple : inventory){
if(GREEN.equals(apple.getColor())){ // 녹색사과만 선택한다.
result.add(apple);
}
}
return result;
}
하지만 누군가는 사과를 무게로 필터링하고 싶을 수 있다. 그러면 우리는 아래와 같이 코드를 구현할 수 있을 것이다.
public static List<Apple> filterHeavyApples (List < Apple > inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory){
if(apple.getWeight() > 150){ // 150을 초과하는 무게를 가진 사과만 선택한다.
result.add(apple);
}
}
return result;
}
위 두개의 메서드는 if문만 다르다
만약 두 메서드가 단순히 크기를 기준으로 사과를 필터링하는 상황이었다면 인수로 (150,1000)을 넘겨주어 150그램 이상의 사과를 선택하거나 (0,80)을 넘겨주어 80그램 이하의 사과를 선택할 수 있을 것이다.
자바 8에서는 코드를 인수로 넘겨줄 수 있으므로 filter 메서드를 중복으로 구현할 필요가 없다.
앞의 코드를 다음처럼 자바 8에 맞게 구현할 수 있다.
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> inventroy, 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);
위와 같이 메서드를 전달하는 것은 유용한 기능이나 isHeavyApple, isGreenApple처럼 한 두번만 사용할 메서드를 매번 정의하는 것은 귀찮은 일이다. 자바 8에서는 이 문제도 간단히 해결할 수 있다.
filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()));
//또는 다음과 같이 구현한다.
filterApples(inventory, (Apple a) -> a.getWeight() > 150);
//심지어 다음과 같이 구현할 수도 있다.
filterApple(inventory, (Apple a) -> a.getWeight() < 80 || RED.equals(a.getColor()));
즉, 한 번만 사용할 메서드는 따로 정의를 구현할 필요가 없다. 위 코드는 우리가 넘겨주려는 코드를 애써 찾을 필요가 없을 정도로 더 짧고 간결하다.
하지만 람다가 몇 줄 이상으로 길어진다면 익명 람다보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용 하는것이 바람직하다.