collect()와 Collectors


  • collect()는 스트림의 요소를 수집하는 최종 연산으로 앞서 배운 리듀싱 (reducing)과 유사하다.
  • collect()가 스트림의 요소를 수집하려면, 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데 이 방법을 정의한 것인 바로 컬렉터(collector)이다.
  • 컬렉터는 Collector인터페이스를 구현한 것이다.
  • Collectors클래스는 미리 작성된 다양한 종류의 컬렉터를 반환하는 static메서드를 가지고 있다.
collect() 	스트림의 최종연산. 매개변수로 컬렉터를 필요로 한다.
Collector 	인터페이스, 컬렉터는 이 인터페이스를 구현해야한다.
Collectors 	클래스, static메서드로 미리 작성된 컬렉터를 제공한다.

 

  • collect()는 매개변수의 타입이 Collector인데,매개변수가 Collector를 구현한 클래스의 객체이어야 한다는 뜻이다. 그리고 collect()는 이 객체에 구현된 방법대로 스트림의 요소 를 수집한다

 

 

 

 

 

 

 

스트림을 컬렉션과 배열로 변환- toList(),toSet( ),toMap( ),toCollection( ),toArray()


  • 스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스의 toList()와 같은 메서드를 사용하면 된다.
  • List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollection()에 해당 컬렉션의 생성자 참조를 매개변수로 넣어주면 된다.
  • Map은 키와 값의 쌍으로 저장해야하므로 객체의 어떤 필드를 키로 사용할지와 값으로 사용할지를 지정해줘야 한다.
  • 예시화면과같이 요소의 타입이 Person인 스트림에서 사람의 주민번호(regld)를 키로 하고, 값으로 Person객체를 그대로 저장하는 식이다.

 

 

 

 

  • 스트림에 저장된 요소들을 'T[ ]’타입의 배열로 변환하려면, toArray()를 사용하면 된다.
  • 단, 해당 타입의 생성자 참조를 매개변수로 지정해줘야 한다.
  • 만일 매개변수를 지정하지 않았다면, 반환받는 배열의 타입은 'Object[ ]’이다.

 

 

 

 

 

통계 - counting(), summinglnt(), averaginglnt( ),maxBy(), minBy()


  • 앞서 살펴보았던 최종 연산들이 제공하는 통계 정보를 collectO로 똑같이 얻을 수 있다.
  • 이후groupingBy()와 함께 사용할 때 해당 메서드들이 유용하게 쓰인다. 그룹별로 통계정보를 얻을 수 있기 때문이다. 

 

  • counting()앞에는 Collectors가 생략되어있고 제대로 쓴다면 Collectors.counting() 로 작성해야 한다.
  • 보다 간결한 코드를 위해 Collectors의 static메서드를 호출할 때는 ‘Collectors.’를 생략하였다.
  • static import되어 있다고 가정하자.

 

 

 

 

리듀싱 - reducing()


  • 리듀싱 역시 collect()로 가능하다
  • collect()의 reducing은 앞선 reduce()와는 달리 '그룹별로' 리듀싱이 가능하다는 차이점이 있다.
  • 전자는 전체에 대한 리듀싱만 가능하다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

스트림의 최종 연산


  • 최종 연산은 스트림의 요소를 소모해서 결과를 만들어낸다.
  • 그래서 최종 연산후에는 스트 림이 닫히게 되고 더 이상 시용할 수 없다.
  • 최종 연산의 결과는 스트림 요소의 합과 같은 단일 값이거나, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있다

 

 

 

 

forEach()


  • forEach()는 peek()와 달리 스트림의 요소를 소모하는 최종연산이다.
  • 반환 타입이 void 이므로 스트림의 요소를 출력하는 용도로 많이 사용된다.
  • forEachOrdered는 순서대로 결과를 출력한다. 
  • sequential()은 직렬로 스트림을 연산한다. 
  • pararell()은 병렬로 스트림을 연산한다.

 

 

 

조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()


  • 스트림의 요소에 대해 지정된 조건에 모든 요소가 일치하는지, 일부가 일치하는지 아니면 어떤 요소도 일치하지 않는지 확인하는데 사용할 수 있는 메서드들이다.
  • 이 메서드들은 모두 매개변수로 Predicate(조건식)를 요구하며, 연산결과로 boolean을 반환한다.
  • allMatch()모든 요소가 조건을 만족시키면 true를 반환한다.
  • anyMatch()하나라도 조건을 만족시키면  true를 반환한다.
  • noneMatch모든 요소가 조건을 만족시키지 않을 때  true를 반환한다.

 

 

 

findFirst()와 findAny()


  • 조건에 일치하는 첫 번째 것을 반환하는 findFirst()가 있는데, 주로 filter()와 함께 사용된다.
  • 이는 조건에 맞는 스트림의 요소가 있는지 확인하는데 사용된다.
  • 병렬 스트림인 경우에는 findFirst()대신 findAny()를 사용해야 한다.

 

 

 

 

 

 

