It's going to be one day 🍀

안녕하세요! 매일 매일 공부하려고 노력하는 백엔드 개발자 지망생의 공부 흔적입니다.

Back-End/Java # 교육

[Java 교육] 스트림 Optional/생성/가공/결과 (다시 복습 필수)

2jin2 2024. 2. 20. 17:37

[공부 내용 정리]

매핑 (map, flatMap)

map : 스트림의 요소를 하나씩 특정 값으로 변환하는 메소드임. 이렇게 변환된 값은 새로운 스트림으로 만들어짐.

List<String> list = Arrays.asList("a", "b", "c", "d", "e");
        // 하나씩 대문자로 변환 가능
        list.stream().map(x -> x.toUpperCase()).forEach(System.out::println);

 

flatMap : map과 마찬가지로 스트림의 요소들을 다른 값으로 대체하는 것은 같지만, 대체하는 값이 스트림일 경우에 flatMap을 사용함. 2차원, 2단계 배열 또는 List 타입에 대해서 일괄적으로 하나의 Stream에서 연산할 수 있도록 함.

  • flatMap은 한 차원씩 배열을 평면화함.

List<String> list = Arrays.asList("Hello World", "Java stream");

// [Hello, World, Java, stream]
list.stream()
	.flatMap(str -> Arrays.stream(str.split(" ")))
	.forEach(System.out::println);

정렬 (sorted)

sorted : 스트림의 요소들을 정렬하기 위해 사용하는 메소드. 오름차순 정렬 기본

List<Integer> list = Arrays.asList(12, 4, 2, 8, 11);

// [2, 4, 8, 11, 12]
list.stream()
	.sorted()
	.forEach(System.out::println);
List<String> list = Arrays.asList("e", "a", "c", "z", "d");

// [a, c, d, e, z]
list.stream()
	.sorted()
	.forEach(System.out::println);
  • 역순으로 (내림차순) 정렬하고 싶다면 Comparator 사용하면 됨.
  • Comparator.reverseOrder()
List<String> list = Arrays.asList("e", "a", "c", "z", "d");

// [z, e, d, c, a]
list.stream()
	.sorted(Comparator.reverseOrder())  // 역순 정렬
	.forEach(System.out::println);

루핑 (peek, forEach)

peek : “가공” 단계에서 사용. 그냥 확인해본다는 단어 뜻처럼 특정 결과를 반환하지 않음. 작업 처리 중간에 결과를 확인할 때 사용할 수 있음.

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

int sum = list.stream()
			.mapToInt(n -> n)
			.filter(n -> n % 2 == 0)
			.peek(n -> System.out.println(n))   // [2, 4]
			.sum();  // sum을 사용하지 않으면 peek은 동작하지 않음
			
System.out.println(sum);    // 6

 

forEach : “결과” 단계에서 사용. 따라서 다른 결과 메소드와 중복해서 사용할 수 없음.

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

list.stream()
	.mapToInt(n -> n)
	.filter(n -> n % 2 == 0)
	.forEach(n -> System.out.println(n));   // [2, 4]

수집 (collect())

collect : 스트림의 최종 단계에서 사용하는 메소드. 스트림으로 가공한 데이터를 최종적으로 컬렉션으로 변환한다고 보면 됨. 자주 사용하는 작업은 Collectors 클래스에서 제공함.

ex) 리스트의 숫자 중에서 짝수만 골라내어 새로운 리스트를 만드는 예제

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

// 짝수만 골라내어 새로운 리스트로 collect
List<Integer> newList = list.stream()
				.filter(n -> n % 2 == 0)
				.collect(Collectors.toList());
							
newList.stream().forEach(System.out::println);  // [2, 4]

 

그룹핑하여 수집

collect 메소드는 컬렉션의 요소들을 그룹핑해서 Map 객체를 생성하는 기능도 제공함. Collectors의 groupingBy 사용

- groupingBy 의 예시 (Collecotrs.counting()은 그룹핑 후 집계하는 메소드!!)

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

