CodeGym /행동 /JAVA 25 SELF /Stream API에서 sum, count, average, max, min 메서드

Stream API에서 sum, count, average, max, min 메서드

JAVA 25 SELF
레벨 31 , 레슨 0
사용 가능

1. 요소 개수 세기: count()

숫자나 객체 컬렉션으로 작업할 때는 거의 항상 무언가를 세게 됩니다. 예를 들어 요소 개수, 합계, 평균, 최댓값, 최솟값 등입니다. 회사의 직원 수, 모든 급여의 합, 학생들의 평균 나이, 가장 비싼 상품, 최대 주문 수량 같은 것들이죠. Stream API가 등장하면서 이런 작업이 더 간단하고 표현력 있게 바뀌었습니다.

동작 방식

메서드 count()는 스트림의 요소 개수를 반환하는 Stream API의 종단 연산입니다.

long count = employees.stream().count();

특정 조건을 만족하는 요소만 세고 싶다면 필터 filter(...)를 사용하세요:

long richCount = employees.stream()
    .filter(e -> e.getSalary() > 100_000)
    .count();

우리 앱 예시:

제품 목록이 있다고 가정합시다:

List<Product> products = List.of(
    new Product("우유", 80),
    new Product("치즈", 250),
    new Product("빵", 40)
);

100보다 비싼 제품이 몇 개인지 계산해 봅시다:

long expensiveCount = products.stream()
    .filter(p -> p.getPrice() > 100)
    .count();

System.out.println("비싼 제품 수: " + expensiveCount);

Optional이란?

Java에는 특별한 컨테이너 Optional이 있습니다. 값이 있을 수도, 없을 수도 있음을 표현하는 래퍼 객체죠. 결과가 없을 수도 있는 상황을 명시적으로 드러낼 때 사용합니다. 예를 들어 min(), max(), average()는 컬렉션이 비어 있으면 빈 결과를 반환할 수 있습니다. 이때 null 대신 Optional을 반환합니다.

Optional의 주요 메서드

  • isPresent() / isEmpty() — 값 존재 여부 확인.
  • orElse(값) — 값이 없으면 기본값을 반환.
  • orElseThrow() — 값이 없으면 예외를 던짐.
  • ifPresent(...) — 값이 있을 때만 동작을 수행.

예시:

OptionalInt min = products.stream()
    .mapToInt(Product::getPrice)
    .min();

if (min.isPresent()) {
    System.out.println("최소 가격: " + min.getAsInt());
} else {
    System.out.println("목록이 비어 있습니다");
}

더 간단히 작성하면:

int minValue = min.orElse(-1);
System.out.println("최소 가격: " + minValue);

이런 접근 방식은 NullPointerException을 피하게 해 주고 코드를 더 안전하게 만듭니다.

2. 수치 데이터의 합계, 평균, 최솟값, 최댓값

프리미티브 스트림: IntStream, DoubleStream, LongStream

수치 데이터에 대해 Stream API는 IntStream, DoubleStream, LongStream 같은 프리미티브 스트림을 제공합니다. 이를 사용하면 합계, 평균, 최솟값, 최댓값을 쉽고 빠르게 계산할 수 있습니다.

숫자 스트림으로 변환

객체 스트림에서 숫자 스트림을 얻으려면 mapToInt, mapToDouble, mapToLong 메서드를 사용하세요.

int sum = products.stream()
    .mapToInt(Product::getPrice)
    .sum();
System.out.println("가격 총합: " + sum);

메서드

  • sum() — 모든 요소의 합.
  • average() — 산술 평균(OptionalDouble을 반환).
  • min(), max() — 최솟값과 최댓값(OptionalInt/OptionalDouble/OptionalLong을 반환).

예시:

// 모든 가격의 합
int total = products.stream()
    .mapToInt(Product::getPrice)
    .sum();

// 평균 가격
OptionalDouble avg = products.stream()
    .mapToInt(Product::getPrice)
    .average();

// 최솟값과 최댓값
OptionalInt min = products.stream()
    .mapToInt(Product::getPrice)
    .min();

OptionalInt max = products.stream()
    .mapToInt(Product::getPrice)
    .max();

Optional에서 값을 꺼내는 방법

double average = avg.orElse(0.0); // 컬렉션이 비어 있으면 0.0
int minValue = min.orElse(-1);    // 컬렉션이 비어 있으면 -1
int maxValue = max.orElse(-1);    // 컬렉션이 비어 있으면 -1

프로그래머 농담: 빈 리스트는 오류가 아니라 데이터가 없을 뿐입니다. 하지만 비어 있는 Optional에서 getAsInt()를 호출하면 NoSuchElementException을 만나게 됩니다. 항상 orElseisPresent()를 사용하세요!

3. 객체 집계: Collectors.summingInt, averagingInt, maxBy, minBy

객체 스트림에서 어떤 기준으로 값을 집계하려면 Collectors의 전용 컬렉터를 사용하세요.

summingInt, averagingInt

int totalSalary = employees.stream()
    .collect(Collectors.summingInt(Employee::getSalary));

double avgSalary = employees.stream()
    .collect(Collectors.averagingInt(Employee::getSalary));

minBy, maxBy

필드 값이 최댓값/최솟값인 객체를 찾으려면:

Optional<Employee> richest = employees.stream()
    .collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)));

Optional<Employee> poorest = employees.stream()
    .collect(Collectors.minBy(Comparator.comparingInt(Employee::getSalary)));

