language/java

14. 람다와 스트림

wooweee 2023. 9. 8. 09:19
728x90

1. 람다식

1.1. 람다식 사용 조건

  1. Java 8 이상 버전

  2. 함수형 인터페이스(Functional Interface) 일 때 사용 가능
    이름이 거창할 뿐이지 익명 클래스 중 아래의 조건일 때 람다식으로 변경이 가능하는 뜻
    • 함수형 인터페이스: 하나의 추상 메서드만을 가지고 있는 인터페이스
      • default 메서드는 제외
      • Object 메서드 제외
    • @functionalInterface : 애노테이션을 이용해서 조건에 부합한 인터페이스인지 체크 가능

    • 익명클래스
      • 함수형 인터페이스 ⊂ 익명클래스 
        • 익명클래스 : 인터페이스, class 상관 없이 기준이 되는 class나 interface 를 가지고 익명 객체를 생성할 수 있다. 물론 부모의 method의 개수도 제약이 없다.
        • 함수형 인터페이스 : lambda 식을 위한 익명객체의 부모, lambda 식의 익명객체는 함수형 인터페이스를 통해서만 적용이 가능하고, 해당 인터페이스의 추상 method는 무조건 1개여야만 하고 default, static method는 상관 없다.

          cf) method를 하나를 가지고 있는 부모 class의 method를 오버라이딩 해서 익명 클래스를 만들어도 이것을 람다식으로 사용하는 것은 불가하다. 람다식은 함수형 인터페이스를 통해서 만든 익명 클래스란 조건에서 사용이 가능하다.
  3. 람다식은 익명 함수(Anonymous function)로서, 이름이 없다.
    • 익명객체 사용시 객체 명은 없지만 내부에서 구현한 method는 이름을 가진다.
    • 하지만 lambda식으로 변경 시 내부 method 이름조차 생략한다.
    • 이름만 생략한 것이지 익명 객체와 작동원리가 동일하다. - 익명객체가 사용되는 3곳
    • 사용되는 곳이 정해진 이유는 익명객체 자체는 이름이 없기 때문에 반환값을 받아줄 수 있는 변수나 메서드가 필요 
      1. 필드값
      2. 메서드 내부
      3. 매개변수

  4. 람다식은 변수나 매개변수, 리턴값 등으로 전달되어야 한다. -> 3번에서 얘기한 익명 객체 작동원리

  5. 람다식은 일급 객체(First-class object)로서, 변수나 데이터 구조 안에 저장하거나, 인자로 전달하거나, 반환값으로 사용될 수 있어야 한다. -> 4번에 부합하면 5번은 자동 성립

 

 

1.2. 람다식 사용

  • 위의 조건을 다시 한번 정리하면서 사용까지 도출
  • 익명객체를 사용하는데 익명객체를 보니깐 인터페이스를 구현하는 것이고 인터페이스가 1가지 추상메서드만 가짐
  • 이는 함수형 인터페이스라는 것을 알게 되고 익명 객체 생성 시 내부 메서드를 람다식으로 줄임
  • 이제부터 해당 람다식의 실제 메서드 이름을 호출해서 반환값을 받으면 됨. 3. 번에서 반환 값을 받는 곳이 3곳이 있다고 했음
  • 결론: 람다식은 익명객체 호출 시 메서드를 구현할 때 간략하게 하기 위함이지 그 이상도 그 이하도 아니다.
    •  장점
      • 코드의 단순화
      • 코드 가독성 높임
    • 단점
      • 일정 수준이상 단순화 시 가독성 낮춤
      • 사용조건이 까다로움

* 나중에 익명 클래스 블로그할 때 참고 할 자료들

더보기

 

1.2. 람다식 사용 예 

1. 변수명

// 람다식 사용 조건
@FunctionalInterface
interface MyFunction {
    void run();
}

public class Try1 {
    public static void main(String[] args) {
    // 람다식 사용 준비 비교
        
        // 익명객체로 표현
        MyFunction f1 = new MyFunction() {
            @Override
            public void run() {
                System.out.println("1.1 변수에 넣은 익명객체");
            }
        };
        // 람다식으로 표현
        MyFunction f2 = ()->System.out.println("1.2 람다식을 활용한 익명객체, 익명클래스명과 메서드명 생략가능");

        // 람다식 사용
        f1.run();
        f2.run();
    }
}

 

