Stream API

Java Stream API

Lambda Expression

기존에 간편하게 사용하던 람다식을 보면 다음과 같다.

람다식을 사용하지 않은 경우 스레드 구동 코드

Thread t = new Thread(new Runnable(){ 
        @Override public void run(){ 
            System.out.println("hello"); 
        } 
    }); 
t.start();

람다식을 사용한 경우 스레드 구동 코드

Thread t = new Thread(()->{ 
    System.out.println("hello"); 
}); 
t.start();

java.lang.Runnable 인터페이스는 메소드가 단 한개뿐인 인터페이스이므로 자리만 맞다면 클래스 이름이나, 메소드 이름, 반환형, 매개변수 타입 등은 굳이 적어주지 않아도 컴퓨터가 알 수 있는 정보들이 된다. 물론, 메소드가 단 한개 일 경우에만 해당되며 두 개 이상일 경우에는 성립하지 않는다.

java.lang.Runnable 인터페이스 형태

public interface Runnable { 
    public abstract void run(); 
}

람다식을 이용하면 다음과 같은 효과를 얻을 수 있다.

  • 짧고 간결한 프로그래밍 코드

  • 병렬 프로그래밍

  • 메소드 레퍼런스

  • 함수형 인터페이스 만들기

함수형 인터페이스는 메소드를 단 한개만 가지고 있어야 하는데, 이를 강제하기 위해 @FunctionalInterface라는 주석을 사용한다. 주석이 붙어있을 경우 메소드 개수가 1개가 아니면 예외가 발생하여 함수형 인터페이스를 쉽게 구현할 수 있게 된다.

@FunctionalInterface 
interface Calculator{ 
    int process(int a, int b); 
}

람다식을 사용하지 않고 Calculator를 이용해 덧셈 계산

Calculator add = new Calculator() { 
    @Override public int process(int a, int b) { 
        return a + b; 
    } 
}; 
int result = add.process(10, 20); 
System.out.println("result = "+result);

람다식을 사용하여 Calculator를 이용해 덧셈 계산

Calculator add = (a, b)->a+b; 
int result = add.process(10, 20); 
System.out.println("result = "+result);

람다식을 사용하는 것이 코드가 더 간편해짐을 확인할 수 있다. 이렇게 직접 만들어 사용해도 좋지만, 자바 8에서 기본적으로 지원해주는 함수형 인터페이스들을 이용하면 다양한 기능들을 쉽게 구현할 수 있다.

자바의 주요 함수형 인터페이스

위의 인터페이스를 이용하여 메소드를 매개변수로 전달할 수 있기 때문에 매우 유용하게 사용할 수 있다.

Consumer 사용 예

Consumer<String> printer = text->System.out.println(text); 
printer.accept("this is consumer");

Predicate의 사용 예

Predicate<String> check = string -> string.isEmpty(); 
System.out.println(check.test(""));//true 
System.out.println(check.test("1"));//false

Supplier의 사용 예

Supplier<Integer> lotto = () -> (int)(Math.random() * 45) + 1; 
System.out.println(lotto.get());

Function의 사용 예

Function<Integer, Integer> square = n -> n*n; 
int result = square.apply(11); 
System.out.println("result = "+result);

메소드 레퍼런스(Method Reference)

앞에에서 살펴봤던 함수형 인터페이스들에 메소드 참조를 설정할 수 있다. 아래 예제를 통해 확인해본다.

Consumer의 메소드 레퍼런스 활용

package lambda; 
import java.util.function.Consumer; 
public class Lambda04 { 
    public static void test(String text) { 
        System.out.println(text); 
    } 
    public static void main(String[] args) { 
        Consumer<String> c = Lambda04::test; 
        c.accept("method reference"); 
    } 
}

main 메소드에서 c라는 Consumer 레퍼런스를 만들어 Lambda04에 존재하는 static 메소드인 test를 직접 참조하도록 설정하고 있다. 이러한 메소드 참조가 가능해지면 복잡한 코드도 메소드만 명시적으로 전달해줄 수 있으므로 코드의 가독성을 높이며 효율도 증대시킬 수 있다는 장점이 생기게 된다. 또한 기존의 자바에서 사용하던 전형적인 콜백 객체 생성방식에서 벗어날 수 있다.

Function 의 메소드 레퍼런스 사용

Function<String, Boolean> check = String::isEmpty; 
System.out.println(check.apply(""));//true 
System.out.println(check.apply(" "));//false

Java 1.8 에서는 이러한 함수형 인터페이스와 람다, 메소드 레퍼런스를 이용하여 Collection을 제어할 수 있는 Stream api를 제공한다

Java Collection

자바 버전별로 List에 간단한 데이터를 넣는 방법은 다음과 같이 변해왔다.

자바 1.7 이전 (java.util.ArrayList)

List<Integer> list = new ArrayList<>(); 
for(int i = 1; i <= 10; i++) { 
    list.add(i); 
} 
System.out.println(list);

list는 확장이 가능하다.

자바 1.8 버전 (java.util.Arrays$ArrayList)

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 
System.out.println(list);

