주의!
여기서 말하는 '스트림' 은 Java 8 에서부터 추가된 스트림 API 를 뜻한다.
'입출력 스트림'과는 전혀 다른 개념이다.
자바 8 이전에는 배열 또는 Collection 인스턴스를 for 또는 foreach 문을 돌면서 요소 하나씩을 꺼내서 다루는 방법을 사용
자바 8부터 Stream API와 람다식, 함수형 인터페이스 등...을 지원하면서, Java를 이용해 함수형으로 프로그래밍할 수 있는 API 들을 제공해주고 있다.
(원래 Java는 객체지향 언어이기 때문에, 기본적으로 함수형 프로그래밍이 불가능)
람다를 활용할 수 있는 기술 中 하나
Stream 연산들은 매개변수로 함수형 인터페이스를 받고,
람다식은 반환값으로 함수형 인터페이스를 반환한다.
스트림은 한 요소씩 수직적으로(vertically) 실행된다.
참고: 자바 컬렉션 (Java Collection)
참고: 람다식(람다 표현식, Lambda Expression)
List<String> sortedList = nameStream.sorted()
							.collect(Collections.toList());
Stream API는 원본의 데이터를 조회하여, 원본의 데이터가 아닌 별도의 요소들로 Stream을 생성
→ 원본의 데이터로부터 읽기만 할 뿐
→ 정렬이나 필터링 등...의 작업은 별도의 Stream 요소들에서 처리
userStream.sorted().forEach(System.out::print);
// 스트림이 이미 사용되어 닫혔으므로 에러 발생
int count = userStream.count(); 
// IllegalStateException 발생
java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
    at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
 // 반복문이 forEach라는 함수 내부에 숨겨져 있다.
nameStream.forEach(System.out::println);
병렬처리(multi-threading)가 가능하다
참고: 운영체제 (OS) - 3) 멀티 스레드 (Multi Thread)
Iterator 를 사용할 경우
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1,2,3));
Iterator<Integer> iter = list.iterator();
while(iter.hasNext()) {
    int num = iter.next();
    System.out.println("값 : "+num);
}
자바 6이전까지는 ArrayList에서 요소를 순차적으로 처리하기 위해, Iterator라는 반복자를 사용했다.
Stream 을 사용할 경우
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1,2,3));
Stream<Integer> stream = list.stream();		// 1. stream()메소드로 스트림 객체를 얻은 후
stream.forEach(num -> System.out.println("값 : "+num));		// 2. ArrayList에 있는 요소들을 하나씩 출력
람다식으로 기술 된 부분에는 Stream 이 들어가는 부분이 많다.
장점
데이터를 직접 정렬할 경우
String[] nameArr = {"IronMan", "Captain", "Hulk", "Thor"}
List<String> nameList = Arrays.asList(nameArr);
// 원본의 데이터가 직접 정렬됨
Arrays.sort(nameArr);
Collections.sort(nameList);
for (String str: nameArr) {
  System.out.println(str);
}
for (String str : nameList) {
  System.out.println(str);
}
Stream 을 사용한 코드
String[] nameArr = {"IronMan", "Captain", "Hulk", "Thor"}
List<String> nameList = Arrays.asList(nameArr);
// 원본의 데이터가 아닌 별도의 Stream을 생성함
Stream<String> nameStream = nameList.stream();
Stream<String> arrayStream = Arrays.stream(nameArr);
// 복사된 데이터를 정렬하여 출력함
nameStream.sorted().forEach(System.out::println);
arrayStream.sorted().forEach(System.out::println);
list.stream()
  .filter(el -> {
    System.out.println("filter() was called.");
    return el.contains("a");
  })
  .map(el -> {
    System.out.println("map() was called.");
    return el.toUpperCase();
  })
  .findFirst();
