[자바] 스트림, 람다는 낯설어서

자바8에 도입된 스트림, 람다에 대해 알아봅시다.

1. 스트림

1) 정의

Oracle - stream 에 따르면

연속된 정보를 순서대로 접근하기 위한 객체입니다.

2) 연속된 정보란

바로 생각나는 것은 배열과 리스트가 있습니다.

그리고, Map, Set과 같은 컬렉션도 해당되는데

그 이유는 컬렉션 인터페이스에 내장된 default 메서드로 구현되어있기 때문입니다.

다만, 배열은 stream 메서드가 없기 때문에 메서드 호출 대신 Arrays.stream 정적 메서드를 호출합니다.

즉 스트림이란, 배열과 컬렉션을 순서대로 접근하기 위한 인터페이스 입니다.

참고

자바 8부터 인터페이스 default 메서드가 추가 되었습니다. 그 이유는 이미 많은 사람들이 리스트, 셋을 사용하고 있는데

컬렉션 인터페이스에 stream을 추가하면 모두 구현해야하고, 그렇지 않으면 에러가 발생하기 때문입니다.

낮은 버전의 호환성을 위해 기본 메서드가 추가되었습니다.

그리고, Collection 인터페이스의 stream은 default 메서드 입니다.

2. 구조

0) 개요

스트림의 연산은 크게 3가지로 구분됩니다.

  • 스트림 생성: 데이터를 스트림 객체로 변환합니다. stream메서드는 Collection 인터페이스에 default 메서드로 선언되어 있습니다.

  • 중개 연산 - 생성된 스트림을 처리합니다. 결과를 리턴하지 못합니다.

  • 종단 연산 - 중개 연산 결과를 종합하여 리턴합니다.

1) 스트림 생성

  • 컬렉션 - Collection 인터페이스stream 기본 메서드 호출
  • 배열 - Arrays.stream 클래스 메서드 호출

정수배열의 경우 IntStream.of 를 사용할 수 있는데 결국에는 Arrays.stream을 사용합니다.

import java.util.*;
import java.util.stream.IntStream;

class Main {
    public static void main(String[] args) {

        // 1. 정수 배열
        int[] a = {1, 2, 3};
        IntStream.of(a);

        // 2. 객체 배열
        Person[] personList = {new Person("a", 1)};
        Arrays.stream(personList);
    }
}

class Person {
    private String name;
    private int age;
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
}

2) 중개연산

사실 직접 해보는것이 더 좋아서 자세하게 안적습니다.

  • forEach - 순회
  • filter - 조건식 필터링
  • map - 콜백에 따라 연산
  • flatMap - 중복된 스트림 1차원 평면화
  • reduce - 결과 취합
  • sorted - 정렬

3) 중단 연산

collect - 파라미터 값으로 변환
toArray - 배열로 변환

4) 예시

배열의 원소를 제곱수로 변경

import java.util.Arrays;

class Main {
    public static void main(String[] args) {
        // 정수 배열
        int[] a = {1, 2, 3};

        // 1. 스트림 생성 - 배열을 스트림으로 생성
        int[] mapArr = Arrays.stream(a)
                // 2. 중개연산 원소를 제곱수로 변경
                        .map(x -> x * x)
                // 3. 종단연산 - 배열로 변환
                        .toArray();

        // 메서드 레퍼런스로 출력
        Arrays.stream(mapArr).forEach(System.out::println);
        // 1 
        // 4 
        // 9
    }
}

3. 스트림 단점

단점이라기 보다는 스트림의 특징을 제대로 활용하지 않았을 때 발생하는 문제점입니다.

과연 반복문 사용하는것보다 항상 좋은가?

그것은 아닙니다.

1) 순회 중단에 종료가 불가능합니다.

스트림의 정의는 연속된 정보를 순서대로 접근하기 위한 객체입니다.

예시 처럼 1, 2, 3, 4 배열에서 3이상은 보고 싶지 않다. 순회 그만하자고 break를 넣을 수 없고, return을 적어도 결국 끝까지 모두 순회합니다.

