이 장의 주요 내용
- 자바가 거듭 변화하는 이유
- 컴퓨팅 환경의 변화
- 자바에 부여되는 시대적 변화 요구
- 자바 8과 자바 9의 새로운 핵심 기능 소개
자바 역사를 통틀어 가장 큰 변화가 자바 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));
멀티코어 CPU 대중화와 같은 하드웨어적인 변화도 자바 8에 영향을 미쳤다.
synchronized
키워드를 쓰지 않아도 된다.→ 조금 다른 관점에서 보면, 결국 자바 8의 스트림 API 덕에 다른 두 가지 기능도 존재할 수 있음을 알 수 있다.
이 개념은 ‘병렬성을 공짜로 얻을 수 있다’라는 말에서 시작된다.
스트림 메소드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전해야 실행되어야 한다.
자바 8의 스트림을 이용하면 기존의 자바 스레드 API보다 쉽게 병렬성을 활용할 수 있다.
synchronized
를 사용하면 생각보다 훨씬 큰 대가를 치러야 할 수 있다.함수형 프로그래밍의 패러다임
- 공유되지 않은 가변 데이터, 메소드
- 함수 코드를 다른 메소드로 전달하는 기능
명령형 프로그래밍의 패러다임
- 일련의 가변 상태로 프로그램을 정의
기존 값을 변화시키는 데 집중했던 고전적인 객체지형에서 벗어나, 함수형 프로그래밍으로 다가섰다는 것이 자바 8의 가장 큰 변화이다.
→ 왜 함수가 필요할까? 를 생각해보자.
하지만 이게 중요할까? (값 : 일급 시민 , 구조체 : 이급 시민)
중요하다!
런타임에 메소드를 전달할 수 있다면? = 메소드를 일급 시민으로 만든다면?
→ 프로그래밍에 굉장히 유용하게 활용할 수 있다!
메소드 참조
가정 : 디렉토리에서 모든 숨겨진 파일을 필터링한다.
/*
다행히 File 클래스는 이미 isHidden() 메소드를 제공한다.
아래 코드처럼 FileFilter 객체 내부에 위치한 isHidden의 결과를
File.listFiles() 메소드로 전달하는 방법으로 숨겨진 필터를 필터링 할 수 있다.
*/
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
});
자바 8에서는 아래 코드처럼 간단하게 구현할 수 있다.
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
메소드 참조(:: 이 메소드를 값으로 사용하라)를 이용해 더 이상 메소드는 이급 값이 아닌 일급 값이다.
모든 예제는 한빛미디어 웹페이지에서 내려받을 수 있다.
특정 학목을 선택해서 반환하는 동작을 필터(filter)라고 한다.
필터 사용 예제
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
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) {**
result.add(apple);
}
}
return result;
}
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public 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);
Predicate
란?true
나 false
로 반환하는 함수를 predicate
라고 한다.//초록 사과 골라내기
filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );
//무거운 사과 골라내기
filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
하지만, 람다가 몇 줄 이상으로 길어진다면(즉, 복잡한 동작을 수행한다면)?
→ 코드가 수행하는 일을 잘 설명하는 이름을 가진 메소드를 정의하고, 이전처럼 메소드 참조를 활용하자.
→ 코드의 명확성이 우선시 되어야 한다.
여기까지는 멀티코어 CPU가 자바 8 설계자들의 가정에 없을 때의 이야기였다.
//위에서 쓴 람다식 사용 예제
filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
//새롭게 제시된 기능의 함수 원형
static <T> Collcetion<T> filter(Collection<T> c, Predicate<T> p);
//무거운 사과를 골라내도록 스트림 사용법
filter(inventory, (Apple a) -> a.getWeight > 150 );
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<>();
transactionsForCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction); //현재 탐색된 트랜잭션을 해당 통화 그룹에 추가
}
}
import static java.util.stream.Collectors.groupingBy;
Map<Currency, List<Transaction>> transactionsByCurrencies =
transaction.stream()
.filter((Transaction t) -> t.getPrice() > 1000) //고가의 트랜잭션 필터링
.collect(groupingBy(Transaction::getCurrency)); //통화로 그룹핑
스트림 API를 이용하면 컬렉션 API와는 상당히 다른 방식으로 데이터를 처리할 수 있다는 사실만 기억하자.
또한 위에서 제시한 동작들을 쉽게 병렬화할 수 있다면?
포크-조인 프레임워크
스트림은 내부 요소를 쉽게 병렬로 처리할 수 있는 환경을 제공한다는 것이 핵심이다.
조금 이상하게 들릴 수 있지만, 컬렉션을 가장 빨리 필터링하는 방법?
→ 컬렉션을 스트림으로 바꾸고, 병렬 처리 후 다시 리스트로 복원하는 방법이다.
예제 코드
//순차 처리 방식 코드
List<Apple> heavyApples =
inventory.**stream**().filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
//병렬 처리 방식 코드 : 사실상 병렬성을 공짜로 얻었다!
List<Apple> heavyApples =
inventory.**parallelStream**().filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
컬렉션은 어떻게 데이터를 저장하고 접근할 지에 중점을 두는 반면,
스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다는 것을 기억하자.
List<Apple> heavyApples1 = inventory.stream()
.filter((Apple) a) -> a.getWeight() > 150)
.collect(toList());
List<Apple> heavyApples2 = inventory.parallelStream()
.filter((Apple) a) -> a.getWeight() > 150)
.collect(toList());
🌱 하나의 클래스에서 여러 인터페이스를 구현할 수 있다.
= 여러 인터페이스에 다중 디폴트 메소드가 존재할 수 있다
= 다중 상속이 허용된다??→ 엄밈히 말하면 다중 상속은 아니지만, 어느 정도는 ‘그렇다’.
→ 9장에서 악명 높은 다이아몬드 상속 문제에 대해 다뤄보자.
함수형 프로그래밍의 핵심적인 두 아이디어
- 메서드와 람다를 일급값으로 사용하는 것.
- 가변 공유 상태가 없는 병렬 실행을 이용해 효율적이고 안전하게 함수나 메서드를 호출하는 것
null
을 회피하는 기법NullPointerException
을 피할 수 있도록 도와주는 Optional<T>
클래스를 제공한다."Brakes 클래스는 Car 클래스를 구성하는 클래스 중 하나입니다.
Brakes를 어떻게 처리해야 할지 설정하지 않았습니다."
잘보고갑니당