Map<Integer, Long> result = list.stream()
			.collect(
				Collectors.groupingBy(
					n -> n,
					Collectors.counting()  // 그룹핑 후 집계하는 메소드
					)
				);

/*
 * 1: 2개
 * 2: 3개
 * 3: 1개
 * 4: 1개
 * 5: 3개
 */
result.forEach((key, value) -> System.out.println(key + ": " + value + "개"));

- groupingBy의 첫 번째 파라미터는 그룹핑을 위한 키 값임. n -> n은 요소 값 그 자체를 키로 사용한다는 의미임. (?)

- 두 번째 파라미터는 집계 함수임. 그룹핑 후 어떻게 집계할 것인지 정할 수 있는데, 여기서는 그룹 별 카운트를 함.


복습 문제 ) Member에 있는 요소들을 Person으로 옮기기

package chapter13;

public class Member {
    private String name;
    private int age;
    private String color;

    public Member(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getColor() {
        return color;
    }
}
package chapter13;

public class Person {
    private String name;
    private int age;

    public Person (String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\\'' +
                ", age=" + age +
                '}';
    }
}
package chapter13;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Quiz {
    public static void main(String[] args) {
        List<Member> memberList = Arrays.asList(
                new Member("LHJ", 23, "skyblue"),
                new Member("LLL", 24, "red")
    );

        List<Person> personList = memberList.stream()
                .map(member -> new Person(member.getName(), member.getAge()))
                .collect(Collectors.toList());
        System.out.println(personList);
    }
}

Optional

NullPointerException

// student 객체가 null일 경우 NullPointerException 발생
Student student = null;
// null 값에 getName이라는 메소드는 존재하지 않기 때문
student.getName();

위 예시처럼 null 값을 가진 객체를 참조하려고 했을 때 NPE가 발생함.

 

Optional이란?

NPE를 방지할 수 있도록하는 Optional 클래스.

아래와 같이 Optional로 컬렉션의 null 여부를 체크하면 에러를 방지할 수 있음.

List<Integer> list = null;

Optional.ofNullable(list)
	.orElseGet(Collections::emptyList)
	.forEach(System.out::println);

 

Optional 객체 생성 메소드

Optional 클래스는 of와 ofNullable 메소드를 지원함.

- of : null 값을 허용하지 않음.

- ofNullable : null 값을 허용함.

// 비어있는 Optional 객체 반환
// null 대신 빈 Optional<T> 객체 사용
Optional<String> empty = Optional.empty();

// null이 아닌 값을 갖는 Optional 객체 반환
Optional<String> of = Optional.of("Optional 객체");

// null을 허용하는 Optional 객체 반환
Optional<String> nullableOpt = Optional.ofNullable("Nullable Optional 객체");

 

아래 예시에서는 Optional.ofNullable을 사용했기 때문에 list가 null 임에도 불구하고 에러가 발생하지 않음.

List<Integer> list = null;

Optional.ofNullable(list)
	.orElseGet(Collections::emptyList)
	.forEach(System.out::println);

+) orElseGet 메소드도 Optional에서 제공하는데, 만약 list가 null일 경우 어떤 값을 사용할 것인지 정할 수 있음.

여기서는 list가 null일 경우 비어있는 리스트(emptyList)를 사용함.

 

Optional 객체 반환

기본적으로 Optional 클래스의 get() 메서드를 사용하여 저장된 값을 반환하지만,

만약 Optional 객체가 null 일 경우엔 예외가 발생함. 따라서 안전하게 객체를 꺼내오기 위해서는 null일 경우 사용할 default 값을 정해줌.

→ 이때 사용하는 메소드가 orElse(), orElseGet() 메소드임.

Optional.ofNullable(stringList)
         .get()      // stringList가 null일 경우 NoSuchElementException 예외 발생!!
List<String> newList = Optional.ofNullable(list)
	.orElse(Collections.emptyList());

 

List<String> newList = Optional.ofNullable(list)
	.orElseGet(Collections::emptyList);  // 람다 표현식을 넣는 경우 

→ 보통 get() 보단 예외 처리를 해주는 안전한 orElse(), orElseGet() 를 더 많이 사용함.

 

 