2. 메서드 구현부

 

// 람다식 사용 조건
@FunctionalInterface
interface MyFunction {
    void run();
}
public class Try1 {
    // 메서드 구현부 준비
    // 반환타입은 사용할 함수형 인터페이스로 해주면 해당 익명객체의 주소를 받을 수 있다.
    // 꼭 반환타입을 함수형 인터페이스로 할 필요는 없다.
    // 아래와 같이 바로 출력되게 할 수있다.

    static MyFunction getMyFunction(){
        MyFunction f = new MyFunction() {
            @Override
            public void run() {
                System.out.println("2. 익명객체로 표현");
            }
        };
        return f;
    }
    
    static MyFunction getMyFunction1(){
        MyFunction f1 = ()-> System.out.println("2.1 메서드 구현부에서 람다식으로 작성, 익명객체로도 표현 가능");
        return f1;
    }
    
    static void getMyFunction2(){
        MyFunction f2 = ()-> System.out.println("2.2 메서드 구현부에서 람다식으로 작성");
        f2.run();
    }
    
    public static void main(String[] args) {
        // 람다식 사용
        getMyFunction().run();
        getMyFunction1().run();
        getMyFunction2();
    }
}

 

3. 매개변수 타입에 넣기

// 람다식 사용 조건
@FunctionalInterface
interface MyFunction {
    void run();
}

public class Try1 {
    // 매개변수로 준비
    // 매개변수에 인터페이스 타입이 들어갈 수 있다.
    // 이걸 사용할 때 lambda 식을 작성해야 한다.
    static void execute(MyFunction f) {
        f.run();
    }

    public static void main(String[] args) {
        // 람다식 작성 및 사용
        // 매개변수에는 MyFunction type의 객체가 들어가야하므로 실제 구현한 객체를 넣던가
        // 내부에 익명객체로 넣어도 되고
        // 조건이 성립시 람다식으로 더 간단하게 넣을 수 있다.
        execute(()-> System.out.println("3. 매개변수 타입으로 넣어줌, 1.번 예제서 만든 f1을 넣어도 작동하고, 익명객체로 만드는 것도 가능"));
    }
}

 

2. 함수형 인터페이스

 

2.1. java.util.function pacakge

  • 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓은 것
  • 개발 시 자기가 만드는 것보다 똑똑한 사람들한테 검증받은 패키지를 활용하는 것이 더욱 안전하다.
  • 해당 패키지 사용 시 장점
    1. 메서드 이름 통일
    2. 재사용성 좋음
    3. 유지보수 좋음
  • 함수형 인터페이스
함수형 인터페이스 메서드 설명
java.lang.Runnable void run() 매개변수 없고 반환값도 없음
Supplier<T> T get()  -> T type 객체주소 반환 매개변수 없고 반환값만 있음
Consumer<T> T 매개변수 주입 -> void accept(T t) Supplier와 반대로 매개변수만 존재하고 반환값 없음
Function<T, R> T 매개변수 주입 -> R apply (T t) -> R 타입 객체 주소 반환 일반적인 함수 매개변수 받고 결과 반환
Predicate<T> T 매개변수 주입 -> boolean apply (T t) -> true / false 반환 조건식을 표현하는데 사용됨.
매개변수는 하나, 반환 타입은 boolean

 

  • 매개변수가 2개인 함수형 인터페이스
함수형 인터페이스 메서드 설명
BiConsumer<T,U> T 매개변수 주입 -> void accept(T t, U u) 매개변수만 존재하고 반환값 없음
BiFunction<T, U, R> T, U 매개변수 주입 -> R apply (T t, U u) -> R 타입 객체 주소 반환 일반적인 함수 매개변수 받고 결과 반환
BiPredicate<T, U> T, U 매개변수 주입 -> boolean apply (T t, U u) -> true / false 반환 조건식을 표현하는데 사용됨.

 

  • UnaryOperator와 BinaryOperator: function의 자손 종류
