본문 바로가기
Programming Language/Java

[Effective Java] Stream은 주의해서 사용해라(Item 45)

by 곰민 2023. 1. 12.

Effective Java에서 말하는 Stream 사용시 주의 사항에 대해서 확인해보도록 하겠습니다.

Stream과 관련된 Effective Java Item 45~48까지 학습한 것에 대한 포스팅을 이어서 하겠습니다.

 

Stream을 과용하는 것을 피하자 프로그램이 읽거나 유지 보수하기 어려워진다


  • 아나그램 예시
    • 아나그램(anagram) : 알파벳이 같고 순서만 다른 단어를 말한다.
      • ex) staple , aelpst, petals, aelpst → 아나그램 그룹

❌ 스트림을 과용하여 코드 가독성을 떨어트리고 유지 보수 비용을 늘리는 케이스 예시

public class StreamAnagrams {
    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)) {
            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);
        }
    }

 

✅ 사전 하나를 훑어 원소 수가 많은 아나그램 그룹들 출력

import static java.util.stream.Collectors.groupingBy;

// 코드 45-3 스트림을 적절히 활용하면 깔끔하고 명료해진다. (271쪽)
public class HybridAnagrams {
    public static void main(String[] args) throws IOException {
				
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);
				//파일 내용은 java 문자열 stream 으로 생성
        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(groupingBy(word -> **alphabetize(word))**) 
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .forEach(g -> System.out.println(g.size() + ": " + g));
        }
    }

    private static String **alphabetize**(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

 

 

아래 코드가 덜 복잡하고 로직 파악하기가 더 용이합니다.

  • 파일의 모든 라인으로 구성된 stream을 얻고 stream 변수의 이름을 words로 지어 안의 각 원소가 word임을 명확하게 했습니다.
  • 중간 연산은 없으며 최종 연산에서 모든 단어를 수집해 아나그램끼리 묶어놓은 Map으로 모읍니다.
  • Map에 해당하는 value들을 다시 Stream<List> 스트림을 엽니다.
  • minGroupSize보다 적은 것은 filter로 거릅니다.
  • 최종 연산인 foreach로 출력합니다.

 

Lambda 매개변수명 & 도우미 메서드의 활용


Lambda에서는 타입 이름을 추론이 가능하기 때문에 자주 생략하기 때문에
매개변수 이름을 잘 지어야 가독성이 좋아집니다.

alphabetize와 같이 세부 구현을 프로그램 로직 밖으로 빼내 코드 가독성을 높일 수 있습니다.
이 와 같은 도우미 메서드를 활용하는 것 은 stream pipe line에서 타입 정보가 명시되지 않고 임시 변수를 자주 사용하기에 중요합니다.

stream과 char


"Hello world!".chars().forEach(System.out::print);

721011081081113211911111410810
//반환 스트림 원소 char가 아닌 int를 반환합니다.

"Hello world!".chars().forEach(x-> System.out.print((char) x));
//명시적으로 형변환을 해줘야 합니다.

 

chars()는 String에서 Int Stream 인스턴스를 반환합니다.

  • 문자를 문자로 변환하지 않고 정수 표현으로 작업하면 각 정수를 Character 객체에 boxing 할 필요가 없으므로 성능이 약간 향상될 수 있습니다.
  • 출력 시에 문자로 표시하려면 명시적으로 형변환을 해줘야 합니다.

기본 타입인 char 용 stream을 지원하지 않아 잘못 구현할 가능성이 크고 느려질 수도 있기 때문에 char 값 처리 시 stream 사용은 삼가는 편이 낫습니다.

 

Stream Pipe Line과 안성맞춤인 경우와 아닌 경우(code block & stream)


반복 코드에서는 코드 블록을 사용해서 되풀이되는 계산을 표현합니다.

stream pipe line은 되풀이되는 계산을 함수 객체로 표현합니다.

 

❌ Stream과는 맞지 않은 경우

  1. 범위 안의 지역변수를 읽고 수정할 수 있는 경우
    • lambda에서는 final이거나 Effective final인 변수만 읽을 수 있습니다.
  2. return 문을 사용해 메서드에서 빠져나가거나 break나 contiune 문으로 불록 바깥의 반복문을 종료하거나 건너뛰어야 되는 경우
    • code block에서만 가능하고 lambda에서는 불가능합니다.

✅ Stream과 안성맞춤인 경우

  1. 원소들의 시퀀스를 일관되게 변환한다.
  2. 원소들의 시퀀스를 필터링한다.
  3. 원소들의 시퀀스를 하나의 연산을 사용해 결합한다 (더하기, 연결, 최솟값 등등)
  4. 원소들의 시퀀스를 컬렉션에 모은다 (공통된 속성을 기준으로 묶어갈 가능성 높음)
  5. 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.

😫 Stream 으로 처리하기 어려운 일

  • 한 데이터가 파이프라인의 여러 단계를 통과할 때 이 데이터의 각 단계에서 값들에 동시에 접근해야 하는 경우
  • 스트림은 한 값을 다른 값에 매핑하고 나면 원래의 값은 잃는 구조이기 때문해당 매핑 객체가 필요한 곳이 여러 군대일수록 더더욱
  • 코드 양 증가, 지저분하여 스트림 사용하는 주목적에 어긋납니다.
  • 원래 값과 새로운 값을 쌍으로 저장해 버리는 객체를 매핑하는 우회법도 있지만 만족스럽지 못합니다.
  • 매핑을 거꾸로 수행하는 방법이 낫다
  • 앞단계의 값이 필요할 때 매핑을 거꾸로 수행하는 예시
//메르센 수 구하는 예시
//2^p -1 형태
// p가 소수면 헤당 메르센수도 소수

public class MersennePrimes {
    static Stream<BigInteger> primes() {
        return Stream.iterate(TWO, BigInteger::nextProbablePrime);
				//무한 스트림 매개변수 (스트림 첫번쨰 원소, 스트림에서 원소 생성해주는 함수)
				//nextProbablePrime 소수 생성
    }

//pow
// return this^exponent
    public static void main(String[] args) {
        primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
										// p , 2^(pow)p(p.intValueExact()) - 1(subtract(one))
                .filter(mersenne -> mersenne.isProbablePrime(50)) //1-(1/2)^certainty
                .limit(20)
                .forEach(mp -> System.out::println);
    }
}

 

만약에 2^p 지수 p값을 출력하길 원한다고 하면 값이 초기 스트림에만 나타나서 접근할 수 없습니다.

매핑을 거꾸로 진행해야 합니다.

 

.forEach(mp-> System.out.println(mp.bitLength() + ":" + mp));
//지수는 숫자를 이진수로 표현한 다음 몇 비트인지 세면 나온다...

 

Stream을 반환하는 메서드는
이름을 원소의 정체를 알려주는 복수 명사로 쓰기를 강력히 추천한다.


static Stream<BigInteger> primes() {
    return Stream.iterate(TWO, BigInteger::nextProbablePrime);
		//무한 스트림 매개변수 (스트림 첫번쨰 원소, 스트림에서 원소 생성해주는 함수)
		//nextProbablePrime 소수 생성
}

위 예시 코드에서 사용한 primes()와 같이 stream을 반환하는 경우에는 stream의 원소의 정체를 알려주는 복수 명사로 메서드 이름사용하면 stream pipe line의 가독성이 좋아지므로 권장하고 있습니다.

 

Stream과 반복 중 뭘 쓸까?


  • 결국 개인의 취향과 프로그래밍 환경의 문제이다
  • 프로그래머 개인과 동료들이 이해하고 선호하는 방식을 사용하자

 

참조


EffectiveJava

https://stackoverflow.com/questions/22435833/why-is-string-chars-a-stream-of-ints-in-java-8

 

반응형

댓글