Skip to content

Latest commit

 

History

History
233 lines (150 loc) · 7.71 KB

스트림은_주의해서_사용하라.md

File metadata and controls

233 lines (150 loc) · 7.71 KB

아이템 45 : 스트림은 주의해서 사용하라

스트림이란?

여기 보고 가세요 :)

스트림 API

  • 메서드 연쇄를 지원하는 플루언트 API(fluent API)

    • 즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있음

    • 파이프라인 여러 개를 연결해 표현식 하나로 만들 수도 있음

  • 기본적으로 스트림 파이프라인은 순차적으로 수행됨

    • 파이프라인을 병렬로 실행하려면 파이프라인을 구성하는 스트림 중 하나에서 parallel 메서드를 호출해주면 됨

    • 그러나 효과를 볼 수 있는 상황은 많지 않음


스트림 API의 사용

  • 스트림 API는 다재다능하여 사실상 어떠한 계산이라도 해낼 수 있음

    • 하지만 할 수 있다는 뜻이지, 해야한다는 뜻은 아님

      • 스트림을 제대로 사용하면 프로그램이 짧고 깔끔해짐

      • 잘못 사용하면 읽기 어렵고 유지보수도 힘들어짐

  • 스트림을 언제 써야하는지를 규정하는 확고부동한 규칙은 없음

    • 그래도 참고할만한 노하우는 있음

왜 주의해서 사용해야 할까?

예시 : 사전 파일에서 단어를 읽어 사용자가 지정한 문턱값보다 원소 수가 많은 아나그램(anagram)* 그룹을 출력하는 프로그램

아나그램
철자를 구성하는 알파벳이 같고 순서만 다른 단어들
ex) staple, petals -> aelpst

이 프로그램에서 사용되는 아나그램 그룹
자료구조 : 맵
맵의 키 : 단어를 구성하는 철자들을 알파벳순으로 정렬한 값 ex) aelpst
맵의 값 : 같은 키를 공유한 단어들을 담은 집합 ex) staple, petals


1. 스트림 없이 구현한 코드

public class Anagrams {
	public static void main(String[] args) throws IOException {
		File dictionary = new File(args[0]); // 사전 파일
		int minGroupSize = Integer.parseInt(args[1]); // 사용자가 지정한 원소 수 문턱값

		// key : 알파벳 순으로 정렬한 값, value : 같은 키를 공유한 단어들을 담은 집합
		Map<String, Set<String>> groups = new HashMap<>();

		try (Scanner s = new Scanner(dictionary)) { //사전 파일에서 단어 읽음
			while (s.hasNext()) {
				String word = s.next();
				groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
			}
		}

		//minGroupSize 보다 원소 수가 많은 아나그램 그룹 출력
		for (Set<String> group : groups.values()) {
			if (group.size() >= minGroupSize) {
				System.out.println(group.size() + " : " + group);
			}
		}
	}

	// 아나그램 그룹의 key를 만들어줌
	private static String alphabetize(String s) {
		char[] a = s.toCharArray();
		Arrays.sort(a);
		return new String(a);
	}
}
  • JAVA 8에 추가된 computeIfAbsent 메서드 사용

    • public V computeIfAbsent(K key, Function<? super K, ? extends V>)

      • key 값이 존재하는 경우 : Map 안에 있는 value 반환

      • key 값이 존재하지 않는 경우 : Map 에 새로운 key 와 value(람다 함수 실행 결과) 반환


2. 스트림을 과하게 사용한 코드

public class Anagrams {
	public static void main(String[] args) throws IOException {
		Path dictionary = Paths.get(args[0]); // 사전 파일 경로
		int minGroupSize = Integer.parseInt(args[1]); // 사용자가 지정한 원소 수 문턱값

		try (Stream<String> words = Files.lines(dictionary)) { // try-with-resources 문을 사용해 파일을 닫음
			// 사전을 여는 부분을 제외하고 프로그램 전체가 단 하나의 표현식으로 처리
			words.collect(
					groupingBy(word -> word.chars().sorted()
							.collect(StringBuilder::new,
									(sb, c) -> sb.append((char) c),
									StringBuilder::append).toString()))
					.values().stream()
					.filter(group -> group.size() >= minGroupSize) //
					.map(group -> group.size() + " : " + group)
					.forEach(System.out::println);
		}
	}
}
  • 확실히 짧지만 읽기가 어려움

  • 스트림에 익숙하지 않은 프로그래머에게 더욱 어려움


