모르는게 많은 개발자

[Java8] Stream, Optional 개념/예제 본문

자바

[Java8] Stream, Optional 개념/예제

Awdsd 2020. 12. 2. 23:44
반응형

저번 포스팅인 함수형 인터페이스, 람다 표현식 개념/예제에 이어서 Stream, Optional을 정리해보려 한다.


1. Stream

자바8 이전에서는 List에 담긴 데이터를 처리하려면 foreach, for를 이용해 순회하며 데이터를 처리했다. 하지만 이렇게 작성된 코드는 길이도 길고 가독성이 떨어졌다. 이러한 문제점을 해결하기 위해 stream기능이 추가됐다.

 

Stream은 Array, Collections와 같이 연속된 형태의 객체다. 그리고 2개의 연산 과정을 거쳐 데이터를 가공할 수 있다.

  • 생성 연산 : Stream객체 생성
  • 중개 연산 (filter, map) : List에 들어있는 데이터를 조건에 맞게 가공(ex: 특정 String만  골라내기)하는 연산들을 의미
    Stream을 반환 즉, 중개 연산은 여러개 사용 가능
  • 최종 연산 (count, collect) : 중개연산에서 가공된 데이터를 사용하는 연산(리스트 반환, 배열 반환, 개수 반환 등)을 의미
    마지막에 딱 한번 사용 가능
list.stream().filter().map().count();

 

Stream의 특징은 다음과 같다.

  • Stream은 원본 데이터를 변경하지 않음
  • 데이터를 담고 있는 저장소(컬렉션)이 아님
  • Stream은 재사용이 가능한 컬렉션과는 달리 한 번만 사용할 수 있음
  • 중개 오퍼레이션은 근본적으로 lazy함
  • 손쉽게 병렬처리 가능(parellel)

1.1 생성 연산(Create Operations)

먼저 Stream을 생성하는 방법을 보자.

배열
public static void main(String[] args) 
    String[] str = new String[] {"a", "b", "c"};
    Stream < String > s1 = Arrays.stream(str);

    List < String > list1 = new ArrayList < > ();
    list1.add("a");
    list1.add("b");
    list1.add("c");
    list1.add("d");
    Stream < String > s2 = list1.stream();
Builder
public static void main(String[] args) {
    Stream < String > builder = Stream. < String > builder()
        .add("a")
        .add("b")
        .build();
}

 

1.2 중개 연산(intermediate Operations)

Filter

원하는 요소를 추출하는 연산이다. 인자로 Predicate를 받아 반환값 Boolean이 True인 데이터만 모은다.

Stream<T> filter(Predicate<? super T> predicate);

아래 예제의 경우 각 요소에서 "Test"인 것을 찾아 List로 반환하는 예제이다.

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("test");
    list.add("b");
    list.add("c");
    list.add("d");
    //list의 각 요소들중 'test'인값만 골라 List로 반환
    List < String > filter = list.stream()
              .filter(s - > s.equals("test")).collect(Collectors.toList());
    //List 출력
    filter.forEach(System.out::println);
}
결과
test

 

Map
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Map은 Stream의 각 요소들을 가공한다. 각 요소 T라는 인자의 값을 활용해 R타입의 값을 반환한다.

public class Test {
    private String testStr;
    public Test() {
        this.testStr = "new no has param test";
    }
    public Test(String str) {
        this.testStr = str;
    }
    public String getTestStr() {
        return testStr;
    }
}

아래의 예제는 Test라는 객체가 든 list에서 map을 통해 tesetStr을 뽑아 List를 만드는 코드이다.

testStr은 String형태이므로 String List가 추출된다.

public static void main(String[] args) {
    List < Test > list = new ArrayList < > ();
    list.add(new Test("a"));
    list.add(new Test("b"));
    list.add(new Test("c"));
    list.add(new Test("d"));

    List < String > test = list.stream()
             .map(Test::getTestStr)
             .collect(Collectors.toList());
    test.forEach(System.out::println);
}
결과
a
b
c
d

 

FlatMap
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

flatMap은 중첩 구조를 한 단계 제거하고 하나의 Collection으로 풀어주는 역할을 한다.

아래 코드의 경우 List안에 List를 넣은 이중 list를 flatMap을 사용해 하나의 list로 풀어 처리하는 코드이다.

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");

    List < List < String >> flat = new ArrayList < > ();
    flat.add(list);
    flat.add(list);
    flat.add(list);

    flat.stream()
        .flatMap(strings - > strings.stream())
        .filter(s - > s.equals("a"))
        .forEach(System.out::println);
    //flatMap에 메소드 레퍼런스 사용
    flat.stream()
        .flatMap(Collection::stream)
        .filter(s - > s.equals("a"))
        .forEach(System.out::println);
}