list는 크기가 고정이며 확장이 불가능하다. 물론 1.7 이전의 방식도 사용할 수 있다.

자바 9 버전 (java.util.ImmutableCollections$ListN)

List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 
System.out.println(list);

list는 크기가 고정이며 확장이 불가능하다. 이전 방식 모두 사용할 수 있다. 어느 방식으로 생성해도 stream api를 이용할 수 있으므로 앞으로는 List.of() 를 이용하여 간편하게 데이터를 초기화 하는 코드를 사용하기로 한다.

Java Collection과 Stream API

자바 컬렉션에서 사용하는 Stream API는 배열과 Collection에서 동작하며, .stream() 명령을 이용하여 스트림 참조를 얻는것부터 시작한다.

List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 
System.out.println(list.stream());

형태는 java.util.stream.ReferencePipeline$Head이며, 상태 변경을 위한 각종 명령들을 체이닝 형식으로 사용할 수 있다.

List의 출력 코드(기존 방식)

List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 
for(int i=0; i < list.size(); i++) { 
    System.out.println(list.get(i)); 
}

List의 출력 코드(확장 for 이용)

List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for(Integer n : list){ 
    System.out.println(n); 
}

List의 출력 코드(Stream API 이용)

List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 
list.stream().forEach(d->System.out.println(d));

List의 출력 코드(Stream API + Method Reference)

List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 
list.stream().forEach(System.out::println);

Stream API를 사용할 수록 코드가 조금 더 간결해지는 것을 확인할 수 있다. 이와 같은 현상은 Collection과 관련한 처리가 많아질 수록 더 심화된다.

List에서 짝수만 출력하는 코드(기존 방식)

List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 
for(int i=0; i < list.size(); i++) { 
    if(list.get(i) % 2 == 0) { 
        System.out.println(list.get(i)); 
    } 
}

List에서 짝수만 출력하는 코드(Stream API + Method Reference)

List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 
list.stream()
        .filter(v -> v % 2 == 0) 
        .forEach(System.out::println);

List에서 앞 5개 요소의 합을 구하는 코드(기존 방식)

List<Integer> list = List.of(1, 2 ,3, 4, 5, 6, 7, 8, 9, 10); 
int total = 0; 
for(int i=0; i < list.size(); i++) { 
    if(i < 5) { 
        total += list.get(i); 
    } 
} 
System.out.println("합계 = "+total);

List에서 앞 5개 요소의 합을 구하는 코드(Stream API + Method Reference)

List<Integer> list = List.of(1, 2 ,3, 4, 5, 6, 7, 8, 9, 10); 
int total = list.stream() 
                                .limit(5)//앞에서 5개만 뽑아서 
                                .reduce(0, (a, b) -> a+b);//0을 시작값으로 하여 합산 
System.out.println("합계 = "+total);

Stream API의 명령들

  • forEach

  • count

  • max

  • min

  • findAny

  • findFirst

  • allMatch

  • anyMatch

  • noneMatch

  • toArray

  • reduce

  • collect

  • sort

Stream API 특징

  • 원본을 변경하지 않는다

  • 반복하여 사용이 불가능하다

  • 연산이 최종 시점에 합산되어 처리된다

    • 필요시 병렬 스트림을 사용할 수 있다

간단한 문제

아래 단어들을 List에 저장한 뒤 다음 문제 풀기

  • Java, C, Python, Ruby, Perl, Go, Swift

  • 첫글자가 p인 요소를 대소문자 구분하지 않고 출력

  • 3글자 이상인 요소를 출력

List<String> list = List.of("Java", "C", "Python", "Ruby", "Perl", "Go", "Swift"); 
System.out.println("### 첫글자 p인 요소만 출력 ###"); 
list.stream().map(sub -> sub.toLowerCase()) 
                        .filter(sub -> sub.startsWith("p")) 
                        .forEach(System.out::println); 
System.out.println("### 3글자 이상인 요소만 출력 ###"); 
list.stream().filter(sub -> sub.length() >= 3) 
                        .forEach(System.out::println);

Stream 간의 연결

스트림 연결 스트림을 서로 연결하고 싶은 경우에는 Stream에 있는 concat 메소드를 사용한다

Stream\ concat(Stream\ s1, Stream\ s2)

List<String> list1 = List.of("h", "e", "l", "l", "o"); 
List<String> list2 = List.of("s", "t", "r", "e", "a", "m"); 
Stream<String> stream = Stream.concat(list1.stream(), list2.stream()); 
stream.distinct().forEach(System.out::println);

.distinct()는 중복제거를 수행한다

원시형 스트림

Stream API에서는 원시형 데이터에 대한 스트림도 제공한다.

IntStream을 이용하여 1부터 100까지 담아 출력하는 코드

IntStream stream = IntStream.range(1, 100); 
stream.forEach(System.out::println);

IntStream을 이용하여 1부터 100 사이 난수 100개를 저장하는 코드

Random random = new Random(); 
IntStream stream = random.ints(50/*개*/, 1/*부터*/, 100/*가지*/); 
stream.forEach(System.out::println);

Last updated