멀티코어 CPU의 대중화와 하드웨어적인 변화도 자바 8에 변화를 미쳤다. 자바 8이 등장하기 이전에는 나머지 코어를 활용하려면 스레드를 사용하는 것이 좋다는 의견이 있었다. 하지만 스레드를 사용하면 관리가 어렵다.
자바 8은 간결한 코드, 멀티코어 프로세서이 쉬운 활용이라는 두 가지 요구사항을 기반으로 한다.
자바 8에 추가된 스트림 API 덕분에 다른 두 가지 기능, 즉 메서드에 코드를 전달하는 간결 기법(메서드 참조와 람다)과 인터페이스의 디폴트 메서드가 존재할 수 있음을 알 수 있다.
스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다. 이론적으로 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어들이며 마찬가지로 출력 스트림으로 데이터를 한 개씩 기록한다.
우선은 스트림 API가 조립 라인처럼 어떤 항목을 연속으로 제공하는 어떤 기능이라고 단순하게 생각하자.
스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만 이제 자바8에서는 우리가 하려는 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다. 또한 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있다. 스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수도 있다.
자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다. 메서드를 다른 메섣의 인수로 넘겨주는 개념을 동작 파라미터화라고 부른다.
스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초한다.
스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행할 수 있어야 한다. 그러기 위해서는 공유된 가변 데이터에 접근하지 않아야 한다. 이러한 함수를 순수 함수, 부작용 없는 함수, 상태 없는 함수라 부른다.
다중 프로세싱 코에에서 synchronized를 사용하면 생각보다 더 비싼 대가를 치뤄야할 수도 있다.
공유되지 않은 가변 뎅터, 메서드, 함수 코드를 다른 메서드로 전달하는 두 가지 기능은 함수형 프로그래밍 패러다이밍의 핵심 사상이다.
자바 8에서는 함수를 새로운 값의 형식으로 추가했다. 이는 뒤에 나오는 멀티코어에서 병렬 프로그래밍을 활용할 수 있는 스트림과 연계될 수 있도록 함수를 만들었기 때문이다.
런타임에 메서드를 전달할 수 있다면, 즉 메서드를 일급 시민으로 만들면 프로그래밍에 유용하게 활용할 수 있다.
메서드 참조라는 기능을 보자.
디렉터리에서 모든 숨겨진 파일을 필터링하는 코드다.
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
이미 isHidden 함수 준비되어 있으므로 메서드 참조 ::
(이 메서드를 값으로 사용하라는 의미)를 이용해서 listFiles에 직접 전달할 수 있다.
자바8에서는 메서드가 이급값이 나니 일급값이다.
자바 8에서는 메서드를 일급값으로 취급할 뿐 아니라 람다 (또는 익명 함수)를 포함하여 함수도 값으로 취급할 수 있다.
Apple 클래스와 getColor 메서드가 있고 Apples 리스트를 포함하는변수 inventory가 있다. 모든 녹색 사과를 선택해서 리스트를 반환하는 프로그램을 구현해보자.
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;
}
만약 사과를 무게(150g이상)으로 필터링한다면 아래와 같이 할 것이다. (복붙해서)
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;
}
복붙 코드는 좋지 않다. 한 쪽에 문제가 있으면 모두 고쳐야 한다.
자바 8에서는 코드를 인수로 넘겨줄 수 있으므로 filter 메서드를 중복으로 구현할 필요가 없다.
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)) {
reresult.add(apple);
}
}
return result;
}
다음 처럼 메서드를 호출할 수 있다.
filterApples(invnetory, Apple::isGreenApple);
filterApples(inventor,y Apple:isHeavyApple);
Predicate란?
filterApples는 Predicate<Apple>을 파라미터로 받고 있다. 인수로 받아 true나 false를 반환하는 함수를 프레디케이트라고 한다.
메서드를 값으로 전달하는 것은 유용하지만, 한두 번만 사용할 메서드를 매번 정의하는 것은 귀찮다.
다음 처럼 익명 함수 또는 람다를 이용해 코드를 구현할 수 있다.
filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );
filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
하지만 람다가 몇 줄이상으로 길어진다면 익명 람다보다는 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직하다.
컬렉션에서는 반복 과정을 직접 처리해야했다. 이런 방식의 반복을 외부 반복이라고 한다. 스트림을 쓰면 라이브러리 내부에서 모든 데이터가 처리된다. 이를 내부 반복이라 한다.
스트림에서는 필터링, 추출, 그룹화 등의 기능을 쉽게 병렬화 할 수 있다.
예를 들어 두 CPU를 가진 환경에서 리스트를 필터링할 때 한 CPU는 리스트의 앞 부분을 처리하고, 다른 CPU는 리스트의 뒷부분을 처리하도록 요청할 수 있다. 이 과정을 포킹 단계라고 한다. 그리고 각각의 CPU는 자신이 맡은 절반의 리스트를 처리하고, 마지막으로 하나의 CPU도 정리한다.
요즘은 외부에서 만들어진 컴포넌트를 이용해서 시스템을 구축하는 경향이 있다. 이전까지는 특별한 구조가 아닌 평범한 자바 패키지를 포함하는 JAR 파일을 제공하는 것이 전부였다.
자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 패키지 모음을 포함하는 모듈을 정의할 수 있다. 모듈 덕분에 JAR 같은 컴포넌트에 구조를 적용할 수 있으며 문서화와 모듈 확인이 용이해졌다.
자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다. 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 것이다.
예를 들어 자바 8에서는 List에 직접 sort 메서드를 호출할 수 있다. 이는 자바 8의 List 인터페이스에 다음과 같은 디폴트 메서드 정의가 추가되었기 때문이다.
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
따라서 자바 8 이전에는 List를 구현하는 모든 클래스가 sort를 구현해야했지만 자바 8부터는 디폴트 sort를 구현하지 않아도 된다.