리듀싱 reduce()


  • 스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환한다.
  • 그래서 매개변수의 타입이 BinaryOperator다.
  • 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다.
  • 이 과정에서 스트림의 요소를 하나씩 소모하게 되며, 스트림의 모든 요소를 소모하게 되면 그 결과를 반환한다.

 

  • 연산결과의 초기값(identity)을 갖는 reduce()도 있는데,
  • 이 메서드들은 초기값과 스트림의 첫 번째 요소로 연산을 시작한다.
  • 스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로,반환 타입이 Optional<T>가 아니라 T다.

 

 

  • forEach는 배열 strArr 내 요소들을 출력한다. 

 

  • 이때 이를병렬로 처리할 수 있는데 중간연산으로 parallel()을 사용하면 된다.
  • 출력 결과가 순서와 상관없이 출력되고있는 것을 확인할 수 있다. 

 

  • 병렬로 처리하되 최종연산으로 forEachOrdered()를 사용하면 순서대로 출력된다. 

 

  • noneMatch는 predicate 조건문을 사용함으로서 조건에 일치하는 결과가 하나도 없을때 true를 반환한다.
  • 조건대로 길이가 0인 배열의 요소는 없으므로 true가 출력되었다. 

 

 

  • 이때 배열요소의 길이를 3으로 조건 수정시, false를 반환한다.
  • "sum"이있어 해당조건을 일치하는 요소가 하나이상 있기 때문이다. 

 

  • findFirst를 사용함으로서 첫 글자가 's'인 배열의 요소중 첫번째로 발견한 것을 찾는다.
  • 순서대로 찾기에 "stream"이 가장먼저 출력되었다. 

 

  • 이 때 중간연산으로 parallel()을 사용하면, 순서와 상관없이 첫번째 조건에 맞는 결과를 반환하게 되므로
  • "sum" 역시 출력될 수 있다.

 

 

  • mapToInt 와 map은 똑같이 변환을 시켜주지만 전자는 기본형으로 변환이 된다는 차이점이 있다.
  • 즉, 같은 연산이어도 mapToInt는 Stream<String>을 IntStream으로 변환하지만 
  • map은 Stream<String>을 Stream<Integer>로 변환한다.

 

 

 

 

 

 

 

 

 

Optional<T>와 Optionallnt


  • 최종 연산의 결과 타입이 Optional인 경우가 있다.
  • Opti0nal<T>은 지네릭 클래스로 ‘T타입의 객체’를 감싸는 래퍼 클래스이다.
  • 그래서 Optional타입의 객체에는 모든 타입의 참조변수를 담을 수 있다.
  • 최종 연산의 결과를 그냥 반환하는 게 아니라 Optional객체에 담아서 반환하는 것이다.
  • 이처럼 객체에 담아서 반환을 하면, 반환된 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해서 간단히 처리할 수 있다. 널 체크를 위한 if문 없이도 NullPointerException이 발생하지 않는 코드를 작성하는 것이 가능해진다.
public final class Optional<T> {
	private final T value; // T타입의참조변수
}

 

 



Optional<T>


  • Optional 클래스란, T타입 객체의 래퍼 클래스이다.
  • Optional객체를 생성할 때는 of() 또는 ofNullable()을 사용한다
  • 그동안 null을 다룰때, 직접적으로 다루는것은 NullPointerException을 발생시킬 수 있어 위험했다.
  • (1) 그래서 null을 ""을 이용하여 간접적으로 다루거나
  • (2) if문을 사용하여 null을 체크하곤 했는데,
  • Optional<T>는 이러한 점을 해결하고 null을 간접적으로 다루기 위해 사용될 수 있는 래퍼클래스이다.

 

 

  • 만일 참조변수의 값이 null일 가능성이 있으면, of()대신 ofNullable()을 사용해야한다.
  • of()는 매개변수의 값이 null이면 NullPointerException일 발생하기 때문이다.
  • Optional〈T>타입의 참조변수를 기본값으로 초기화할 때는 empty()를 사용한다.
  • null로 초기화하는 것이 가능하지만, empty()로 초기화 하는 것이 바람직하다

 

 

 

 

  • 즉 null을 그대로 참조하는 것이 아닌, 주소값이 있는 객체에 담고 다시 다른 객체에서 앞선 주소를 참조하면,
  • 해당 null값은 항상 null이 아니게 되는 것이다.

 

 



Optional<T> 객체를 생성하는 다양한 방법


  • of 스태틱 메소드를 이용해 Optional 객체를 생성할 수 있다.
  • 단, of 메소드에 null을 그대로 담고 다루는 것은 NullPointException이 발생한다.
  • 이를 방지하기 위해 ofNullable 메소드를 사용하는데, 이때는 null을 그대로 담아도 예외가 발생하지 않는다. 
  • Optional 객체 생성시 null을 그대로 넣을 수는 있지만 바람직하지않고
  • empty() 메소드를 이용해 빈 객체로 초기화하는 방법을 사용할 수 있다.

 

 

 

  • 마찬가지로 배열또한 Object[] objArr = null로 넣는 것이 아니라 new Object[0]을 이용해 빈채로 넣어주는등 
  • 널 포인트 익셉션을 줄이기 위한 방법으로 접근해야 한다.

 

 

 