중요: 결과는 Optional입니다. 컬렉션이 비어 있을 수 있기 때문입니다.

예시

Product 클래스를 만들어 봅시다:

public class Product {
    private final String name;
    private final int price;
    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }
    public String getName() { return name; }
    public int getPrice() { return price; }
}

제품 목록을 만듭니다:

List<Product> products = List.of(
    new Product("우유", 80),
    new Product("치즈", 250),
    new Product("빵", 40)
);

가장 비싼 제품을 찾아봅시다:

Optional<Product> maxProduct = products.stream()
    .collect(Collectors.maxBy(Comparator.comparingInt(Product::getPrice)));

maxProduct.ifPresent(p -> System.out.println("가장 비싼 제품: " + p.getName()));

4. 그룹화와 결합하기

집계 메서드는 그룹화와 함께 자주 사용됩니다. 예를 들어, 이름의 첫 글자별로 제품의 평균 가격을 구할 수 있습니다:

Map<Character, Double> avgPriceByLetter = products.stream()
    .collect(Collectors.groupingBy(
        p -> p.getName().charAt(0),
        Collectors.averagingInt(Product::getPrice)
    ));

System.out.println(avgPriceByLetter);

결과:

{우=80.0, 치=250.0, 빵=40.0}

5. 실전 예시

예시 1: 학생과 점수

학생 목록과 점수:

public class Student {
    private final String name;
    private final int score;
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    public String getName() { 
        return name; 
    }
    public int getScore() { 
        return score; 
    }
}

List<Student> students = List.of(
    new Student("Alisa", 90),
    new Student("Bob", 75),
    new Student("Vasya", 100)
);

점수가 80보다 큰 학생은 몇 명일까요?

long count = students.stream()
    .filter(s -> s.getScore() > 80)
    .count();
System.out.println("점수가 > 80인 학생 수: " + count);

평균 점수:

double avg = students.stream()
    .mapToInt(Student::getScore)
    .average()
    .orElse(0.0);
System.out.println("평균 점수: " + avg);

가장 성적이 높은 학생:

students.stream()
    .max(Comparator.comparingInt(Student::getScore))
    .ifPresent(s -> System.out.println("가장 성적이 높은 학생: " + s.getName()));

예시 2: 고유 상품 개수

long uniqueCount = products.stream()
    .map(Product::getName)
    .distinct()
    .count();
System.out.println("고유 상품 수: " + uniqueCount);

6. 유용한 팁

시각적 흐름: 집계 메서드가 동작하는 방식

객체 컬렉션
   │
   ▼
stream()
   │
   ▼
mapToInt/Double/Long (필요하다면)
   │
   ▼
sum() / average() / min() / max() / count()
   │
   ▼
결과 (int, double, long, Optional)

Optional 처리: 안전하게 값 꺼내기

왜 메서드가 Optional을 반환할까요?
컬렉션이 비어 있을 수 있습니다(예: 빈 리스트에서 최댓값을 찾는 경우). 이때는 빈 Optional이 반환됩니다. 바로 getAsInt()/get()을 호출하면 예외가 던져집니다.

올바른 방법:

  • orElse(기본값) 사용
  • 값이 있을 때만 동작하도록 ifPresent(...) 사용
  • 명시적으로 예외를 던지려면 orElseThrow(...) 사용

예시:

OptionalDouble avg = products.stream()
    .mapToInt(Product::getPrice)
    .average();

System.out.println("평균 가격: " + avg.orElse(0.0));

프리미티브 스트림을 언제 쓰고, Collectors를 언제 쓸까

  • 숫자 필드의 합/평균/최소/최대만 필요하다면 mapToInt와 해당 메서드(sum, average, min, max)를 사용하세요.
  • 객체 자체를 집계하고 싶다면(예: 특정 필드가 최대인 객체 찾기) maxBy/minBy 컬렉터를 사용하세요.
  • 그룹별로 집계를 계산하려면 groupingBy와 집계 컬렉터를 조합하세요.

7. 집계 메서드 사용 시 흔한 실수

오류 №1: Optional을 처리하지 않음. 초보자는 min, max, averageOptional을 반환한다는 사실을 잊기 쉽습니다. 빈 컨테이너에서 값을 꺼내려 하면 NoSuchElementException이 발생합니다. 항상 orElse, orElseThrow 또는 ifPresent를 사용하세요.

오류 №2: 일반 stream을 mapToInt/mapToDouble 대신 사용. stream().sum()처럼 작성하면 컴파일러가 "그런 메서드는 없습니다!"라고 말할 것입니다. 합계/평균/최소/최대에는 프리미티브 스트림이 필요합니다.

오류 №3: 비교 시 타입 불일치. 객체에서 최댓값/최솟값을 찾을 때는 올바른 비교자를 사용하세요: Comparator.comparingInt(Employee::getSalary). 그렇지 않으면 컴파일 오류나 잘못된 결과가 나올 수 있습니다.

오류 №4: 객체를 null과 비교. 컬렉션에 null 요소가 있을 수 있다면 집계 중에 NullPointerException이 발생할 수 있습니다. 먼저 그런 요소를 필터링하거나 Comparator.nullsLast(...)/nullsFirst(...)를 사용하세요.

오류 №5: 컬렉션이 비어 있을 때 sum/average/min/max의 결과를 고려하지 않음. 컬렉션이 비어 있으면 sum()0을 반환하고, average()/min()/max()는 빈 Optional을 반환합니다. 이 경우를 반드시 처리하세요.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION