[Java] Stream (스트림)

8 minute read

Stream (스트림)

데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소

컬렉션 반복을 멋지게(?) 처리하는 기능

멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리 가능

특징

  • 가독성 향상
  • 유연성 향상
  • 성능 향상

컬렉션과 마찬가지로 연속된 값 집합의 인터페이스를 제공함

  • 컬렉션: ArrayList or LinkedList 등 어떤 것을 사용할 것인지에 대한 시공간 복잡성과 관련된 저장 및 접근 연산이 주, 데이터 중심
  • 스트림 : filter, sorted, map 처럼 표현 계산식이 주, 계산 중심

제공된 소스가 정렬되어 있다면, 스트림을 생성해도 정렬이 유지되어 있음

filter, map, reduce, find, match, sort 등 데이터 조작 가능

Pipelining

스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환함 Laziness, Short-circuiting과 같은 최적화 가능

Internal Repitition

내부 반복 지원

Global

@Getter
@ToString
@AllArgsConstructor
public class Dish {

    private final String name;
    private final boolean vegetarian;
    private final Integer calories;
    private final DishType dishType;
}
public enum DishType {
    MEAT, FISH, OTHER
}

As-is Vs. To-be

@Slf4j
@Component
public class CompareAsIs {

    @EventListener(ApplicationStartedEvent.class)
    public void main() {
        log.info("===== Stream / Compare AS Is =====");
        asIs();
        toBe();
        getThreeHighCaloricDishNames();
    }

    public void getThreeHighCaloricDishNames() {
        List<Dish> menu = Dishes();

        List<String> threeHighCaloricDishNames = menu.stream()
            .filter(d -> d.getCalories() > 300) // 람다를 인수로 받아 스트림에서 특정 요소 제외
            .map(Dish::getName) // 람다를 이용해서 요소를 변환하거나 정보를 추출
            .limit(3) //스트림의 크기 축소(truncate)
            .collect(Collectors.toList()); // 파이프라인 처리 후 스트림을 다른 형식으로 결과 변환

        log.debug("threeHighCaloricDishNames : {}", threeHighCaloricDishNames);
    }

    public void toBe() {
        List<Dish> menu = Dishes();

        List<String> lowCaloricDishesName = menu.stream().filter(d -> d.getCalories() < 400) // 조건
            .sorted(Comparator.comparing(Dish::getCalories)) // 정렬
            .map(Dish::getName) // 추출
            .collect(Collectors.toList()); // 저장

        lowCaloricDishesName.stream().forEach(d -> log.debug("toBe.dishName: {}", d)); // 로깅
    }

    public void asIs() {
        List<Dish> menu = Dishes();

        // Garbage Variable
        List<Dish> lowCaloricDishes = new ArrayList<>();

        for (Dish dish : menu) {
            if (dish.getCalories() < 400) {
                lowCaloricDishes.add(dish);
            }
        }

        Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
            public int compare(Dish dish1, Dish dish2) {
                return Integer.compare(dish1.getCalories(), dish2.getCalories());
            }
        });

        List<String> lowCaloricDishName = new ArrayList<>();

        for (Dish dish : lowCaloricDishes) {
            lowCaloricDishName.add(dish.getName());
        }

        for (String dishName : lowCaloricDishName) {
            log.debug("asIs.getName() = {}", dishName);
        }

    }

    private static List<Dish> Dishes() {
        return Arrays.asList(
            new Dish("pork", false, 800, DishType.MEAT),
            new Dish("beef", false, 700, DishType.MEAT),
            new Dish("chicken", false, 400, DishType.MEAT),
            new Dish("french", true, 530, DishType.OTHER),
            new Dish("rice", true, 350, DishType.OTHER),
            new Dish("season fruit", true, 120, DishType.OTHER),
            new Dish("pizza", true, 550, DishType.OTHER),
            new Dish("prawns", false, 330, DishType.MEAT),
            new Dish("salmon", false, 450, DishType.MEAT)

        );
    }

}

Stream and Collection (스트림과 컬렉션)

데이터를 언제 계싼하느냐가 가장 큰 차이

Stream Collection
요청 시에만 요소 계산 모든 값을 메모리에 저장
요소 추가 및 삭제 불가 요소 추가 및 삭제 가능
계산 중간에 결과를 확인할 수 있음 계산이 완료될 때까지 기다려야 함
반복을 알아서 처리하고 스트림 값을 어딘가에 저장함(내부 반복) 사용자가 직접 요소를 반복해야 함(외부 반복)

하나의 스트림에서는 딱 한번만 탐색할 수 있음

Stream Operations (스트림 연산)

데이터 소스 → 중간 연산(파이프라인 구성) → 최종 연산(파이프라인 실행 및 결과 도출)