3. 스트림을 적절히 활용한 코드

public class Anagrams {
	public static void main(String[] args) throws IOException {
		Path dictionary = Paths.get(args[0]); // 사전 파일 경로
		int minGroupSize = Integer.parseInt(args[1]); // 사용자가 지정한 원소 수 문턱값

		try (Stream<String> words = Files.lines(dictionary)) { // try-with-resources 문을 사용해 파일을 닫음
			words.collect(groupingBy(Anagrams::alphabetize)) // alphabetize 메서드로 단어들을 그룹화함
					.values().stream()
					.filter(group -> group.size() >= minGroupSize) // 문턱값보다 작은 것을 걸러냄
					.forEach(g -> System.out.println(g.size() + " : " + g)); // 필터링이 끝난 리스트 출력
		}
	}

	// 아나그램 그룹의 key를 만들어줌
	private static String alphabetize(String s) {
		char[] a = s.toCharArray();
		Arrays.sort(a);
		return new String(a);
	}
}
  • 스트림을 적절히 활용하면 깔끔하고 명료해짐

  • 람다의 매개변수 이름을 잘 지어야 파이프라인의 가독성이 유지됨

    • 람다에서 타입 이름을 자주 생략하기 때문

    • forEach() 안 g의 경우 group이라고 변경하는 것이 좋음

  • 도우미 메서드를 적절히 활용해야함

    • 파이프라인에서는 타입 정보가 명시되지 않거나 임시 변수를 자주 사용하기 때문

    • alphabetize : 단어의 철자를 알파벳 순으로 정렬하는 별도의 메서드 생성

      • 스트림으로 구현했다면 명확성이 떨어지고 잘못 구현될 가능성이 커짐
        -> 자바가 char용 스트림을 지원하지 않기 때문

🌟 기존 코드는 스트림을 사용해도록 리팩터링하되, 새 코드가 더 나아 보일 때만 반영하자


코드블록 vs 람다블록

되풀이되는 계산을 할 때

스트림 파이프라인 : 함수 객체(주로 람다나 메서드 참조)로 표현함

반복 코드 : 코드블록을 사용해 표현

코드블록으로만 할 수 있는 일

  • 범위 안의 지역변수를 읽고, 수정할 수 있음

    • 🙅‍♀️ 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있고, 지역 변수를 수정하는건 불가능함
  • 코드 블록에서는

    • return을 사용해 메서드에서 빠져나갈 수 있음

    • break나 continue 문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수 있음

    • 메서드 선언에 명시된 검사 예외를 던질 수 있음

    • 🙅‍♀️ 람다로는 이 중 아무것도 할 수 없음


스트림으로 처리하기에 안성맞춤인 일

  • 원소들의 시퀀스를 일관되게 변환하기

  • 원소들의 시퀀스를 필터링하기

  • 원소들의 시퀀스를 하나의 연산(더하기, 연결하기, 최솟값 구하기 등)을 사용해 결합하기

  • 원소들의 시퀀스를 컬렉션에 모으기

  • 원소들의 시퀀스에서 특정 조건을 만족하는 원소 찾기


스트림으로 처리하기 어려운 일

  • 파이프라인의 여러 단계에서의 값들에 동시에 접근하기

    • 스트림 파이프라인은 일단 한 값을 다른 값에 매핑하고 나면 원래의 값은 잃는 구조이기 때문

어떤걸 사용하면 좋을까?

스트림과 반복 방식은 각각에 알맞은 일이 있음

수 많은 작업은 이 둘을 조합했을 때 가장 멋지게 해결됨

🌟 스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택하라