Page tree
Skip to end of metadata
Go to start of metadata

람다와 스트림

아이템42. 익명 클래스보다 람다를 사용하라

  • 예전 자바에서는 함수 타입을 표현할 때 추상 메서드를 하나만 담은 인터페이스를 사용. (함수 객체)
  • 아래의 코드처럼 특정 행동을 익명 클래스를 통해 표현. (전략패턴)
익명 클래스의 인스턴스를 함구 객체로 사용 - 낡은 기법
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});


  • 위의 방식은 코드가 너무 길다.
  • 함수형 인터페이스들에 대해 람다식으로 다음과 같이 표현이 가능해짐 (자바8이상)
  • 컴파일러가 문맥을 살펴 타입을 추론해줌 (Generic)
  • 타입을 직접 명시해야 하는 경우
    • 컴파일러가 타입을 추론할 수 없다는 에러를 발생할 때
    • 타입을 명시해야 코드가 더 명확할 때
람다식을 함수 객체로 사용
Collections.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));
생성자 메서드 사용
Collections.sort(words, comparingInt(String::length));


  • 함수형 인터페이스를 사용한 열거타입 예제
  • java.util.function 패키지가 제공하는 다양한 함수형 인터페이스 참고
함수 객체(람다)를 인스턴스 필드에 저장해 상수별 동작을 구현한 열거 타입
public enum Operation {
    PLUS  ("+", (x, y) -> x + y),
    MINUS ("-", (x, y) -> x - y),
    TIMES ("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override public String toString() { return symbol; }

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }

    // 아이템 34의 메인 메서드 (215쪽)
    public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));
    }
}


  • 람다 사용시 주의점
    • 이름이 없고 문서화가 불가
    • 코드 자체로 동작이 명확이 설명되지 않거나 코드 줄 수가 많아지는 경우
    • 자기 자신을 참조하지 못함 (this가 불가)
    • 람다를 직렬화하는 경우 → private static 중첩 클래스 인스턴스 활용


아이템 43. 람다보다는 메서드 참조를 사용하라

  • 람다와 메서드 참조 방식 비교
  • 람다로 표현할 수 있다면 메서드 참조 방식으로 표현이 가능하다
  • 람다와 메서드 참조방식 중 더 간결하고 편리한 쪽을 선택해 사용
map.merge를 이용해 구현한 빈도표
public static void main(String[] args) {
    Map<String, Integer> frequencyTable = new TreeMap<>();
    
    for (String s : args)
        frequencyTable.merge(s, 1, (count, incr) -> count + incr); // 람다
    System.out.println(frequencyTable);

    frequencyTable.clear();
    for (String s : args)
        frequencyTable.merge(s, 1, Integer::sum); // 메서드 참조
    System.out.println(frequencyTable);

}
메서드 참조 유형같은기능을 하는 람다
정적Integer::parsIntstr → Integer.partInt(str)
한정적(인스턴스)Instant.now()::isAfter

Instant then = Instant.now();

t → then.isAfter(t)

비한정적(인스턴스)String::toLowerCasestr → str.toLowerCase()
클래스 생성자TreeMap<K,V>::new() → new TreeMap<K,V>()
배열 생성자int[]::newlen → new int[len]


  • 람다로는 불가능하나 메서드 참조로 가능한 유일한 예는 제네릭 함수 타입 구현
  • 아래의 예처럼 람다식으로 표현이 불가한 경우가 있음. (제네릭 람다식이란게 없음)
함수형 인터페이스를 위한 제네릭 함수 타입
interface G1 { <E extends Exception> Object m() throws E; }
interface G2 { <F extends Exception> Object m() throws Exception; }
interface G extends G1, G2 {}


//함수형 인터페이스 G를 함수 타입으로 표현하는 경우
<F extends Exception> () -> String throws F


아이템 44. 표준 함수형 인터페이스를 사용하라.

  • API를 작성하는 사례의 변경
    • 템플릿 메서드 패턴 → 함수 객체를 받는 static factory나 생성자 제공
  • 함수형 인터페이스는 java.util.function 패키지에 있다. 
    • 웬만하면 잘 정의되어 제공되고 있는 함수형 인터페이스를 사용하자.
    • 총 43개 이지만 몇가지만 기억하면 나머지는 유추가 가능하다.