import java.util.Arrays;

class Main {
    public static void main(String[] args) {
        // 정수 배열
        int[] a = {1, 2, 3, 4};

        Arrays.stream(a).forEach(x -> {
            System.out.println("before Check " + x);
            if(x > 2) return;
            System.out.println("x " + x);
        });
    }
}

/*
출력 결과
before Check 1
x 1
before Check 2
x 2
before Check 3
before Check 4
*/

2) 동시성 관리를 위해 Atomic 자료형을 사용합니다.

만약 스트림을 순회하면서 외부 변수에 접근하려고 하면 동시성 관리를 위해 Atomic 자료형을 사용해야합니다.

Atomic 자료형은 멀티스레드 환경에서 동시성 관리를 위해 도입된 자료형으로 CAS - Compare and Swap 알고리즘을 사용합니다.

(물론 아래처럼 배열의 합을 구할때는 sum()메서드 사용합니다. 그냥 예시입니다.)

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

class Main {
    public static void main(String[] args) {
        // 정수 배열
        int[] a = {1, 2, 3, 4};
        AtomicInteger res = new AtomicInteger(0);

        Arrays.stream(a).forEach(x -> {
            res.addAndGet(x);
        });

        System.out.println("res " + res);
    }
}

4. 람다

1) 정의

람다는 함수를 하나의 식으로 표현현것 입니다. 이름이 없는 익명함수의 한 종류로 볼 수 있습니다.

람다식을 사용하는 이유는 불필요한 코드를 줄이고 가독성을 높이기 위함입니다.

2) 함수형 인터페이스

함수형 인터페이스는 추상메서드가 1개만 있는 인터페이스로 함수형 인터페이스를 통해 람다식을 생성할 수 있습니다.

class Main {
    public static void main(String[] args) {
    
    	// 익명 클래스로 객체 생성
        Person person = new Person() {
            @Override
            public void eat() {
                System.out.println("eat!!");
            }
        };
        
        person.eat();
    }
}

interface Person {
    void eat();
}

3) 함수형 인터페이스를 람다로 바꾸기 위한 조건

인터페이스에 추상 메서드가 단 1개만 있어야합니다.
(default, static 메서드의 개수는 상관 없습니다)

위의 예시는 람다식으로 이렇게 변경할 수 있습니다.

class Main {
    public static void main(String[] args) {
        Person person = () -> System.out.println("eat!!");

        person.eat();
    }
}

interface Person {
    void eat();
}

4) @FunctionalInterface

만약 누군가가 인터페이스에 메서드를 하나 더 추가하면, 더 이상 람다식으로 만들 수 없고 에러가 발생합니다.

따라서, @FunctionalInterface 어노테이션을 통해 추상 메서드가 단 1개임을 명시합니다.

추상메서드가 2개 이상이면 컴파일 에러가 발생합니다.

@FunctionalInterface
interface Person {
    void eat();

    default void greeting() {
        System.out.println("Hello");
    }

    static void sayHi() {
        System.out.println("Hi");
    }
}

5. Java.util.function 패키지

자바 스트림의 연산은 Functional 인터페이스를 파라미터로 받습니다. Functional 인터페이스를 통해 람다식을 사용할 수 있습니다.

  • Predicate - 두 개의 객체 비교 - filter
  • Supplier - generic 타입 리턴 - Collect, Generate
  • Consumer - 리턴값 없음 - then
  • Function - 객체 타입 변환에 사용 - mapToLong
  • UnaryOperator - 한개의 변수 받아 연산에 사용 - map
  • BinaryOperator - 두개의 변수 받아 연산에 사용 - reduce

예시) map에 사용되는 UnaryOperator 함수형 인터페이스

  • 추상 메서드가 단 1개만 있습니다.

6. 람다 단점

1) 재사용 불가

람다식은 재사용이 불가능합니다.

좋은 웹페이지 즐겨찾기