함수형 인터페이스 메서드 설명
UnaryOperator<T> T 매개변수 주입 -> T apply (T t) -> T 타입 객체 주소 반환 Function 자손
BinaryOperator<T> T 매개변수 주입 -> T apply (T t, T t) -> T 타입 객체 주소 반환 BiFunction 자손

 

  • Predicate 인터페이스의 default 메서드
and();     -   &&(and)
or();      -   ||(or)
negate();  -   !(not)

 

 

 

2.2. 컬렉션 프레임워크와 함수형 인터페이스

  • 컬렉션 프레임워크 인터페이스의 default method의 매개변수 타입이 함수형 인터페이스로 된 목록
  • 2.1. 과의 차이점
    • 함수형 인터페이스 패키지 낱개낱개로 사용하지 않는다.
    • 매개변수 타입으로 넣는다.
      1.2의 3번 예제같이 사용한다는 뜻
  • 현재 method를 보여줄 때 선언부만 작성되어 있지만 실제로는 구현부도 이미 구현된 상태이다.
  • 우리는 method 선언부를 보고 상황에 맞게 사용하면 되는 것이다.
  • 실제 사용하는 것을 보면 따로 타입을 지정하는 과정이 없는데 이는 Class type의 지네릭스로 부터 이미 타입을 받았기 때문
인터페이스 메서드 설명
Collection boolean removeIf(Predicate<E> filter) 조건에 맞는 요소 삭제
List void replaceAll(UnaryOperator<E> operator) 모든 요소를 변환하여 대체
Iterable void forEach(Consumer<T> action) 모든 요소에 작업 action을 수행
Map V compute(K key, BiFunction<K, V, V> f) 지정된 키의 값에 작업 f 수행
V computeIfAbsent(K key, Function<K,V> f) 키가 없으면 작업 f 수행후 추가
V computePresent(K key, BiFunction<K, V, V> f) 지정된 키의 값에 작업 f 수행
V merge(K key, V value, BiFunction<V, V, V> f) 모든 요소에 병합작업 f 수행
void forEach(BiConsumer<K, V> action) 모든 요소에 작업 action 수행
void replaceAll(BiFunction<K, V, V> f ) 모든 요소에 치환작업 f 수행

 

2.3. 메서드 참조

  • 조건: 하나의 메서드만 호출하는 람다식 -> 거의 대부분
  • 목적: 그냥 더 줄이고 싶은 것뿐 연연하지 말기

 

  • 아래와 같이 변경 가능
    1. 클래스이름::메서드이름
    2. 참조변수::메서드이름
// 원래 람다식
Consumer<Integer> c = k -> System.out.println(k);
Function<String, Integer>f = s->Integer.parseInt(s);

// 메서드 참조로 변경
Consumer<Integer> c = System.out::println;
Function<String, Integer>f = Integer::parseInt;

// 생성자 경우
Supplier<MyClass> s = () -> new MyClass();
Supplier<MyClass> s = MyClass:new;

 

 

 

3. Stream

 

  • 데이터를 다룰 때 컬렉션, 배열에 담고 원하는 결과를 출력하기 위해 for문과 Iterator를 이용해서 코드를 작성했다.
  • 해당 방식의 문제점
    1. 코드의 가독성이 떨어진다.
    2. 재사용성도 떨어진다.
    3. 데이터 소스마다 다른 방식으로 다뤄야 한다.
      • List 정렬에서는 Collections.sort() 사용
      • array 정렬에서는 Arrays.sort()를 사용
  • stream의 장점
    • 배열, 컬렉션, 파일에 저장된 데이터 모두 같은 방식으로 다루기 가능

 

  • stream loadMap
  • (컬렉션, list, set, map, array, 등등 data 소스들) -> Stream type으로 만들기 -> 중간 연산(0~n 번 수행) -> 최종연산(1번만 수행) -> 결과 출력
  • stream.distinct(). sorted(). forEach(System.out::print)
    • stream: 스트림 타입
    • distinct~sorted: 중간 연산
    • forEach(): 최종연산

 

  • Stream 특징
    1. 스트림은 데이터 소스를 변경하지 않는다.

    2. 스트림은 일회용이다.

    3. 스트림은 작업을 내부 반복으로 처리 : 반복문을 메서드 내부에 숨겼음

    4. 지연된 연산: 한번 중간연산부터 최종연산까지 뭐가 있는지 싹 훑어봄

    5. Stream <Integer>와 IntStream
      • 기본형으로 다루는 스트림과 IntStream, LongStream, DoubleStream이 존재
      •  IntStream, LongStream, DoubleStream: 추가 메서드 제공
    6. 병렬 스트림: 빅 데이터, 멀티 쓰레드에서 사용

 

