Just Do It

[JAVA] Stream(2)

by 핫도구
반응형

오늘은 지난번에 이야기 했던 Stream의 연산들에 대해 알아 볼 예정이다. 대부분 설명보다는 코드가 대부분이니 잘 이해하면서 넘어가야 한다.

중간연산(Intermediate Operations)

1. 필터링 연산
filter(), distinct()는 각각  원하는 데이터를 뽑거나 똑같은 값이 있을 때 배제시켜주는 역할을 한다. 특히 distinct()는 equals()와 hashCode()를 기준으로 중복을 판단하며 순서를 유지하면서 중복을 제거하는 역할을 한다. distinctByKey는 커스텀 헬퍼 메서드를 사용하여 카테고리별로 중복을 체크하는 역할을 한다.

public class filterStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<String> strings = Arrays.asList("apple", "", "banana", null, "cherry", "");

        List<Integer> evenNumbers = numbers.stream()
                .filter(n -> n % 2 == 0)
                .collect(toList());
        System.out.println(evenNumbers); // [2, 4, 6, 8, 10]

        List<String> validStrings = strings.stream()
                .filter(Objects::nonNull)
                .filter(s -> !s.isEmpty())
                .collect(Collectors.toList());
        System.out.println(validStrings); // [apple, banana, cherry]
        List<Person> people = Arrays.asList(
                new Person("Alice", 25, 50000),
                new Person("Bob", 17, 0),
                new Person("Charlie", 30, 75000),
                new Person("David", 15, 0)
        );

        List<Person> adults = people.stream()
                .filter(a -> a.getAge() > 19)
                .collect(Collectors.toList());
        System.out.println("성인 : " + adults); // [Person{name='Alice', age=25, salary=50000}, Person{name='Charlie', age=30, salary=75000}]

        List<Person> workAdults = people.stream()
                .filter(s -> s.getAge() > 19)
                .filter(s -> s.getSalary() > 0)
                .collect(Collectors.toList());
        System.out.println("일 하면서 어른 : " + workAdults); // [Person{name='Alice', age=25, salary=50000}, Person{name='Charlie', age=30, salary=75000}]

        List<Integer> numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5, 5);
        List<Integer> uniqueNum = numbersWithDuplicates.stream()
                .distinct()
                .collect(Collectors.toList());
        System.out.println("중복 제거 : " + uniqueNum); // [1, 2, 3, 4, 5]

        List<String> words = Arrays.asList("apple", "banana", "apple", "cherry", "banana");
        List<String> uniqueWords = words.stream()
                .distinct()
                .collect(Collectors.toList());
        System.out.println("단어 중복 제거 : " + uniqueWords); // [apple, banana, cherry]

        List<Product> products = Arrays.asList(
                new Product("Laptop", "Electronics", 1000),
                new Product("Mouse", "Electronics", 20),
                new Product("Keyboard", "Electronics", 50),
                new Product("Desk", "Furniture", 300),
                new Product("Chair", "Furniture", 150)
        );

        List<Product> uniqueCategory = products.stream()
                .filter(distinctByKey(Product::getCategory))
                .collect(Collectors.toList());
        System.out.println("카테고리별 첫 제품 : " +uniqueCategory); // [Laptop(Electronics:$1000), Desk(Furniture:$300)]
    }
    // 특정 키로 중복 제거하는 헬퍼 메서드
    public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
        Set<Object> seen = ConcurrentHashMap.newKeySet();
        return t -> seen.add(keyExtractor.apply(t));
    }
}

class Person {
    private String name;
    private int age;
    private double salary;
    
    public Person(String name, int age, double salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }
    
    // Getters
    public String getName() { return name; }
    public int getAge() { return age; }
    public double getSalary() { return salary; }
    
    @Override
    public String toString() {
        return String.format("Person{name='%s', age=%d, salary=%.0f}", name, age, salary);
    }
}

class Product {
    private String name;
    private String category;
    private double price;
    
    public Product(String name, String category, double price) {
        this.name = name;
        this.category = category;
        this.price = price;
    }
    
    public String getName() { return name; }
    public String getCategory() { return category; }
    public double getPrice() { return price; }
    