인터페이스함수 시그니처
UnaryOperator<T>T apply(T t)String::toLowerCase
BinaryOperator<T>T apply(T t)BigInteger::add
Predicate<T>boolean test(T t)Collection:isEmpty
Function<T,R>R apply(T t)Arrays::asList
Supplier<T>T get()Instant::now
Consumer<T>void accept(T t)System.out::println
  • 새로운 함수형 인터페이스를 작성하고자 할 때는 @FunctionalInterface 를 붙인다.
    • 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
    • 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일이 되게 해준다.
    • 유지보수 과정에서 실수로 메서드를 추가하지 못하게 막아준다.
  • 함수형 인터페이스를 API에서 사용할 때 주의점
    • 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중으로 정의하면 안된다. → 모호함
    • 함수형 인터페이스에 박싱된 기본 타입을 넣지 말자. → 아이템 61에 위배


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

  • 스트림 : 스트림은 데이터 원소의 유한 혹은 무한 시퀀스 

    • 원소 

      • 컬렉션 (Collection)
      • 배열 (Array)
      • 파일 (File)
      • 정규표현식 (Regex Pattern Matcher)
      • 난수 생성기 (Random Generator)
      • 다른 스트림 (Other Stream)
    • 객체 참조나 기본 타입 지원: 기본타입의 경우 IntStream, LongStream, DoubleStream을 사용하는 것이 성능상 좋음
      • Stream : 객체 참조타입에 대한 Stream
      • IntStream : int 타입에 대한 Stream
      • LongStream : long 타입에 대한 Stream
      • DoubleStream : double 타입에 대한 Stream

    • 스트림 API method chaining을 지원하는 Fluent API
  • 스트림 파이프라인 : 데이터 원소들로 수행하는 연산 단계 표현

    • Stream.of(1,2,3,4).filter(value -> value / 2 == 0).filter( value -> value < 3).count()

    • (Single) Stream Generator 

    • (0 or More) 중간연산(intermediate operation) : 스트림을 어떠한 방식으로 변환하는 연산

      • filter(Predicate<? super T> predicate) : predicate 함수에 맞는 요소만 사용하도록 필터

      • map(Function<? Super T, ? extends R> function) : 요소 각각의 function 적용

      • flatMap(Function<? Super T, ? extends R> function) : 스트림의 스트림을 하나의 스트림으로 변환

      • distinct() : 중복 요소 제거

      • sort() : 기본 정렬

      • sort(Comparator<? super T> comparator) : comparator 함수를 이용하여 정렬

      • skip(long n) : n개 만큼의 스트림 요소 건너뜀

      • limit(long maxSize) : maxSize 갯수만큼만 출력

    • (Single) 종단연산(terminal operation) : 마지막 중간 연산이 내놓은 스트림에 가하는 마지막 연산

      • forEach(Consumer<? super T> consumer) : Stream의 요소를 순회

      • count() : 스트림 내의 요소 수 반환

      • max(Comparator<? super T> comparator) : 스트림 내의 최대 값 반환

      • min(Comparator<? super T> comparator) : 스트림 내의 최소 값 반환

      • allMatch(Predicate<? super T> predicate) : 스트림 내에 모든 요소가 predicate 함수에 만족할 경우 true

      • anyMatch(Predicate<? super T> predicate) : 스트림 내에 하나의 요소라도 predicate 함수에 만족할 경우 true

      • noneMatch(Predicate<? super T> predicate) : 스트림 내에 모든 요소가 predicate 함수에 만족하지않는 경우 true

      • sum() : 스트림 내의 요소의 합 (IntStream, LongStream, DoubleStream)

      • average() : 스트림 내의 요소의 평균 (IntStream, LongStream, DoubleStream)


    • 종단연산을 빼먹으면 동작하지 않는다.
    • 중간 연산이 여러개 존재할 경우 종단 연산에서 지연 평가된다.

      • 지연평가(lazy evaluation) : 계산의 결과값이 필요할 때까지 계산을 늦추는 기법

  • 스트림을 과용하지 마라(you can doesn't mean you should!, happy medium!)

    • 과용하면 가독성이 떨어지고 유지보수가 어려워짐(probablly느려짐)

    • 기존 코드는 스트림을 사용하도록 리펙토링하되, 새 코드가 더 나아보일 때만 반영

      사전File에서 단어를 읽어 사용자가 지정한 값보다 원소수가 많은 Anagroup 출력
      public class IterativeAnagrams {
      	// anagram = test, tets 등
          // 사용자가 지정한 minGroupSize 값보다 원소 수가 많은 anagram 그룹을 출력한다.
          public static void main(String[] args) throws IOException {
              File dictionary = new File(args[0]);
              int minGroupSize = Integer.parseInt(args[1]);
      
      		// Key = anagram, Value : 같은 anagram인 단어의 집합 Set
              Map<String, Set<String>> groups = new HashMap<>();
              try (Scanner s = new Scanner(dictionary)) {
                  while (s.hasNext()) {
                      String word = s.next();
      				// (1) Map 안에 Key가 있는지 찾음
      				// (2-1)있으면 단순히 Key mapping 된 Value 반환
      				// (2-2)없으면 함수 객체를 Key에 적용하여 Value를 계산
      				// (3) Key와 계산된 Value를 mapping하고, 계산된 value를 반환
      				// * computeIfAbsent : 맵 안에 키가 있는지 찾은 다음, 있으면 단순히 그 키에 매핑된 값을 반환한다.(java8+)
                      groups.computeIfAbsent(alphabetize(word),
                              (unused) -> new TreeSet<>()).add(word);
                  }
              }
      
              for (Set<String> group : groups.values())
                  if (group.size() >= minGroupSize)
                      System.out.println(group.size() + ": " + group);
          }
      
          private static String alphabetize(String s) {
              char[] a = s.toCharArray();
              Arrays.sort(a);
              return new String(a);
          }
      }
      
      
      /* alphabetize까지 변환 */
      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);
              }
          }
      }
      
      /* alphabetize 제외 Stream 변환 : 적절 */
      public class HybridAnagrams {
          public static void main(String[] args) throws IOException {
              Path dictionary = Paths.get(args[0]);
              int minGroupSize = Integer.parseInt(args[1]);
      
      		//groupBy : 알파벳화한 단어를 알파벳화 결과가 같은 단어들의 리스트로 매핑하는 맵을 생성
              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);
          }
      }
    • char용 스트림을 지원하지 않기 때문에 char값을 처리할 때는 스트림을 삼가는 것이 좋음

      char 스트림
      public class CharStream {
          public static void main(String[] args) {
              // Does not produce the expected result
      		// 결과 값은 '721011081081113211911111410810033' 출력
      		// String's char method returns IntStream
              "Hello world!".chars().forEach(System.out::print);
              System.out.println();
      
      
              // Fixes the problem
      		// 결과 값 'Hello world!' 출력
              "Hello world!".chars().forEach(x -> System.out.print((char) x));
              System.out.println();
          }
      }



  • 스트림 권장

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

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

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

    • 원소들의 시퀀스를 하나의 collection에 모음 (공통된 속성을 기준으로 묶어가며)

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


  • 스트림을 권장하지 않는 경우

    • 값에 동시에 접근해야할 때
      • 한 스트림 파이프라인(peek)에서 각 단계의 값들에 동시에 접근 할 수 없음(한 데이터가 파이프라인의 여러 단계를 통과할 때)
      • 스트림 파이프라인은 일단 한 값을 다른 값에 맵핑하고 나면 원래의 값을 잃음(원래 값을 다른 곳에 저장 가능하지만, 지저분해 짐)
    동시에 접근해야 할 때
    Int[] arr = new int[]{10, 2, ,3, 4, 9};
    Int sum = Arrays.stream(arr)
    		.filter(firstValue -> firstValue % 2 == 0)
    		.filter(secondValue -> secondValue > 3)
    		.peak(secendValue -> {
    			int temp = firstValue + secondValue; //compile error
    			return secondValue;
    		}).sum();
    개인의 취향과 프로그래밍 환경에 따라 결정하고, 둘 다 해보고 더 나아 보이는 쪽을 하라
    public class Card {
    /*카드는 숫자(rank)와 무늬(suit)를 묶은 불변 값 클래스이고 숫자와 무늬는 열거타입이다. */    
    	public enum Suit { SPADE, HEART, DIAMOND, CLUB }
        public enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN,
                           EIGHT, NINE, TEN, JACK, QUEEN, KING }
    
    
        private final Suit suit;
        private final Rank rank;
    
        @Override public String toString() {
            return rank + " of " + suit + "S";
        }
    
        public Card(Suit suit, Rank rank) {
            this.suit = suit;
            this.rank = rank;
    
        }
        private static final List<Card> NEW_DECK = newDeck1(); //or newDeck2()
    
        // (1) for-each 사용 / 데카르트 곱 계산을 반복 방식으로 구현
        private static List<Card> newDeck1() {
            List<Card> result = new ArrayList<>();
            for (Suit suit : Suit.values())
                for (Rank rank : Rank.values())
                    result.add(new Card(suit, rank));
            return result;
        }
        // (2) 스트림을 중복하여 사용
        private static List<Card> newDeck2() {
            return Stream.of(Suit.values())
                    .flatMap(suit ->  
                            Stream.of(Rank.values())
                                    .map(rank -> new Card(suit, rank)))
                    .collect(toList());
        }
    
    	//flatMap : 스트림의 원소 각각을 스트림으로 맵핑한 다음 그 스트림을 다시 하나의 스트림으로 합침(flattening)
        public static void main(String[] args) {
            System.out.println(NEW_DECK);
        }
    }
    