3.1 스트림 만들기

컬렉션, 배열, 임의의 수, 특정범위의 정수, 람다식, 파일과 비어있는 스트림

 

3.1.1 컬렉션 -> 스트림

  • Collection interface: stream() 정의
// 정의
Stream<E> stream();
// 사용 예시

List<Integer> list = new ArrayList();
// list를 Stream으로 변경
Stream<Integer> intStream = list.stream(); // stream은 한번만 사용 가능

intStream.forEach(System.out::println); // stream 한번 썻으니깐 intStream 재사용 불가
intStream.forEach(System.out::println); // error

 

3.1.2 배열 -> 스트림

  • Stream static method 정의 : of()
  • Arrays static method에 정의: stream()
  • 기본형 배열을 소스로 하는 스트림: of()
  • Arrays static method에 정의: stream(int []) ->IntStream 반환
// 정의
Stream<T> Stream.of(T... values)
Stream<T> Stream.of(T[])

Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)

IntStream IntStream.of(int... values)
IntStream Stream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)

 

// stream 변환
Stream<String> strStream = Stream.of("a","b","c");
Stream<String> strStream = Stream.of(new String[]{"a","b","c"});
Stream<String> strStream = Arrays.stream(new String[]{"a","b","c"});
Stream<String> strStream = Arrays.stream(new String[]{"a","b","c"}, 0, 2);

 

 

3.1.3 임의의 수 -> 스트림

  • Random class의 인스턴스 method 중 IntStream, LongStream, DoubleSteram 반환하는 메서드
  • 해당 타입의 난수들로 이루어진 스트림을 반환
    • 크기가 정해져 있지 않는 무한 스트림이다.
    • 매개변수를 사용하지 않을 때 limit() 이용 
IntStream ints();
IntStream ints(long streamSize); // 매개변수로 스트림 크기를 제한

LongStream longs();
LongStream longs(long streamSize);

DoubleStream doubles();
DoubleStream doubles(long streamSize);

// 난수 범위
IntStream ints(int begin, int end); // 무한 stream
IntStream ints(long streamSize, int begin, int end); // 유한 stream

LongStream longs(long begin, long end);
LongStream longs(long streamSize, long begin, long end);


DoubleStream doubles(double begin, double end);
DoubleStream doubles(long streamSize, double begin, double end);

 

// 사용예제
IntStream intStream = new Random().ints();
intStream.limit(5).forEach(System.out::println); // 개수 5개로 제한

 

3.1.4 특정범위 정수 -> 스트림

IntStream IntStream.range(int begin, int end) // end 미포함
IntStream IntStream.rangeClosed(int begin, int end) // end 포함

 

// 사용
IntStream intStream = IntStream.range(1,4); //1,2,3
IntStream intStream = IntStream.rangeClosed(1,4); //1,2,3,4

 

 

 

3.1.5 람다식 iterate(), generate()

  • iterate()와 generate()는 람다식을 매개변수로 받아서 무한 스트림 생성
  • 기본형 스트림 타입 사용 불가 ex) IntStream...
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)
//사용
Stream<Integer> iterate = Stream.iterate(0, n -> n + 2);
Stream<Double> generate = Stream.generate(Math::random);

 

3.1.6 파일, 비어있는 것 -> 스트림

  • 자바의 file pacakage의 메서들 중 list()라는 것이 있는데 dir의 파일 목록을 소스로 하는 stream을 생성해서 반환한다.
Stream<Path> Files.list(Path dir)
  • 요소가 하나도 없는 비어있는 스트림을 생성 가능
// 빈 스트림
Stream emptyStream = Stream.empty();

 

 

4. 스트림 중간 연산

 