filter() was called.
filter() was called.
map() was called.
요소는 3개 : filter 두 번, map 이 한 번 출력된다.
모든 요소가 첫 번째 중간 연산을 수행하고 남은 결과가 다음 연산으로 넘어가는 것이 아니라
한 요소가 모든 파이프라인을 거쳐서 결과를 만들어내고, 다음 요소로 넘어가는 순
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList
    .stream()							// 생성하기
    .filter(s -> s.startsWith("c"))			// 가공하기 : 데이터 필터링
    .map(String::toUpperCase)			// 가공하기 : 데이터 변형
    .sorted()							// 가공하기 : 데이터 정렬
    .count();	
Stream은 데이터를 처리하기 위해 다양한 연산들을 지원한다.
스트림에 대한 연산은 생성하기, 가공하기, 결과 만들기 3가지 단계로 나눌 수 있다.
// List로부터 스트림을 생성
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> listStream = list.stream();
Collection 인터페이스에는 stream()이 정의되어 있기 때문에,
Collection 인터페이스를 구현한 객체들(List, Set 등)은 모두 이 메소드를 이용해 Stream을 생성할 수 있다.
→ stream() : 인터페이스에 추가된 디폴트 메소드 
stream()을 사용하면, 해당 Collection의 객체를 소스로 하는 Stream을 반환한다.
// 배열에서 스트림을 생성
Stream<String> stream1 = Arrays.stream(arr);
Stream<String> stream = Stream.of(new String[] {"a", "b", "c"});
Stream<String> stream = Arrays.stream(new String[] {"a", "b", "c"});
Stream<String> stream = Arrays.stream(new String[] {"a", "b", "c"}, 0, 3); // end범위 포함 x
// 배열의 특정 부분만을 이용한 스트림을 생성
Stream<String> stream2 = Arrays.stream(arr, 1, 3);
Stream<String> stream = Stream.of("a", "b", "c");
// range() : 4이상 10 이하의 숫자를 갖는 IntStream
IntStream stream = IntStream.range(4, 10);
// rangeClosed()
IntStream stream2 = IntStream.rangeClosed(1, 4);
// 필요한 경우 boxed 메소드를 이용해서 박싱(boxing)할 수 있다.
Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed();
(객체를 위한 Stream 외에도) int, long, double 같은 원시 자료형들을 사용하기 위한 특수한 종류의 Stream(IntStream, LongStream, DoubleStream) 들도 사용 가능
제네릭을 사용하지 않기 때문에, 불필요한 오토박싱(auto-boxing)이 일어나지 않는다.
(단, 필요한 경우 boxed 메소드를 이용해서 박싱(boxing)할 수 있다.)
IntStream stream = new Random().ints(4);
// iterate() 메소드 예시 1
IntStream stream = Stream.iterate(2, n -> n + 2); 	// 2, 4, 6, 8, 10, ...
// iterate() 메소드 예시 2
Stream<Integer> iteratedStream = Stream.iterate(30, n -> n + 2).limit(5); // [30, 32, 34, 36, 38]
  	// 30이 초기값이고 값이 2씩 증가하는 값
  
// generate() 메소드
Stream<String> generatedStream = Stream.generate(() -> "gen").limit(5); // [el, el, el, el, el]
	// 5개의 “gen” 이 들어간 스트림이 생성됨 
iterate()
generate()
String<String> stream = Files.lines(Path path);
// 예시 1
Stream<Object> stream = Stream.empty();
// 예시 2
public Stream<String> streamOf(List<String> list) {
  return list == null || list.isEmpty() 
    ? Stream.empty() 
    : list.stream();
}
Stream<String> builderStream = 
  Stream.<String>builder()
    .add("Eric").add("Elena").add("Java")
    .build(); // [Eric, Elena, Java]
// 스트링의 각 문자(char)를 IntStream 으로 변환 
IntStream charsStream = "Stream".chars(); // [83, 116, 114, 101, 97, 109]
// 정규표현식(RegEx)을 이용해서 문자열을 자르고, 각 요소들로 스트림을 만듦 
Stream<String> stringStream = Pattern.compile(", ").splitAsStream("Eric, Elena, Java");  // [Eric, Elena, Java]
// 병렬 스트림 생성
Stream<Product> parallelStream = productList.parallelStream();
// 병렬 여부 확인
boolean isParallel = parallelStream.isParallel();
boolean isMany = parallelStream
  .map(product -> product.getAmount() * 10)
  .anyMatch(amount -> amount > 200);