    @Override
    public String toString() {
        return String.format("%s(%s:$%.0f)", name, category, price);
    }
}

2. 변환연산
map()은 각 요소를 다른 갑승로 변환하는 중간연산이다. flatMap()은 각 내부 리스트를 stream으로 변환 후 하나로 합친 후 평면화를 하는 것이다. map()과 flatMap의 차이점은 map()은 1:1변환이며 flatMap()은 1:N 변환 후 평면화를 시킨다. 즉, flatMap은 Stream<T> -> Stream<Stream<R>> -> Stream<R>로 변환하는 것이다. 그래서 List<List<T>>를 List<T>로 평면화 시킨다고 할 수 있다.

public class MapStreamExample {
    public static void main(String[] args) {
        // 기본 데이터
        List<String> names = Arrays.asList("alice", "bob", "charlie", "david");
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        List<String> upperNames = names.stream()
                .map(String::toUpperCase)
                .collect(Collectors.toList());
        System.out.println("대문자 이름 : " + upperNames); // [ALICE, BOB, CHARLIE, DAVID]

        List<Integer> squared = numbers.stream()
                .map(n -> n * n)
                .collect(Collectors.toList());
        System.out.println("제곱 : " + squared); // [1, 4, 9, 16, 25]

        List<Employee> employees = Arrays.asList(
                new Employee(1, "Alice", "IT", 60000),
                new Employee(2, "Bob", "HR", 45000),
                new Employee(3, "Charlie", "IT", 75000),
                new Employee(4, "David", "Finance", 55000)
        );
        List<String> employeeName = employees.stream()
                .map(Employee::getName)
                .collect(Collectors.toList());
        System.out.println("직원 이름 : " + employeeName); // [Alice, Bob, Charlie, David]

        List<EmployeeDTO> dtos = employees.stream()
                .map(emp -> new EmployeeDTO(emp.getName(), emp.getDepartment()))
                .collect(Collectors.toList());
        System.out.println("DTO 목록 : " + dtos); // [EmployeeDTO{name='Alice', dept='IT'}, EmployeeDTO{name='Bob', dept='HR'}, EmployeeDTO{name='Charlie', dept='IT'}, EmployeeDTO{name='David', dept='Finance'}]

        // 중첩된 리스트 평면화
        List<List<Integer>> nestedNumbers = Arrays.asList(
                Arrays.asList(1, 2, 3),
                Arrays.asList(4, 5),
                Arrays.asList(6, 7, 8, 9)
        );
        List<Integer> flatNumbers = nestedNumbers.stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toList());
        System.out.println("평면화된 숫자 : " + flatNumbers);

        // 문자열을 단어로 분리
        List<String> sentences = Arrays.asList(
                "Hello World",
                "Java Stream API",
                "FlatMap Example"
        );

        List<String> words = sentences.stream()
                .flatMap(s -> Arrays.stream(s.split(" ")))
                .collect(Collectors.toList());
        System.out.println("모든 단어 : " + words); // [Hello, World, Java, Stream, API, FlatMap, Example]

        // 객체의 컬렉션 속성 평면화
        List<Developer> developers = Arrays.asList(
                new Developer("Alice", Arrays.asList("Java", "Python", "JavaScript")),
                new Developer("Bob", Arrays.asList("Java", "C++", "Go")),
                new Developer("Charlie", Arrays.asList("Python", "Ruby", "JavaScript"))
        );

        List<String> allSkills = developers.stream()
                .flatMap(dev -> dev.getSkills().stream())
                .distinct()
                .sorted()
                .collect(Collectors.toList());
        System.out.println("모든 스킬 : " +allSkills); // [C++, Go, Java, JavaScript, Python, Ruby]

        List<String> items = Arrays.asList("1", "2", "abc", "3", "def", "4");
        List<Integer> validNums = items.stream()
                .map(MapStreamExample::parseOptional)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .collect(Collectors.toList());
        System.out.println("유효한 숫자 : "+validNums); // [1, 2, 3, 4]

    }
    // 문자열을 정수로 파싱 (Optional 반환)
    private static Optional<Integer> parseOptional(String s) {
        try {
            return Optional.of(Integer.parseInt(s));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }
}