Optional<T> 객체의 값 가져오기 


  • Optional 객체에 저장된 값을 가져올 때는 get()을 사용한다.
  • 값이 null일 때는 NoSuchElementException이 발생하며, 이를 대비해서 orElse()로 대체할 값을 지정할 수 있다.
  • 이때 orElse("") 즉, 저장된 값이 null 일경우 지정된 문자열을 반영시키는 메소드를 사용할 수 있다.
  • orElse( )의 변형으로는 null을 대체할 값을 반환하는 람다식을 지정할 수 있는 orElseGet( )
  • null일 때 지정된 예외를 발생시키는 orElseThrow( )가 있다.
  • orElseGet() 메소드의 경우 매개변수안에 람다식을 사용할 수 있다. 
  • orElseThrow() 메소드로는 null일시 예외까지 지정해 다루어 줄 수 있다. 
  • orElseGet(), orElseThrow()는 입력값없이 반환값만 계속 생성하는 Supplier 클래스에 속해있다. 

  • Stream처럼 Optional객체에도 filter(), map(), flatMap()을 사용할 수 있다.
  • map()의 연산결과가 Optional〈Optional〈T〉〉일 때, flatMap()을 시용하면 Optional〈T〉를 결과로 얻는다.
  • 만일 Optional객체의 값이 null이면, 이 메서드들은 아무 일도 하지 않는다.

 

 

  • isPresent()메소드는 null 일시 boolean 타입으로 결과를 반환한다. 
  • ifPresent() 메소드는 null이 아닐때만 작업을 수행하며, null일경우 아무 작업도 하지 않는다. 

 

 

  • 예제를 보자 
  • 먼저, 배열에 빈값을 넣고싶다면 arr = { }; 와같이 작성해 줄 수 있는데 
  • 이경우 콘솔 출력시 배열 안에 아무것도 없으므로 길이는 0이 출력된다. 

 

 

  • 그러나 배열에 직접 null을 작성해 다뤄줄 시 에러가 발생하게 된다. 
  • 빈값인데 길이를 반환하려고 했기 때문이다.  

 

 

  • 이때 arr 참조변수 안에 new int[0] 와같이 작성해주면 에러를 발생시키지 않고도 빈값을 채워넣을 수 있다. 

 

 

  • Optional 객체를 생성할때, 빈값을 넣어주고 싶다면 null을 직접 넣고 다루는 것은 바람직하지 않다. 
  • empty 메소드를 넣으면 빈값인 채로 객체를 생성할 수 있다. 

 

 

  • 그러나 앞서 언급했듯 get() 메소드를 이용시, null에 접근하는 get은 예외를 반환시킨다. 

 

 

  • get메소드를 사용해야 겠다면 try~ catch 문으로 감싸 예외 발생시 작성할 구문을 또다시 작성해주면 된다. 
  • 예외 발생시 "" 빈문자열로 초기화되도록 작성되었다. 

 

 

  • 그러나 이렇게 예외구문으로 감쌀시 코드가 길고 지저분해지므로 
  • 손쉽게 orElse() 메소드를 사용할 수 있다. 
  • orElse는 접근한 객체의 값이 null일 경우 () 안에 작성된 지정된 문자열을 반환한다. 

 

 

  • orElse("EMPTY")를 작성시 빈값에 대해 EMPTY문자열이 대신 출력되었다.  

 

 

  • 메서드 참조식을 람다식으로 바꾸는 연습도 계속 해보아야 한다. 

 

 

  • orElse 메서드의 클래스 명세서를 살펴보면 삼항연산자로 정의되어있는 것을 확인할 수 있다. 
  • 해당 메서드를 사용하면 매번 삼항연산자를 사용하는 수고를 덜 수 있다. 

 

 

 

 

OptionalInt, OptionalLong, OptionalDouble 


  • Optional<T>를 사용해도 무방하지만 해당 클래스들은 기본형만을 다뤄 성능을 향상시키기위해 쓰인다. 
  • 기본형을 감싸는 래퍼 클래스 들이다. 
  • opt와 opt2에 똑같이 빈값을 담았지만 메소드 비교시 다르게 간주됨을 확인할 수 있다.
  • opt에 담긴것은 int형 정수 0이고, empty에 담긴것은 빈객체 null이다. 
  • 그러므로 값이 존재하는지 묻는 is Present() 메소드를 사용하면
  • 0값을 가진 opt는 true를, null을 가진 opt2는 false를 반환한다.  
  • 두 빈값이 같은지 묻는 equals() 메소드 사용시 역시 false를 반환하게 된다. 

 

 

 

 

  • 메소드 참조를 람다식으로 변경하면 다음과 같다. 

 

 

  • result1변수엔 "123"을 담은뒤 null 값을 체크하는 filter 중간연산을 통과해 int형으로 반환하여 정수 123이 출력된다.
  • 그러나 result2에는 "" 빈값을 담았기에 filter 중간연산에서 null 로 간주된다. 그러므로 null값일시 괄호안의 내용을 반환하는 orElse 연산자의 도움으로 map 메소드에서 -1로 반환된다.  
  • result3 역시 값이 "456" 존재하므로 ifPresent 연산자를 거쳐 값이 잘 출력된다. 

 

 

  • 값 존재 여부에 따라 참,거짓을 반환하는 isPresent메소드의 활용이다. 
  • 0은 값을 존재한다고 간주해 true를 반환하고, 빈 객체만을 생성한 empty()는 null로 간주해 false를 반환한다. 

 

 

  • null값에 getAsInt() 메소드 사용시 NoSuchElementException이 발생한다.

 

 

  • 0값과 null 값은 다르기에 두 객체가 동일한지 묻는 equal 메소드 사용시 false가 출력된다. 

 

 

 

 

 

 

 

 

 