4.1. 스트림 중간 연산 메서드

Stream<T> distinct() // 중복을 제거
Stream<T> filter(Predicate<T> predicate) // 조건 false인 요소 제외
Stream<T> limit(long maxSize) // 스트림의 일부를 잘라낸다.
Stream<T> skip(long n) // 스트림의 일부를 건너띈다. 요소 인덱스 시작은 1부터 시작
Stream<T> peek(Consumer<T> action) // 스트림의 요소에 작업수행, print로 중간 점검
Stream<T> sorted() // 스트림의 요소 정렬
Stream<T> sorted(Comparator<T> comparator) // 스트림 요소 정렬

// 중요
Stream<R>    map(Function<T,R> mapper) // T type이 뭐이든 상관없이 map method 사용시 Stream<R> type으로 반환
DoubleStream mapToDouble(ToDoubleFunction<T> mapper) // 같은 원리인데 예제를 찾아봐야 할듯
IntStream    mapToInt(ToIntFunction<T> mapper)
LongStream   mapToLong(ToLongFunction<T> mapper)

// map과 동일한데 배열형태의 스트림의 내부 스트림을 완전히 빼버림
Stream<R>    flatMap(Function<T,R> mapper) 
DoubleStream flatMapToDouble(ToDoubleFunction<T> mapper)
IntStream    flatMapToInt(ToIntFunction<T> mapper)
LongStream   flatMapToLong(ToLongFunction<T> mapper)

 

  • sorted() - 문자열 정렬 
strStream : 문자열 스트림

// 기본정렬
strStream.sorted()
strStream.sorted(Comparator.naturalOrder())
strStream.sorted((s1,s2)->s1.compareTo(s2))
strStream.sorted(String::compareTo)

// 기본정렬 역순
strStream.sorted(Comparator.reverseOrder())
strStream.sorted(Comparator.<String>naturalOrder().reversed())

// 대소문자 구분안함
strStream.sorted(String.CASE_INSENSITIVE_ORDER)
strStream.sorted(String.CASE_INSENSITIVE_ORDER.reversed())

// 동일문자 길이순 정렬 (오름차순)
strStream.sorted(Comparator.comparing(String::length))
strStream.sorted(Comparator.comparingInt(String::length))

// 동일문자 길이순 역순 정렬 (내림차순)
strStream.sorted(Comparator.comparing(String::length).reversed)

 

4.2. Comparator의 메서드

  1. Comparator 기본 개념
    • 함수형 인터페이스
    • Comparator를 이용한 정렬방식을 이용하고 싶으면 Comparator 인터페이스를 익명객체로 구현하든 class에 구현 필요
    • java8 추가
      • static 메서드와 default 메서드 추가 - 모두Comparator <T> 반환
      • 의미: static 이용해서 Comparator 구현체 생성이 가능, default를 이용해서 새로운 Comparator 구현체 생성가능
  2. comparing(), thenComparing()
    • public static comparing(params)
      • 인터페이스명을 참조변수로 잡고 바로 사용가능
    • default thenComparing(params)
      • comparing()으로 Comparator 구현 이후 사용가능
      • default는 인스턴스 메서드이다.

  3. comparing 소스코드
    • comparing 내부 매개변수 종류가 2가지로 오버로딩
    • 정렬하는 class에 Comparable 없어도 상관없음
// comparing 소스코드

// Function만 있는 경우
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
{
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

// Function과 Comparator 존재하는 경우
public static <T, U> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor,
        Comparator<? super U> keyComparator)
{
    Objects.requireNonNull(keyExtractor);
    Objects.requireNonNull(keyComparator);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
                                          keyExtractor.apply(c2));
}

// 비교대상이 기본형인 경우
comparingInt(ToIntFunction<T> keyExtractor)
comparingLong(ToLongFunction<T> keyExtractor)
comparingDouble(ToDoubleFunction<T> keyExtractor)

 

  • sorted(Comparator comparator) 매개변수 주입 방법
    1. Comparator 직접 만들기
    2. comparing과 themComparing 이용
    3. 특정 조건
      1. 예시의 naturalOrder() 사용하고 싶으면 studentStream의 내부의 실제 class에 Comparable이 구현 필요
      2. Comparable 구현하기 싫으면 익명객체로 직접 정렬조건 작성하면 된다.
  •  원리만 이 정도로 알면 정렬 쪼개고 쪼개고 하는 복잡한 것은 구글링 하면 금방 이해될 것
