수학에서는 인수로 값을 받아 true나 false를 반환하는 함수를 predicate라고 한다. boolean으로 반환.
동적 파라미터화 : 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록.
동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. 코드 블록은 나중에 프로그램에서 호출된다. (실행이 나중으로 미뤄짐) 예를 들어 나중에 실행될 메서드의 인수로 코드 블록을 전달할 수 있다. 결과적으로는 메서드의 동작이 파라미터화 되는 것이다.
동작파라미터화를 추가하려면 쓸데없는 코드가 늘어난다. 자바 8은 람다 표현식으로 이 문제를 해결한다.
// 초기 데이터
List<Apple> inventory = Arrays.asList(new Apple(80,"green"), new Apple(155, "green"), new Apple(120, "red"));
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;
}
// [Apple{color='green', weight=80}, Apple{color='green', weight=155}]
여기서 만약 다른 색으로 필터링 하고 싶다면?
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(apple.getColor().equals(color)){
result.add(apple);
}
}
return result;
}
// red로 파라미터 전달
// [Apple{color='red', weight=120}]
색 이외에도 무게로 구분할 수 있으면 좋겠다.
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(apple.getWeight() > weight){
result.add(apple);
}
}
return result;
}
// 100으로 파라미터 전달
// [Apple{color='green', weight=155}, Apple{color='red', weight=120}]
// 선택 조건을 결정하는 인터페이스
interface ApplePredicate {
public boolean test(Apple a);
}
// 무거운 사과만 선택
static class AppleWeightPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
// 녹색 사과만 선택
static class AppleColorPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
}
이렇게 메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행할 수 있다.
public static List<Apple> filter(List<Apple> inventory, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) { // 프레디케이트 객체로 사과 검사 조건을 캡슐화
result.add(apple);
}
}
return result;
}
static class AppleRedAndHeavyPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return "red".equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
List<Apple> list = filterAapples(inventory, new AppleRedHeavyPredicate());
filterApples 메서드 동작을 파라미터화 한 것. = ‘코드를 전달'할 수 있는 것이나 다름없다.
람다 표현식 : 메서드로 전달할 수 있는 익명 함수를 단순화한 것. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있다.
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
(String s) -> s.length()
// String 형식의 파라미터를 가지며, int를 반환
(Apple a) -> a.getLength() > 150
// Apple 형식의 파라미터를 가지며, boolean을 반환
(int x, int y) -> {
System.out.println("Result :");
System.out.println(x + y); }
// int 형식의 파라미터 2개를 가지며, 리턴값이 없음
() -> 42
// 파라미터가 없으며, int 42를 반환
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
// Apple 형식의 파라미터 2개를 가지며, int(두 사과의 무게 비교 결과)를 반환
List<Apple> list = filter(inventory, (Apple a) -> "green".equals(a.getColor()));
람다는 정확히 말하자면 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다. 위 예제에서는 함수형 인터페이스 Predicate<T>를 기대하는 filter 메서드의 두 번째 인수로 람다 표현식을 전달했다.
정확히 하나의 추상 메서드를 지정하는 인터페이스.
많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스다.
추상 메서드가 하나라는 뜻은 default method 또는 static method 는 여러 개 존재해도 상관 없다는 뜻.
그리고 @FunctionalInterface 어노테이션을 사용하는데, 이 어노테이션은 해당 인터페이스가 함수형 인터페이스 조건에 맞는지 검사해준다. 이 어노테이션이 없어도 함수형 인터페이스로 동작하고 사용하는 데 문제는 없지만, 인터페이스 검증과 유지보수를 위해 붙여주는 게 좋다.
함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 부른다.
예를 들어 Runnable 인터페이스의 유일한 추상 메서드 run은 인수와 반환값이 없으므로(void) Runnable 인터페이스는 인수와 반환 값이 없는 시그니처로 생각할 수 있다.() -> void 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미한다.
Predicate : test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불린을 반환함.
Consumer : 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의함.
Function<T, R> : 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 apply라는 추상 메서드를 정의함. 입력을 출력으로 매핑하는 람다를 정의할 때 활용할 수 있음.
인자를 받지 않고 리턴값도 없는 인터페이스.
코드를 조금 더 단순화 시킬 수 있다. 자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)를 이용하여 람다 표현식과 관련된 함수형 인터페이스를 추론하다. 즉, 대상 형식을 이용하여 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론 가능하다. 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있기 때문에 람다 문법에서 이를 생략할 수 있다.
// 형식을 추론하지 않음
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 형식을 추론함
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
그런데, 붙이는게 더 가독성을 위해 좋은 것이 아닌가?
상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고, 형식을 배제하는 것이 가독성을 향상시킬 때도 있다고 한다.
메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 레퍼런스를 활용 가능.
ex) Apple::getWeight = Apple 클래스에 정의된 getWeight의 메서드 레퍼런스
결과적으로 메서드 레퍼런스는 람다 표현식 (Apple a) → a.getWeigth()를 축약한 것.
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2)); // 둘이 동일
str.sort(String::conpareToIgnoreCase);
(arg0, rest) -> arg0.instanceMethod(rest) // 람다식. arg0은 ClassName 형식
ClassName::instanceMethod // 메서드 참조
(String s) -> s.length() // 람다식
String::length // 메서드 참조
(String s1, String s2) -> s1.split(s2) // 람다식
String::split // 메서드 참조
(String s1, String s2) -> s2.split(s1) // s1과 s2의 위치를 바꾼 경우 메서드 참조 불가능
(args) -> expr.instanceMethod(args) // 람다식
expr::instanceMethod // 메서드 참조
Transaction expensiveTransaction = new Transaction(1);
() -> expensiveTransaction.getValue() // 람다식
expensiveTransaction::getValue // 메서드 참조
(int value) -> expensiveTransaction.setValue(value) // 람다식
expensiveTransaction::setValue // 메서드 참조
() -> Apple
Supplier<Apple> c1 = Apple::new; // 둘이 동일
Supplier<Apple> c1 = () -> new Apple;
inventory.sort(comparing(Apple::getWeight));
‘Apple을 weight별로 비교해서 inventory를 sort하라’
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
inventory.sort(comparing(Apple::getWeight)
.reversed() // 역정렬
.thenComparing(Apple::getCountry)); // 같다면 메서드로 두 번째 비교자(나라)
// 빨간 색이 아닌 사과 (negate)
Predicate<Apple> notRedApple = redApple.negate();
// 빨간색이면서 무거운 사과 (and)
Predicate<Apple> RedHeavyApple = redApple.and(apple -> apple.getWeight > 150);
// 빨간색이면서 무거운 사과 또는 초록색 사과 (or)
Predicate<Apple> RedHeavyOrGreenApple =
redApple.and(apple -> apple.getWeight > 150)
.or(apple -> GREEN.equals(a.getColor()));
자바8 이전
List <Dish> lowCalDishes = new ArrayList<>(); // 가비지 변수 사용
for(Dish dish : menu)[
if(dish.getCAlories() < 400){
lowCalDishes.add(dish);
}
}
Collection.sort(lowCalDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2){
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
List <String> lowCalDishesNm = new ArrayList<>();
for(Dish dish : lowCalDishes){
lowCalDishesNm.add(dish.getName());
}
스트림을 이용하면 선언형으로 컬렉션 데이터를 처리할 수 있다.
List<String> lowCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() < 400) // 400칼로리 이하 요리 선택
.sorted(comparing(Dish::getCalories)) // 칼로리로 요리 정렬
.map(Dish::getName) // 요리명 추출
.collect(toList()); // 모든 요리명을 리스트에 저장
스트림 : 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소
컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조다. 즉, 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다. 반면 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조다. 이러한 스트림의 특성은 게으른 생성을 가능하게 한다.
List<Stirng> names = menu.stream() // 메뉴에서 스트림을 얻는다
.filter(dish -> dish.getCalories > 300) // 파이프 라인 연산 만들기. 필터링
.map(Dish::getname) // 요리명 추출
.limit(3) // 선착순 세 개만 선택
.collect(toList()); // 결과를 다른 리스트로 저장
// [pork, beef, chicken]
스트림은 연결할 수 있는 스트림 연산인 중간 연산과 스트림을 닫는 연산인 최종 연산으로 구성된다.
List<Stirng> names = menu.stream() // 요리 리스트에서 스트림 얻기
.filter(dish -> dish.getCalories > 300) // 중간 연산
.map(Dish::getname) // 중간 연산
.limit(3) // 중간 연산
.collect(toList()); // 스트림을 리스트로 변환
스트림 인터페이스는 filter 메서드를 지원한다. filter메서드는 프레디케이트(불리언 반환하는 함수)를 인수로 받아 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());
스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다. (중복 제거)
List<Dish> vegetarianMenu = menu.stream()
.filter(i -> i % 2 ==0)
.disticnt()
.forEach(System.out::printtln);
주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다.
skip(n)은 처음 n개의 요소를 제외, 이후 요소 만을 포함하는 스트림을 반환한다.
특정 객체에서 특정데이터를 선택하는 작업.
Dish객체를 요소로 가지고 있는 스트림 → 매핑 → Dish객체의 calories값을 요소로 가지는 스트림
List<String> dishNames =
menu.stream()
.map(Dish::getCalorie)
.collect(toList());
자바의 &&, ||와 같은 스트림의 쇼트서킷 기법.
(쇼트서킷은 &&, || 같이 앞 조건식의 결과에 따라 뒤 조건식의 실행여부를 결정하는 논리연산자.)
스트림의 모든 요소가 주어진 Predicate과 일치하는지 여부 반환스트림의 요소 중에서 Predicate과 일치하는 경우가 적어도 하나라도 있는지 여부 반환allMatch와 반대되는 연산public boolean isLikeUser(String userId) {
if (null == userId) return false;
return likeUsers.stream()
.anyMatch(u -> u.getUserId().equals(userId));
}
값의 존재나 부재 여부를 표현하는 컨테이너 클래스.
collect는 스트림의 요소를 요약 결과로 누적하는 최종 연산.
.toList // 각 요소를 리스트로 만들어라
.groupingBy // 각 키, 그리고 키에 대응하는 요소 리스트를 값으로 포함하는 Map을 만들어라
리듀싱 연산 : 모든 스트림 요소를 처리해서 값으로 도출.
long howManyDishes = menu.stream.collect(Collectors.counting());
//Comparator 구현
Comparator caloriesComparator = Comparator.comparingInt(Dish::getCalories);
//스트림 연산
Optional mostCalorieDish = menu.steam()
.collect(maxBy(caloriesComparator));
int totalCalories = menu.steam().collect(summingInt(Dish::getCalories));
String shortMenu = menu.stream().map(Dish::getName).collect(joining( 구분값 ));
// groupingBy 사용
Map<Dish.Type, List> dishesByType =
menu.stream().collect(groupingby(Dish::getType));
// {FISH=[prawns, salmon], MEAT=[pork,beef,chicken] }
분할은 분할 함수라 불리는 프리디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다.
분할 함수는 불리언을 반환하므로 맵의 키 형식은 Boolean이다.
결과적으로 그룹화 맵은 최대 두 개의 구룹으로 분류된다. 분류 함수로 사용했을 때 결과 Map의 키는 true, false로 나온다.
Map<Boolean, List> partitionedMenu =
menu.stream.collect(partitioningBy(Dish::isVegetarian));
// {false = [pork, beef, chicken, salmon],
// true = [french fries, rice, fruit]}
각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림이다. 따라서 병렬 스트림을 이용하면 모든 멀티코어 프로세서가 각각의 청크를 처리하도록 할당할 수 있다.
(Chunk : 아이템이 트랜잭션에 commit되는 수)
ex) 1부터 n까지 합을 더하는 연산
public long iterativeSum(long n){
long result = 0;
for (long i = 1L; i <= n; i++){
result += i;
}
return result;
}
스트림을 활용하면 스레드별로 task를 나누어 병렬적으로 처리하는 것이 가능하다. 또한 이 방법이 더 효과적이다.
Stream.itereate(1L, i -> i + 1)
.limit(n)
.parallel() // parallel() 메소드는 실제 이 메소드가 호출 되는 시점에 어떤 작업을 처리하는 것이 아니라 이 pipeline을 병렬로 처리하라고 하는 정보를 제공해주는 메소드
.reduce(0L, Long::sum);
몇 개의 스레드를 사용해야 하는지, 어떻게 결과 변수를 동기화 할 건지, 몇 개의 스레드를 사용해야 할까? 숫자는 어떻게 생성할까? 생성된 숫자는 누가 더할까?
= 리듀싱 연산으로 스트림의 모든 숫자를 더한다. 이전 코드와 다른 점은 스트림이 여러 청크로 분할되어 있다는 것이다. 따라서 리듀싱 연산을 여러 청크에 병렬로 수행할 수 있다. 마지막엔 리듀싱 연산으로 생성된 부분 결과를 다시 리듀싱 연산으로 합쳐서 전체 스트림의 리듀싱 결과를 도출한다.
1000개 이상일 때 병렬 스트림 쓰자와 같이 양이 기준이 되면 안된다. 하지만 힌트는 된다.