* 스트림의 중간연산 지난 수업 복습

 

 



스트림의 요소 변환하기 - map()


  • 스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때가 있다.
  • 이 때 사용하는 것이 바로 map()이다.
  • 이 메서드의 선언부는 아래와 같으며 매개변수로 T타입을 R타입으로 변환해서 반환하는 함수를 지정해야한다.

 

  • 스트림의 요소 T를 R로 변환해준다.
  • Stream<File> -> Stream<String>

  • map() 역시 중간 연산이므로, 연산결과는 String을 요소로 하는 스트림이다.
  • map()으로 Stream〈File〉을 Stream〈String〉으로 변환했다고 볼 수 있다
  • map()도 filter()처럼 하나의 스트림에 여러 번 적용할 수 있다.

 

 

  • 메소드 참조 File::getName은 클래스명::메소드명이다.
  • 메소드 참조를 람다식으로 변환하면 (참조변수) ->  참조변수.getName(); 가 된다.

 

 

  • 확장자만 추출하는 예제이다.
  • 스트림 변환 후 중간연산으로는 확장자 파일만 filter로 걸러내고 map으로 확장자만 잘라 변경, 대문자로 변경, 중복제거, 최종연산의 과정을 거쳤다.

 

  • 중간, 최종연산을 거친후 JAVA, BAK, TXT 확장자가 출력되었다.

 

 

  • 중간연산 확장자 추출과 대문자 변환 중복제거를 주석처리하면 다음과같이 콘솔창에 출력된다.

 

 




스트림의 요소를 소비하지않고 엿보기


  • 스트림을 출력하는 것으론 최종연산 forEach가 있었다.
  • 그런데 이는 최종연산으로서 스트림을 소모하여 재활용이 불가능하다.
  • peek은 스트림의 요소를 소비하지않고 현재 연산된 값을 출력해 확인해볼 수 있는 중간연산이다.
  • forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 끼워 넣어도 문제가 되지 않는다
  • 주로 디버깅 용으로 사용된다.





 

'스트림의 스트림'을 '스트림'으로 변환 - flatMap()


  • 스트림의 요소가 배열이거나 map()의 연산결과가 배열인 경우, 즉 스트림의 타입이 Stream〈T[ ]>인 경우, Stream〈T>로 다루는 것이 더 편리할 때가 있다. 그럴 때는 map() 대신 flatMap()을 사용하면 된다.
  • Stream〈String[]〉을 ‘map(Arrays::stream)’으로 변환한 결과는 Stream〈String〉이 아닌, Stream〈Stream〈String〉〉이다.
  • 즉, 스트림의 스트림인 것이다.

 

 

  • 스트림을 flatMap으로 변환 시 '스트림의 스트림'화 되지않고 '스트림'으로 잘 담긴다.
  • flatMap()은 map()과 달리 아래의 그림처럼,스트림의 스트림이 아닌 스트림으로 만들어 준다.

 

 

 

  • 그러므로 중간연산 및 최종연산이 진행될 수 있다. 

 

 

  • 스트림을 스트림으로 변환하려고 할시 Stream<Stream<String> 참조변수> 로 바뀐다.
  • 이러한 스트림의 스트림화를 막아주는 것이 flatMap 이다. 

 

 

 

  • 아래와 같이 여러 문장을 요소로 하는 스트림이 있을 때, 이 문장들을 split()으로 나눠서 요소가 단어인 스트림을 만들고 싶다면 어떻게 해야 할 까?
  • map()은 Stream〈String〉이 아니라 Stream〈Stream 〈String〉〉을 결과로 돌려준다.
  • 이럴 때도 map()대신 flatMap()으로 원하는 결과를 얻을 수 있다

 

\

 

  • map( )과 flatMap( )의 차이를 간단히 정리하면 다음과 같다.

 

 

 

 

 

 

스트림의 중간연산

 

 

 

스트림 자르기 skip(), limit()


  • skip()과 limitO은 스트림의 일부를 잘라낼 때 사용하며, 사용법은 아주 간단하다.
  • skip(3)은 처음 3개의 요소를 건너뛰고,
  • limit(5)는 스트림의 요소를 5개로 제한한다.

 

 

스트림의 요소 걸러내기 


  • distinct()스트림에서 중복된 요소들을 제거하고,
  • filter()는 주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다.

 

 

 