Optional을 이용한 Null 처리 방법 예시

아래의 예시는 컬렉션이 null은 아니지만 아무런 값도 가지고 있지 않은 경우이다.

List<Integer> list = new ArrayList<>();

// NoSuchElementException 예외 발생
double avg = list.stream()
		.mapToInt(Integer::intValue)
		.average()
		.getAsDouble();
				
System.out.println(avg);

 

예외를 피하기 위한 첫 번째 방법은 Optional 객체를 얻어 값이 실제로 존재할 때만 평균을 출력하는 것이다. Optional의 isPresent는 값이 존재하는지 여부를 반환하는 메소드임.

List<Integer> list = new ArrayList<>();

OptionalDouble optional = list.stream()
				.mapToInt(Integer::intValue)
				.average();

// 값이 실제로 존재하는지 체크
if (optional.isPresent()) {
	System.out.println(optional.getAsDouble());
}

 

두 번째 방법은 값이 없을 경우 디폴트 값을 정하는 Optional의 orElse를 사용함.

List<Integer> list = new ArrayList<>();

double avg = list.stream()
		.mapToInt(Integer::intValue)
		.average()
		.orElse(0.0);  // 평균 값이 존재하지 않을 경우 0을 사용
				
System.out.println(avg);

 

세 번째 방법스트림에서 ifPresent 메소드를 사용하는 것임. Optional의 ifPresent에는 값이 실제로 존재할 경우에만 동작하는 코드를 작성할 수 있음.

List<Integer> list = new ArrayList<>();

list.stream()
	.mapToInt(Integer::intValue)
	.average()
	.ifPresent(avg -> System.out.println(avg));  // 평균 값이 있을 경우에만 출력

Daily Quiz

💡 스트림과 컬렉션을 사용하여 코드를 작성해주세요.

스트림으로 해결한 버전과, 스트림으로 해결하지 않은 버전 모두 작성해주세요.

1. 큰 수 출력하기

N(1<=N<=100)개의 정수를 입력받아, 자신의 바로 앞 수보다 큰 수만 출력하는 프로그램을 작성하세요. (첫 번째 수는 무조건 출력합니다)

**입력설명**
첫 줄에 자연수 N이 주어지고, 그 다음 줄에 N개의 정수가 입력됩니다.

**출력설명**
자신의 바로 앞 수보다 큰 수만 한 줄로 출력합니다.

**입력예제 1**
6
7 3 9 5 6 12

**출력예제 1**
7 9 6 12

<코드 작성>

public class PrintMoreBigNumber {
    public List<Integer> solution(int n, int[] array) {
        List<Integer> answer = new ArrayList<>();

        // 코드 작성

        return answer;
    }

    public static void main(String[] args) {
        PrintMoreBigNumber printMoreBigNumber = new PrintMoreBigNumber();
        Scanner scanner = new Scanner(System.in);

        int number = scanner.nextInt();
        int[] array = new int[number];

        for (int i = 0; i < number; i++) {
            array[i] = scanner.nextInt();
        }

        for (int x : printMoreBigNumber.solution(number, array)) {
            System.out.print(x + " ");
        }
    }
}

 

→ 스트림 X

public class PrintMoreBigNumber {
    public List<Integer> solution(int n, int[] array) {
        List<Integer> answer = new ArrayList<>();
        answer.add(array[0]);
        for (int i = 1; i < n-1; i++) {
            if (array[i] < array[i+1]) {
                answer.add(array[i+1]);
            }
        }
        return answer;
    }
}

 

→ 스트림 O