studentStream.sorted
       (
        Comparator.comparing(Student::getBan).thenComparing(Comparator.naturalOrder())
        ).forEach(System.out::println);

 

 

 

4.3.map, flatMap

  • map()
    • 스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아낸다.
    • 필드를 특정 형태로 변환가능하다.
  • 주의
    • stream의 중간연산이므로 현재 type이 stream인데 내부의 실제 class type을 다른 타입으로 변경을 한다.
    • map과 flatMap에서 stream type은 껍데기일 뿐이라 생각하고 실제 작동은 내부의 class type과 작동한다고 이해해야 한다.
  • 예제
File[] fileArr = {new File("a.java"), new File("b.java"), new File("c.java"), new File("d"),new File("e")};
    Stream<File> fileStream = Stream.of(fileArr);
    // fileStream.forEach(s-> System.out.println(s.exists())); // 현재 type이 File이여서 관련 method 사용되는지 확인 용도

    // map의 1번째 기능 -> 내부 타입 변환
    Stream<String> stringStream = fileStream.map(File::getName);
    // Stream<String> stringStream 인 이유
    // File class - getName() 반환타입: String
    // map: stream 내부의 class type을 변경하는 것.-> Stream 내부에서 File->String으로 변경됨.
    // Stream 내부에 존재하는 것은 변화 없음

    // stringStream.forEach(s-> System.out.println(s.exists())); // 에러발생, String이기 때문
     stringStream.forEach(System.out::print);

    fileStream = Stream.of(fileArr); // stream은 1회성이라 다시 생성

    fileStream.map(File::getName)
            .filter(s -> s.indexOf(".") != -1)
            // map의 2번째 기능 -> 요소 내부 변환
            .map(s -> s.substring(s.indexOf('.')+1))
            .map(String::toUpperCase)
            .forEach(System.out::print);


}

 

  • flatMap()
    • map에서 해결 못하는 Stream 벗겨내기 기능이 존재
      • map의 경우 stream 내부의 class type을 변경( == 내부에서 작동).
      • 그래서 원래 둘러싸인 Stream은 그대로이다.
      • 내부가 그럼 왜 Stream <String> 이 된 것인가?
        • Arrays::stream은 배열을 stream 타입으로 변경하는 것이기 때문에 string 타입의 배열을 stream으로 변경
        • 배열인 경우가 대표적인 것
      • 다른 상황은 없나?
        •  만약 map 사용 시 stream 내부에 stream이나 지저분하게 상자 내부에 상자 형식으로 되면 flatmap 사용
    • flatMap은 stream을 아예 벗기고 내부가 각각의 상자로 분리되어 있어도 각각의 type을 변경한 후 마지막으로 하나의 상자에 담기 때문
Stream<String[]> stream = Stream.of(new String[]{"a", "b"},new String[]{"1", "2"},new String[]{"ㅁ", "ㅠ"} );
Stream<Stream<String>> streamStream = stream.map(Arrays::stream);

stream = Stream.of(new String[]{"a", "b"},new String[]{"1", "2"},new String[]{"ㅁ", "ㅠ"} );
Stream<String> stringStream = stream.flatMap(Arrays::stream);

 

5. Optional <T> 객체 생성하기 -값 가져오기

  • T 타입의 객체를 감싸는 래퍼 클래스
  • Optional 내부에는 모든 타입의 객체를 담을 수 있다.
  • null도 담을 수 있다. -> 디자인 패턴 중 future pattern

 

  • Optional 객체 생성
    • of() // null 아닌 참조변수 넣기
    • ofNullable() // null까지 다룰 수 있다.
      • Optioinal.<T> empty(); // 의도적으로 null 객체 생성 때 사용
Optional<String> optVal = Optional.of("abc");

Optional<String> optVal = Optional.of(null); // NullPointException 발생
Optional<String> optVal = Optional.ofNullable("abc"); // ok

Optional<String> optionalInteger = null; // 가능은 하지만 권장 안함
Optional<String> optionalInteger1 = Optional.<String>empty(); // 빈객체로 초기화

 

  • Optional 객체 값 가져오기
    1. get() // optional에 저장된 객체 반환. nullException 발생
    2. orElse("null일 때 나오는 값") // get과 동일하지만 null 다룰 수 있다.
      1. orElseGet(Supplier <? extends T> other) // null 대체 값을 람다식으로 지정 가능
      2. orElseThrow(Supplier <? extends X> exceptionSupplier) // null일 때 지정된 예외 발생
    3. isPresent() // 지정된 객체 null이면 false, 아니면 true 반환
    4. ifPresent() // null 아닐 때만 작동

 

  • OptionalInt, Long, Double
    1. 기본형 Stream의 최종 연산은 기본형 Optional을 반환한다.
    2. 값을 반환하는 메서드(get())를 제외하고 나머지 메서드는 동일
    3. 기본형 Optional에 제공되는 method 존재
// 값을 반환하는 메서드
OptionalInt int getAsInt();
OptionalLong long getAsLong();
OptionalDouble double getAsDouble();

// 추가 메서드 - OptionalInt 타입으로 반환하는 최종 연산 method
findAny()
findFirst()
reduce(IntBinaryOperator op)
max()
min()
average()

// 사용 예
OptionalInt opt = OptionalInt.of(0);
OptionalInt opt = OptionalInt.ofNullable(0);
OptionalInt opt = OptionalInt.empty();

 

6. 스트림 최종 연산

6.1. 스트림 최종 연산 메서드

void forEach(Consumer<? super T> action) // 각 요소에 지정된 작업 수행
void forEachOrdered(Consumer<? super T> action) // 병렬스트림 작업시 순서유지

long count() // 스트림의 요소의 개수 반환

Optional<T> max(Comparator<? super T> comparator) // 스트림 최대값 반환
Optional<T> min(Comparator<? super T> comparator) // 스트림 최소값 반환

Optional<T> findAny() // 아무거나 하나 반환
Optional<T> findFirst() // 첫 번째 요소 반환

boolean allMatch(Predicate<T> p) // 모두 만족하면 true
boolean anyMatch(Predicate<T> p) // 하나라도 만족하면 true
boolean noneMatch(Predicate<T> p) // 모두 만족하지 않으면 true

Object[] toArray() // 객체에 담아서 배열로 반환
A[] toArray(IntFunction<A[]> generator) // 스트림 모든 요소를 배열로 반환

// 핵심 reduce, collect

// reduce : 스트림의 요소를 하나씩 줄여가면서 계산
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<T> combiner) // combiner는 병렬처리된 결과 합하기 위한 조건

// collect : 스트림의 요소를 수집 -> 스트림으로 낸 결과를 따로 컬렉션에 저장
R collect(Collector<T, A, R> collector)
R collect(Supplier<R> supplier, BiConsumer<R, T> accumulator, BiConsumer<R, R> combiner)

 

  • reduce()
    • identity: stream 요소의 몇 번째부터 연산을 시작할지 선택
    • identity 없는 경우: 첫 번째 요소부터 연산
    • 연산할 요소들이 없으면 초기값이 반환 -> Optional이 반환되지 않고 해당 type이 반환된다.
    • combiner: 병렬처리된 stream의 연산 결과를 합하는 것
  • collect()
    • 그룹별 리듀싱  <-> reduce(): 전체 리듀싱
    • 스트림의 요소를 수집하는 최종연산으로 스트림을 어떻게 수집할 것인가에 대한 방법 정의
    • 스트림 수집 방법 = Collector 이용 : collect에 필요한 method 정의한 인터페이스
    • Collector <T, A, R>
      • T 요소를 A에 누적한 다음, 결과를 R type으로 변환 후 반환한다.
      • 항상 3가지를 다 사용할 필요 없다.
      • method 마다 받는 지네릭스 type이 정해져 있다.
      • 그래서 타입만 반환하는 경우, 연산 후 그대로 반환하는 경우, 연산 후 타입 변경해서 반환하는 경우 다양하게 존재한다.
    • Collctors: Collector를 미리 구현한 클래스

 