아이템 46. 스트림에서는 부작용 없는 함수를 사용하라.

  • forEach는 잘 생각해보고 써라.
    • forEach은 종단연산 중 가장 기능이 적고, 스트림답지 않음
    • forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말아 함
foreach 사용을 삼가라
//단어 별 수를 세어 빈도표로 만드는 코드. 
public class Freq {
    public static void main(String[] args) throws FileNotFoundException {
        File file = new File(args[0]);


		/* freq1 따라하지 말 것
		Uses the streams API but not the paradigm--Don't do this!
		스트림을 가장한 반복적 코드 forEach 에서 freq 를 계속해서 수정함  */
        Map<String, Long> freq1 = new HashMap<>(); 
        try (Stream<String> words = new Scanner(file).tokens()) {
            words.forEach(word -> { 
                freq.merge(word.toLowerCase(), 1L, Long::sum);
           });
        }

        // Proper use of streams to initialize a frequency table 
        Map<String, Long> freq2;
        try (Stream<String> words = new Scanner(file).tokens()) {
            freq = words
                    .collect(groupingBy(String::toLowerCase, counting()));
				//classifier(분류함수)로 받고, Collector(수집기 = 카테고리별로 모아 놓은 맵)로 출력
				//다운스트림 수집기로 counting()을 건네 각 키(카테고리)를 카테고리에 속하는 원소의 개수와 매핑하여 맵을 얻음
				//다운스트림 수집기로 summing(), averaging(), summarizing()도 있음
        }

        System.out.println(freq2);

        // Pipeline to get a top-ten list of words from a frequency table
        List<String> topTen = freq.keySet().stream()
                .sorted(comparing(freq::get).reversed())
                .limit(10)
                .collect(toList());

        System.out.println(topTen);
    }
}


  • 스트림에서는 순수함수(pure function)을 이용해야 함
    • 오직 입력만이 결과에 영향을 주는 함수
    • 다른 가변 상태를 참조하지 않고, 함수 스스로 다른 상태를 변경하지 않음


  • java.util.stream.Collectors를 활용하라 (현재 총 43개 메서드)
    • https://docs.oracle.com/javase/10/docs/api/java/util/stream/Collectors.html

    • Collectors를 이용하면 스트림의 원소를 쉽게 Collection으로 모을 수 있음(toList(), toSet(), toCollection())
    • toMap()
      • 스트림 원소를 key에 mapping하는 함수(valueMapper)와 value에 mapping하는 함수(keyMapper)를 인자로 받음
    • groupingBy()
      • 스트림의 요소를 그룹화하는 메서드
      • 분류함수(classifier)를 인자로 받아, 스트림의 원소들을 카테고리별로 모아놓은 map list를 반납

        • 비슷하게 분류함수자리에 prediate을 받고 boolean인 맵을 반환하는 partitionBy도 참고
    • Joining()
      • CharSequence 타입만 받을 수 있음

      • 단순히 원소를 연결하는 수집기 반환

      • 접두문자, 구분문자, 접미문자를 매개변수로 받음

        • 접두([), 접미(]), 구분(,)을 설정했을 경우 - [came, saw, ...] 등으로 출력


