Effective Java에서 말하는 Stream 사용시 주의 사항에 대해서 확인해보도록 하겠습니다.
Stream과 관련된 Effective Java Item 45~48까지 학습한 것에 대한 포스팅을 이어서 하겠습니다.
Stream을 과용하는 것을 피하자 프로그램이 읽거나 유지 보수하기 어려워진다
- 아나그램 예시
- 아나그램(anagram) : 알파벳이 같고 순서만 다른 단어를 말한다.
- ex) staple , aelpst, petals, aelpst → 아나그램 그룹
- 아나그램(anagram) : 알파벳이 같고 순서만 다른 단어를 말한다.
❌ 스트림을 과용하여 코드 가독성을 떨어트리고 유지 보수 비용을 늘리는 케이스 예시
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과는 맞지 않은 경우
- 범위 안의 지역변수를 읽고 수정할 수 있는 경우
- lambda에서는 final이거나 Effective final인 변수만 읽을 수 있습니다.
- return 문을 사용해 메서드에서 빠져나가거나 break나 contiune 문으로 불록 바깥의 반복문을 종료하거나 건너뛰어야 되는 경우
- code block에서만 가능하고 lambda에서는 불가능합니다.
✅ Stream과 안성맞춤인 경우
- 원소들의 시퀀스를 일관되게 변환한다.
- 원소들의 시퀀스를 필터링한다.
- 원소들의 시퀀스를 하나의 연산을 사용해 결합한다 (더하기, 연결, 최솟값 등등)
- 원소들의 시퀀스를 컬렉션에 모은다 (공통된 속성을 기준으로 묶어갈 가능성 높음)
- 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.
😫 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과 반복 중 뭘 쓸까?
- 결국 개인의 취향과 프로그래밍 환경의 문제이다
- 프로그래머 개인과 동료들이 이해하고 선호하는 방식을 사용하자
참조
https://stackoverflow.com/questions/22435833/why-is-string-chars-a-stream-of-ints-in-java-8
'Programming Language > Java' 카테고리의 다른 글
[Effective Java] 반환 타입으로는 Stream 보다 Collection이 낫다. (item 47) (1) | 2023.01.13 |
---|---|
[Effective Java] Stream에서는 부작용 없는 함수를 사용하라. (item 46) (0) | 2023.01.12 |
[Java] Stream (Stream이 밀려온다) (0) | 2023.01.09 |
[Java] 람다(Lambda)를 소화시켜 보자 (0) | 2023.01.08 |
[Java] Functional Interface(함수형 인터페이스) (0) | 2023.01.08 |
댓글