6.1.2 collector의 메서드

  1. 변환 - 컬렉션, 배열
  2. 문자열 결합
  3. 통계 
  4. 리듀싱
  5. 그룹화와 분할
    1. 분할 - 2 분할
    2. 그룹화 - n 분할

 

1. 컬렉션, 배열 변환

  • toList(), toSet(), toMap(), toCollections()
// collection 변환
toList(), toSet(), toMap(), toCollection()

// 사용 예
List<String> namesList = stuStream.map(Student::getName)
                                 .collect(Collectors.toList())
Set<String> namesSet = stuStream.map(Student::getName)
                                 .collect(Collectors.toSet())
ArrayList<String> arrayList = namesList.stream()
                                 .collect(Collectors.toCollection(ArrayList::new))

Map<String, Person> map = personStream
                                 .collect(Collectors.toMap(p->p.getRegId(), p->p))
  • toArray()
// 배열로 변환
Student[] stuNmaes = studentStream.toArray(Student[]::new);
Object[]  stuNames = studentStream.toArray(); // 자동 형변환이 안된다.

 

2. 문자열 결합

  • joining(): 모든 요소를 하나의 문자열로 연결해서 반환
// joining(): 모든 요소를 하나의 문자열로 연결해서 반환
String studentNames = stuStream.map(Student::getName).collect(joining());
String studentNames = stuStream.map(Student::getName).collect(joining(","));
String studentNames = stuStream.map(Student::getName).collect(joining("," , "[" , "]"));

// map 미사용시 결과: stream 각각 요소의 toString()을 호출한 결과를 결합
String studentNames = stuStream.collect(joining(","));

 


예제 작성하기에 너무 힘들어서 핵심 원리만 작성, 작성방법 외우는 것보다 사용해야 될 경우 구글링 하는 게 빠를 듯

 

 

 

통계&리듀싱과 그룹화&분할

  • 통계, 리듀싱 메서드들은 최종연산 메서드에 다 존재하는 것
  • 그룹화, 분할을 같이 사용 안 할 시 (=단일로 사용 시) 최종연산 메서드와 동일한 역할을 수행
  • 그룹화와 분할의 메서드와 함께 사용 시 통계, 리듀싱 메서드들은 분할된 그룹별 연산 결과를 출력해 준다.
    • 예 - 학생 클래스를 담은 list가 존재 시 성별로 분할(=그룹화) 한 요소들의 남자수, 여자수, 여자 1등, 남자 1등 값을 출력
  • counting(), summingInt(), maxBy(), minBy()

 

3. 통계

counting(), summingInt(), maxBy(Comparator c), minBy(Comparator c)

 

4. 리듀싱

앞선 reducing()과 동일

 

5. 2 분할

  1. 분할 시 Map으로 반환
    • key: boolean valeu: class type
  2. partitioningBy(Predicate predicate) -> 2 분할
  3. partitioningBy(Predicate predicate, Collector downstream)
    • 2 분할 후, 통계나 리듀싱 method 사용해서 각 분할들에게 해당 결과 출력
    • 2 분할 후, 내부에서 또 한 번 2 분할

 

6. n분할

  1. 분할 시 Map으로 반환
    • key: 분할하려는 기준 value: 기준에 부합한 모든 것들
  2. groupingBy(Function classifier) -> n분할
  3. groupingBy(Function classifier, Collector downstream) 
    • n분할 후, 통계나 리듀싱 method 사용해서 각 분할들에게 해당 결과 출력
    • n분할 후, 내부에서 또 한 번 2 분할 혹은 n분할
  4. groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
    1. mapFactory로 mapping() 메서드 사용
    2. mapping()의 매개변수로 함수형 인터페이스가 존재하므로 람다식을 통한 내가 원하는 분할 가능
    3. 예 - 1~10점 f , 10~30점 d , 30~60점 c, 60~80점 b, 80~100점 A 이런 식으로 가능해짐

 

 

이전 발행글 : 2023.04.07 - [java/java 기본] - 13. 쓰레드

 

다음 발행글 : 2023.03.26 - [java/java 기본] - 15. 입출력