class Employee {
    private int id;
    private String name;
    private String department;
    private double salary;

    public Employee(int id, String name, String department, double salary) {
        this.id = id;
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    public int getId() { return id; }
    public String getName() { return name; }
    public String getDepartment() { return department; }
    public double getSalary() { return salary; }

    @Override
    public String toString() {
        return String.format("Employee{id=%d, name='%s', dept='%s', salary=%.0f}",
                id, name, department, salary);
    }
}

class EmployeeDTO {
    private String name;
    private String department;

    public EmployeeDTO(String name, String department) {
        this.name = name;
        this.department = department;
    }

    @Override
    public String toString() {
        return String.format("EmployeeDTO{name='%s', dept='%s'}", name, department);
    }
}

class Developer {
    private String name;
    private List<String> skills;

    public Developer(String name, List<String> skills) {
        this.name = name;
        this.skills = skills;
    }

    public String getName() { return name; }
    public List<String> getSkills() { return skills; }
}

3. 제한연산
IntStream.rangeClose(a, b)는 a부터 b까지의 정수 스트림을 생성하는 것이고 boxed()는 int를 Integer로 박싱하는 것이다. 이처럼 기본타입 스트림을 참조 타입으로 변환하는 이유는 Collection에서 참조타입을 취급하기 때문이다.  limit()은 스트림의 크기를 제한하는 중간연산자로 처음 n개의 요소만 통과시키고 나머지는 버린다. 이는 무한 스트림을 유한하게 만들 경우에 유용하게 사용된다. skip()메서드는 처음 n개의 요소를 건너 뛰고 나머지를 반환하는 중간연산자이다. 일반적으로 skip()과 limit()의 조합을 통해 페이징 구현을 사용하는 경우가 많다. 뿐만 아니라 Top N 쿼리, 샘플링, 배치 처리 등에서도 활용할 수 있다.

public class LimitSkipStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.rangeClosed(1, 20)
                .boxed()
                .collect(Collectors.toList());
        System.out.println("원본 : " + numbers); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]

        List<Integer> first5 = numbers.stream()
                .limit(5)
                .collect(Collectors.toList());
        System.out.println("처음 5개 : " + first5); // [1, 2, 3, 4, 5]

        List<Integer> after10 = numbers.stream()
                .skip(10)
                .collect(Collectors.toList());
        System.out.println("10개 건너뛴 후 : " + after10); // [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]

        // 페이징 구현
        int pageNumber = 2; // 3번째 페이지 (0부터 시작)
        int pageSize = 5;

        List<Integer> page = numbers.stream()
                .skip(pageNumber * pageSize)
                .limit(pageSize)
                .collect(Collectors.toList());
        System.out.println("3번째 페이지 : " + page); // [11, 12, 13, 14, 15]

        List<Employee> employees = Arrays.asList(
                new Employee(1, "Alice", "IT", 60000),
                new Employee(2, "Bob", "HR", 45000),
                new Employee(3, "Charlie", "IT", 85000),
                new Employee(4, "David", "Finance", 55000),
                new Employee(5, "Eve", "IT", 75000),
                new Employee(6, "Frank", "HR", 48000)
        );
        List<Employee> top3Earners = employees.stream()
                .sorted(Comparator.comparing(Employee::getSalary).reversed())
                .limit(3)
                .collect(Collectors.toList());
        System.out.print("상위 3명의 고액연봉자 : ");
        top3Earners.forEach(System.out::print); // Employee{id=3, name='Charlie', dept='IT', salary=85000}Employee{id=5, name='Eve', dept='IT', salary=75000}Employee{id=1, name='Alice', dept='IT', salary=60000}
    }
}

