03.스트림과 람다를 이용한 효과적 프로그래밍
컬렉션 API 개선
컬렉션 팩토리
반환 객체는 불변
List Factory
//Before
List<String> friends = Arrays.asList("Park", "Kim", "Jeong");
//After
List<String> friends = List.of("Park", "Kim", "Jeong")
Set Factory
Set<String> friends = Set.of("Park", "Kim", "Jeong");
Map Factory
// 열 개 이하의 키/값 쌍을 가진 작은 맵을 만들 경우
Map<String, Integer> ageOfFriends = Map.of("Park", 20, "Kim", 21, "Jeong", 25);
// 그 이상의 맵 생성 (Map.enty: Map.Entry 객체를 만드는 새로운 팩토리 메서드)
import static java.util.Map.entry;
Map<String, String> ageOfFriends = Map.ofEntries(
entry("Park", 20),
entry("Kim", 21),
entry("Jeong", 25));
리스트와 집합 처리
기존 컬렉션 객체와 Iterator 객체를 혼용한 삭제, 수정은 쉽게 문제를 일으켰었다.
그래서, java8에서는 List, Set Interface 에 새로운 메서드가 추가되었다. (기존 컬렉션 자체를 수정)
removeIf
Predicate를 만족하는 요소 제거 (List, Set)
transactions.removeIf(transaction ->
Character.isDigit(transaction.getReferenceCode().charAt(0)));
replaceAll
UnaryOperator 함수를 이용하여 요소 교체 (List)
referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));
sort
리스트 정렬 (List)
list.sort(Comparator.naturalOrder()); // 오름차순
list.sort(Comparator.reverseOrder());
list.sort(String.CASE_INSENSITIVE_ORDER); // 대소문자 구분없이 오름차순
list.sort(Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER));
맵 처리
1. forEach
기존 맵에서 키/값을 반복하며 확인했다면, forEach 메서드를 사용해보자.
ageOfFriens.forEach((friend, age) -> System.out.println(friens + " is" + age + " years old"));
2. Order
Entry.comparingByValue / Entry.comparingByKey
favouriteMovies.entrySet().stream()
.sorted(Entry.comparingByKey()) // 오름차순
// .sorted(Entry.comparingByKey(Comparator.reverseOrder())) // 내림차순
.forEachOrdered(System.out::println);
3. getOrDefault
찾으려는 키가 존재하지 않으면 기본값을 반환 getOrDefault(key, default)
Map<String, String> favouriteMovies = Map.ofEntries(
entry("Raphael", "Star Wars"),
entry("Olivia", "James Bond"));
System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix")); // James Bond
System.out.println(favouriteMovies.getOrDefault("Thibaut", "Matrix")); // Matrix
4. Compute Pattern
computeIfAbsent
제공된 키에 해당하는 값이 없으면(값이 없거나 널), 키를 이용해 새 값을 계산하고 맵에 추가
현재 키와 관련된 값이 맵에 존재하며 널이 아닐 때만 새 값을 계산
Map<String, byte[]> dataToHash = new HashMap<>();
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
lines.forEach(line ->
dataToHash.computeIfAbsent(line, // 맵에서 찾을 키
this::calculateDigest)); // 키가 존재하지 않을 경우 동작
private byte[] calculateDigest(String key) {
return messageDigest.digest(key.getBytes(StandardCharsets.UTF_8));
}
// Map이 여러 값을 저장하는 형태일 경우
friendsToMovies.computeIfAbsent("Raphael", name -> new ArrayList<>())
.add("Star Wars");
computeIfPresent
제공된 키가 존재하면 새 값을 계산하고 맵에 추가
compute
제공된 키로 새 값을 계산하고 맵에 저장
5. Remove Pattern
//제공된 키에 해당하는 맵 항목 제거
favouriteMovies.remove(key)
//키가 특정한 값과 연관되어 있을 경우에만 항목을 제거하는 오버로드 버전 메서드
favouriteMovies.remove(key, value);
//특정 조건에 해당하는 항목 삭제
favouriteMovies.entrySet().removeIf(entry -> entry.getValue < 10);
6. Replace Pattern
replaceAll
BiFunction 을 적용한 결과로 각 항목의 값을 교체
Map<String, String> favouriteMovies = new HashMap<>();
favouriteMovies.put("Raphael", "Star Wars");
favouriteMovies.put("Olivia", "james bond");
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
Replace
키가 존재하면 맵의 값 교체
7. Merge
두 개의 맵에서 값을 합치거나 교체할 경우 사용
중복된 키가 없을 경우
Map<String, String> family = Map.ofEntries(
entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(
entry("Raphael", "Star Wars"));
Map<String, String> everyone = new HashMap<>(family);
everyone.putAll(friends);
중복된 키가 있을 경우
Map<String, String> family = Map.ofEntries(
entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(
entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"));
Map<String, String> everyone = new HashMap<>(family);
friends.forEach((k, v) -> // forEach + merge 로 key 충돌 해결
everyone.merge(k, v, (movie1, movie2) -> movie1 + " & " + movie2));
초기 검사 구현이 필요할 경우
지정된 키와 연관된 값이 없거나 널이면, 두 번째 인수를 키와 연결
그렇지 않을 경우, 세 번째 인자의 BiFunction을 적용
moviesToCount.merge(movieName, 1L, (key, count) -> count + 1L);
ConcurrentHashMap
ConcurrentHashMap 클래스는 동시 친화적이며 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용
forEach
각 (키/값) 쌍에 주어진 액션 실행
reduce
모든 (키/값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
search
널이 아닌 값을 반환할 떄까지 각 (키/값) 쌍에 함수 적용
(키/값) 인수를 이용한 네 가지 연산 형태 지원
ConcurrentHashMap 상태를 잠그지 않고 연산을 수행하므로, 연산에 제공한 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하면 안 됨!
키/값으로 연산 :
forEach
,reduce
,search
키로 연산 :
forEachKey
,reduceKeys
,searchKeys
값으로 연산 :
forEachValue
,reduceValues
,searchValues
Map.entry 객체로 연산 :
forEachEntry
,reduceEntries
,searchEntriess
Count
mappingCount
맵의 매핑 개수를 반환 (매핑 개수가 int의 범위를 넘어서는 이후의 상황 대처)
리팩터링, 테스팅, 디버깅
리팩터링
코드 가독성 개선
코드 가독성이 좋다?란 '어떤 코드를 다른 사람도 쉽게 이해할 수 있음'을 의미한다.
1. 익명 클래스를 람다 표현식으로 리팩터링하기
익명 클래스에서 사용한 this와 super는 람다 표현식에서 다른 의미를 갖는다.
익명 클래스에서 this는 익명 클래스 자신
람다에서 this는 람다를 감싸는 클래스
익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다.
람다 표현식으로는 불가
/*
* 익명 클래스 사용
*/
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Hello");
}
};
/*
* 람다 표현식 사용
*/
Runnable r2 = () -> System.out.println("Hello");
interface Task {
public void execute();
}
public static void doSomething(Runnable r) { r.run(); }
public static void doSomething(Task a) { r.execute(); }
// 람다 표현식의 대상 형식이 모호할 경우 명시적 형변환을 이용
doSomething((Task)() -> System.out.println("Hello"));
doSomething((Runnable)() -> System.out.println("Hello"));
2. 람다 표현식을 메서드 참조로 리팩터링하기
/*
* 람다 표현식 사용
*/
Map<CaloricLevel, List<Dish> dishesByCaloricLevel =
menu.stream().collect(
groupingBy(dish -> {
if (dish.getCalories() <= 400) { return CaloricLevel.DIET; }
else if (dish.getCalories() <= 700) { return CaloricLevel.NORMAL; }
else { return CaloricLevel.FAT; }
}));
/*
* 메서드 참조 사용
*/
public class Dish {
//...
public CaloricLevel getCaloricLevel() {
if (dish.getCalories() <= 400) { return CaloricLevel.DIET; }
else if (dish.getCalories() <= 700) { return CaloricLevel.NORMAL; }
else { return CaloricLevel.FAT; }
}
}
Map<CaloricLevel, List<Dish>< dishesByCaloricLevel =
menu.stream().collect(groupingBy(Dish::getCaloricLevel));
// 람다 표현식 사용
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 정적 헬퍼 메서드(comparing, maxBy) 참조 사용
inventory.sort(comparing(Apple::getWeight));
// 람다 표현식 사용
int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2);
// 내장 헬퍼 메서드(sum, maximum) 참조 사용
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
3. 명령형 데이터 처리를 스트림으로 리팩터링하기
스트림 API로 데이터 처리 파이프라인의 의도를 더 명확하게 보여주자.
/*
* 명령형 코드
*/
List<String> dishNames = new ArrayList<>();
for(Dish dish: menu) {
if(dish.getCalories() > 300) {
dishNames.add(dish.getName());
}
}
/*
* 스트림 API
*/
menu.parallelStream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.collect(toList());
4. 코드 유연성 개선
람다 표현식을 이용해서 동작 파라미터화를 쉽게 구현해보자.
함수형 인터페이스 적용
람다 표현식을 이용하기 위해 함수형 인터페이스를 추가
조건부 연기 실행
// Before : 복잡한 제어 흐름 코드 (상태 노출 및 매번 상태 체크) if (logger.isLoggable(Log.FINER)) { logger.finer("Problem: " + generateDiagnostic()); } // After : 가독성과 캡슐화 강화 public void log(Level level, Supplier<String> msgSupplier) { if(logger.isLoggable(level)) { log(level, msgSupplier.get()); } } logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());
실행 어라운드
준비, 종료 과정을 처리하는 로직을 재사용하여 코드 중복 줄이기
String oneLine = processFile((BufferedReader b) -> b.readLine()); // 람다 전달 String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine()); // 다른 람다 전달 public static String processFile(BufferedReaderProcessor p) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader("Aaron/MJIA/test.txt"))) { return p.process(br); // 인수로 전달된 BufferedReaderProcessor 실행 } } // IOException을 던질 수 있는 람다의 함수형 인터페이스 public interface BufferedReaderProcessor { String process(BufferedReader b) throws IOException; }
람다 테스팅
Example
public class Point {
private final int x;
private final int y;
public final static Comparator<Point> compareByXAndThenY =
comparing(Point::getX).thenComparing(Point::getY);
private Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
public Point moveRightBy(int x) {
return new Point(this.x + x, this.y);
}
}
@Test
public void testMoveRightBy() throws Exception {
Point p1 = new Point(5, 5);
Point p2 = p1.moveRightBy(10);
assertEquals(15, p2.getX());
assertEquals(5, p2.getY());
}
1. 보이는 람다 표현식의 동작 테스팅
람다는 익명이므로 테스트 코드 이름을 호출할 수 없다.
필요 시
람다를 필드에 저장
해서 재사용하거나, 람다 로직 테스트가 가능하다.람다 표현식은 함수형 인터페이스의 인스턴스를 생성하므로
인스턴스의 동작으로 람다 표현식을 테스트
할 수 있다
public class Point {
//...
public final static Comparator<Point> compareByXAndThenY =
comparing(Point::getX).thenComparing(Point::getY);
//...
}
@Test
public void testComparingTwoPoints() throws Exception {
Point p1 = new Point(10, 15);
Point p2 = new Point(10, 25);
int result = Point.compareByXAndThenY.compare(p1, p2);
assertTrue(result < 0);
}
2. 람다를 사용하는 메서드의 동작에 집중하자
람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록,
하나의 조각으로 캡슐화
하는 것세부 구현을 포함하는 람다 표현식을 공개하지 말아야 한다.
public static List<Point> moveAllPointsRightBy(List<Point> points, int x) {
return points.stream()
.map(p -> new Point(p.getX() + x, p.getY()))
.collect(toList());
}
@Test
public void testMoveAllPointsRightBy() throws Exception {
List<Point> points = Arrays.asList(new Point(5, 5), new Point(10, 5));
List<Point> expectedPoints = Arrays.asList(new Point(15, 5), new Point(20, 5));
List<Point> newPoints = Point.moveAllPointsRightBy(points, 10);
assertEquals(expectedPoints, newPoints);
}
3. 복잡한 람다를 개별 메서드로 분할하기
많은 로직을 포함하는 복잡한 람다 표현식의 경우, 람다 표현식을 메서드 참조로 바꾸자.
// 람다 표현식을 메서드 참조로 바꾼 예시
public class Dish {
//...
public CaloricLevel getCaloricLevel() {
if (dish.getCalories() <= 400) { return CaloricLevel.DIET; }
else if (dish.getCalories() <= 700) { return CaloricLevel.NORMAL; }
else { return CaloricLevel.FAT; }
}
}
Map<CaloricLevel, List<Dish> dishesByCaloricLevel =
menu.stream().collect(groupingBy(Dish::getCaloricLevel));
4. 고차원 함수 테스팅
메서드가 함수를 인수로 받을 경우, 다른 람다로 메서드 동작을 테스트
@Test public void testFilter() throws Exception { List<Integer> numbers = Arrays.asList(1, 2, 3, 4); List<Integer> even = filter(numbers, i -> i % 2 == 0); List<Integer> smallerThanThree = filter(numbers, i -> i < 3); assertEquals(Arrays.asList(2, 4), even); assertEquals(Arrays.asList(1, 2), smallerThanThree); }
다른 함수를 반환하는 메서드의 경우, 함수형 인터페이스의 인스턴스로 간주하고 함수의 동작을 테스트
디버깅
1. 스택 트레이스 확인하기
스택 트레이스를 통해 프로그램이 어디서, 어떻게 멈추게 되었는지 살펴보자.
다만, 람다 표현식은 이름이 없어서 복잡한 스택 트레이스가 생성된다.
메서드 참조를 사용해도 스택 트레이스에서는 메서드명이 나타나지 않는다.
따라서, 람다 표현식과 관련한 스택 트레이스는 이해하기 어려울 수 있다.
2. 정보 로깅
peek
스트림 연산을 활용하여 스트림 파이프라인에 적용된 각 연산이 어떤 결과를 도출하는지 확인할 수 있다.
List<Integer> result =
Stream.of(2, 3, 4, 5) //or numbers.stream()
.peek(x -> System.out.println("from stream: " + x)) // 소스에서 처음 소비한 요소
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x)) // map 동작 실행 결과
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x)) // filter 동작 후 선택된 숫자
.limit(3)
.peek(x -> System.out.println("after limit: " + x)) // limit 동작 후 선택된 숫자
.collect(toList());
Last updated