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)

replaceAll

  • UnaryOperator 함수를 이용하여 요소 교체 (List)

sort

  • 리스트 정렬 (List)

맵 처리

1. forEach

  • 기존 맵에서 키/값을 반복하며 확인했다면, forEach 메서드를 사용해보자.

2. Order

  • Entry.comparingByValue / Entry.comparingByKey

3. getOrDefault

  • 찾으려는 키가 존재하지 않으면 기본값을 반환 getOrDefault(key, default)

4. Compute Pattern

computeIfAbsent

  • 제공된 키에 해당하는 값이 없으면(값이 없거나 널), 키를 이용해 새 값을 계산하고 맵에 추가

    • 현재 키와 관련된 값이 맵에 존재하며 널이 아닐 때만 새 값을 계산

computeIfPresent

  • 제공된 키가 존재하면 새 값을 계산하고 맵에 추가

compute

  • 제공된 키로 새 값을 계산하고 맵에 저장

5. Remove Pattern

6. Replace Pattern

replaceAll

  • BiFunction 을 적용한 결과로 각 항목의 값을 교체

Replace

  • 키가 존재하면 맵의 값 교체

7. Merge

두 개의 맵에서 값을 합치거나 교체할 경우 사용

  • 중복된 키가 없을 경우

  • 중복된 키가 있을 경우

  • 초기 검사 구현이 필요할 경우

    • 지정된 키와 연관된 값이 없거나 널이면, 두 번째 인수를 키와 연결

    • 그렇지 않을 경우, 세 번째 인자의 BiFunction을 적용

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는 람다를 감싸는 클래스

  • 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다.

    • 람다 표현식으로는 불가

2. 람다 표현식을 메서드 참조로 리팩터링하기

3. 명령형 데이터 처리를 스트림으로 리팩터링하기

스트림 API로 데이터 처리 파이프라인의 의도를 더 명확하게 보여주자.

4. 코드 유연성 개선

람다 표현식을 이용해서 동작 파라미터화를 쉽게 구현해보자.

  • 함수형 인터페이스 적용

    • 람다 표현식을 이용하기 위해 함수형 인터페이스를 추가

  • 조건부 연기 실행

  • 실행 어라운드

    • 준비, 종료 과정을 처리하는 로직을 재사용하여 코드 중복 줄이기

람다 테스팅

Example

1. 보이는 람다 표현식의 동작 테스팅

  • 람다는 익명이므로 테스트 코드 이름을 호출할 수 없다.

  • 필요 시 람다를 필드에 저장해서 재사용하거나, 람다 로직 테스트가 가능하다.

  • 람다 표현식은 함수형 인터페이스의 인스턴스를 생성하므로 인스턴스의 동작으로 람다 표현식을 테스트할 수 있다

2. 람다를 사용하는 메서드의 동작에 집중하자

  • 람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록, 하나의 조각으로 캡슐화 하는 것

  • 세부 구현을 포함하는 람다 표현식을 공개하지 말아야 한다.

3. 복잡한 람다를 개별 메서드로 분할하기

  • 많은 로직을 포함하는 복잡한 람다 표현식의 경우, 람다 표현식을 메서드 참조로 바꾸자.

4. 고차원 함수 테스팅

  • 메서드가 함수를 인수로 받을 경우, 다른 람다로 메서드 동작을 테스트

  • 다른 함수를 반환하는 메서드의 경우, 함수형 인터페이스의 인스턴스로 간주하고 함수의 동작을 테스트

디버깅

1. 스택 트레이스 확인하기

  • 스택 트레이스를 통해 프로그램이 어디서, 어떻게 멈추게 되었는지 살펴보자.

  • 다만, 람다 표현식은 이름이 없어서 복잡한 스택 트레이스가 생성된다.

    • 메서드 참조를 사용해도 스택 트레이스에서는 메서드명이 나타나지 않는다.

  • 따라서, 람다 표현식과 관련한 스택 트레이스는 이해하기 어려울 수 있다.

2. 정보 로깅

  • peek 스트림 연산을 활용하여 스트림 파이프라인에 적용된 각 연산이 어떤 결과를 도출하는지 확인할 수 있다.

Last updated