아이템 47. 반환타입으로는 스트림보다 컬렉션이 낫다

  • (~java7) Array형태의 Linear한 자료구조를 반환하는 기존 메서드 반환타입

    • 종류
      • Collection<E>, Set<E>, List<E>와 같은 컬렉션 인터페이스
      • E[]와 같은 배열
      • Iterable<E> 인터페이스
    • 기본은 Collection<E> 타입
    • for-each 문에서만 쓰이거나, (contain(Object) 같은) 일부 Collection 메서드를 구현 할 수 없을 때는 Iterable 인터페이스를 사용

    • 성능에 민감한 상황이면, E[] 형태의 배열을 주로 사용

  • (java8~) Stream 추가
    • 스트림은 반복을 지원하지 않지 않기때문에 Stream과 반복을 알맞게 조합해야 좋은 코드
      • API를 Stream만 반환하여 사용하도록 하면 (반환하여) for-each를 사용하고자 하는 개발자는 불편을 겪을 것
        (반복문을 for-each로 처리하여 반환하길 요청하였는데, Stream으로 반환받는다면 코드처리가 지저분해 짐)
    • Stream은 Iterator 인터페이스가 정의한 추상메서드를 포함하지만(동일 동작)
      • Iterator를 확장(extend)하진 않아 for-each로 반복하지 못함
      • Stream::iterator를 형변환(Iterator<String>)해주면 for-each문 사용 가능하지만 Collection을 이용한 for-each보다는 성능이 떨어질 수 있음
      • Collection도 Iterator의 하위 타입이며, stream메서드 지원
