Stream 정의
stream API는 람다식(Lambda Expression)을 아용한 기술 중 하나, 데이터 소스(collection,arrays,난수,file...)을 조작 및 가공, 변환하여 원하는 값으로 변환해주는 인터페이스
-Java 1.8 이상의 버전에서 제공
-import java.util.stream에 포함
람다식(Lambda Expression)
함수를 하나의 식으로 표현한 함수형 인터페이스로 함수를 람다식으로 표현하면 메소드의 이름이 없기 때문에 익명함수(Anonymous Funtion)의 한 종류
컬렉션 프레임워크란?
-컬렉션 프레임워크는 프레임워크 내 Collection(List,Set),Queue,Map의 인터페이스로 구성이 되어 있는 프레임워크를 의미
-Java 1.2 버전 이상 제공
-import java.util 내 포함
컬렉션 인터페이스 내의 클래스 종류
List 인터페이스에는 컬렉션 클래스:ArrayList, Vector, Stack, LinkedList
Map 인터페이스에는 컬렉션 클래스: HashMap, TreeMap, LikendHashMap
Set 인터페이스에는 컬렉션 클래스: HashSet, TreeSet, LinkedHashSet
Stream 타입 별 객체의 종류
Stream 타입 별 객체의 종류
Stream의 특징
원본의 데이터를 변경하지 않는다.
-Stream API는 원본 데이터를 조회하여 별도의 Stream 객체로 생성을 합니다. 그렇기에 배열의 정렬이나 필터링 작업을 하더라도 원본 데이터를 변경하지 않습니다.
재사용이 불가능 -일회용으로 사용
이미 사용이 되어 닫혔다면 재 사용이 불가능하여 java.lang.IllegalStateException 오류가 발생하며 새로운 Stream을 생성해주어야 합니다.
내부 반복으로 작업을 처리
내부적으로 반복문을 처리하기에 간결한 소스코드의 작성이 가능합니다.
Stream의 과정
객체를 구성하고자 할때에 "Stream 생성 → 중간 연산→ 최종 연산" 의 세 단계의 과정을 통하여서 처리
자바의 정석 -- stream 과 람다
📕1. 람다식(Lamda Expression)
함수형 언어란?
Bigdata가 뜨면서 함수형 언어가 각광받고 있음.
대표적인 함수형 언어로는 Scala, Haskell 등이 있음.
Java는 OOP언어인데, JDK 1.8부터 함수형 언어 기능이 추가됨.
람다식(Lamda Expression)
함수(메서드)를 간단한 식(expression)으로 표현한 것.
익명 함수(이름이 없는 함수, anonymous function)
함수와 메서드의 차이
-⠀근본적으로 동일. 함수는 일반적 용어, 메서드는 객체지향개념 용어
-⠀함수는 클래스에 독립적, 메서드는 클래스에 종속적(자바에는 메서드밖에 없음!)
📕2. 람다식 작성하기
람다식 작성하는 법
⠀1. 메서드의 이름과 반환타입을 제거하고 ->를 블록{}앞에 추가
⠀2. 반환값이 있는 경우, 식이나 값만 적고 return문 생략가능(끝에 ; 안 붙임)
⠀3. 매개변수의 타입이 추론 가능하면 생략가능(대부분의 경우 생략가능)
람다식 작성시 주의사항
⠀1. 매개변수가 하나인 경우, 괄호() 생략가능(단, 타입이 없을 때만)
⠀2. 블록 안의 문장이 하나뿐일 때, 괄호{} 생략가능(끝에 ; 안 붙임)
⠀3. 하나뿐인 문장이 return 문이라면 괄호{} 생략불가
📕3. 람다식의 예
📕4. 람다식은 익명 함수? 익명 객체!
자바에서는 메서드만 따로 존재할 수 없으므로 람다식은 익명 함수가 아니라 익명 객체임.
⠀⠀⠀⠀ → 익명클래스는 객체의 선언과 생성이 동시에 일어남
람다식(익명 객체)을 다루기 위한 참조변수가 필요. 참조변수의 타입은?
⠀⠀⠀⠀ → 클래스 또는 인터페이스가 가능. 또 참조변수로 익명 객체(람다식)의 메서드를
⠀⠀⠀⠀⠀⠀ 호출하기 위해서는, 람다식과 동등한 메서드가 정의되어 있는 것이어야 함.
📕5. 함수형 인터페이스(Functional Interface)
함수형 인터페이스란 단 하나의 추상 메서드만 선언된 인터페이스를 말함.
⠀⠀⠀⠀→ 인터페이스에서 public abstract 생략되더라도 public abstract!⭐
함수형 인터페이스 타입의 참조변수로 람다식을 참조할 수 있음
(단, 함수형 인터페이스의 메서드와 람다식의 매개변수 개수와 반환타입이 일치해야 함)
인터페이스를 통해 람다식(익명 객체)을 다룰 수 있으며, 람다식을 다루기 위한 인터페이스를 함수형 인터페이스로 부르기로 함.
함수형 인터페이스에 @FunctionalInterface를 붙여주면 단 하나의 추상 메서드만 가지는 지 체크해줌
ex) 익명 객체를 람다식으로 대체
📕6. 함수형 인터페이스 타입의 매개변수, 반환 타입
람다식을 변수처럼 메서드를 주고받는 것이 가능
함수형 인터페이스 타입의 매개변수
함수형 인터페이스 타입의 반환타입
예제는 pass
📕7. java.util.function패키지
⠀⠀⠀ → 예를 들어 원래 Predicate<Integer, Boolean>이라고 써야하지만, 반환타입은
⠀⠀⠀⠀⠀ 항상 Boolean이므로 Boolean은 쓰지않음
매개변수가 2개인 함수형 인터페이스
⠀⠀⠀ → 접두사 'Bi'가 붙음
⠀⠀⠀ → 세 개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 아래와 같이 직접
⠀⠀⠀⠀⠀ 만들어서 써야함.
@FunctionalInterface
interface TriFunction<T,U,V,R> [
R apply(T t, U u, V v);
}
UnaryOperator과 BinaryOperator
⠀⠀⠀ → Function의 또 다른 변형.
기본형을 사용하는 함수형 인터페이스
ex)
💡 항등함수란?
⠀input 값과 output 값이 일치하는 함수
📕8. java.util.function패키지 예제
pass
📕9. Predicate의 결합
여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있음
Predicate의 끝에 negate()를 붙이면 조건식 전체가 부정이 됨
static메서드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용. (주소비교 x)
isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정
Predicate p = Predicate.isEqual(str1);
boolean result = p.test(str2); // str1과 str2가 같은지 비교하여 결과를 반환
boolean result = Predicate.isEqual(str1).test(str2);
⠀⠀⠀⠀→ 위의 두 줄은 아래의 한 줄과 같음
+α 기본편에는 안나온 메서드
.andThen() : 기존의 함수 2개를 연결해서 새로운 함수를 만듦 ex) f.andThen(g);
.compose() : 기존의 함수 2개를 연결하는데 순서가 바뀜 ex) f.compose(g);
📕10. Predicate의 결합 에제
public static void main(String[] args) {
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);
Function<Integer, String> g = (i) -> Integer.toBinaryString(i);
Function<String, String> h = f.andThen(g);
Function<Integer, Integer> h2 = f.compose(g);
System.out.println(h.apply("FF")); // "FF" → 255 → "11111111"
System.out.println(h2.apply(2)); // 2 → "10" → 16
Function<String, String> f2 = x -> x; // 항등 함수(identity function)
System.out.println(f2.apply("AAA")); // AAA가 그대로 출력됨
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i%2 == 0;
Predicate<Integer> notP = p.negate(); // i >= 100
Predicate<Integer> all = notP.and(q.or(r));
System.out.println(all.test(150)); // true
String str1 = "abc";
String str2 = "abc";
// str1과 str2가 같은지 비교한 결과를 반환
Predicate<String> p2 = Predicate.isEqual(str1);
boolean result = p2.test(str2);
// Predicate.isEqual(str1).test(str2); 위의 두 줄을 한 줄로 합친 것
System.out.println(result);
}
⠀⠀⠀⠀→ 아래와 같이 출력
⠀⠀⠀⠀⠀⠀11111111
⠀⠀⠀⠀⠀⠀16
⠀⠀⠀⠀⠀⠀AAA
⠀⠀⠀⠀⠀⠀true
⠀⠀⠀⠀⠀⠀true
📕11. 컬렉션 프레임웍과 함수형 인터페이스
함수형 인터페이스를 사용하는 컬렉션 프레임웍의 메서드(단순화 위해 와일드 카드 생략)
📕12. 컬렉션 프레임웍과 함수형 인터페이스 예제
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
for(int i=0;i<10;i++)
list.add(i);
// list의 모든 요소를 출력
list.forEach(i->System.out.print(i+","));
System.out.println();
// list에서 2 또는 3의 배수를 제거
list.removeIf(x-> x%2==0 || x%3==0);
System.out.println(list);
list.replaceAll(i->i*10); // list의 각 요소에 10을 곱함
System.out.println(list);
Map<String, String> map = new HashMap<>();
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
map.put("4", "4");
// map의 모든 요소를 {k,v}의 형식으로 출력
map.forEach((k,v)-> System.out.print("{"+k+","+v+"},"));
System.out.println();
// Iterator it = map.entrySet().iterator();
// while(it.hasNext()) {
// System.out.println(it.next());
// }
}
⠀⠀⠀⠀→ 스트림 사용함으로써 코드 많이 줄일 수 있음
📕13. 메서드 참조
하나의 메서드만 호출하는 람다식은 메서드 참조로 더 간단히 할 수 있음
⠀⠀⠀⠀→ 맨 마지막은 거의 안 쓰임
ex) static메서드 참조
📕14. 생성자의 메서드 참조
생성자와 메서드 참조
Supplier s = () -> new MyClass(); // 람다식
Supplier s = MyClass::new; // 메서드 참조
Function<Integer, MyClass> s = (i) -> new MyClass(i); // 람다식
Function<Integer, MyClass> s = MyClass::new; // 메서드 참조
BiFunction<Integer, String, MyClass> s = (i, s) -> new MyClass(i, s); // 람다식
BiFunction<Integer, String, MyClass> s = MyClass::new; // 메서드 참조
배열과 메서드 참조
Function<Integer, int[]> f = x -> new int[x]; // 람다식
Function<Integer, int[]> f2 = int[]::new; // 메서드 참조
📕15. 스트림(stream)
컬렉션이나 배열에 데이터를 담고 for문과 Iterator를 이용해서 코드를 작성하는 것은 코드도 길고, 알아보기도 어렵고, 재사용성이 떨어짐.
또한 데이터 소스마다(List, Set, Map, 배열) 다른 방식으로 다뤄야 함.
스트림은 다양한 데이터 소스를 추상화하여 데이터 소스가 무엇이든 간에 표준화된 방법으로(같은 방식으로) 다루기 위한 것.
스트림이 제공하는 기능 : 중간 연산과 최종 연산
1.⠀스트림 만들기
2.⠀중간연산 (0~n번) : 연산결과가 스트림인 연산. 반복적으로 적용가능.
3.⠀최종연산 => 결과 (0~1번) : 연산결과가 스트림이 아닌 연산. 단 한번만 적용가능
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀(스트림의 요소를 소모)
📕16. 스트림의 특징
스트림은 데이터 소스로부터 데이터를 읽기만할 뿐 원본을 변경하지 않음!⭐
스트림은 Iterator처럼 일회용 (필요하면 다시 스트림 생성해야 함)
최종 연산 전까지 중간연산이 수행되지 않음 ← 지연된 연산
중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야 하는지를 지정해주는 것일 뿐.
최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모됨.
스트림은 작업을 내부 반복적으로 처리
내부 반복이라는 것은 반복문을 메서드의 내부로 숨겼다는 것을 의미. forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용함.
스트림은 작업을 병렬로 처리 ← 병렬스트림
스트림으로 데이터를 다룰 때의 장점 중 하나가 병렬 처리가 쉽다는 점
parallel()이라는 메서드를 호출만 해주면 내부적으로 Java에서 제공하는 fork & join 프레임웍을 이용해서 자동적으로 연산을 병렬로 수행
반대로 병렬로 처리되지 않게 하려면 sequential()을 호출하면 됨. 모든 스트림은 기본적으로 병렬 스트림이 아니므로 parallel()을 호출한 것을 취소할 때만 사용
기본형 스트림 - IntStream, LongStream, DoubleStream
-⠀오토박싱 & 언박싱의 비효율이 제거됨
⠀ 일반적으로 Stream보다 IntStream 사용하는 것이 더 효율적
-⠀숫자와 관련된 유용한 메서드를 Stream보다 더 많이 제공
📕17. 스트림 만들기 - 컬렉션
스트림의 소스가 될 수 있는 대상은 배열, 컬렉션, 임의의 수 등 다양
Collection인터페이스의 stream()으로 컬렉션을 스트림으로 변환
Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있음
⠀ex) List로부터 스트림을 생성하는 코드
List list = Arrays.asList(1,2,3,4,5); // 가변인자
Stream intStream = list.stream(); // list를 소스로 하는 스트림 생성
📕18. 스트림 만들기 - 배열
배열로부터 스트림 생성하기
Stream Stream.of(T... values) // 가변인자
Stream Stream.of(T[])
Stream Arrays.stream(T[])
Stream Arrays.stream(T[] array, int startInclusive, int endExclusive)
ex) 문자열 스트림 생성
Stream strStream = Stream.of("a", "b", "c"); // 가변인자
Stream strStream = Stream.of(new String[]{"a", "b", "c"});
Stream strStream = Arrays.stream(new String[]{"a", "b", "c"});
Stream strStream = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3);
⠀⠀⠀ex)
String[] strArr = new String[] {"a", "b", "c", "d"};
Stream strStream = Stream.of(strArr);
strStream.forEach(System.out::println);
기본형 배열로부터 스트림 생성하기
ex) int 배열을 소스로 하는 스트림 생성
IntStream IntStream.of(int... values) // 가변인자
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)
⠀⠀⠀ → long, double도 이름만 바꾸면 됨
📕19. 스트림 만들기 - 임의의 수
난수를 요소로 갖는 스트림 생성하기
📕20. 스트림 만들기 - 특정 범위의 정수
IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성 가능
IntStream intStream = IntStream.range(1, 5); // 1,2,3,4
IntStream intStream = IntStream.rangeClosed(1, 5); // 1,2,3,4,5
⠀⠀⠀→ range()의 경우 경계의 끝인 end가 범위에 포함되지 않고,
⠀⠀⠀⠀⠀rangeClosed()의 경우는 포함됨
지정된 범위(begin~end)의 난수를 요소로 갖는 스트림을 생성하는 메서드. 단 end는 범위에 포함 x
📕21. 스트림 만들기 - 람다식 iterate(), generate()
람다식을 소스로 하는 스트림 생성하기
iterate()은 씨앗값(seed)으로 지정된 값부터 시작해서 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복
ex)
Stream evenStream = Stream.iterate(0, n->n+2); // 0, 2, 4, 6, ...
generate()도 iterate()처럼 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해서 반환하지만, iterate()과 달리 이전 결과를 이용해서 다음 요소를 계산하지 않음
ex)
Stream randomStream = Stream.generate(Math::random);
generate()에 정의된 매개변수의 타입은 Supplier이므로 매개변수가 없는 람다식만 허용됨.
iterate()와 generate()에 의해 생성된 스트림을 아래와 같이 기본형 스트림 타입의 참조변수로 다룰 수 없음!⭐
ex)
IntStream evenStream = Stream.iterage(0, n->n+2); // 에러
⠀⠀⠀⠀필요하다면 아래와 같이 mapToInt()와 같은 메서드로 변환해야 함
IntStream evenStream = Stream.iterage(0, n->n+2).mapToInt(Integer::valueOf);
Stream stream = evenStream.boxed(); // IntStream → Stream
📕22. 스트림 만들기 - 파일과 빈 스트림
파일을 소스로 하는 스트림 생성하기
java.nio.file.Files는 파일을 다루는데 필요한 유용한 메서드들을 제공.
⠀⠀⠀⠀→ list()는 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을
⠀⠀⠀⠀⠀⠀생성해서 반환
⠀⠀⠀⠀→ lines()는 파일 뿐만 아니라 다른 입력대상으로부터도 데이터를 행단위로 읽어올
⠀⠀⠀⠀⠀⠀수 있음
⠀⠀⠀⠀→ Path는 하나의 파일 또는 경로를 의미
빈 스트림 생성하기
스트림에 연산을 수행한 결과가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 나음.
Stream emptyStream = Stream.empty(); // empty()는 빈 스트림을 생성해서 반환
long count = emptyStream.count(); // count의 값은 0
📕23. 스트림의 연산
스트림에 정의된 메서드 중에서 데이터 소스를 다루는 작업을 수행하는 것을 연산(operation)이라 함
스트림이 제공하는 기능 - 중간 연산과 최종 연산
⠀⠀⠀⠀→ 중간 연산 : 연산 결과가 스트림이므로 0~n번 가능
⠀⠀⠀⠀→ 최종 연산 : 연산 결과가 스트림이 아니므로 0~1번 가능 (스트림의 요소를 소모)
ex)
📕24. 스트림의 연산 - 중간연산
📕25. 스트림의 연산 - 최종연산
📕26. 스트림의 중간연산 - skip(), limit()
스트림 자르기 - skip(), limit()
📕27. 스트림의 중간연산 - filter(), distinct()
스트림의 요소 걸러내기 - filter(), distinct()
distinct() 사용방법
filter()는 매개변수로 Predicate를 필요로 하는데, 아래와 같이 연산결과가 boolean인 람다식을 사용해도 됨
필요하다면 filter()를 다른 조건으로 여러 번 사용할 수도 있음
⠀⠀⠀⠀→ 두 문장은 동일
📕28. 스트림의 중간연산 - sorted()
스트림을 정렬할 때는 sorted()를 사용하면 됨
Stream sorted()
Stream sorted(Comparator<? super T> comparator)
⠀⠀⠀→ Comparator을 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)으로
⠀⠀⠀⠀⠀정렬됨. 스트림의 요소가 Comparable을 구현한 클래스가 아니면 예외 발생.
⠀⠀⠀→ Comparator을 매개변수로 넣으면 지정된 Comparator로 스트림을 정렬하는데,
⠀⠀⠀⠀⠀Comparator 대신 int값을 반환하는 람다식을 사용하는 것도 가능
ex)
Stream strStream = Stream.of("dd", "aaa", "CC", "cc", "b");
strStream.sorted().forEach(System.out::print); // CCaaabccdd
문자열 스트림의 다양한 정렬 방법
📕29. 스트림의 중간연산 - Comparator의 메서드
Comparator의 comparing()으로 정렬 기준을 제공
comparing(Function<T, U> keyExtractor)
comparing(Function<T, U> keyExtractor, Comparator keyComparator)
⠀⠀⠀ → 스트림의 요소가 Comparable을 구현한 경우, 매개변수 하나짜리를 사용하면 됨.
⠀⠀⠀⠀⠀ 그렇지 않은 경우, 추가적인 매개변수로 정렬기준(Comparator)을 따로 지정해줘야 함.
비교 대상이 기본형인 경우 comparing() 대신 아래의 메서드를 사용하면 오토박싱과 언박싱 과정이 없어서 효율적임
comparingInt(ToIntFunction keyExtractor)
⠀⠀⠀ → long과 double도 마찬가지
정렬 조건을 추가할 때는 thenComparing()을 사용
thenComparing(Comparator other)
thenComparing(Function<T, U> keyExtractor)
thenComparing(Function<T, U> keyExtractor, Comparator keycomp)
⠀⠀⠀ ex)
📕30. 스트림의 중간연산 - map()
스트림의 요소에 저장된 값 중 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때 map() 사용
Stream map(Function<? super T, ? extends R> mapper) // Stream⠀→ Stream
ex) File의 스트림에서 파일의 이름만 뽑아서 출력할 때
ex) 파일 스트림(Stream)에서 파일 확장자를 대문자로 중복없이 뽑아내기
기본 스트림을 스트림으로 변환 - mapToObj(), boxed()
📕31. 스트림의 중간연산 - map() 예제
각 요소에 매핑 함수를 적용하여 새로운 Stream을 반환
Strea<String> stream = Stream.of("apple","banana","cheryy");
Stream<Integer> mappedStream = stream.map(str -> str.lenght());
📕32. 스트림의 중간연산 - peek()
연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶을 때 즉, 디버깅 시 peek() 사용하면 됨
forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 끼워 넣어도 됨
ex)
📕33. 스트림의 중간연산 - flatMap()
스트림의 스트림을 스트림으로 변환 - flatMap()
ex)
map() 사용시
flatMap()사용시
📕34. 스트림의 중간연산 - flatMap() 예제
public static void main(String[] args) {
String[] lineArr = {
"Believe or not It is true",
"Do or do not There is no try",
};
Stream<String> lineStream = Arrays.stream(lineArr);
lineStream.flatMap(line -> Stream.of(line.split(" +")))
.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(System.out::println);
System.out.println();
}
⠀⠀⠀→ .split(" +") 에서 " +"는 정규식 표현으로 하나 이상의 공백을 의미!⭐