스트림 정렬하기


  • 스트림을 정렬할 때는 sorted()를 사용하면 된다.
  • Comparator를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬한다.
  • 단, 스트림의 요소가 Comparable을 구현한 클래스가 아니면 예외가 발생한다.

 

 

 

  • Comparator의 comparing()으로 정렬기준을 제공
  • 정렬에 사용되는 메서드의 개수가 많지만,가장 기본적인 메서드는 comparing()이다.

 

 

  • 추가 정렬기준을 제공하려면 thenComparing()을 사용하여 중간연산시 정렬을 이어가야 한다.

 

 

  • 예제를 보자. 다음의 예제는 학생의 성적을 반별 오름차순, 총점별 내림차순으로 정렬하여 출력한다
  • 기본적인 출력은 다음과 같다.

 

  • 기본정렬은 총점의 내림차순이다. 

 

 

 

 

 

 

 

 

 

 

(+)

메소드 참조를 다시 람다식으로 바꿀 수 있다.

 

 

 

 

 

 

 

스트림의 연산


  • 스트림이 제공하는 기능에는 중간연산과 최종연산이 있다. 
  • 중간연산은 횟수에 상관없으며, 연산결과가 스트림인 연산이다. 
  • 최종연산은 단 한번만 적용 가능하며, 연산결과가 스트림이 아닌연산이다.
  • 최종연산 이후엔 스트림이 닫혀 재활용할 수 없다는 것에 주의한다.

 

  • 스트림의 중간연산으로 사용될 수 있는 연산자들이 다음과같이 존재한다.

 

 

  • 스트림에 최종연산에 사용할 수 있는 연산에는 다음과 같은 것들이 있다.

 

 

 

 

 

 

 

 

 

 

 

 

스트림 만들기 - 컬렉션 


  • 컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있다.
  • 그래서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다.
  • stream()은 해당 컬렉션을 소스(source)로 하는 스트림을 반환한다.
Stream <T> Collection.stream()

 

  • Collection 인터페이스의 stream() 메소드를 통해 쉽게 스트림으로 변환할 수 있다. 
  • 한 가지 주의할 점은 forEach()가 스트림의 요소를 소모하면서 작업을 수행하므로 같은 스트림에 forEach()를 두 번 호출할 수 없다는 것이다. 그래서 스트림의 요소를 한번 더 출력하려면 스트림을 새로 생성해야 한다.

 

 

 

  • 다음과같이 List를 사용하며 배열을 입력하고 출력할 수 있다. 
  • 이때 원본은 건들지 않으며 list 참조변수를 통해 새로운 스트림을 생성하여 변환한다. 

 

 

  • 한번 출력한 intStream을 재 출력하려고 하면 에러가 발생하는 것을 확인할 수 있다. 

 

 

  • 다음과같이 스트림을 재 생성해주어야 한다. 
  • 정리하면, Stram은 1회용이며 Stream이 최종연산된 이후에는 닫힌다. 

 

 

 

 

스트림 만들기 - 배열 


 

객체배열로부터 스트림 생성하기

  • 배열을 소스로 하는 스트림을 생성하는 메서드는 다음과 같이 Stream과 Arrays에 static 메서드로 정의되어 있다.

 

  • IntStream과 LongStream은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성 해서 반환하는 range()와 rangeClosed()를 가지고 있다.
  • range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed( )의 경우는 포 함된

 

  • Stream<T> Array.stram(T[] array, int startInclusive, int endExclusive)
  • T[] array에는 배열을, int startInclusive에는 시작인덱스를, endExclusive에는 마지막인덱스를 작성할 수 있다.
  • 이때 endExclusive 인덱스는 포함되지않는다. (int startInclusive <= n < int endExclusive)

 

 

 

  • 기본형 배열로부터 스트림 생성하기
  • 앞서 오토박싱과 언박싱의 비효율을 제거해주는 기본형 스트림을 배웠었다. 

 

 

 

  • 배열을 스트림으로 생성시 다음과같이 작성할 수 있다.
  • 위와 아래는 동일한 결과를 출력한다. 

 

 

  • 기본형 스트림은 count 와같이 숫자와 관련된 메소드를 제공한다. 

 

 

 

 

스트림 만들기 - 임의의 수 


  • 난수를 요소로 갖는 스트림 생성하기 
  • 난수를 생성하는데 사용하는 Random클래스에는 아래와 같은 인스턴스 메서드들이 포함 되어 있다.
  • 이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 ‘무한 스트림(infinite stream)’이므로 limit()도 같이 사용해서 스트림의 크기를 제한해 주어야 한다.
  • limit()은 스트림의 개수를 지정하는데 사되며, 무한 스트림을 유한 스트림으로 만들어 준다

  • 아래의 메서드들은 매개변수로 스트림의 크기를 지정해서 ‘유한 스트림’을 생성해서 반환 하므로 limit()을 사용하지 않아도 된다.

 

  • 이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.
  • ints() 메서드는 무한 스트림 내에서도 크기가 지정된 난수 스트림을 반환하도록 한다. 
  • ints, longs, doubles 등은 기본적으로 무한 스트림이며 시작, 마침 범위의 유무에 따라 유한된 값을 출력할 수 있다.  

 

 

 

  • ints 메서드는 범위가 정해지지 않을시 무한개의 난수 stream을 반환한다. 

 

 

 

 

스트림 만들기 - 특정 범위의 정수


  • 특정 범위의 정소를 요소로 갖는 스트림 생성하기 
  • IntStream.range(int begin, int end) : begin ≤ n < end 사이의 난수 n을 반환한다. 이때 end는 포함되지않는다.
  • IntStream.rangeClosed(int begin, int end) : begin ≤ n ≤ end 사이의 난수 n을 반환한다. 이때 end는 포함된다.

 

 

 

