Java8 - Java8 이란?
이 포스트에서는 Java8 에 추가된 기능인 아래 내용에 대해 간략히 살펴본다.
- Stream
- 메서드 레퍼런스
- 람다 표현식
- 디폴트 메서드
소스는 github 에 있습니다.
목차
1. Java8 에 추가된 기능
1.1. Stream 처리
스트림은 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다.
Java8 에는 Stream API 가 추가되었는데 지금을 일단 단순히 어떤 항목을 연속으로 제공하는 어떠한 기능이라고 생각하면 된다.
Stream API 는 아래와 같은 특징이 있다.
- 기존에는 한 번에 한 항목을 처리했지만 Stream API 를 이용하여 일련의 스트림으로 만들어 처리 가능
- 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당 가능
→ 스레드를 사용하지 않으면서도 병렬 처리 가능
Stream API 의 좀 더 자세한 내용은 Java8 - Stream
, Java8 - Stream 활용 (1): 필터링, 슬라이싱, 매핑, 검색, 매칭
, Java8 - Stream 활용 (2): 리듀싱, 숫자형 스트림, 스트림 생성
, Java8 - Stream 으로 데이터 수집 (1): Collectors 클래스, Reducing 과 Summary, Grouping
, Java8 - Stream 으로 데이터 수집 (2): Partitioning, Collector 인터페이스, Custom Collector , Java8 - Stream 으로 병렬 데이터 처리 (1): 병렬 스트림, 포크/조인 프레임워크 , Java8 - Stream 으로 병렬 데이터 처리 (2): Spliterator 인터페이스 를 참고하세요.
1.2. 동작 파라메터화로 메서드에 코드 전달
코드의 일부를 API 로 전달하는 것으로 메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공한다.
동작 파라메터화의 좀 더 자세한 내용은 Java8 - 동작 파라메터화 을 참고해주세요.
1.3. 병렬성과 공유 가변 데이터
다른 코드와 동시에 실행하더라도 안전하게 실행할 수 있는 코드를 만들려면 공유된 가변 데이터에 접근하지 않아야 한다.
공유된 가변 데이터에 접근하면 어떤 상황이 발생하는지는 Java8 - Stream 으로 병렬 데이터 처리 (1): 병렬 스트림, 포크/조인 프레임워크 의 1.3. 병렬 스트림의 올바른 사용 를 참고하세요.
만일 공유된 변수가 객체가 있으면 병렬성에 문제가 발생한다. (예 - 공유된 변수를 동시에 변경 시도)
기존처럼 synchronized 를 이용하여 공유된 가변 데이터를 보호할 수도 있지만 일반적으로 synchronized 는 시스템 성능에 악영향을 미친다.
Java8 의 Stream 을 이용하면 기존의 Java 스레드 API 보다 쉽게 병렬성을 활용할 수 있다.
2. Java 함수
Java8 에서는 함수를 새로운 값의 형식으로 추가했다. 기존엔 함수가 일급 시민이 아니었지만, Java8 부터는 일급 시민의 조건을 충족한다.
- 변수나 데이터에 할당 가능해야 함
- 객체의 인자로 넘길 수 있어야 함
- 객체의 리턴값으로 리턴할 수 있어야 함
2.1. 메서드 레퍼런스: ::
Java8 에서는 메서드가 일급 시민이므로 기존에 객체 레퍼런스(new 로 객체 레퍼런스를 생성) 를 이용하여 객체를 주고 받는 것이 아닌, ::
문법을 이용하여 메서드 레퍼런스를 만들어 전달 가능하다.
::
: 이 메서드를 값으로 사용하라는 의미, 예) File::isHidden
아래는 기존 자바를 메서드 레퍼런스를 이용한 예시이다.
// 기존 자바
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
});
// 메서드 레퍼런스 이용
File[] hiddenFiles2 = new File(".").listFiles(File::isHidden); // File 클래스에 있는 isHidden 함수를 값으로 사용
또 다른 예로 Apple 이라는 클래스가 있고, Apple 리스트를 담고 있는 inventory 리스트가 있을 때 여기서 빨간 사과와 100g 이 넘는 사과를 필터링하려면 기존 자바로는 아래와 같이 구현 가능하다.
List<Apple> inventory = Arrays.asList(new Apple(10, "red"),
new Apple(100, "green"),
new Apple(150, "red"));
// Apple 이라는 클래스
public static class Apple {
private int weight;
private String color;
public Apple(int weight, String color) {
this.weight = weight;
this.color = color;
}
public int getWeight() {
return weight;
}
public String getColor() {
return color;
}
...
}
기존 자바
// 기존 자바에서 red 사과 필터링 시
public static List<Apple> filterRedApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if ("red".equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
// 기존 자바에서 100 이상 필터링 시
public static List<Apple> filterWeightApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if (apple.getWeight() >= 100) {
result.add(apple);
}
}
return result;
}
// 호출 시
// 기존 자바에서 red 사과 필터링 시
List<Apple> redApples = filterRedApples(inventory);
// [Apple{weight=10, color='red'}, Apple{weight=150, color='red'}]
System.out.println(redApples);
// 기존 자바에서 100 이상 필터링 시 (filterRedApples() 와 중복)
List<Apple> weightApples = filterWeightApples(inventory);
// [Apple{weight=100, color='green'}, Apple{weight=150, color='red'}]
System.out.println(weightApples);
메서드 레퍼런스로 변경
// 메서드 레퍼런스로 필터링 시
public static boolean isRedApple(Apple apple) {
return "red".equals(apple.getColor());
}
public static boolean isWeightApple(Apple apple) {
return apple.getWeight() >= 100;
}
static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
// p 가 제시하는 조건에 사과가 맞는가?
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
// 호출 시
// 메서드 레퍼런스로 red 사과 필터링 시
List<Apple> redApples2 = filterApples(inventory, Chap01Application::isRedApple); // Chap01Application 은 클래스명
// [Apple{weight=10, color='red'}, Apple{weight=150, color='red'}]
System.out.println(redApples2);
// 메서드 레퍼런스로 100 이상 필터링 시
List<Apple> weightApples2 = filterApples(inventory, Chap01Application::isWeightApple);
// [Apple{weight=100, color='green'}, Apple{weight=150, color='red'}]
System.out.println(weightApples2);
Predicate
위에서 Apple::isRedApple 은 filterApples 의 두 번째 인자인 Predicate<Apple> p 로 넘겨주었다.인수를 값으로 받아서 true/false 로 반환하는 함수를 Predicate 라고 한다.
메서드 레퍼런스의 좀 더 상세한 내용은 Java8 - 람다 표현식 (2): 메서드 레퍼런스, 람다 표현식과 메서드의 조합 을 참고하세요.
2.2. 람다 (익명 함수)
Java8 은 메서드를 일급값으로 취급할 뿐 아니라 람다(=익명 함수)를 포함하여 함수도 값으로 취급할 수 있다.
예) (int x) -> x + 1; // x 을 인수로 호출하면 x+1 리턴
람다 문법으로 구현된 프로그램을 함수형 프로그래밍(=함수를 일급값으로 넘겨줌) 을 구현한다라고 한다.
바로 위의 메서드 레퍼런스로 사과를 필터링하는 부분을 다시 보자.
메서드를 값으로 전달하는 것도 좋지만 만일 한 번만 사용할 기능이라면 조건이 늘어날때마다 isRedApple(), isWeightApple() 를 추가해주는게 아니라 람다로 개발하는 것이 편하다.
람다로 필터링 시
// 람다로 red 사과 필터링 시
List<Apple> redApples3 = filterApples(inventory, (Apple a) -> "red".equals(a.getColor()));
// [Apple{weight=10, color='red'}, Apple{weight=150, color='red'}]
System.out.println(redApples3);
// 람다로 100 이상 필터링 시
List<Apple> weightApples3 = filterApples(inventory, (Apple a) -> a.getWeight() >= 100);
// [Apple{weight=100, color='green'}, Apple{weight=150, color='red'}]
System.out.println(weightApples3);
람다의 좀 더 상세한 내용은 Java8 - 람다 표현식 (1): 함수형 인터페이스, 형식 검사 의 1. 람다 표현식 을 참고하세요.
3. Stream
Java 애플리케이션은 Collection 을 많이 활용한다.
Stream API 를 이용하면 Collection API 와는 상당히 다른 방식으로 데이터를 처리할 수 있다.
Collection 에서는 반복 작업 시 for-each 를 통해 각 요소를 반복하면서 직접 반복 작업을 진행(=외부 반복)하는 반면
Stream API 에서는 루프를 신경쓸 필요없이 라이브러리 내부에서 모든 데이터가 처리된다. (=내부 반복)
또한 Collection 을 이용할 때 많은 요소를 가진 목록을 반복한다면 단일 CPU 로는 힘들수도 있기 때문에 서로 다은 CPU 코어에 작업을 각각 할당해서 처리 시간을 줄이는 것이 좋다.
이전의 자바에서 제공하는 스레드 API 로 멀티스레딩을 구현해서 병렬성을 이용하는 것은 쉽지 않다.(각 스레드가 동시에 공유된 데이터에 접근하고 데이터를 갱신할 수 있음)
Java8 은 Stream API 로 Collection 을 처리하면서 발생하는 반복적인 코드 문제와 멀티코어 활용의 어려움을 해결한다.
Stream API 는 요소를 병렬로 쉽게 처리할 수 있는 환경을 제공한다.
아래는 각각 순차/병렬 처리 방식의 코드이다.
순차 처리 방식
List<Apple> weightApples = inventory.stream().filter((Apple a) -> a.getWeight() >= 100)
.collect(toList());
병렬 처리 방식
List<Apple> weightApples = inventory.parallelStream().filter((Apple a) -> a.getWeight() >= 100)
.collect(toList());
Collection 을 필터링할 수 있는 가장 빠른 방법은 Collection 을 Stream 으로 변경하여 병렬로 처리한 다음 List 로 다시 복원하는 것임.
병렬 처리와 성능에 대해서는 Java8 - Stream 으로 병렬 데이터 처리 (1): 병렬 스트림, 포크/조인 프레임워크 와 Java8 - Stream 으로 병렬 데이터 처리 (2): Spliterator 인터페이스 를 참고하세요.
4. 디폴트 메서드: default
디폴트 메서드는 메서드 바디를 제공하여 인터페이스를 구현하는 다른 클래스에서 해당 메서드를 구현하지 않아도 되는 기능이다. 즉, 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스가 포함할 수 있도록 한다.
Java8 에서 List 의 sort 를 직접 호출할 수 있는 이유도 Java8 의 List 인터페이스에 아래와 같은 디폴트 메서드 정의가 추가되었기 때문이다.
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
따라서 Java8 이전에는 List 를 구현하는 모든 클래스가 sort 를 구현해야 했지만 Java8 부터는 디폴트 sort 를 구현하지 않아도 된다.
이 외에도 NullPointer 예외를 피할 수 있도록 해주는 Optional<T>
클래스도 제공한다.
Optional<T>
는 값을 갖거나 갖지 않을 수 있는 컨테이너이다.
Optional<T>
에 대한 좀 더 상세한 내용은 Java8 - Optional 클래스 를 참고하세요.
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트 저자의 Java 8 in Action을 기반으로 스터디하며 정리한 내용들입니다.