이 포스팅은 공부 목적으로 작성된 포스팅입니다. 왜곡된 내용이 포함되어 있을 수 있습니다.
47. 반환 타입으로는 스트림보다 컬렉션이 낫다.
원소의 시퀀스를 반환하기 위해 자바7까지는 Collection, Set, List 와 같은 컬렉션 인터페이스 이거나 부모인 Iterable 또는 배열을 사용했다. 일반적인 관례는 Collection을 사용하고 구현할 수 없는 경우에 Iterable을 사용합니다(래퍼클래스를 커스텀 하는경우) 만약에 성능을 높여야한다면 배열을 사용하기도 한다.(스트림은 스트림 생성비용과 상대적으로 부족한 최적화 상태로 아직 배열보다 낮은 성능을 가진다) 자바 8에서 스트림을 도입하면서 반환타입에 관한 논쟁이 되어 버렸다.
스트림을 반복을 지원하지 않는다.(재사용 할 수 없다) 따라서 적절한 API를 사용해야한다.(여담으로 Stream interface는 iterable iterface의 추상 메서드를 모두 포함하고, iterable iterface가 정의한 방식대로 동작한다.)
책에서는 stream에서 반복을 사용하기 위해 iterator으로 변환하는 어댑터를 호출하는 방식을 제시한다.
public static<E>Iterable<E>iterableOf(Stream<E> stream){
return stream::iterator;
}
해당 함수를 호출하면 iterator로 변환하여 반복 접근을 할 수 있다. (물론 forEach로 접근해도 된다)
그러면 반대의 경우는 어떨까? iterator를 사용하면 stream의 장점(높은 유지보수성+내부 방법)을 사용할 수 없기 때문에 마찬가지로 iterator를 stream으로 변환하는 어뎁터를 통해 변환한다.
public static<E>Stream<E>streamOf(Iterable<E> iterable){
return StreamSupport.stream(iterable.spliterator(),false);
}
실제로 Collection 함수의 stream()함수와 동일한 형태로 우리가 익히 알고 있는 list.stream()와 같이 컬렉션을 stream으로 변환하기 위해 사용된다.
만약 객체 시퀀스를 반환해야하는 경우에서 오직 스트림 파이프라인에서만 쓰인다면 스트림을 반환하자
collection 인터페이스는 tterable의 하위 타입이고 stream 메서드를 제공하기 때문에 반복과 스트림을 동시에 지원한다. 따라서 원소 시퀀스를 반환하는경우 collection을 사용하는 것이 최선이다.(또는 하위 타입)
Arrays.asList를 통해 배열을 collection으로 변환하여 스트림으로 사용할 수 도 있다.
collection이 상대적으로 메모리를 많이 차지할 수 있음을 주의하자
책에서 스트림으로 반환하는 두가지 예시를 보여준다. 첫번째는 멱집합을 컬렉션으로 반환하는 예시이고 두번째는 list의 모든 부분 집합을 list로 만드는 예제이다.
public class SubLists{
public static <E> Stream<List<E>> of(List<E>list){
return Stream.concat(Stream.of(Collections.emptyList()),
prefixes(list).flatMap(SubLists::suffixes));
}
private static <E> Stream<List<E>>prefixes(List<E> list){
return IntStream.rangeClosed(1,list.size())
.mapToObj(end->list.subList(0,end));
}
private static <E>Stream<List<E>>suffixes(List<E>list){
return IntStream.range(0,list.size())
.mapToObj(start->list.subList(start,list.size()));
}
}
스트림 문법에 익숙하지 않는지라 글쓴이 입장에서는 코드가 반복문을 이용하는 것보다 더 오래걸렸다. 스트림 공부의 필요성을 느꼈다.(반복문은 for문 2개로 가능하기 때문에)
각설하여 이러한 어댑터를 사용하기 보다는 Collection을 사용하자~
48. 스트림 병렬화는 주의해서 적용하라
자바는 스레드, 동기화, wait/notify를 지원한다. 자바 5부터는 동시성 컬렉션인 java.util.concurrent와 executor를 지원한다. 자바7부터는 fork-join(병렬 분해) 가 추가되었고, 자바 8에는 parallel 메서드를 지원하여 더 쉬운 병렬 구현을 지원했다. 그래도 병렬 프로그래밍은 항상 주의해서 사용해야한다.
아이템 45에 나온 베르센 소수를 생성하는 프로그램을 parellel() 호출하여 더 빠른 성능을 기대할 수 있을까?
결과는 제대로 작동하지 않는다. 이는 stream.iterate와 limit를 사용하는경우 병렬 연산으로의 이점이 얻기 어렵다고 한다. 사실 이것이외에도 병렬처리에서 신경써야하는 것이 한두가지가 아니다. 스트림 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스이거나 배열,int 범위, long 범위일때 병렬화 효과가 가장 좋다.해당 자료구조들이 데이터를 분할하기 쉽다는 건데, Spliterator가 Stream을 Iterable을 나눈다.
또한 이 자료구조들은 모두 참조 지역성이 뛰어나다는 특징이 있는데 참조 지역성이란 최근 접근한 리소스에 대해서 해당 리소스및 주변 리소스에 다시 접근할 확률이 높다는 이야로 주위 리소스들을 캐싱한다면 좋은 성능을 기대해볼 수 있다는
의견이다. 배열의 경우 물리적으로 연속적으로 저장되어 있기때문에 참조 지역성이 뛰어나다고 할 수 있다. 이러한 참조 지역성은 벌크연산에서 중요해지는데 병렬 처리 또한 이에 해당한다.
종단 연산의 메소드가 병렬 처리의 성능에 영향을 주기도 한다.예를들어 가변 축소 collect메소드는(스트림을 하나의 collection으로 merge)병렬화에 적합하지 않는 것에 비에 축소 연산의 경우(max,min 과 같은 벌크 단위에서 개별로 진행할 수 있는 연산)적절한 메소드이다.
병렬화의 이점을 이용하기 위해서는 spliterator 메서드를 재정의해야한다.(책에서는 다루지 않는다.)
스트림 병렬화의 문제점은 속도가 느려질 수 있는 것 뿐만아니라 결과 자체가 잘못될 수 있다.(병렬 처리에서 꼬이는 경우)
이러한 오동작을 안전 실패(safey failure)라고 한다. 이러한 안전 실패에 대해서 Stream 명세에서 엄중히 규악을 정의하였다.
Stream의 reduce 연산에 건네지는 accumulator(누적기)와 combiner(결합기)함수는 반드시 결합법칙을 만족하고, 간섭받지 않고(non-interfering)상태를 갖지 않아야(stateless)한다.
병렬 처리가 제대로 수행되었더라고 출력 순서에 대해서는 보장되지 않는다 이경우 forEach를 forEachOrdered으로 변경하면 된다. 만약 병렬 처리로 성능향상을 확인하고 싶다면 스트림의 원소수의 원소당 수행되는 코드줄수가 수십만 이상되야한다.
스트림병렬화는 성능 최적화를 위해 사용하는 것으로 반드시 사용하지 않을 때와 사용할때의 차이가 있는지를 확인해야한다.
그렇다고 스트림 병렬화를 사용하지 말라는 건 아니다. 스트림 병렬화를 사용하면 반드시 이점은 존재한다.
static long pi(long n){
return LongStream.rangeClosed(2,n)
.mapToObj(BigInteger::valueOf)
.filter(i->i.isProbablePrime(50))
.count();
}
위 코드는 소수를 계산한다. 10^8수로 하는 경우 시간이 많이 걸린다(1분 이상 걸렸다)
static long pi(long n){
return LongStream.rangeClosed(2,n)
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(i->i.isProbablePrime(50))
.count();
}
자 이제 parallel()한줄 추가해보자 20초 안에 계산되었다.(사용시 jvm이 cpu를 100%사용하는 경험을 할 수 있다.)
사실 소수 판정 알고리즘을 사용하면 더 빠르게 판별할 수 있지만 이렇게 flat한 연산에 대해서는 병렬처리를 고려해볼만하다.
49. 매개변수가 유효한지 검사하라
메서드와 생성자의 매개변수는 valid를 별도로 진행하지 않는다. 인덱스 값은 음수이면 안되고, 객체 참조는 null이 아니여야한다. 이러한 제약을 반드시 메서드가 시작되기 전에 가능한 빨리 검증해야한다. 검증을 늦게 할수록 해당 오류를 감지하기 어려울 뿐만 아니라 오류의 원인을 찾기도 어려워진다. 이러한 매개변수 검사에 실패하면 실패 원자성을 어기게된다.
매개변수에 유효성 검증에대한 에러를 문서화해야한다. IllegalArgumentException, IndexOutOfBoundsException, NullPointer 중 하나가 될 수도 있고, customException을 추가할수도 있다.
/*
항상 음이 아닌 BigInterger를 반환하다는 점에서 remainder 메서드와 다르다.
@Param m 계수(양수여야 한다.)
@return 현재 값 mod m
@throws ArithmeticException m이 0보다 작거나 같은 발생한다.
*/
public BigInteger mod(BigInteger m){
if(m.signum()<=0)
throw new ArithmeticException("계수(m)는 양수여야 합니다"+m);
//
}
m 이 null인 경우에 대해서는 설명이 없는데, BigInteger 클래스 수준에서 기술 한 것이기 때문이다.
자바 7에 추가된 java.util.Objects.requireNonNull 메서드를 통해 null검사를 추가할수도 있다.
자바 9에서 checkFromIndexSize. checkFromToIndex,checkIndex을 통해 range valide을 할 수도 있다.
assert를 활용하여 매개변수 유효성을 검증 할 수도 있다.
private static void sort(long a[], int offset, int length){
assert a !=null;
assert offset >= 0 && offset<=a.length;
assert length >=0 && length <= a.length - offset;
}
assert을 사용할 경우, 해당 검증에 실패했을때, AssertionError를 반환한다.
이러한 유효성 검사에는 예외의 경우가 존재하는데, 검증 비용이 지나치게 높거나 실용적이지 못한 경우이다.
예를 들어 sort 함수에서 각각의 컴포넌트가 올바른 타입을 가지는지 검증하는경우 sort 실행이전에 하는 경우 별다른 이익이 없는 것에 반에 sort 내부에 진행한다면 검증 규칙을 위배하는 것과 달리(최대한 검증을 빨리해야한다) 별다른 손해가 없기 때문이다. 그러나 이러한 암묵적 유효성 검사에 의존하는 경우 실패 원자성을 해칠 수 있어 주의야한다.
유효성 검사에 대해서 잘못된 에러를 반환할 수 있는데, 이의 경우 예외 번역 관용구를 사용해야한다.
'Java' 카테고리의 다른 글
[이펙티브 자바] 아이템 54, 55 (0) | 2024.05.20 |
---|---|
List.toArray() (0) | 2024.05.19 |
[이펙티브 자바] 아이템 39, 40, 41 (1) | 2024.03.24 |
[이펙티브 자바] 아이템 10, 11 (1) | 2024.01.14 |
[이펙티브 자바] 아이템 7,8,9 (1) | 2024.01.13 |