API에서 Stream만 반환하는 경우
/**
* Returns a snapshot of all processes visible to the current process.
* <p>
* <em>Note that processes are created and terminate asynchronously. There
* is no guarantee that a process in the stream is alive or that no other
* processes may have been created since the inception of the snapshot.
* </em>
*
* @return a Stream of ProcessHandles for all processes
* @throws SecurityException if a security manager has been installed and
*         it denies RuntimePermission("manageProcess")
* @throws UnsupportedOperationException if the implementation
*         does not support this operation
*/
static Stream<ProcessHandle> allProcesses() {
    return ProcessHandleImpl.children(0);
}


/* ----------------------타입추론에러로컴파일되지 않음---------------------- */
// Test.java:6 error: method reference not expected here                
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
}


/* -------------오류 바로잡기 Iterable Casting - 책에는 컴파일된다고 나왔으나 컴파일 안됨---------------------- */
// ClassCastException
for(ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.allProcesses().iterator()) {
}




/* -------------오류 바로잡기 - 어댑터 사용 ---------------------- */
// Stream<E>를 Iterable<E>로 중개해주는 어댑터 : iterableOf 메서드를 통해 명시적으로 Iterable으로 반환
for (ProcessHandle ph : iterableOf(ProcessHandle.allProcesses()) {
}

// Adapter from  Stream<E> to Iterable<E> (
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
        return stream::iterator;
}


 // Adapter from Iterable<E> to Stream<E>
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
       return StreamSupport.stream(iterable.spliterator(), false);
}


    • 반환타입
      • 객체 시퀀스를 반환하는 메서드를 작성할 때, 메서드가 오직 Stream 파이프라인에서만 쓰인다면 마음놓고 Stream을 반환
      • for-each를 사용하는 개발자와 Stream을 사용하는 개발자를 모두 배려하여 Stream과 Iterable을 동시에 제공할 수 있도록 하는 것이 좋음
      • 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위타입을 쓰는 것이 일반적


    • 전용컬렉션 사용(멱집합 예제)
      • 멱집합 :  한 집합의 모든 부분집합을 원소로 하는 집합(n이 커질 수럭 기하급수적으로 커짐)
        •  (a, b, c)의 멱집합은 ((), (a), (b), (c), (a, b), (a, c), (b, c), (a, b, c))
        • 원소의 갯수가 n개일 때, 원소의 갯수는 2^n개
      반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션 구현하라.
      public class PowerSet {
          // Returns the power set of an input set as custom collection 
      	/* 입력 집합의 원소 수가 30을 넘으면 PowerSet.of가 예외를 던진다.
      			(size() 메서드의 리턴타입은 int이기 때문에 최대길이는 2^31 - 1 또는 Integer.MAX_VALUE로 제한 되기 때문)
      			이는 Stream이나, Iterable이 아닌 Collection을 쓸 때의 단점을 보여준다.
      			(Stream이나 Iterable은 size에 대한 고민이 필요없기 때문)
      			반환하는 시퀀스의 크기가 메모리에 올려도 안전할 만큼 작다면 ArrayList나 HashSet 같은 표준 컬렉션 구현체를 반환하는게 최선
      			(하지만 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안됨) 
      			반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션 구현*/
          public static final <E> Collection<Set<E>> of(Set<E> s) {
              List<E> src = new ArrayList<>(s);
              if (src.size() > 30)
                  throw new IllegalArgumentException("너무 많음 예외 : minimum 30 " + s);
      		
              return new AbstractList<Set<E>>() {
      			//size와 contains 등을 구현하기 어려운 경우, 스트림이나 Iterable을 반환하는 편이 낫다.
                  @Override public int size() {
                      return 1 << src.size(); // 멱집합의 크기는 2를 원래 집합의 원소 수만큼 거듭제곱한 것과 같음
                  }
      
      
                  @Override public boolean contains(Object o) {
                      return o instanceof Set && src.containsAll((Set)o);
                  }
      
                  @Override public Set<E> get(int index) {
                      Set<E> result = new HashSet<>();
                      for (int i = 0; index != 0; i++, index >>= 1)
                          if ((index & 1) == 1)
                              result.add(src.get(i));
                      return result;
                  }
              };
          }
      
          public static void main(String[] args) {
              Set s = new HashSet(Arrays.asList(args));
              System.out.println(PowerSet.of(s));
          }
      }
      