4. 정렬연산
  sorted()는 스트림 요소를 정렬하는 중간연산으로 기본적으로 N(자연 순서)로 정렬이 되며 Comparator를 제공하면 커스텀으로 정렬할 수 있다. Comparator 인터페이스를 구현한 타입은 기본 정렬이 가능하며 숫자는 오름차순, 문자열은 사전순으로 정렬이 된다. Comparator.reverseOrder()를 활용해 자연순서와 반대로 정렬할 수 있다. 커스텀으로 정렬할 때 Comparator.comparing()을 활용하는데 String::length는 문자열의 길이를 기준으로 정렬할 수 있다. 그리고 thenComparing()을 활용해 이전과 비교가 같을 경우에 추가적으로 비교할 수 있는 메서드로 아래에 예시된 코드로 예를 들면 학생들의 학년을 기준으로 정렬하고 점수를 내림차순으로 정렬하고 또 이를 이름으로 오름차순 정렬하는 것을 볼 수 있다. 또한, nullFirst()메서드를 통해 null을 맨 앞에 배치 할 수 있으며 nullLast() 메서드를 통해 null을 맨 뒤에 배치할 수 있다.

public class SortStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 3, 7, 4, 6);

        List<Integer> ascending = numbers.stream()
                .sorted()
                .collect(Collectors.toList());
        System.out.println("오름차순 : " + ascending); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

        List<Integer> descending = numbers.stream()
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
        System.out.println("내림차순 : " + descending); // [9, 8, 7, 6, 5, 4, 3, 2, 1]

        // 문자열 정렬
        List<String> fruits = Arrays.asList("banana", "apple", "cherry", "date", "elderberry");

        List<String> alphabetic = fruits.stream()
                .sorted()
                .collect(Collectors.toList());
        System.out.println("알파벳순 : " + alphabetic); // [apple, banana, cherry, date, elderberry]

        List<String> byLength = fruits.stream()
                .sorted(Comparator.comparing(String::length))
                .collect(Collectors.toList());
        System.out.println("길이순 : " + byLength); // [date, apple, banana, cherry, elderberry]

        // 복잡한 객체 정렬
        List<Student> students = Arrays.asList(
                new Student("Alice", 85, "A"),
                new Student("Bob", 92, "B"),
                new Student("Charlie", 85, "A"),
                new Student("David", 78, "B"),
                new Student("Eve", 92, "A")
        );

        List<Student> byScore = students.stream()
                .sorted(Comparator.comparing(Student::getScore))
                .collect(Collectors.toList());
        System.out.println("점수순 : " + byScore); // [Student{name='David', score=78, grade='B'}, Student{name='Alice', score=85, grade='A'}, Student{name='Charlie', score=85, grade='A'}, Student{name='Bob', score=92, grade='B'}, Student{name='Eve', score=92, grade='A'}]

        List<Student> multiSort = students.stream()
                .sorted(Comparator.comparing(Student::getGrade)
                    .thenComparing(Student::getScore, Comparator.reverseOrder())
                    .thenComparing(Student::getName))
                .collect(Collectors.toList());
        System.out.println("학년 > 점수(역순) > 이름순 : " + multiSort); // [Student{name='Eve', score=92, grade='A'}, Student{name='Alice', score=85, grade='A'}, Student{name='Charlie', score=85, grade='A'}, Student{name='Bob', score=92, grade='B'}, Student{name='David', score=78, grade='B'}]

        List<String> withNulls = Arrays.asList("apple", null, "banana", "cherry", null);
        List<String> nullFirst = withNulls.stream()
                .sorted(Comparator.nullsFirst(String::compareTo))
                .collect(Collectors.toList());
        System.out.println("null 먼저 : " + nullFirst); // [null, null, apple, banana, cherry]

    }


    static class Student {
        private String name;
        private int score;
        private String grade;

        public Student(String name, int score, String grade) {
            this.name = name;
            this.score = score;
            this.grade = grade;
        }

        public String getName() { return name; }
        public int getScore() { return score; }
        public String getGrade() { return grade; }

        @Override
        public String toString() {
            return String.format("Student{name='%s', score=%d, grade='%s'}", name, score, grade);
        }
    }
}
최종연산(Terminal Operations)

1. 수집연산
collect()는 스트림의 요소를 수집하는 최종연산으로 Collector를 매개변수로 받아서 요소를 특정 자료구조로 변환한다. Collectors 클래스는 다양한 수집 방법을 제공하는 유틸리티 클래스이다. 이 안에서 toList()는 가장 기본적인 수집방법으로 ArrayList로 수집한다. toSet은 중복이 자동으로 제거되는 set으로 수집되며 HashSet을 사용하기 때문에 순서가 보장이 되지는 않는다. toMap()은 key-value 쌍으로 수집되며 객체 자체를 반환한다. groupingBy()는 특정 기준으로 그룹화하며 SQL에서의 GROUP BY와 유사하다. 여기에서는 부서별로 그룹화 한 것을 볼 수 있다. paritioningBy()는 true와 false로 그룹을 분할하여 결과를 반환한다. 아래에서는 연봉을 기준으로 높다면 true, 적다면 false를 활용한 것을 볼 수 있다. joining()은 문자열을 연결하여 하나의 문자열을 생성하는 메서드로 joining(구분자, 접두사, 접미사)로 사용된다. summarizingDouble()은 숫자 데이터의 통계정보를 한번에 수집하는 메서드로 count, sum, min, max, average를 모두 계산할 수 있다.

public class CollectStream {
    public static void main(String[] args) {
        List<Employee> employees = Arrays.asList(
                new Employee(1, "Alice", "IT", 60000),
                new Employee(2, "Bob", "HR", 45000),
                new Employee(3, "Charlie", "IT", 75000),
                new Employee(4, "David", "Finance", 55000),
                new Employee(5, "Eve", "IT", 65000),
                new Employee(6, "Frank", "HR", 48000),
                new Employee(7, "Grace", "Finance", 70000)
        );

        List<String> nameList = employees.stream()
                .map(Employee::getName)
                .collect(Collectors.toList());
        System.out.println("이름 리스트 : " + nameList); // [Alice, Bob, Charlie, David, Eve, Frank, Grace]

        Set<String> departmentSet = employees.stream()
                .map(Employee::getDepartment)
                .collect(Collectors.toSet());
        System.out.println("부서집합 : "+ departmentSet); // [Finance, HR, IT]

        List<String> departmentList = employees.stream()
                .map(Employee::getDepartment)
                .distinct()
                .collect(Collectors.toList());
        System.out.println("부서집합 : " + departmentList); // [IT, HR, Finance]

        Map<Integer, Employee> employeeMap = employees.stream()
                .collect(Collectors.toMap(
                        Employee::getId,
                        Function.identity()
                ));
        System.out.println("직원 맵 : " + employeeMap.get(1)); //Employee{id=1, name='Alice', dept='IT', salary=60000}

        Map<String, List<Employee>> byDepartment = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment));
        System.out.println("부서별 직원 : ");
        byDepartment.forEach((dept, emps) -> {
            System.out.println(dept + " : " +emps.stream()
                    .map(Employee::getName)
                    .collect(Collectors.joining(", ")));
        });

        Map<Boolean, List<Employee>> highEarner = employees.stream()
                .collect(Collectors.partitioningBy(e -> e.getSalary() > 60000));
        System.out.println("고액연봉자 : " + highEarner.get(true).stream()
                .map(Employee::getName)
                .collect(Collectors.toList())); // [Charlie, Eve, Grace]
        System.out.println("일반연봉자 : " + highEarner.get(false).stream()
                .map(Employee::getName)
                .collect(Collectors.toList())); // [Alice, Bob, David, Frank]

        String allNames = employees.stream()
                .map(Employee::getName)
                .collect(Collectors.joining(", ", "[", "]"));
        System.out.println("모든 이름 : " + allNames); // [Alice, Bob, Charlie, David, Eve, Frank, Grace]

        DoubleSummaryStatistics salaryStats = employees.stream()
                .collect(Collectors.summarizingDouble(Employee::getSalary));
        System.out.println("급여통계 : ");
        System.out.println("평균 : " + salaryStats.getAverage()); // 59714.28571428572
        System.out.println("최대 : " + salaryStats.getMax()); // 75000.0
        System.out.println("최소 : " + salaryStats.getMin()); // 45000.0
        System.out.println("합계 : " + salaryStats.getSum()); // 418000.0

        Map<String, Double> avgSalaryByDept = employees.stream()
                .collect(Collectors.groupingBy(
                        Employee::getDepartment,
                        Collectors.averagingDouble(Employee::getSalary)
                ));
        System.out.println("부서별 평균 급여 : " + avgSalaryByDept);

        Map<String, Optional<Employee>> topEarnerByDept = employees.stream()
                .collect(Collectors.groupingBy(
                        Employee::getDepartment,
                        Collectors.maxBy(Comparator.comparing(Employee::getSalary))
                ));
        System.out.println("부서별 최고 연봉자 : ");
        topEarnerByDept.forEach((dept, emp) ->
                emp.ifPresent(e -> System.out.println(dept + " : " + e.getName())));
    }
}

 2. 검색과 매칭연산
findFirst()는 순서가 있는 스트림에서 조건을 만족하는 첫 번쨰 요소를 반환하는 메서드이다. findAny()메서드는 순서에 상관없이 조건을 만족하는 아무 요소나 반환하는 스트림으로 병렬 스트림에서 성능상 이점이 존재한다. 여기에서 findFirst()와 findAny()의 차이점으로 findFirst()는 순서가 보장이 되며 순차/병렬 모두 첫 번째 요소를 반환하며 findAny()는 순서를 보장하지 않으며 병렬에서 더 빠른 성능을 보여준다. anyMatch()는 하나라도 조건을 만족하는지 확인하는 메서드로 조건을 만족하면 true 모두 조건에 만족하지 않으면 false를 반환한다. allMatch()는 모든 요소가 만족하는지 확인하는 메서드로 모든 요소가 조건을 만족하면 true, 하나라도 만족하지 않으면 false를 반환한다. noneMath()는 모든 요소가 조건을 만족하지 않는지 확인하는 메서드로 조건을 만족하는 요소가 하나도 없으면 true 그렇지 않으면 false를 반환한다.

public class SearchMatchExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");

        Optional<Integer> firstEven = numbers.stream()
                .filter(n -> n % 2 == 0)
                .findFirst();
        System.out.println("첫번째 짝수 : " + firstEven.orElse(-1)); // 2

        Optional<String> anyLongWord = words.stream()
                .filter(s -> s.length() > 5)
                .findAny();
        System.out.println("5글자 이상 단어 : " + anyLongWord.orElse(("없음"))); // banana

        boolean hasEven = numbers.stream()
                .anyMatch(i -> i % 2 == 0);
        System.out.println("짝수가 있는가 ? : " + hasEven); // true

        boolean hasLongWord = words.stream()
                .anyMatch(i -> i.length() > 10);
        System.out.println("10글자 초과인 단어 유무 : " + hasLongWord); // false

        boolean noNegative = numbers.stream()
                .noneMatch(i -> i < 0);
        System.out.println("음수 유무 : " + noNegative); // true

        boolean noZ = words.stream()
                .noneMatch(i -> i.contains("z"));
        System.out.println("z가 포함된 단어 유무 : " + noZ); // true

        List<Order> orders = Arrays.asList(
                new Order(1, "PENDING", 100),
                new Order(2, "COMPLETED", 200),
                new Order(3, "PENDING", 150),
                new Order(4, "CANCELLED", 300),
                new Order(5, "COMPLETED", 250)
        );

        boolean hasPendingOrder = orders.stream()
                .anyMatch(i -> "PENDING".equals(i.getStatus()));
        System.out.println("대기중인 주문 존재 유무 : " + hasPendingOrder); // true

        boolean allCompleted = orders.stream()
                .allMatch(i -> "COMPLETED".equals(i.getStatus()));
        System.out.println("모든 주문 완료 유무 : " + allCompleted); // false

        boolean noHighValueOrders = orders.stream()
                .noneMatch(i -> i.getAmount() >= 500);
        System.out.println("고액 주문 없음 유무 : " + noHighValueOrders); // true
        
    }
}

class Order {
    private int id;
    private String status;
    private double amount;

    public Order(int id, String status, double amount) {
        this.id = id;
        this.status = status;
        this.amount = amount;
    }

    public int getId() { return id; }
    public String getStatus() { return status; }
    public double getAmount() { return amount; }
}