Arrays.stream(arr).parallel();
IntStream intStream = IntStream.range(1, 150).parallel();
boolean isParallel = intStream.isParallel();
// 시퀀셜(sequential) 모드로 돌리고 싶다면, sequential 메소드를 사용
IntStream intStream = intStream.sequential();
boolean isParallel = intStream.isParallel();
Stream<String> stream1 = Stream.of("Java", "Scala", "Groovy");
Stream<String> stream2 = Stream.of("Python", "Go", "Swift");
Stream<String> concat = Stream.concat(stream1, stream2);
// [Java, Scala, Groovy, Python, Go, Swift]
public <T> Stream<T> collectionToStream(Collection<T> collection) {
    return Optional
      .ofNullable(collection)		// 인자로 받은 컬렉션 객체를 이용해 옵셔널 객체를 만들고
      .map(Collection::stream)		// 스트림을 생성후 리턴
      .orElseGet(Stream::empty);		// 만약 컬렉션이 비어있는 경우라면, 빈 스트림을 리턴
  }
  
// 예시
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> strList = Arrays.asList("a", "b", "c");
Stream<Integer> intStream = 
  collectionToStream(intList); // [1, 2, 3]
Stream<String> strStream = 
  collectionToStream(strList); // [a, b, c]  
NullPointerException 예외가 발생한다면?
→ Optional 을 이용해서 null에 안전한(Null-safe) 스트림을 생성할 수 있다.
제네릭을 이요하므로, 어떤 타입이든 받을 수 있다.

// 예시 1
Stream<String> stream = 
  list.stream()
  .filter(name -> name.contains("a"));		// String의 stream에서 a가 들어간 문자열만을 포함하도록 필터링
  
// 예시 2
Stream<String> stream = 
  names.stream()
  .filter(name -> name.contains("a"));		// [Elena, Java]
스트림 내 요소들을 하나씩 평가해서 걸러내는 작업
Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어내는 연산
Java에서는 filter 함수의 인자로 함수형 인터페이스 Predicate를 받고 있기 때문에, boolean을 반환하는 람다식을 작성하여 filter 함수를 구현할 수 있다.

// 예시 1 : 스트림 내 String 의 toUpperCase 메소드를 실행해서 대문자로 변환한 값들이 담긴 스트림을 리턴
Stream<String> stream = 
  names.stream()
  .map(s -> s.toUpperCase());		// [ERIC, ELENA, JAVA]
  
// 예시 2 :  요소 내 들어있는 Product 개체의 수량을 꺼내올 수도 있다. 각 ‘상품’을 ‘상품의 수량’으로 맵핑
Stream<Integer> stream = 
  productList.stream()
  .map(Product::getAmount);		// [23, 14, 13, 23, 13]
기존의 Stream 요소들을 변환하여, 새로운 Stream을 형성하는 연산
스트림 내 요소들을 하나씩 특정 값으로 변환
→ 값을 변환하기 위한 람다를 인자로 받는다.
Java에서는 map 함수의 인자로 함수형 인터페이스 function을 받고 있다.
Stream<File> fileStream = Stream.of(new File("Test1.java"), new File("Test2.java"), new File("Test3.java"));
// 파일의 Stream 을 파일 이름의 Stream 으로 변경
// Stream<File> --> Stream<String> 변환
Stream<String> fileNameStream = fileStream.map(File::getName);  
// 중첩된 리스트가 있다.
List<List<String>> list = 
  Arrays.asList(Arrays.asList("a"), 
                Arrays.asList("b"));		// [[a], [b]]
// 이를 flatMap을 사용해서 중첩 구조를 제거한 후 작업
List<String> flatList = 
  list.stream()
  .flatMap(Collection::stream)
  .collect(Collectors.toList());		// [a, b]
  