스트림 만들기 - 람다식 iterate(), generate()


  • 람다식을 소스로 하는 스트림 생성하기 
  • Stream클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의 해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.
  • iterate()는 씨앗값(seed)으로 지정된 값부터 시작해서, 람다식 공에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다. 아래의 evenStream은 0부터 시작해서 값이 2씩 계속 증가한다.
Stream <Integer> evenStream = Stream.iterate(0,n - > n+2) ; / / 0, 2, 4, 6, ...
  • generate()도 iterate()처럼,람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해서 반환하지만,
  • iterate()와 달리, 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

 

  • iterate는 seed값을 초기값으로 삼아 람다식을 적용하며 그러므로 seed를 시작으로한 이전요소에 종속적이다. 
  • generate는 seed값이 없으며 일종의 Supplier로서 계속해서 반환값만 출력하기 때문에 이전요소에 독립적이다. 
  • 그리고 generate()에 정의된 매개변수의 타입은 Supplier이므로 매개변수가 없는 람다식만 허용된다.
  • 한 가지 주의할 점은 iterate()와 generate()에 의해 생성된 스트림을  기본형 스트림 타입의 참조변수로 다룰 수 없다는 것이다.

 

 

  • 무한 stream인 iterate는 seed값을 기반으로 람다식을 적용하며 값을 제한하지않을시 계속해서 출력된다.

 

  • Stream의 limit으로 중간연산 수행시 원하는 값의 수를 제한 하여 출력할 수 있다. 

 

 

  • generate 는 seed 값을 필요로 하지 않으며 람다식의 의해 같은 값을 무한대로 출력한다. 
  • 마찬가지로 원하는 값의 갯수를 limit으로 지정할 수 있다. 

 

 

 

 

스트림 만들기 - 파일과 빈 스트림 


  • 파일을 소스로 하는 스트림 생성하기 
  • Files.lines(Path path) 사용시 파일내용을 한줄 한줄 라인단위로 읽어 String으로 반환할 수 있다. 
  • java.nio.file.Files는 파일을 다루는데 필요한 유용한 메서드들을 제공하는데,
  • list()는 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환한다.

 

 

 

비어있는 스트림 생성하기


  • 요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다.
  • 스트림에 연산을 수행한 결과 가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 낫다.
  • Stream.empty()를 이용해 비어있는 스트림을 생성해 반환할 수도 있다. 

 

 

 

 

 

 

 

 

 

 

스트림 


  • 지금까지 많은 수의 데이터를 다룰 때, 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 이용해서 코드를 작성해왔다.
  • 또 데이터 소스마다 다른 방식으로 다뤄야한다는 것이다. List의 사용법과 Map의 사용법이 다른게 그것이다. 
  • 이러한 문제점들을 해결하기 위해서 만든 것이 ‘스트림(Stream)’이다.
  • 스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다.
  • 데이터 소스를 추상화하였다는 것은, 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.
  • 스트림을 이용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

 

 

  • 이를테면 컬렉션과 배열을 다루는 문제를 들 수 있는데, 이들은 하나의 표준화된 방법이 없었다. 
  • 스트림을 이용하면 컬렉션이든 배열이든 다양한 중간 연산을 거쳐 하나의 최종연산을 통해 결과를 얻을 수 있다. 
  • 컬렉션은 .stream() 이라는 메소드를 통해 쉽게 스트림으로 변환할 수 있다. 
  • 배열을 Stream.of(new연산자 )... 를 통해 스트림을 사용할 수 있다. 
  • 람다식은 Stream.iterate() 와 Stream.generate()를 통해 스트림을 사용할 수 있다. 
  • inis()을 사용하면 크기가 정해진 난수를 스트림으로 변환하여 받을 수 있다. 

 

 

 

  • Stream의 의미는 '흐름'이다. 
  • 즉, 이는 데이터의 연속적인 흐름을 의미한다. 

 

 

 

  • 스트림사용은 세가지 흐름을 거친다. 바로 1. 스트림을 만들고 2. 중간연산 3. 최종연산이다.
  • 스트림이 제공하는 기능에는 중간연산과 최종연산이 있다.

 

중간 연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음 
최종 연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능
  • 중간 연산이란 연산결과가 스트림인 연산이며 0~n번등 반복적으로 적용이 가능하다. 
  • 최종 연산이란 연산결과가 스트림이 아닌 연산이며 단 한번만 적용 가능하다. 

 

  • 최종연산시엔 스트림의 요소를 소모하므로 스트림이 닫혀버린다.
  • Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼,스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다.
  • 즉, 한번 소모된 스트림은 재 사용할 수 없어 다시 생성해야한다.  

 

 

 

 

스트림의 특징


  • 스트림은 데이터 소스로부터 데이터를 읽기만(Read Only) 할 뿐 변경하지않는다. 
  • 예제에서 볼 수 있듯 원본 데이터 참조변수 list와 스트림으로 생성된 참조변수 sortedList의 값은 서로 다르다. 
  • 원본은 읽기만 할뿐 변경하는건 새로 얻어온 데이터 이기 때문이다. 

 

 