리스트에 (연속적인) 부분리스트를 모두 반환하는 메서드
public class SubLists {
	//Stream.concat 메서드는 반환되는 Stream에 빈 리스트를 추가
	// flatMap은 모든 Stream을 하나의 Stream으로 만든다
    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()));
    }

	/*(a, b, c)의 prefixes는 (a), (a, b), (a, b, c) 
	(a, b, c)의 suffixes는 (c), (b, c), (a, b, c).*/


	// 위와 같은 로직(for문 사용)
	for (int start = 0; start < src.size(); start++) {
   	 for (int end = start + 1; end <= src.size(); end++) {
        System.out.println(src.subList(start, end));
   	 }
	}


	// 위와 같은 로직(Stream 중첩) - 더 간결해 지지만 가독성이 떨어질 수 있다.
	public static <E> Stream<List<E>> of(List<E> list) {
	    return IntStream.range(0, list.size())
        .mapToObj(start -> 
                  IntStream.rangeClosed(start + 1, list.size())
                           .mapToObj(end -> list.subList(start, end)))
        .flatMap(x -> x);
	}


    public static void main(String[] args) {
        List<String> list = Arrays.asList(args);
        SubLists.of(list).forEach(System.out::println);
    }
}


  • 요약 
    • Stream이나 Iterable을 리턴하는 API에는 Stream -> Iterable, Iterable -> Stream으로 변환하기 위한 어댑터 메서드가 필요
      • 단, 어댑터는 클라이언트 코드를 어수선하게 만들고 더 느리다 (책에서는 2.3배정도 느리다함)
    • 원소 시퀀스를 반환하는 메서드를 작성할 때는 Stream, Iterator를 모두 지원할 수 있게 작성(되도록 Collection으로 하는게 좋음)
    • 원소의 갯수가 많다면, 멱집합의 예처럼 전용 컬렉션을 리턴하는 방법도 고민
    • 만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 수정된다면, 그때는 안심하고 Stream을 반환하면 됨

아이템 48. 스트림의 병렬화는 주의해서 적용하라

  • 서론 : 주류 언어 중, 동시성 프로그래밍 측면에서는 항상 자바는 앞서왔음을 강조
    • 처음 릴리즈된 1996년부터 스레드, 동기화, wait/notify를 지원

    • (자바 5) 동시성 컬렉션인 java.util.concurrent 라이브러리와 실행자(Excutor) 프레임워크를 지원
    • (자바 7) 고성능 병렬 분해(parallel decom-position) 프레임워크인 fork-join 패키지를 추가
      (Fork-join pool에 대한 설명은 https://okky.kr/article/345720 참고)

  • 자바로 동시성 프로그램을 작성하기가 점점 쉬워지고 있지만, 이를 올바르고 빠르게 작성하는 일은 어려움
    • (자바8) 스트림은 parallel 메서드만 호출하면 자동으로 병렬 실행할 수 있는 스트림을 지원
    • 스트림 병렬화가 성능에 미치는 영향
      *메르센 소수(Mersenne number): 2의 거듭제곱에서 1이 모자란 숫자

      : 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767...


      *스트림을 이용한 처음 20개의 메르센 소수를 생성하는 프로그램
      test1 수행시간 : 8.2초  test2(parallel()을 호출) : 메르센 소수의 값이 프린트 되지 않았고, 강제로 중지 하기 전까지 계속 돌고 있었다.
      아무것도 안된 원인은 stream 라이브러리가 이 파이프라인을 병렬화 하는 방법을 찾아내지 못했기 때문이다.

      • 데이터 소스가 Stream.iterate인 경우
      • 중간 연산으로 limit()를 사용하는 경우 위 두 가지 경우에는 파이프라인 병렬화로 성능 향상을 기대하기 어렵다.
        뿐만 아니라 파이프라인 병렬화는 limit를 다룰 때 CPU 코어가 남는다면, 원소를 몇 개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다고 가정한다.

      따라서 스트림 파이프라인을 마구잡이로 병렬화 해선 안된다. 성능이 오히려 더 나빠질 수 있다.

      파이프라인 병렬화가 불가능 한 경우 성능 개선이 되지 않는다.
      public class ParallelMersennePrimes {
          public static void main(String[] args) {
              primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
                      .parallel()
                      .filter(mersenne -> mersenne.isProbablePrime(50))
                      .limit(20)
                      .forEach(System.out::println);
          }
      
      
          static Stream<BigInteger> primes() {
              return Stream.iterate(TWO, BigInteger::nextProbablePrime);
          }
      }
      
      
      public class ParallelPrimeCounting {
          // Prime-counting stream pipeline - parallel version (Page 225)
          static long pi(long n) {
              return LongStream.rangeClosed(2, n)
                      .parallel()
                      .mapToObj(BigInteger::valueOf)
                      .filter(i -> i.isProbablePrime(50))
                      .count();
      		/* pararell (무)31s / (유)9.2s -> 약 4배 빨라짐 */
          }
      
          public static void main(String[] args) {
              System.out.println(pi(10_000_000));
          }
      }


  • 병렬화를 사용해야 하는 경우
    • 추천
      • ArrayList
      • HashMap
      • HashSet
      • ConcurrentHashMap
      • 배열(Array)
      • int/long 범위
    • 스트림의 데이터 소스가 위와 같은 클래스의 인스턴스 일 때 병렬화의 효과가 가장 좋음
    • 위 자료구조들은 모두 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있어 다수의 스레드에 분배하기에 좋음
    • 나누는 작업은 Spliterator가 담당하며, Spliterator 객체는 Stream, Iterable의 spliterator() 메서드로 얻어올 수 있음
    • 또한 위의 자료구조는 참조 지역성(locality of reference) 이 높아 성능이 좋음(???????????)

참조 지역성의 정의 : 동일한 값 또는 해당 값에 관계된 스토리지 위치가 자주 액세스되는 특성으로, 지역성의 원리(principle of locality)라고도 불립니다. 

참조 지역성의 3가지 기본형 : 시간, 공간, 순차(sequential) 지역성.


- 참조 지역성의 종류 -

1) 공간(spatial) 지역성 : 특성 클러스터의 기억 장소들에 대해 참조가 집중적으로 이루어지는 경향으로, 참조된 메모리 근처의 메모리를 참조합니다.

2) 시간(temporal) 지역성 : 최근 사용되었던 기억 장소들이 집중적으로 액세스되는 경향으로, 참조했던 메모리는 빠른 시간에 다시 참조될 확률이 높습니다.

