[Java] 심화 - 함수형프로그래밍(StreamAPI), Optional 안전처리

이지연·2025년 12월 22일

개요

아래의 내용은 java_grammer 레파지토리 C05AnonymousLambda/C04StreamApi.java 디렉터리에 저장되어있는 내용을 정리함

  • C04StreamApi
    전통 for문 → Stream API → Optional 안전처리 워크플로우

함수형 프로그래밍

설명: 데이터와 객체 중심이 아닌, 입력에 따른 출력만 존재하는 코딩 스타일

  • 입력: 원본에서 꺼내는 행위 → Arrays.stream(arr)
  • 출력: 물리적 출력 + 연산 행위 포함 → forEach, map, sum

Stream API 특징

  • forEach : 스트림 요소를 하나씩 소모하며 동작 수행
  • Arrays.stream(arr) : 원본 → 사본 생성 (원본 보존)
  • 람다표현식 : (a) -> a * 2

핵심 원칙

  • 입력(원본꺼내기) → 출력(연산/출력) 사상만 존재
  • 원본 보존
  • 순수 함수형 처리 (부작용 없음)

전통 vs 함수형 접근

전통적인 for문 접근

int[] arr = {20, 10, 4, 12};

for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);  // 원본 수정 가능
}

함수형 접근

Arrays.stream(arr)                     // 입력: 원본 → 사본
      .forEach(a -> System.out.println(a));  // 출력: 순수 처리
// 원본 arr 그대로 보존됨

Stream API 동작 흐름

생성 → 중개연산 → 소모연산

1. 스트림 생성

List<String> myList = Arrays.asList("java", "python", "c++");
Stream<String> stream1 = myList.stream();

String[] strArr = {"java", "python", "c++"};
Stream<String> stream2 = Arrays.stream(strArr);

int[] intArr = {10, 10, 20, 30, 40, 50};
IntStream stream3 = Arrays.stream(intArr); // 원시 타입 특화

2. 중개연산 (연쇄 호출, 새 스트림 반환)

filter – 조건 필터링

IntStream filterStream = Arrays.stream(intArr)
                               .filter(a -> a >= 30);

map – 값 조작

IntStream mapStream = Arrays.stream(intArr)
                            .map(a -> a * a);

distinct – 중복 제거

IntStream distinctStream = Arrays.stream(intArr).distinct();

sorted – 정렬

IntStream sortedStream = Arrays.stream(intArr).sorted();

mapToInt – 문자열 길이 변환

IntStream mapToIntStream = Arrays.stream(strArr)
                                 .mapToInt(a -> a.length());  // [5,6,3]

3. 소모연산 (한 번 실행하면 스트림 소멸)

System.out.println("30이상 필터링 → sum : " + filterStream.sum());
System.out.println("제곱 조작 → sum : " + mapStream.sum());
System.out.println("중복 제거 → sum : " + distinctStream.sum());
System.out.println("정렬 후 배열화 → " + Arrays.toString(sortedStream.toArray()));

Arrays.stream(intArr).forEach(a -> System.out.println("forEach : " + a));
System.out.println("count : " + Arrays.stream(intArr).count());
System.out.println("max/min : " + Arrays.stream(intArr).max().getAsInt() + ", " +
                                    Arrays.stream(intArr).min().getAsInt());

System.out.println("누적합 : " + Arrays.stream(intArr).reduce(0, (a,b)->a+b));
System.out.println("누적곱 : " + Arrays.stream(intArr).reduce(1, (a,b)->a*b));
System.out.println("문자열 누적합 : " + Arrays.stream(strArr).reduce("", (a,b)->a+b));

System.out.println("첫번째(조건) : " + 
    Arrays.stream(strArr).filter(a->a.length()>=5).findFirst().get());

Map 활용

Map<String, Integer> myMap = new TreeMap<>();
myMap.put("hello", 10);
myMap.put("java", 20);

System.out.println("TreeMap 첫 key : " + 
    myMap.keySet().stream().findFirst().get());

중개연산 정리표

메서드설명예제결과 (intArr = {10,10,20,30,40,50})
filter조건 필터링.filter(a -> a >= 30)[30,40,50]
map값 조작.map(a -> a * a)[100,100,400,900,1600,2500]
distinct중복 제거.distinct()[10,20,30,40,50]
sorted정렬.sorted()[10,10,20,30,40,50]
mapToInt문자열 → IntStream.mapToInt(a -> a.length())[5,6,3]

소모연산 정리표

메서드설명예제결과
sum요소 총합.sum()120
toArray배열 변환.toArray()[10,10,20,30,40,50]
forEach요소 출력.forEach(...)개별 출력
count요소 개수.count()6
max최대값.max().getAsInt()50
min최소값.min().getAsInt()10
reduce누적합.reduce(0,(a,b)->a+b)160
reduce누적곱.reduce(1,(a,b)->a*b)3000000
reduce문자열 결합.reduce("",(a,b)->a+b)"javapythonc++"
findFirst첫 요소.findFirst().get()"java"

배열 변환 주의사항

문제:
Stream<String>.toArray()Object[] 반환 → 타입 에러 발생