// 만약, flatMap을 객체에 적용할 경우 : 학생 객체를 가진 스트림에서 학생의 국영수 점수를 뽑아 새로운 스트림을 만들어 평균을 구한다
students.stream()
  .flatMapToInt(student -> 
                IntStream.of(student.getKor(), 
                             student.getEng(), 
                             student.getMath()))
  .average().ifPresent(avg -> 
                       System.out.println(Math.round(avg * 10)/10.0));
// 인자 없이 그냥 호출할 경우 : 오름차순으로 정렬
IntStream.of(14, 11, 20, 39, 23)
  .sorted()
  .boxed()
  .collect(Collectors.toList());		// [11, 14, 20, 23, 39]
List<String> list = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");
// 스트링 리스트에서 알파벳 순으로 정렬한 코드
Stream<String> stream = list.stream()
  .sorted()		// [Go, Groovy, Java, Python, Scala, Swift]
// Comparator 를 넘겨서 역순으로 정렬한 코드
Stream<String> stream = list.stream()
  .sorted(Comparator.reverseOrder())		// [Swift, Scala, Python, Java, Groovy, Go]
Stream의 요소들을 정렬하기 위해서는 sorted 를 사용
파라미터로 Comparator 를 넘길 수도 있다.
lang.stream()
  .sorted(Comparator.comparingInt(String::length))
  .collect(Collectors.toList());		// [Go, Java, Scala, Swift, Groovy, Python]
lang.stream()
  .sorted((s1, s2) -> s2.length() - s1.length())
  .collect(Collectors.toList());		// [Groovy, Python, Scala, Swift, Java, Go]
클래스가 하나 존재한다.
public class Employee {
    private String name;
    public Employee(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
		  // equals() 오버라이드
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return Objects.equals(name, employee.name);
    }
		  // hashCode() 오버라이드
    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}
주의!
생성한 클래스를 Stream으로 사용할 경우, equals와 hashCode를 오버라이드 해야만 distinct()를 제대로 적용할 수 있다.
중복을 제거하기 위해 distinct를 사용
public class Main {
    public static void main(String[] args) {
        Employee e1 = new Employee("MangKyu");
        Employee e2 = new Employee("MangKyu");
        List<Employee> employees = new ArrayList<>();
        employees.add(e1);
        employees.add(e2);
        int size = employees.stream().distinct().collect(Collectors.toList()).size();
        System.out.println(size);
    }
}
int sum = IntStream.of(1, 3, 5, 7, 9)
  .peek(System.out::println)
  .sum();
"peek" = "확인해본다"
Stream의 요소들을 대상으로 Stream에 영향을 주지 않고, 특정 연산을 수행하기 위한 함수
→ Stream의 각각의 요소들에 대해 특정 작업을 수행할 뿐
→ 결과에 영향을 주지 않는다.
peek 함수는 파라미터로 함수형 인터페이스 Consumer를 인자로 받는다.
예시 : 작업을 처리하는 중간에 결과를 확인해볼 때 사용
// IntStream -> Stream<Integer>
IntStream.range(1, 4)
    .mapToObj(i -> "a" + i)
// Stream<Double> -> IntStream -> Stream<String>
Stream.of(1.0, 2.0, 3.0)
    .mapToInt(Double::intValue)
    .mapToObj(i -> "a" + i)
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
int max = IntStream.of().max().orElse(0);
IntStream.of(1, 3, 5, 7, 9).average().ifPresent(System.out::println);
long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
collect() : 스트림의 최종연산, 매개변수로 Collector를 필요로 한다.
Collector : 인터페이스, collect의 파라미터는 이 인터페이스를 구현해야한다.
Collectors : 클래스, static메소드로 미리 작성된 컬렉터를 제공한다.
// collect의 파라미터로 Collector의 구현체가 와야 한다.
Object collect(Collector collector)
Stream의 요소들을 List나 Set, Map, 등... 다른 종류의 결과로 수집하고 싶은 경우에는 collect 함수를 이용할 수 있다.
collect 함수는 어떻게 Stream의 요소들을 수집할 것인가를 정의한 Collector 타입을 인자로 받아서 처리
// Stream의 요소들을 Product의 이름으로 변환하여, 그 결과를 List로 반환받고 있다.
List<String> nameList = productList.stream()
    .map(Product::getName)
    .collect(Collectors.toList());Collectors.joining()