▶ 스트림의 특징 : 스트림은 Iterator 처럼 일회용이다. 

  • 최종연산시 모든 요소를 소모해 버리므로 재사용이 불가하며 다시 생성해야한다.

 

▶ 스트림의 특징 : 최종연산전까지 중간연산이 수행되지않는다. 

  • 스트림 연산에서 한 가지 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행 되지 않는다는 것이다.
  • 스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것은 아니다.
  • 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다.
  • 최종 연산이 수행되어야 비로소 스트 림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다
  • 끊임없이 동작하는 무한 스트림을 중간연산하여 자르고, 정렬하고, 중복제거 할 수 있는 이유는 최종연산전까지 이런 중간연산이 수행되지않기 때문이다. 
  • 최종연산 전까지 중간연산이 수행되지않기 때문인데 이를 지연된 연산이라고 한다.  

 

 

▶ 스트림의 특징 : 스트림은 작업을 내부 반복으로 처리한다.

  • stream.forEach()는 메소드 내에 for문을 안으로 넣는, 내부 반복문으로 만들어졌다.
  • 성능보다는 코드가 간결해지는 효과를 얻을 수 있다. 

 

 

 

▶ 스트림의 특징 : 스트림의 작업을 병렬로 처리 

  • 스트림사용시 parallel() 메소드를 이용해 병렬 스트림으로 변환할 수 있다. 
  • 스트림으로 데이터를 다룰 때의 장점 중 하나가 바로 병렬 처리가 쉽다는 것이다.
  • 그저 스트림에 parallel()이라는 메서드를 호출해서 병렬로 연산을 수행하도록 지시하면 된다.
  • 반대로 병렬로 처리되지 않게 하려면 sequential()을 호출하면 된다.
  • 모든 스트림은 기본적으로 병렬 스트림이 아니므로 sequential()을 호출할 필요가 없다.
  • 이 메서드는 parallel( )을 호출한 것을 취소할 때만 사용한다.
  • 단, 병렬처리가 항상 더 빠른 결과를 얻게 해주는 것은 아니다.

 

 

▶ 스트림의 특징 : 기본형 스트림 

  • 기본형을 다루는 경우 한해서 기본형 스트림을 사용할 수 있다.
  • Stream<Integer>를 사용하는 경우 괄호안에는 참조형만 들어갈 수 있기에 기본형을 사용하려면 전후로 오토박싱, 언박싱등이 일어나야한다. 
  • 기본형 스트림을 사용하면 이런 반복되는 비효율이 제거된다.
  • 숫자와 관련된 유용한메서드를 Stream<T> 보다 IntStream이 더 많이 제공한다. 
  • Stream<T>는 숫자외에도 다른 타입의 스트림이 가능해야하기 때문에 숫자스트림에만 사용가능한 sum(), average()등을 제공하지않는다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

메서드 참조


  • 메서드 참조 람다식을 더욱 간단히 표현한 것이다. 
  • 단, 항상 간단히 사용할 수 있는것은 아니고, 람다식이 하나의 메서드만 호출하는 경우에는 ‘메서드 참조(method reference)’라는 방법으로 람다식을 간략히 할 수 있다
  • 참조하는 종류는 1. static 메서드 참조와 2. 인스턴스메서드 참조가 있다.
  • 표현방식은 다음과 같다 
클래스 이름::메서드 이름
  • 메서드는 다른 클래스에도 동일한 이름이 존재할 수 있기 때문에 앞에 클래스 이름은 반드시 필요하다.

 

  • 람다식의 일부가 생략되었지만, 컴파일러는 생략된 부분을 우변의 parselnt메서드의 선언부로부터, 또는 좌변의 Function인터페이스에 지정된 지네릭 타입으로부터 쉽게 알아내는 식이다.
  • 특정 객체 인스턴스메서드 참조는 사용빈도가 낮다. 

  • 반환형이 Integer인 static 메서드를 람다식으로 표현하면 다음과 같다. 
  • Function<String, Integer> f = (String s) -> Integer.parseInt(s);
  • 이때 Function 타입으로 받은 이유는 입력값과 출력값이 하나씩 존재하기 때문이다. 
  • (String s) 는 생략이 가능하다. 왜냐하면 이미 Funtion의 입력값으로 작성되어있기때문에 유추가 가능하기 때문이다.
  • 입력 변수를 의미하는 parseInt(s)의 (s)역시 생략가능하다.
  • 이미 반환형에 입력값이 존재함이 명시되어있기 때문이다. 
  • 메서드 참조형의 표현방식인 클래스명::메소드명으로 변환하면 다음과같다. 
  • Integer::parseInt

 

 

 

  • 메서드 참조를 사용할 수 있는 경우가 한가지 더 있는데,
  • 이미 생성된 객체의 메서드를 람다식에서 시용한 경우에는 클래스 이름 대신 그 객체의 참조변수를 적어줘야 한다.
  • 하나의 메서드만 호출하는 람다식은 ‘클래스이름::메서드이름’또는‘참조변수::메서드이름’으로 바꿀 수 있다.

 

 

 

 

  • 예제로 테스트 해본다. 
  • 입력값과 출력값이 존재하므로 java.util.function 패키지 내 Function 메소드를 사용한다.
  • 그러므로 메소드 옆 <T, R> 값은 각각 입출력이 된다. 
  • 이를 람다식으로 표현하면 다음과 같을 것이다. 

 

 

  • 메소드 참조형은 람다식을 더 간단히 작성하여 [클래스이름::메소드이름]으로 작성하기로 하였으므로 
  • 클래스 이름자리에는 Integer가, 메소드 이름자리에는 parseInt가 들어가면 된다. 
  • 생략이 가능한 이유는 참조형 메서드인 Function메서드에서 이미 입출력값의 유무, 입출력값의 형식 등이 유추가 가능하기 때문이다. 
  • 연습을 위해 메소드 참조형을 다시 람다식으로 바꿔보는것도 좋다.  

 

 