중간 연산

  • 연산끼리 연결되어 파이프라인을 형성
  • 또 다른 스트림 반환
  • 최종 연산 수행 전까지 아무 연산도 수행되지 않음
  • filter, sorted와 같은 연산
연산 반환 형식 인수 함수 디스크립터
filter Stream Predicate T → boolean
map Stream Function<T, R> T → R
limit Stream Comparator (T, T) → int
sorted Stream Comparator (T, T) → int
distinct Stream Comparator (T, T) → int

최종 연산

  • 파이프라인을 실행한 다음 닫음
  • 스트림 파이프라인에서 결과를 도출함
연산 반환 형식 목적
forEach void 스트림의 각 요소를 소비하면서 람다 적용
count long(generic) 스트림 요소 개수 반환
collect - 스트림을 리듀스해서 리스트, 맵, 정수 형식의 컬렉션을 만듬.

Filtering (필터링)

Predicate(불리언을 반환하는 함수)를 인수로 받아 일치하는 모든 요소를 포함하는 스트림을 반환 .filter()

@Slf4j
@Component
public class Filtering {

    @EventListener(ApplicationStartedEvent.class)
    public void main() {
        log.info("===== Stream / Utilization / Filtering =====");
        List<Dish> menu = Dish.menu();

        // filter
        List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(Collectors.toList());

        vegetarianMenu.forEach(m -> log.debug("Name : {}, Vegetarian : {}", m.getName(), m.isVegetarian()));

        // distinct
        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 4, 2);

        List<Integer> distinctResult = numbers.stream().filter(i -> i % 2 == 0).distinct().collect(Collectors.toList());
        distinctResult.stream().forEach(r -> log.debug("result : {}", r.toString())); // 2, 4

    }
}

Slicing (슬라이싱)

스트림의 요소를 선택하거나 스킵

Predicate

.takeWhile() : predicate가 거짓이되면 이전 요소 모두 반환

.dropWile() : predicate가 거짓이되면 남은 요소 모두 반환

@Slf4j
@Component
public class Slicing {

    @EventListener(ApplicationStartedEvent.class)
    public void main() {
        log.info("===== Stream / Utilization / Slicing =====");
        List<Dish> menu = Dish.menu();

        menu.forEach(d -> log.debug("getName : {}, getCalories : {}", d.getName(), d.getCalories()));

        // takeWhile
        List<Dish> slicedMenu1 = menu.stream().takeWhile(d -> d.getCalories() < 320).collect(Collectors.toList());
        slicedMenu1.forEach(d -> log.debug("takeWile_getName : {}", d.getName()));

        // dropWhile : 조건과 다른 것들만 리턴
        List<Dish> slicedMenu2 = menu.stream().dropWhile(d -> d.getCalories() < 320).collect(Collectors.toList());
        slicedMenu2.forEach(d -> log.debug("dropwhile_getName : {}", d.getName()));
    }
}

Stream Reduction

.limit(n) : 최대 n개 요소 반환

Skip Element

.skip(n) : 처음 n개 요소 제외

Mapping (매핑)

특정 데이터 매핑 및 변환 .map()

flatMap

.flatMap() : 스트림의 각 값을 다른 스트림으로 만들고, 모든 스트림을 하나의 스트림으로 연결함

@Slf4j
@Component
public class Mapping {

    @EventListener(ApplicationStartedEvent.class)
    public void main() {
        log.info("===== Stream / Utilization / Mapping =====");

        List<Dish> menu = Dish.menu();

        List<String> dishNames = menu.stream()
            .map(Dish::getName)
            .collect(Collectors.toList());

        dishNames.forEach(d -> log.debug("getName() = {}", d));

        List<String> words = Arrays.asList("Modern", "Java", "In", "Action");
        List<Integer> wordLengths = words.stream()
            .map(String::length)
            .collect(Collectors.toList());
        wordLengths.forEach(d -> log.debug("wordLength= {}", d));

        //flatMap
        List<String> hwords = Arrays.asList("Hello", "World");
        List<String> hSplitWords = hwords.stream()
            .map(w -> w.split("")) // 개별 문자 배열로 변환
            .flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
            .distinct()
            .collect(Collectors.toList());

        hSplitWords.stream().forEach(d -> log.debug("hSplitwords = {}", d));
        // H
        // e
        // l
        // o
        // ...

        List<Integer> numbers1 = Arrays.asList(1, 2, 3);
        List<Integer> numbers2 = Arrays.asList(3, 4);

        List<int[]> pairs = numbers1.stream()
            .flatMap(
                i -> numbers2.stream()
                    .map(j -> new int[]{i, j})
            )
            .collect(Collectors.toList());

        pairs.forEach(d -> log.debug("pairs = {}", d));
        // 1, 3
        // 1, 4
        // ...

        List<int[]> pairs2 = numbers1.stream()
            .flatMap(
                i -> numbers2.stream()
                    .filter(j -> (i + j) % 3 == 0)
                    .map(j -> new int[]{i, j})
            )
            .collect(Collectors.toList());

        pairs2.forEach(d -> log.debug("pairs2 = {}", d));
    }
}

