[Codeit Review]/JCF와 Stream API

[05] Stream 추가 연산

uzlru 2026. 1. 13. 18:59

01. Optional Class

Optional<T>는 Java 8에서 도입된, null 안정성을 위한 래퍼 클래스이다. 그렇기에 Optional를 사용하면 NullPointerException(NPE)를 방지하기 위해 null 값을 if 문을 통해 처리했던 과거와 달리, 객체 차원에서 NPE를 방지할 수 있다. 즉, Optional은 값이 존재할 수도 있고, 존재하지 않을 수도 있는 컨테이너이다.

 

1. 기본 구조

public final class Optional<T> {
    private final T value;
}

 

2. 객체 생성 방법

// 1. of()와 ofNullable()을 통한 객체 생성
Optional<String> opt1 = Optional.ofNullable(null);     // null도 허용
Optional<String> opt2 = Optional.ofNullable("123");

// 2. empty()를 통한 빈 객체 생성
Optional<String> opt3 = Optional.<String>empty();       // 명시적 빈 객체 생성

 

3. 값 조회 및 디폴트 처리

// 1. isPresent(): 값 존재 여부 확인
System.out.println(opt1.isPresent());

// 2. get(): 값 가져오기
Optional<String> optString = Optional.of("java");
System.out.println(optString.get());

// 3. orElse(): 기본 값 지정
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("kimcoding");
System.out.println(name);

 

4. 메서드 체이닝

Optional 클래스는 스트림처럼 메서드 체이닝으로 사용할 수 있어 가독성이 좋고, Null-Safe한 코드 작성이 가능하다.

// 1. 단일 메서드 체이닝 예제
List<String> languages = Arrays.asList("Ruby", "Python", "Java", "Go", "Kotlin");
Optional<List<String>> listOptional = Optional.of(languages);

int size = listOptional
        .map(List::size)
        .orElse(0);
        
System.out.println(size);

// 2. 중첩 메서드 체이닝 예제
// CarClub.java
public class CarClub {
    public static void main(String[] args) {
        MemberService memberService = new MemberService();
        String insuranceName =
                memberService
                        .getMember("jungmin")
                        .flatMap(Member::getCar)
                        .flatMap(Car::getInsurance)
                        .map(Insurance::getCompanyName)
                        .orElse("Not result");
        System.out.println(insuranceName);
    }
}

 

5. 익명 클래스 관련 예제

// MemberService.java
public class MemberService {
    public Optional<Member> getMember(String id) {
        Optional<Insurance> insurance = Optional.of(new Insurance("Samsung direct"));
        Optional<Car> car = Optional.of(new Car("XC-60", 60_000_000, insurance));
        return Optional.of(new Member("Jungmin", "jungmin0102", car));
    }
}

// Member.java
public class Member {
    private String name;
    private String id;
    private Optional<Car> car;

    public Member(String name, String id, Optional<Car> car) {
        this.name = name;
        this.id = id;
        this.car = car;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Optional<Car> getCar() {
        return car;
    }

    public void setCar(Optional<Car> car) {
        this.car = car;
    }
}

// Car.java
public class Car {
    private String carName;
    private int price;
    private Optional<Insurance> insurance;

    public Car(String carName, int price, Optional<Insurance> insurance) {
        this.carName = carName;
        this.price = price;
        this.insurance = insurance;
    }

    public String getCarName() {
        return carName;
    }

    public void setCarName(String carName) {
        this.carName = carName;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public Optional<Insurance> getInsurance() {
        return insurance;
    }

    public void setInsurance(Optional<Insurance> insurance) {
        this.insurance = insurance;
    }
}

// Insurance.java
public class Insurance {
    private String companyName;

    public Insurance(String companyName) {
        this.companyName = companyName;
    }

    public String getCompanyName() {
        return companyName;
    }