String listToString = productList.stream()
    .map(Product::getName)
    .collect(Collectors.joining());
// potatoesorangelemonbreadsugar
String listToString = productList.stream()
    .map(Product::getName)
    .collect(Collectors.joining(" "));
// potatoes orange lemon bread sugar
String listToString = productList.stream()
    .map(Product::getName)
    .collect(Collectors.joining(", ", "<", ">"));
// <potatoes, orange, lemon, bread, sugar>
Collectors.averagingInt(), Collectors.summingInt()
Double averageAmount = productList.stream()
    .collect(Collectors.averagingInt(Product::getAmount));
// collect 사용
Integer summingAmount = productList.stream()
    .collect(Collectors.summingInt(Product::getAmount));		// 86
// mapToInt 사용 : collect 보다 더 간단하게 표현 가능
Integer summingAmount = productList.stream()
    .mapToInt(Product::getAmount)
    .sum();		// 86
Collectors.summarizingInt()
// IntSummaryStatistics 객체가 반환되며, 필요한 값에 대해 get 메소드를 이용하여 원하는 값을 꺼내면 된다
IntSummaryStatistics statistics = productList.stream()
    .collect(Collectors.summarizingInt(Product::getAmount));
//IntSummaryStatistics {count=5, sum=86, min=13, average=17.200000, max=23}
3과 같이 작업한 결과의 평균값이나 총합 등...을 구할 경우
& 1개의 Stream으로부터 갯수, 합계, 평균, 최댓값, 최솟값을 한번에 얻고 싶은 경우에 사용
IntSummaryStatistics 객체에는 다음과 같은 정보가 담겨 있다.
개수: getCount()
합계: getSum()
평균: getAverage()
최소: getMin()
최대: getMax()
Collectors.groupingBy()
Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
  .collect(Collectors.groupingBy(Product::getAmount));
/* 결과는 Map 타입으로 나온다
{23=[Product{amount=23, name='potatoes'}, Product{amount=23, name='bread'}], 
 13=[Product{amount=13, name='lemon'}, Product{amount=13, name='sugar'}], 
 14=[Product{amount=14, name='orange'}]}
 */
Collectors.partitioningBy()
Map<Boolean, List<Product>> mapPartitioned = productList.stream()
    .collect(Collectors.partitioningBy(p -> p.getAmount() > 15));
/* 평가를 하는 함수 boolean 를 통해서, 스트림 내 요소들을 true 와 false 두 가지로 나눌 수 있다.
{false=[Product{amount=14, name='orange'}, Product{amount=13, name='lemon'}, Product{amount=13, name='sugar'}], 
 true=[Product{amount=23, name='potatoes'}, Product{amount=23, name='bread'}]}
 */
// 결과를 Set 으로 collect 한 후, 수정 불가한 Set 으로 변환하는 작업을 추가로 실행
Set<Product> unmodifiableSet = 
 productList.stream()
  .collect(Collectors.collectingAndThen(Collectors.toSet(),
                                        Collections::unmodifiableSet));
public static<T, R> Collector<T, R, R> of(
  Supplier<R> supplier, // new collector 생성
  BiConsumer<R, T> accumulator, 	// 두 값을 가지고 계산 → reduce 에서 살펴본 내용과 동일
  BinaryOperator<R> combiner, 		// 계산한 결과를 수집하는 함수 → reduce 에서 살펴본 내용과 동일
  Characteristics... characteristics) { ... }