생성자의 메서드 참조


  • 생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.
  • 클래스명::new 로 작성한다.
  • 입력값이 없을때는 반환값, 즉 새로운 객체만 계속적으로 반환하므로 java.util.function 패키지 내 Supplier 메서드를 사용한다.
  • 입력값이 있을때는 입력값, 반환값이 하나씩 있으므로 java.util.function 패키지 내 Function 메서드를 사용한다.

 

 

 

  • 입력값이 2개 인경우엔 BiFunction<T, U, R>을 사용한다.
  • 이때 입력값은 T, U에 해당한다.

 

 

  • 배열을 메서드 참조하기 위해서 메서드의 반환값을 배열형식으로 지정한다.
  • Funtion<Integer, int[ ]> ... 와 같이 사용한다.
  • 이를 생성자의 메서드 참조형으로 바꿀시엔 클래스명자리에 배열타입( int[ ] ) 을 사용한다. 

 

 

예제를 통해 생성자호출을 메서드 참조를 이용해 표현해보자.

생성자 호출시 입력값은 없고 새로운 객체만 계속해서 반환하고싶으므로 Supplier를 사용한다.

Supplier의 기본 메소드는 get 이다. 

 

 

  • 생성자의 메소드 참조형은 클래스이름::new 로 작성한다.

 

 

  • 새로운 객체를 반환하므로 두번 생성된 객체는 같은 대상이아니다. 
  • 콘솔창의 결과를 보면 각각 생성된 객체의 주소값이 다른 것을 확인할 수 있다. 

 

 

  • 입력값이 있는 생성자 생성에서도 메서드 참조형은 MyClass::new로 동일하게 작성된다.
  • 입력값 (i)가 생략된 이유는 Function 메소드 탓이다. 
  • 해당메소드는 입력값과 출력값이 하나씩 있는 경우에 사용하므로,
  • 이미 입력값이 있다는 것과 그 입력값의 타입은 Integer이란 것을 유추할 수 있기 때문이다.

 

 

  • 반환타입이 배열타입일 때도 메서드 참조형은 동일하게 작성한다.
  • 배열생성시엔 반드시 Function 메서드를 사용해야한다.
  • 입력값과 출력값이 둘다 있기 때문인데 
  • 이때 입력값은 배열의 길이, 출력값은 배열이 될 것이다. 

 

 

 

 

 

 

 

 
 

Predicate의 결합 


  • 여러 조건식을 논리 연산자인 &&(and), | | (or), !(not)으로 연결해서 하나의 식을 구성 할 수 있는 것처럼 , 여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.
  • and(&&), or(||), negate(!) 등이 그것이다. 
  • 단 매개변수에 상관없이 결과는 항상 boolean으로 출력된다. 
  • 참고로 인터페이스가 가질 수 있는 메서드는 세가지 종류가 있다. default메서드, static 메서드, 추상메서드
  • 등가비교를 위한 Predicate의 작성에는 isEqual() 메소드를 사용한다. 문자열을 비교할 때 사용하는 equals와 유사하다. 

 
 

  • 함수 f와 g를 결합하여 함수 h를 만들어 사용할 수 있다. 
  • 단, f의 입력이 String, 출력이 Integer라고 가정했을때, f를 g와 결합하기 위해선 g의 입력형식이 f의 출력형식과 동일해야 한다.
  • 즉 f함수의 출력값이 g 함수의 입력값과 <형식이 같아야> 함수간 결합을 할 수 있다.  

 
 

  • 그림으로 표현하면 다음과같다. 

 

  • 항등함수넣은 값과 출력값이 같도록 하는 것이다.
  • 그리고 identity()는 함수를 적용하기 이전과 이후가 동일한 ‘항등 함수’가 필요할 때 사용 한다.
  • 이 함수를 람다식으로 표현하면 ‘x->x’이다.

 
 

  • 앞서 설명한 것처럼 isEqual은 equals 메소드와 사용법이 유사하다. 

 
 
 

컬렉션 프레임웍과 함수형 인터페이스


  • 컬렉션 프레임웍의 인터페이스에 다수의 디폴트 메서드가 추가되었다.
  • 그 중의 일부는 함수형 인터페이스를 사용한다. 다음은 그 메서드들의 목록이다

 
 
 
 

+ Recent posts