3) 순차(sequential) 지역성 : 데이터가 순차적으로 액세스되는 경향으로, 프로그램 내의 명령어가 순차적으로 구성되어 있다는 것이 대표적인 경우입니다. 공간 지역성에 편입되어 설명되기도 합니다.


  • 스트림 파이프라인의 종단 연산
    • 축소는 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업이다.

    • 예를 들면 min, max, sum, count 같이 완성된 형태로 제공되는 메서드가 있다.

    • anyMatch, allMatch, noneMatch처럼 조건에 맞음년 바로 반환되는 메서드도 병렬화에 적합하다.

    • 반면, 가변 축소(Mutable Reduction)을 수행하는 Stream의 collect 메서드는 병렬화에 적합하지 않다. 컬렉션들을 합치는 부담이 크기 때문이다.

    • 종단 연산 중 병렬화에 가장 적합한 것은 축소(reduction)이다.

    • 스트림 파이프라인의 종단연산의 동작방식 역시 병렬 수행 효율에 영향을 준다.

  • 병렬화에 대해 잘모르면 안하는게 낫다

    • 스트림을 잘못 병렬화하면 (응답 불가를 포함해) 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못한 동작이 발생할
      saftey failure : 결과가 잘못되거나 오동작하는 것. 안전실패
      안전 실패는 병렬화한 파이프라인이 사용하는 mappers, filters 혹은 프로그래머가 제공한 다른 함수 객체가 명시한대로 동작하지 않을 때 발생 
    • Stream 명세는 함수 객체에 대한 규약 참조
      • Stream의 reduce 연산에 건네지는 accumulator(누적기)와 combiner(결합기) 함수는 반드시 결합법칙을 만족해 함
        • 결합 법칙 : (a op b) op c = a op (b op c))
      • 간섭받지 않아야 한다 (non-interfering) - 파이프라인이 수행되는 동안 데이터소스가 변경되지 않아야한다
      • 상태를 갖지 않아야 한다 (stateless) 
      • 위의 요구사항을 지키지 못하더라도 순차적으로 실행하면 올바른 결과를 얻을 수 있다.
      • 하지만 병렬로 수행하면 기대한 결과가 나오지 않을 수 있고, 실패할 수 있으니 주의

  • 스트림 병렬화는 오직 성능 최적화 수단임을 기억하라
    • 다른 최적화와 마찬가지로 변경 전후로 반드시 성능테스트를 진행하여 병렬화를 사용할 가치가 있는지 확인
    • 이상적으로는 운영 시스템과 같은 환경에서 테스트
    • 보통은 병렬 스트림 파이프라인도 공통의 포크-조인 풀에서 수행되므로 (같은 스레드 풀을 사용)잘못된 파이프라인 하나가 다른 부분의 성능에까지 악영향을 줄 수 있음
    • 조건이 잘 갖춰지면 parallel 메서드 호출 하나로 거의 프로세서 코어 수에 비례하는 성능 향상
스트림 파이프라인 병렬화가 효과적인 예
static long pi(long n) {
    return LongStream.rangeClosed(2, n)
                     .mapToObj(BigInteger::valueOf)
                     .filter(i -> i.isProbablePrime(50))
                     .count();
}



static long pi(long n) {
    return LongStream.rangeClosed(2, n)
                     .parallel()
                     .mapToObj(BigInteger::valueOf)
                     .filter(i -> i.isProbablePrime(50))
                     .count();
}


  • Random한 수의 경우
    • 무작위 수들로 이뤄진 스트림을 병렬화하려거든 ThreadLocalRandom(혹은 Random) 보다는SplittableRandom 인스턴스를 이용
    •  SplittableRandom은 정확히 이럴 때 쓰고자 설계된 것이라 병렬화 하면 성능이 선형으로 증가
    •  ThreadLocalRandom은 단일 스레드에서 사용하고자 만들어짐
    • 병렬로 사용하는 경우에는 SplittableRandom > SplittableRandom 성능을 보임
    • 그냥 Random의 경우에는 모든 연산을 동기화하기 때문에 병렬 처리하면 최악의 성능

  • 요약
    • 계산도 올바로 수행하고 성능도 빨라질거라는 확신 없이는 스트림 파이프라인 병렬화는 시도도 금지

    • 스트림 병렬화를 잘못하면 프로그램이 오동작하거나 성능이 급격히 떨어질 수 있음

    • 병렬화를 할 경우에는 성능테스트를 반드시 진행하고, 결과가 정확한지 확인

    • 계산도 정확하고 성능도 좋아졌음이 확실할 때, 그럴 때만 병렬화 버전을 운영 코드에 반영

  • No labels