// 예시
Collector<Product, ?, LinkedList<Product>> toLinkedList = 
  Collector.of(LinkedList::new,		// collector 를 하나 생성 + 컬렉터를 생성하는 supplier 에 LinkedList 의 생성자를 넘겨준다
               LinkedList::add, 	// accumulator 에는 리스트에 추가하는 add 메소드를 넘겨준다
               (first, second) -> {
                 first.addAll(second);		// combiner 를 이용해 결과를 조합 → 생성된 리스트들을 하나의 리스트로 합친다.
                 return first;
               });
// collect 메소드에 커스텀 컬렉터를 넘겨줄 수 있고, 결과가 담긴 LinkedList 가 반환된다.
LinkedList<Product> linkedListOfPersons = 
  productList.stream()
  .collect(toLinkedList);
// 모두 true 를 반환
List<String> names = Arrays.asList("Eric", "Elena", "Java");
boolean anyMatch = names.stream()
    .anyMatch(name -> name.contains("a"));
    
boolean allMatch = names.stream()
    .allMatch(name -> name.length() > 3);
    
boolean noneMatch = names.stream()
    .noneMatch(name -> name.endsWith("s"));
Stream의 요소들이 특정한 조건을 충족하는지 검사하고 싶은 경우에 사용
함수형 인터페이스 Predicate를 받아서 해당 조건을 만족하는지 검사
검사 결과를 boolean으로 반환
match 함수
names.stream()
    .forEach(System.out::println);
// 인자(파라미터)가 1개인 경우 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);
// 예시 : 두 값을 더하는 람다를 넘긴다. 따라서 결과는 6 이다.(1 + 2 + 3 이므로)
OptionalInt reduced = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce((a, b) -> {
    return Integer.sum(a, b);
  });
// 인자(파라미터)가 2개인 경우 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);
// 예시 : 10은 초기값이고, 스트림 내 값을 더해서 결과는 16 이다.(10 + 1 + 2 + 3 이므로)
int reducedTwoParams = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce(10, Integer::sum); // method reference : 람다는 메소드 참조(method reference)를 이용해서 넘길 수 있다.
// 인자(파라미터)가 3개인 경우 (combiner)
<U> U reduce(U identity,
  BiFunction<U, ? super T, U> accumulator,
  BinaryOperator<U> combiner);
  
// 예시
Integer reducedParams = Stream.of(1, 2, 3)
  .reduce(10, // identity
          Integer::sum, // accumulator
          (a, b) -> {		// 마지막 인자인 combiner 는 실행되지 않는다. (다음 코드 참고)
            System.out.println("combiner was called");
            return a + b;
          });
// Combiner 는 병렬 처리 시 각자 다른 쓰레드에서 실행한 결과를 마지막에 합치는 단계이므로, 병렬 스트림에서만 동작한다.
Integer reducedParallel = Arrays.asList(1, 2, 3)
  .parallelStream()
  .reduce(10,
          Integer::sum,
          (a, b) -> {
            System.out.println("combiner was called");
            return a + b;
          });
reduce라는 메소드를 이용해서 결과를 만들어낸다.
reduce 메소드는 총 세 가지의 파라미터를 받을 수 있다.
BinaryOperator< T > : 같은 타입의 인자 두 개를 받아 같은 타입의 결과를 반환하는 함수형 인터페이스
예시
결과는 36이 나온다.
accumulator 는 총 세 번 동작한다. 초기값 10에 각 스트림 값을 더한 세 개의 값(10 + 1 = 11, 10 + 2 = 12, 10 + 3 = 13)을 계산
Combiner 는 identity 와 accumulator 를 가지고 여러 쓰레드에서 나눠 계산한 결과를 합치는 역할이다. 12 + 13 = 25, 25 + 11 = 36 두 번 호출
주의!
간단한 경우에는 부가적인 처리가 필요하기 때문에, 병렬 처리가 오히려 느릴 수도 있다.
기존 코드
list.stream()
  .map(el -> {
    wasCalled();
    return el.substring(0, 3);
  })
  .skip(2)
  .collect(Collectors.toList());