    public void setCompanyName(String companyName) {
        this.companyName = companyName;
    }
}

 

02. 그룹화와 분할

Stream API에서는 collect() 연산과 함께 groupingBy(), partitioningBy()를 사용하여, 데이터를 그룹화 및 조건 분할할 수 있다.

 

함수 메서드   반환 타입
단순 그룹화 groupingBy() 데이터를 특정 기준으로 그룹핑할 때 사용
>> SQL의 GROUP BY 절과 유사
Map<그룹 키, List<T>>
그룹화 후 변환 groupingBy() + mapping() 그룹화와 동시에 요소를 특정 값으로 추출 / 변환하여 저장  
다중 그룹화 groupingBy() 중첩 사용 2단계 기준으로 서브 그룹화할 때 사용
Map<1차Key, Map<2차Key, List<T>>>
조건 분할 parititioningBy() 데이터를 true / false 기분으로 두 개의 그룹으로 분할 Map<boolean, List<T>>

 

public class Student {

    private String name;
    private String gender;
    private int grade;
    private int score;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public int getGrade() {
        return grade;
    }

    public void setGrade(int grade) {
        this.grade = grade;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", grade=" + grade +
                ", score=" + score +
                '}';
    }
}

List<Student> studentList = Arrays.asList(
		new Student("홍길동", "남", 1, 95),
		new Student("강감찬", "남", 2, 80),
		new Student("이순신", "남", 1, 85),
		new Student("심사임당", "여", 2, 90),
		new Student("유관순", "여", 3, 100),
		new Student("문성실", "여", 1, 88)
);

/* 1. groupingBy() - 단순 그룹화 */
// 1-1. 학년별로 그룹화
System.out.println("----- groupingBy(학년) -----");
Map<Integer, List<Student>> gradeGroupMap = studentList.stream()
		.collect(Collectors.groupingBy(Student::getGrade));

gradeGroupMap.forEach((grade, students) -> {
	System.out.println("[ " + grade + "학년 ]");
	students.forEach(student -> System.out.println(student));
});
System.out.println("--------------------------------");

// 1-2. 성별로 그룹화
System.out.println("----- groupingBy(성별) -----");
Map<String, List<Student>> genderGroupMap = studentList.stream()
		.collect(Collectors.groupingBy(Student::getGender));

genderGroupMap.forEach((gender, students) -> {
	System.out.println("[ " + gender + "학생 ]");
	students.forEach(student -> System.out.println(student));
});
System.out.println("--------------------------------");

/* 2. groupingBy() - 그룹화 후 매핑 */
// 2-1. 성별로 그룹화하고 학생 이름만 추출
System.out.println("----- groupingBy() - 그룹화 후 매핑 -----");
Map<String, List<String>> genderNameMap2 = studentList.stream()
		.collect(Collectors.groupingBy(
				Student::getGender,
				Collectors.mapping(Student::getName, Collectors.toList())
		));

System.out.println("성별로 그룹화: " + genderNameMap2);
System.out.println("--------------------------------");

/* 3. partitioningBy() - 분할 */
// 3-1. 90점 이상인 학생과 아닌 학생 분할
System.out.println("----- partitioningBy -----");
Map<Boolean, List<Student>> scorePartition = studentList.stream()
		.collect(Collectors.partitioningBy(student -> student.getScore() >= 90));

System.out.println("==== 90점 이상 학생 ====");
scorePartition.get(true).forEach(System.out::println);
System.out.println("==== 90점 미만 학생 ====");
scorePartition.get(false).forEach(System.out::println);
System.out.println("--------------------------------");

/* 4. 다중 레벨 그룹화 */
// 4-1. 학년별로 그룹화 후 성별로 다시 그룹화
System.out.println("----- 다중 레벨 그룹화 -----");
Map<Integer, Map<String, List<Student>>> gradeAndGenderMap = studentList.stream()
		.collect(Collectors.groupingBy(
				Student::getGrade,
				Collectors.groupingBy(Student::getGender)
		));

gradeAndGenderMap.forEach((grade, genderMap) -> {
	System.out.println("[ " + grade + "학년 ]");
	genderMap.forEach((gender, students) -> {
		System.out.println("- " + gender + "학생: ");
		students.forEach(student -> 
			System.out.println("  " + student.getName() + " (" + student.getScore() + "점)")
		);
	});
});
System.out.println("--------------------------------");

 


 

03. 통계 및 집계

  메서드   반환 값
합계 sum() 스트림 내 숫자를 전부 더함  
평균 average() 스트림 내 존재하는 모든 숫자의 평균 계산 OptionalDouble
최댓값 max() 최댓값을 반환 OptionalInt
최솟값 min() 최솟값을 반환 OptionalInt
결합 reduce() 스트림의 요소들을 하나의 값으로 결합
>> 매개변수의 개수에 따라 3가지 종류로 나뉨
 
전체 계산 수행 summarizingInt() 합계, 평균, 최대 / 최소를 한 번에 계산
>> 자료형에 따라 Int / Double / Long으로 구분
 

 

// 1. sum 예제
int totalScore = studentList.stream()
    .mapToInt(Student::getScore)
    .sum();

System.out.println("총점: " + totalScore);

// 2. average 예제
double average = studentList.stream()
    .mapToInt(Student::getScore)
    .average()
    .orElse(0.0); 				// OptionalDouble 처리

System.out.println("평균 점수: " + average);

// 3. max 예제
int maxScore = studentList.stream()
    .mapToInt(Student::getScore)
    .max()
    .orElse(0);

System.out.println("최고 점수: " + maxScore);

// 4. min 에제
int minScore = studentList.stream()
    .mapToInt(Student::getScore)
    .min()
    .orElse(0);

System.out.println("최저 점수: " + minScore);

// 5. reduce 예제
int total = studentList.stream()
    .map(Student::getScore)
    .reduce(0, (a, b) -> a + b);

System.out.println("총합 (reduce): " + total);

// 5-1. 인자가 1개일 경우
OptionalInt reduceOneParam = IntStream.range(1, 4)		// 1, 2, 3
		.reduce((a, b) -> {
			return Integer.sum(a, b);
		});

System.out.println("reduceOneParam = " + reduceOneParam.getAsInt());

// 5-2. 인자가 2개일 경우
int reduceTwoParam = IntStream.range(1, 4)		// 1, 2, 3
		.reduce(100, Integer::sum);

System.out.println("reduceTwoParam = " + reduceTwoParam);

// 5-3. 인자가 3개일 경우
Integer reduceThreeParam = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
		.reduce(100,
				Integer::sum,
				(x, y) -> x + y);

System.out.println("reduceThreeParam = " + reduceThreeParam);

// 6-1. summarizingInt() 예제
IntSummaryStatistics stats = studentList.stream()
    .collect(Collectors.summarizingInt(Student::getScore));

System.out.println("총합: " + stats.getSum());
System.out.println("평균: " + stats.getAverage());
System.out.println("최대: " + stats.getMax());
System.out.println("최소: " + stats.getMin());

// 6-2. summarizingDouble() 예제
DoubleSummaryStatistics stats = studentList.stream()
    .collect(Collectors.summarizingDouble(Student::getScore));

// 6-3. summarizingLong() 예제
LongSummaryStatistics stats = List.of(100L, 200L, 300L).stream()
    .collect(Collectors.summarizingLong(Long::longValue));

System.out.println("합계: " + stats.getSum());
System.out.println("평균(소수 포함): " + stats.getAverage());

 

int[] intArray = {1,2,3,4,5};

long count = Arrays.stream(intArray).count();
long sum = Arrays.stream(intArray).sum();
double average = Arrays.stream(intArray).average().getAsDouble();
int max = Arrays.stream(intArray).max().getAsInt();
int min = Arrays.stream(intArray).min().getAsInt();
int first = Arrays.stream(intArray).findFirst().getAsInt();

OptionalDouble avgOptional = Arrays.stream(intArray).average();
double result = avgOptional.getAsDouble();
System.out.println("전체 요소의 평균값: " + result);