3. 집계연산
count()는 요소의 개수를 long 타입으로 반환하는 메서드이다. 여기에서 Optional을 사용한 이유는 null일 수 있는 값을 안전하게 다루기 위해 사용되었다. reduce()메서드는 스트림의 요소들을 하나의 값으로 축약하는 메서드이다. 뿐만 아니라 초기값이 있는 reduce도 존재해 reduce(a, (b, c) -> b + c) 이런식으로 된다면 a는 초기값으로 만약 빈 스트림이여도 초기값이 반환된다. 그리고 Integer::sum은 (a, b) -> a + b 와 같은 것으로 이는 메서드 참조를 사용한 reduce메서드이다. 이를 통해 더 간결하게 나타낼 수 있다. 병렬스트림의 reduce는 3개의 매개변수를 사용할 수 있으며 각각 초기값, 부분합계, 부분결과들의 합으로 나뉘며 아래의 코드처럼 나타낼 수 있다.

public class AggregationExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(5, 3, 8, 1, 9, 2, 7, 4, 6, 10);

        long count = numbers.stream()
                .filter(i -> i > 5)
                .count();
        System.out.println("5보다 큰 숫자의 개수 : " + count); // 5

        Optional<Integer> min = numbers.stream()
                .min(Integer::compareTo);
        System.out.println("최소값 : " + min); // Optional[1]

        Optional<Integer> max = numbers.stream()
                .max(Integer::compareTo);
        System.out.println("최대값 : " + max); // Optional[10]

        Optional<Integer> sum1 = numbers.stream()
                .reduce((a, b) -> a + b);
        System.out.println("합계 : " + sum1.orElse(0)); // 5

        Integer sum2 = numbers.stream()
                .reduce(3, (a, b) -> a + b);
        System.out.println("초기값이 있는 합계 : " + sum2); // 58

        Integer sum3 = numbers.stream()
                .reduce(0, Integer::sum);
        System.out.println("메서드 참조 합계 : " + sum3); // 55

        Integer multiply = numbers.stream()
                .reduce(1, (a, b) -> a * b);
        System.out.println("곱셈 : " + multiply); // 3628800

        Optional<Integer> maxWithReduce = numbers.stream()
                .reduce(Integer::max);
        System.out.println("최대값 : " + maxWithReduce.orElse(0)); // 10

        List<String> words = Arrays.asList("Java", "Stream", "API", "Example");

        String concatenated = words.stream()
                .reduce("", (a, b) -> a + " " + b).trim();
        System.out.println("연결된 문자열 : " + concatenated); // Java Stream API Example

        List<CartItem> cart = Arrays.asList(
                new CartItem("Laptop", 1200, 1),
                new CartItem("Mouse", 25, 2),
                new CartItem("Keyboard", 75, 1),
                new CartItem("Monitor", 300, 2)
        );

        double totalAmount = cart.stream()
                .map(i -> i.getPrice() * i.getQuantity())
                .reduce(0.0, Double::sum);
        System.out.println("장바구니 총 금액 : " + totalAmount); // 1925.0

        Integer parallelSum = numbers.parallelStream()
                .reduce(0, Integer::sum, Integer::sum);
        System.out.println("병렬 합계 : " + parallelSum); // 53

    }
}
class CartItem {
    private String name;
    private double price;
    private int quantity;

    public CartItem(String name, double price, int quantity) {
        this.name = name;
        this.price = price;
        this.quantity = quantity;
    }

    public String getName() { return name; }
    public double getPrice() { return price; }
    public int getQuantity() { return quantity; }
}

여기까지 Stream의 메서드들에 대해 알아보았고 다음에는 Spring으로 넘어 갈 예정이다. 현재 계획으로는 spring security를 할 예정이다.

반응형

'JAVA > MID' 카테고리의 다른 글

[JAVA] Stream(1)  (0) 2025.09.24
[JAVA] 람다  (0) 2025.09.20
[JAVA] Optional  (0) 2025.09.17
[JAVA] 제네릭(Generic)  (0) 2025.09.12
[JAVA] 예외처리(2)  (0) 2025.09.11

블로그의 정보

AquaMan

핫도구

활동하기