String[] stArr = {"hello", "java", "world"};
// Arrays.stream(stArr).filter(a->a.length()>=5).toArray();  // 에러!

IntStream은 OK

int[] result = Arrays.stream(intArr).filter(a->a>=30).toArray();

해결법

// 방법1: 생성자 참조 (추천)
String[] answer = Arrays.stream(stArr)
                        .filter(a -> a.length() >= 5)
                        .toArray(String[]::new);

// 방법2: 람다식
String[] answer2 = Arrays.stream(stArr)
                         .filter(a->a.length()>=5)
                         .toArray(a->new String[a]);

출력:

[hello, world]

메소드 참조(Method Reference)

Arrays.stream(stArr).forEach(a -> System.out.println(a));  // 람다
Arrays.stream(stArr).forEach(System.out::println);         // 메소드 참조

실습예제 1: 배열 처리

홀수 요소만 추출

int[] oddArr = Arrays.stream(arrEle)
                     .filter(a->a%2!=0)
                     .toArray();

출력: [1,3,5]

홀수 제곱 배열

int[] oddSquareArr = Arrays.stream(arrEle)
                           .filter(a->a%2!=0)
                           .map(a->a*a)
                           .toArray();

출력: [1,9,25]

홀수 제곱 정렬

int[] oddSortedArr = Arrays.stream(arrEle)
                           .filter(a->a%2!=0)
                           .map(a->a*a)
                           .sorted()
                           .toArray();

출력: [1,9,25]


실습예제 2: Student 객체 처리

리스트 생성

List<Student> studentsList = new ArrayList<>();
studentsList.add(new Student("kim", 20));
studentsList.add(new Student("choi", 30));
studentsList.add(new Student("lee", 35));
studentsList.add(new Student("park", 22));

평균나이

double avgAge = studentsList.stream()
                            .mapToInt(Student::getAge)
                            .average()
                            .getAsDouble();

출력: 26.75

최연소 학생

Student youngest = studentsList.stream()
                               .sorted((o1,o2)->o1.getAge()-o2.getAge())
                               .findFirst().get();

출력: kim(20)

30대 이름 배열

String[] age30List = studentsList.stream()
                                 .filter(a->a.getAge()>=30)
                                 .map(Student::getName)
                                 .toArray(String[]::new);

출력: [choi, lee]


Optional 객체

문제 상황:

String st1 = br.readLine();
if (st1 == null) st1.length(); // NPE 위험

Optional 생성 (3가지)

1. Optional.empty() — 완전한 빈 Optional 생성

Optional<String> opt1 = Optional.empty();
  • 아무 값도 가지지 않는 Optional 객체 생성
  • 보통 "값이 없음"을 표현할 때 사용

2. Optional.ofNullable(value) — null 가능성 있는 값 처리

Optional<String> opt2_1 = Optional.ofNullable(null);
Optional<String> opt2_2 = Optional.ofNullable("hello");
  • null일 수도, 아닐 수도 있는 값을 안전하게 감싸는 방법
  • null이면 자동적으로 빈 Optional 생성 (Optional.empty()와 같음)
  • null이 아니면 내부에 값("hello")을 저장

3. Optional.of(value) — null 불허

Optional<String> opt3 = Optional.of("hello");
  • null을 넣으면 NullPointerException 발생
  • 반드시 값이 존재한다고 보장될 때만 사용

Optional 처리 (4가지)

1. isPresent

if (opt1.isPresent()) System.out.println(opt1.get());
else System.out.println("값이 없습니다.");

2. orElse (기본값 제공)

System.out.println(opt2_1.orElse("값이 없습니다."));

3. orElseGet (지연 실행)

System.out.println(opt2_1.orElseGet(() -> "값이 없습니다."));

4. orElseThrow (예외 발생)

System.out.println(opt2_2.orElseThrow(
    () -> new RuntimeException("값이 없습니다.")));

실습예제 3: Optional 활용

평균 나이 안전처리

OptionalDouble optionalAvg =
    studentsList.stream().mapToInt(Student::getAge).average();

if (optionalAvg.isPresent())
    System.out.println(optionalAvg.getAsDouble());
else
    System.out.println("값이 없습니다.");

orElseThrow 방식 (실무 표준)

double value = studentsList.stream()
                           .mapToInt(Student::getAge)
                           .average()
                           .orElseThrow(() -> new NoSuchElementException("값이 없습니다."));

동적 인덱스 조회

Scanner sc = new Scanner(System.in);
int num = Integer.parseInt(sc.nextLine());

Optional<Student> s1 = (num >= 0 && num < studentsList.size())
        ? Optional.of(studentsList.get(num))
        : Optional.empty();

System.out.println(s1.orElseThrow(
    () -> new NoSuchElementException("없는 회원입니다.")));

전체 워크플로우 정리

단계메서드역할
생성stream(), Arrays.stream()원본 → 사본 스트림 생성
중개filter, map, distinct, sorted연쇄 변환 (지연평가 방식)
소모forEach, sum, reduce, findFirst, toArray실제 결과 도출 (한 번만 실행)

핵심 원칙:
원본 보존 + 순수 함수형 처리 + Comparator/람다 연계

profile
Eazy하게

0개의 댓글