결과
a
a
a
a
a
a

 

Sorted

Stream요소를 정렬하는 연산이다.

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("b");
    list.add("a");
    list.add("d");
    list.add("c");

    list.stream().sorted().forEach(System.out::println);
    list.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
}
결과
a
b
c
d
d
c
b
a

 

Distinct

중복 값 제거

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("a");
    list.add("a");
    list.add("b");
    list.add("b");

    list.stream().distinct().forEach(System.out::println);
}

결과
a
b

 

Limit

limit의 인자의 값만큼만 요소를 가져옴

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");

    list.stream().limit(2).forEach(System.out::println);
}
결과
a
b

 

Skip

limit와 반대로 인자의 값만큼 건너뛰고 나머지 요소를 가져옴

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");

    list.stream().skip(2).forEach(System.out::println);
}
결과
c
d

 

1.3  최종 연산(Terminal Operations)

Count

중개 오퍼레이션을 통해 가공된 값들의 개수를 출력한다.

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");
    //요소중 "b"인 것의 개수
    System.out.println(list.stream().filter(s - > s.equals("b")).count());
}
출력
1

 

AnyMatch, AllMatch, NoneMatch

anyMatch()는 각 요소들 중에 특정 조건을 만족하는 요소가 하나라도 있으면 True반환

allMatch()는 모든 요소가 특정 조건을 만족해야 True반환

noneMatch()는 특정 조건을 하나라도 만족하면 False반환(모든 요소가 만족하지 않아야 True반환)

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");
    //요소중 "a"가 있으므로 true
    System.out.println(list.stream().anyMatch(s - > s.equals("a")));
    //요소중 "not"이 없으므로 false
    System.out.println(list.stream().anyMatch(s - > s.equals("not")));
    //요소들 모두 "z"가 아니므로 false
    System.out.println(list.stream().allMatch(s - > s.equals("z")));
    //요소중 "not"이 하나도 없으므로 true
    System.out.println(list.stream().noneMatch(s - > s.equals("not")));
}

 

 

Reduce

모든 요소들의 데이터를 변환하지 않고 연산을 수행해 결과를 낼 수 있다.

Optional<T> reduce(BinaryOperator<T> accumulator);

T reduce(T identity, BinaryOperator<T> accumulator);
public static void main(String[] args) {
    List < Integer > list2 = new ArrayList < > ();
    list2.add(1);
    list2.add(2);
    list2.add(3);
    list2.add(4);
    Integer test = list2.stream().reduce(4, (integer, integer2) - > integer + integer2);
    System.out.println(test);
    System.out.println(list2.stream().reduce((integer, integer2) - > integer + integer2).get());
}

reduce를 사용하면 인자가 integer, integer2인 걸 볼 수 있는데 이것은 요소를 거치면서 integer가 전에 수행된 연산의 값을 가지게 된다. 위 코드로 봤을 때는 처음 연산이 integer + integer2라면 그 다음의 인자는 reduce(지금까지의 총 합, 다음 인자)이 될 것이다.

reduce의 첫번째 인자에 초기값을 선언하면 초기값부터 연산이 시작된다.

 

collect(Collectors.toList())

요소들을 List로 반환해준다.

public static void main(String[] args) {
    List < String > list3 = new ArrayList < > ();
    list3.add("aa");
    list3.add("bbb");
    list3.add("cc");
    list3.add("dd");
    //요소 길이가 2인 요소들의 List 반환
    List < String > collect = list3.stream().filter(s - > s.length() == 2).collect(Collectors.toList());
    collect.forEach(System.out::println);
}

 

collect(Collectors.joining())

스트림 작업 결과를 하나의 String으로 연결해 반환. 인자에는 세개가 있다.

인자

  • delimiter: 각 요소 중간에 들어가는 구분자
  • prefix: 이어붙인 결과 맨 앞에 붙는 문자
  • suffix: 이어붙인 결과 맨 끝에 붙는 문자
public static Collector < CharSequence, ? , String > joining(CharSequence delimiter,
    CharSequence prefix,
    CharSequence suffix) {
    return new CollectorImpl < > (
        () - > new StringJoiner(delimiter, prefix, suffix),
        StringJoiner::add, StringJoiner::merge,
        StringJoiner::toString, CH_NOID);
}
public static void main(String[] args) {
    List < String > list3 = new ArrayList < > ();
    list3.add("aa");
    list3.add("bbb");
    list3.add("cc");
    list3.add("dd");
    System.out.println(list3.stream().collect(Collectors.joining("-", "start-", "-end")));
}
결과
start-aa-bbb-cc-dd-end

 