List<Integer> answer = new ArrayList<>();
        // Stream
        answer.add(array[0]);
        List<Integer> collect = IntStream.range(1, n) // 1부터 n-1까지의 범위에서 스트림 생성
                .filter(index -> array[index] > array[index-1]) // 필터링으로 index를 뽑아줌
                .mapToObj(x -> array[x]) // 각 인덱스에 해당하는 배열의 요소를 가져와서 객체 스트림으로 변환
                .collect(Collectors.toList()); // 객체 스트림을 리스트로 변환
        answer.addAll(collect); // 기존 생성한 리스트에 앞서 생성한 collect 리스트의 모든 요소를 추가함.
        return answer;
  • mapToObj : IntStream의 요소를 객체 스트림으로 반환하는 역할을 함. 정수 x에 대해 배열 array에서 해당 인덱스의 값을 가져오는 람다식임. 일반적으로 mapToObj 는 기본 데이터 유형(int)을 박싱하여 해당 데이터 유형의 객체(Integer)로 매핑함.

 

2. 두 배열 합치기

오름차순으로 정렬이 된 두 배열이 주어지면 두 배열을 오름차순으로 합쳐 출력하는 프로그램을 작성해보세요.

**입력설명**
첫 번째 줄에 첫 번째 배열의 크기 N(1<=N<=100)이 주어집니다.
두 번째 줄에 N개의 배열 원소가 오름차순으로 주어집니다. 
세 번째 줄에 두 번째 배열의 크기 M(1<=M<=100)이 주어집니다.
네 번째 줄에 M개의 배열 원소가 오름차순으로 주어집니다. 
각 리스트의 원소는 int형 변수의 크기를 넘지 않습니다.

**출력설명**
오름차순으로 정렬된 배열을 출력합니다.

**입력예제 1** 
3
1 3 5
5
2 3 6 7 9

**출력예제 1**
1 2 3 3 5 6 7 9

<코드 작성>

public class AscendingSort {
		public int[] solution(int[] array1, int[] array2) {
				int[] result = new int[array1.length + array2.length];

				// 코드 작성

				return result;
		}

		public static void main(String[] args) {
        AscendingSort ascendingSort = new AscendingSort();
        Scanner scanner = new Scanner(System.in);

        int number = scanner.nextInt();
        int[] array = new int[number];

        for (int i = 0; i < number; i++) {
            array[i] = scanner.nextInt();
        }

        int number2 = scanner.nextInt();
        int[] array2 = new int[number2];

        for (int i = 0; i < number2; i++) {
            array2[i] = scanner.nextInt();
        }

        for (int x : ascendingSort.solution(array, array2)) {
            System.out.print(x + " ");
        }
    }
}

 

→ 스트림 X

public int[] solution(int[] array1, int[] array2) {
        int[] result = new int[array1.length + array2.length];
        /*
        System.arraycopy 메서드 : arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
        src (원본 배열): 복사할 대상이 되는 배열입니다.
        srcPos (원본 배열에서 복사를 시작할 위치): 원본 배열에서 복사를 시작할 인덱스입니다.
        dest (대상 배열): 복사된 요소가 저장될 대상 배열입니다.
        destPos (대상 배열에서 복사를 시작할 위치): 대상 배열에서 복사를 시작할 인덱스입니다.
        length (복사할 요소의 수): 복사할 요소의 개수입니다.
         */

        // array1 복사
        System.arraycopy(array1, 0, result, 0, array1.length);
        // array2 복사
        System.arraycopy(array2, 0, result, array1.length, array2.length);
        Arrays.sort(result);
        return result;
    }

 

+) 스트림X, for문으로

int[] result = new int[array1.length + array2.length];
for (int i = 0; i < array1.length; i++) {
            result[i] = array1[i];
        }
        for (int i = 0; i < array2.length; i++) {
            result[i+ array1.length] = array2[i];
        }
        Arrays.sort(result);
        return result;

 

+) 스트림X, while문으로

int[] result = new int[array1.length + array2.length];

        int i = 0;
        int j = 0;
        int index = 0;
        while(i < array1.length && j < array2.length) {
            if (array1[i] < array2[j]) {
                result[index] = array1[i];
                index++; i++;
            } else {
                result[index] = array2[j];
                index++; j++;
            }
        }
        while (i < array1.length) {
            result[index] = array1[i];
            index++; i++;
        }
        while (j < array2.length) {
            result[index] = array2[j];
            index++; j++;
        }
        return result;

 

→ 스트림 O