System.out.println(counter); // 3
첫 번째 요소인 "Eric", 다음 요소인 "Elena" : 먼저 문자열을 잘라내고, 다음 skip 메소드 때문에 스킵된다.
마지막 요소인 “Java” : 문자열을 잘라내어 “Jav” 가 된 후, 스킵되지 않고 결과에 포함된다.
→ 여기서 map 메소드는 총 3번 호출된다.
변경된 코드 : skip 메소드가 먼저 실행되도록
List<String> collect = list.stream()
  .skip(2)
  .map(el -> {
    wasCalled();
    return el.substring(0, 3);
  })
  .collect(Collectors.toList());
System.out.println(counter); // 1
skip 을 먼저 하기 때문에, map 메소드는 한 번 밖에 호출되지 않는다.
→ 요소의 범위를 줄이는 작업을 먼저 실행하는 것이 불필요한 연산을 막을 수 있어 성능을 향상시킬 수 있다.
→ 이런 메소드로는 skip, filter, distinct 등...이 있습니다.
기존 코드
Stream<String> stream = 
  Stream.of("Eric", "Elena", "Java")
  .filter(name -> name.contains("a"));
// findFirst 메소드를 실행하면서 스트림이 닫히기 때문에, findAny 하는 순간 런타임 예외(runtime exception)이 발생
Optional<String> firstElement = stream.findFirst();
Optional<String> anyElement = stream.findAny(); // IllegalStateException: stream has already been operated upon or closed
변경된 코드 : 데이터를 List 에 저장하고 필요할 때마다 스트림을 생성해 사용
List<String> names = 
  Stream.of("Eric", "Elena", "Java")
  .filter(name -> name.contains("a"))
  .collect(Collectors.toList());
Optional<String> firstElement = names.stream().findFirst();
Optional<String> anyElement = names.stream().findAny();
종료 작업을 하지 않는 한 하나의 인스턴스로서 계속해서 사용이 가능
(단, 종료 작업을 하는 순간 스트림이 닫히기 때문에 재사용은 할 수 없다. 스트림은 저장된 데이터를 꺼내서 처리하는 용도이지 데이터를 저장하려는 목적으로 설계되지 않았기 때문)
findFirst() : stream의 순서를 고려해, 가장 앞쪽에 있는 요소를 반환
findAny() : 멀티 쓰레드에서 가장 먼저 찾은 요소를 반환. stream의 뒤쪽에 있는 요소가 반환될 수도 있다.
(IntelliJ 를 이용함)
collection.stream().forEach() 
  → collection.forEach()
  
collection.stream().toArray() 
  → collection.toArray()
Arrays.asList().stream() 
  → Arrays.stream() or Stream.of()
Collections.emptyList().stream() 
  → Stream.empty()
stream.filter().findFirst().isPresent() 
  → stream.anyMatch()
stream.collect(counting()) 
  → stream.count()
stream.collect(maxBy()) 
  → stream.max()
stream.collect(mapping()) 
  → stream.map().collect()
stream.collect(reducing()) 
  → stream.reduce()
stream.collect(summingInt()) 
  → stream.mapToInt().sum()
stream.map(x -> {...; return x;}) 
  → stream.peek(x -> ...)
!stream.anyMatch() 
  → stream.noneMatch()
!stream.anyMatch(x -> !(...)) 
  → stream.allMatch()
stream.map().anyMatch(Boolean::booleanValue) 
  → stream.anyMatch()
IntStream.range(expr1, expr2).mapToObj(x -> array[x]) 
  → Arrays.stream(array, expr1, expr2)
Collection.nCopies(count, ...) 
  → Stream.generate().limit(count)
stream.sorted(comparator).findFirst() 
  → Stream.min(comparator)
참고: [Java] 자바 스트림(Stream) 사용법 & 예제
참고: [Java] Stream API에 대한 이해 - (1/5)
참고: [Java] Stream API의 활용 및 사용법 - 기초 (3/5)
참고: 스트림의 생성
참고: Java 스트림 Stream (1) 총정리
참고: Java 스트림 Stream (2) 고급
참고: 자바 - 스트림(Stream)