collect(Collectors.partitioningBy())

요소들을 Predicate로 true, false 그룹으로 나누어 Map<Boolean, List<T>>형태로 반

public static <T>
    Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
        return partitioningBy(predicate, toList());
    }
public static void main(String[] args) {
    List < String > list3 = new ArrayList < > ();
    list3.add("aa");
    list3.add("bbb");
    list3.add("cc");
    list3.add("dd");
    Map < Boolean, List < String >> map = list3.stream().collect(Collectors.partitioningBy(s - > s.length() == 3));
    //조건이 맞는 경우 key가 true인 map에 쌓인다
    map.get(true).forEach(System.out::println);
    map.get(false).forEach(System.out::println);
}

2. Optional

Optional은 객체를 포장해 주는 래퍼 클래스이다. 가장 큰 특징은 NullPointerException 예외처리를 간단하게 처할 수 있다.

 

Optional 객체 생성

Optional 객체 생성방법은 Optional.of, Optional.ofNullable(), stream()의 종료 연산으로 생성할 수 있다.

Optional.of()를 사용할 때는 null이 들어갈 수 없다.

어쩌다 null이 들어갈 수도 있다면 Optional.ofNullable()을 사용한다.

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("c");
    list.add("b");
    //stream()의 종료 연산
    Optional < String > op1 = list.stream().findFirst();
    
    //of()
    Optional < String > op2 = Optional.of("a");
    //Optional<String> op2 = Optional.of(null);   //에러

    //ofNullable()
    Optional < String > op3 = Optional.ofNullable(null);
}

 

Get

get()은 Optional안의 값을 반환한다. 하지만 사용을 권장하지 않는다. 왜냐하면 Optional이 null일 경우 런타임 에러가 나기 때문이다. 아래에서 볼 다른 method들은 예외처리도 함께 가능하기에 get()은 되도록 사용하지 않는다.

public static void main(String[] args) {
    List < String > list = new ArrayList < > ();
    list.add("c");
    list.add("b");
    Optional < String > op1 = list.stream().findFirst();
    System.out.println(op1.get());

    Optional < String > op3 = Optional.ofNullable(null);
    //op3는 null이므로 NoSuchElementException 발생
    System.out.println(op3.get());
}
결과
c
NoSuchElementException

 

ifPresent(Consumer)

ifPresent()는 만약 Optional객체가 null아니면 Consumer가 실행되고 null이면 실행되지 않는다.

아래 코드의 경우 op1은 test가 출력되고 op2는 아무것도 실행되지 않는다.

public static void main(String[] args) {
    Optional < String > op1 = Optional.ofNullable("test");
    op1.ifPresent(s - > System.out.println(s));
    Optional < String > op2 = Optional.ofNullable(null);
    op2.ifPresent(s - > System.out.println("s"));
}
결과
test

 

orElse(T)

orElse()는 Optional객체가 null이 아니면 값을 가져오고 null이면 Optional의 인자 타입이랑 같은 타입을 반환하게 할 수 있다.

public static void main(String[] args) {
    Optional < String > op1 = Optional.ofNullable("test");
    //Null이 아니므로 test출력
    System.out.println(op1.orElse("orElse"));
    
    Optional < String > op2 = Optional.ofNullable(null);
    //Null이므로 else출력
    System.out.println(op2.orElse("orElse"));
}
결과
test
orElse

 

orElseGet(Supplier)

orElseGet는 orElse와 비슷하지만 인자로 Supplier를 받아 람다식으로 타입 인자를 반환할 수 있다.

public static void main(String[] args) {
    Optional < String > op1 = Optional.ofNullable(null);
    //Null이므로 else get을 출력
    System.out.println(op1.orElseGet(() - > {
        return "elseGet";
    }));
}
결과
elseGet

 

orElseThrow()

optional이 null이면 NoSuchElementException을 던진다.

public static void main(String[] args) {
    Optional < String > op1 = Optional.ofNullable(null);
    op1.orElseThrow();
}
결과
NoSuchElementException

 

Filter

optional에도 filter()가 존재하는데 filter의 predicate가 true이면 그대로 타입 객체를 포함한 optional이 반환되고 false이면 null optional이 반환된다.

public static void main(String[] args) {
    Optional < String > op2 = Optional.ofNullable("a");
    //s.equals("a")는 true이므로 "a"를 포함한 optional객체 반환
    System.out.println(op2.filter(s - > s.equals("a")).get());

    Optional < String > op1 = Optional.ofNullable("a");
    //false이므로 null Optional객체 반환
    System.out.println(op1.filter(s - > s.equals("b")).get());
}
결과
a
NoSuchElementException

 

반응형
Comments