Search and Matching (검색)

anyMatch

적어도 한 요소와 일치하는지 확인

Predicate가 모든 요소와 일치하는지 확인

allMatch() : 모든 요소가 주어진 Predicate와 일치하는지 확인

noneMatch() : 주어진 Predicate와 일치하는 요소가 없는지 확인

Short Circuit(쇼트서킷)

예를들어, AND 연산을 사용할 때 하나라도 거짓이면, 나머지 결과와 상관없이 전체 결과가 거짓이 되는 상황

Search element

findAny

임의의 요소 반환 findFirst와 차이점이 없어 보이지만, 병렬 처리 시 가장 먼저 찾은 요소를 반환

findFirst

첫 번째 요소 반환 병렬 처리 시 Stream의 순서를 고려하여 가장 앞에 있는 요소 반환

Reducing (연산)

스트림 요소를 조합해서 복잡한 질의 표현

reduce

reduce(초기값, 연산) 초기값이 없으면 Optional<>로 반환

@Slf4j
@Component
public class Searching {

    @EventListener(ApplicationStartedEvent.class)
    public void main() {
        log.info("===== Stream / Utilization / Searching =====");
        List<Dish> menu = Dish.menu();

        // match
        if (menu.stream().anyMatch(Dish::isVegetarian)) {
            log.debug("vegetarian이 하나 이상입니다."); // true
        }

        boolean result = menu.stream()
            .allMatch(dish -> dish.getCalories() < 1000);
        log.debug("result = {}", result); // true

        boolean result2 = menu.stream()
            .noneMatch(dish -> dish.getCalories() >= 1000);
        log.debug("result2 = {}", result2); // true

        // search
        menu.stream()
            .filter(Dish::isVegetarian)
            .findAny()
            .ifPresent(d -> log.debug("name= {}", d.getName()));

        menu.stream()
            .filter(Dish::isVegetarian)
            .findFirst()
            .ifPresent(d -> log.debug("name= {}", d.getName()));

        // reduce
        List<Integer> numbers = IntStream.range(1, 11).boxed().collect(Collectors.toList());
        Integer reduced = numbers.stream().reduce(0, (a, b) -> a + b);
        log.debug("reduced = {}", reduced); // 55

        int maxNumber = numbers.stream().reduce(0, Integer::max);
        log.debug("maxNumber = {}", maxNumber);
    }
}

Practice !

@RequiredArgsConstructor
@Getter
public class Trader {

    private final String name;
    private final String city;

    @Override
    public String toString() {
        return "Trader:" + this.name + " in " + this.city;
    }
}
@RequiredArgsConstructor
@Getter
@ToString
public class Transaction {

    private final Trader trader;
    private final int year;
    private final int value;

    public static List<Transaction> transactions() {
        Trader trader1 = new Trader("Raoul", "Cambridge");
        Trader trader2 = new Trader("Mario", "Milan");
        Trader trader3 = new Trader("Alan", "Cambridge");
        Trader trader4 = new Trader("Brian", "Cambridge");

        return Arrays.asList(
            new Transaction(trader4, 2011, 300),
            new Transaction(trader1, 2012, 1000),
            new Transaction(trader1, 2011, 400),
            new Transaction(trader2, 2012, 710),
            new Transaction(trader2, 2012, 700),
            new Transaction(trader3, 2012, 950)
        );
    }
}
@Component
@Slf4j
public class Answers {

    @EventListener(ApplicationStartedEvent.class)
    public void main() {
        log.info("===== Stream / Practice =====");

        List<Transaction> transactions = Transaction.transactions();

        List<Transaction> result1 = transactions.stream()
            .filter(t -> t.getYear() == 2011)
            .sorted(comparing(Transaction::getValue)).toList();
        result1.forEach(t -> log.debug("result = {}", t));

        List<String> result2 = transactions.stream()
            .map(t -> t.getTrader().getCity())
            .distinct()
            .toList();
        result2.forEach(t -> log.debug("result2 = {}", t));

        List<Trader> result3 = transactions.stream()
            .map(Transaction::getTrader)
            .filter(t -> t.getCity().equals("Cambridge"))
            .distinct()
            .sorted(comparing(Trader::getName))
            .toList();
        result3.forEach(t -> log.debug("result3 = {}", t));

        String result4 = transactions.stream()
            .map(t -> t.getTrader().getName())
            .distinct()
            .sorted()
            .reduce("", (x, y) -> x + "" + y);
        log.debug("result4 = {}", result4);

        if (transactions.stream().anyMatch(t -> t.getTrader().getCity().equals("Milan"))) {
            log.debug("result5 = Milan에 거래자가 있습니다.");
        }

        List<Integer> result6 = transactions.stream()
            .filter(t -> t.getTrader().getCity().equals("Cambridge"))
            .map(Transaction::getValue)
            .toList();
        result6.forEach(r -> log.debug("r = {}", r.toString()));

        Optional<Integer> result7 = transactions.stream()
            .map(Transaction::getValue)
            .reduce(Integer::max);
        log.debug("max = {}", result7);

        // .max()
        Optional<Transaction> result7t1 = transactions.stream()
            .max(comparing(Transaction::getValue));
        log.debug("min = {}", result7t1);

        Optional<Integer> result8 = transactions.stream()
            .map(Transaction::getValue)
            .reduce(Integer::min);
        log.debug("min = {}", result8);

        // .min()
        Optional<Transaction> result8t1 = transactions.stream()
            .min(comparing(Transaction::getValue));
        log.debug("min = {}", result8t1);

    }

}

Numeric Stream (숫자형 스트림)

Primitive Stream Specialization (기본형 특화 스트림)

IntStream, DoubleStream, LongStream

sum, max 와 같이 숫자 관련 리듀싱 연산 수행 메서드 제공

필요 시 객체 스트림으로 복원하는 기능 제공

박싱 과정에서 일어나는 효율성에만 관련 있고, 추가 기능은 제공하지 않음.

@Slf4j
@Component
public class Numeric {

    @EventListener(ApplicationStartedEvent.class)
    public void main() {
        log.info("===== Stream / Numeric Stream =====");

        List<Dish> menu = Dish.menu();

        // Primitive Stream Specialization

        int calories = menu.stream()
            .mapToInt(Dish::getCalories) // InStream으로 변환
            .sum();

        OptionalInt maxCalories = menu.stream() // Optional 제공
            .mapToInt(Dish::getCalories)
            .max();

        int maxCaloriesWithDefault = menu.stream() // Optional + Default
            .mapToInt(Dish::getCalories)
            .max()
            .orElse(1);

        Stream<Integer> boxedStream = menu.stream()
            .mapToInt(Dish::getCalories)
            .boxed(); // 특화 스트림을 일반 스트림으로 변환

        // Range

        OptionalInt maxValue = IntStream.rangeClosed(1, 100)
            .max();
        log.debug("maxValue = {}", maxValue); // 100

        Stream<double[]> pythagoreanTriples = IntStream.rangeClosed(1, 100)
            .boxed()
            .flatMap(a -> IntStream.rangeClosed(a, 100)
                .mapToObj(b -> new double[]{a, b, Math.sqrt(a * a + b * b)})
                .filter(t -> t[2] % 1 == 0)
            );

        pythagoreanTriples.limit(5)
            .forEach(v -> log.debug("pythagoreanTriples ={}", v));
    }
}

Generate Stream (스트림 생성)

스트림을 생성하는 방법

  • empty stream
  • nullable stream
  • infinite stream
  • value → stream
  • array → stream
  • file → stream
@Slf4j
@Component
public class GenerateStream {

    @EventListener(ApplicationStartedEvent.class)
    public void main() {
        log.info("===== Stream / Generate Stream =====");

        // value to stream
        Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");
        stream.map(String::toUpperCase)
            .forEach(v -> log.debug("value v ={}", v));

        // empty stream
        Stream<String> emptyStream = Stream.empty();

        // nullable Stream
        Stream<Object> values = Stream.ofNullable(System.getProperty("home"));
        values.forEach(v -> log.debug("nullable v = {}", v));

        // Array to Stream
        int[] numbers = {2, 3, 5, 7, 11, 13};
        int sum = Arrays.stream(numbers).sum();
        log.debug("sum = {}", sum);

        // File to Stream
        long uniqueWords = 0;
        try (Stream<String> lines = Files.lines(Paths.get("src/main/java/com/megazone/modern/Stream/GenerateStream/data.txt"), Charset.defaultCharset())) {
            uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                .distinct()
                .count();
        } catch (IOException e) {

        }
        log.debug("uniqueWords = {}", uniqueWords);

        // Infinite Stream (Unbounded Stream)
        Stream.iterate(0, n -> n + 2)
            .limit(10) // 무한한 값을 출력하지 않도록 사용 권장
            .forEach(v -> log.debug("v = {}", v)); // 0, 2, 4, ...

        Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]})
            .limit(10)
            .forEach(t -> log.debug("fibonacci = {}", t[0]));

        Stream.generate(Math::random)
            .limit(10)
            .forEach(v -> log.debug("random v = {}", v));

    }
}

data.txt

a b c d e
f g h i j

boxed

.boxed() : primitive 자료형을 Wrapper 클래스(객체)로 변환하는 메서드 int → Integer

Categories:

Updated:

Leave a comment