Java 코딩 테스트 준비 - Stream 과 Optional

기운찬곰·2023년 10월 2일
0
post-thumbnail

Overview

자바에서 Stream은 매우 중요한 개념이다. 자바 8에서 나온거 같은데... 요즘 이걸 모르면 자바를 한다고 할 수 없지. 그만큼 사용하면 매우 편리한 녀석이다. 다만... 알아야(외워야) 할 부분이 늘어난 점이...


Stream 이란

자바 8에서 추가한 스트림은 람다를 활용할 수 있는 기술 중 하나입니다. 스트림은 '데이터의 흐름’입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.

또 하나의 장점은 간단하게 병렬처리(multi-threading)가 가능하다는 점입니다. 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것을 병렬 처리라고 하는데, 즉 쓰레드를 이용해 많은 요소들을 빠르게 처리할 수 있습니다.

스트림에 대한 내용은 크게 세 가지로 나눌 수 있습니다.

  1. 생성하기 : 스트림 인스턴스 생성
  2. 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업
  3. 결과만들기 : 최종적으로 결과를 만들어내는 작업

Stream 생성하기

배열에서 스트림 생성하기

배열은 다음과 같이 Arrays.stream 메서드를 사용해서 생성할 수 있다.

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);

컬렉션에서 스트림 생성하기

컬렉션 타입(Collection, List, Set)의 경우 인터페이스에 추가된 디폴트 메소드 stream 을 이용해서 스트림을 만들 수 있습니다.

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림

기본 타입형 스트림

제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 다룰 수도 있습니다. 참고로, range와 rangeClosed는 잘 알아두면 좋습니다. ㅎㅎ

IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

제네릭을 사용하지 않기 때문에 불필요한 오토박싱(auto-boxing)이 일어나지 않습니다. 필요한 경우 boxed 메소드를 이용해서 박싱(boxing)할 수 있습니다.

Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed();

Stream 가공하기

Filtering

Stream을 만들었다면 그 다음부터는 참 많은 것을 할 수 있습니다. 대표적인게 바로 필터링.

Stream<String> stream = names.stream()
							.filter(name -> name.contains("a")); // [Elena, Java]

Mapping

map은 요소들을 특정조건에 해당하는 값으로 변환해 줍니다.

Stream<String> stream = names.stream()
  							.map(String::toUpperCase); // [ERIC, ELENA, JAVA]
                            
// .map(s -> s.toUpperCase);  이렇게도 사용가능

혹은 클래스에서 특정 요소만 뽑아서 리스트를 만들 수도 있죠.

List<String> humanNames = humans.stream()
            .map(h -> h.getName())
            .collect(Collectors.toList());

Sorting

자바에서 정렬하는 방법은 여러 개가 있는데, Stream에도 있습니다. 먼저, 인자 없이 그냥 호출할 경우 오름차순 정렬을 해줍니다.

IntStream.of(14, 11, 20, 39, 23)
  .sorted()
  .boxed()
  .collect(Collectors.toList());
// [11, 14, 20, 23, 39]

역순으로 정렬하고 싶으면 다음과 같이 해줍니다.

List<String> lang = 
  Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");

lang.stream()
  .sorted(Comparator.reverseOrder())
  .collect(Collectors.toList());
// [Swift, Scala, Python, Java, Groovy, Go]

혹은 String compareTo를 직접 사용해서 정렬해줄수도 있습니다.

lang.stream()
  .sorted((o1, o2) -> o2.compareTo(o1))
  .collect(Collectors.toList());

마지막으로 문자열 길이를 기준으로 정렬하는 예시입니다.

lang.stream()
  .sorted((s1, s2) -> s2.length() - s1.length())
  .collect(Collectors.toList());
// [Groovy, Python, Scala, Swift, Java, Go]

mapToInt

스트림을 IntStream으로 변환해주는 메서드다. 주로 아래와 같이 사용한다. 보면 알겠지만 Integer를 int로 변환해준다.

List<Integer> integerList = Arrays.asList(20,30,50,88,100);
int[] mapToInt = integerList.stream().mapToInt(x->x).toArray(); // Integer -> int(primitive type)
// mapToInt(x->x)는 mapToInt(Integer::intValue)로도 가능한듯

mapToInt를 해야 sum, max 같은걸 할 수 있다. 이건 IntStream에 내장되어있어서 그렇다.

int[] arr = {1,2,3,10};

int sum = IntStream.of(arr).sum();
System.out.println(sum);

forEach

JavaScript에서도 forEach가 있듯이 자바에도 forEach가 있다.

IntStream.range(1,11).forEach(i-> {
   System.out.println(i);
});

IntStream.range(1,11).forEach(System.out::println);

boxed

mapToInt와는 반대로 IntStream 같이 원시 타입에 대한 스트림 지원을 클래스 타입(예: IntStream -> Stream<Integer>)으로 전환해준다.

// int[] -> IntStream -> Stream<Integer> -> Integer[] 하는 방법

int[] num = {3, 4, 5};

//1. int[] -> IntStream
IntStream stream = Arrays.stream(num);

//2. IntStream -> Stream<Integer>
Stream<Integer> boxed = stream.boxed();

//3. Stream<Integer> -> Integer[]
Integer[] result = boxed.toArray(Integer[]::new);

System.out.println(Arrays.toString(result));

// one line
Integer[] oneLineResult = Arrays.stream(num)
								.boxed()
                .toArray(Integer[]::new);

Stream 결과만들기

Collecting

Collectors.toList()를 이용해서 리스트로 결과를 가져옵니다. 이를 사용하려면 java.util.stream.Collectors 를 필수로 해줘야 된다.

List<String> collectorCollection =
  productList.stream()
    .map(Product::getName)
    .collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]

만약 Set으로 변환하고 싶다면 Collectors.toSet()을 해주면 된다. Array는 그냥 toArray 해주면 된다.

만약 Stream 요소를 1개의 String 객체로 변환해주고 싶다면 Collectors.joining()을 사용할 수 있다.

List<String> colors = Arrays.asList("RED", "BLUE", "BLACK", "GREEN");
 
// 요소를 문자열로 변환하고 쉼표로 구분하여 연결합니다.
String joined = colors.stream().collect(Collectors.joining(", "));

Count

중개 오퍼레이션을 통해 가공된 값들의 개수를 출력한다.

public static void main(String[] args) {
    List <String> list = new ArrayList<>();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");

    //요소중 "b"인 것의 개수
    System.out.println(list.stream().filter(s -> s.equals("b")).count()); // 1
}

findFirst

findFirst()는 filter 조건에 일치하는 element 1개를 Optional로 리턴합니다. 조건에 일치하는 요소가 없다면 empty가 리턴됩니다.

List<String> elements = Arrays.asList("a", "a1", "b", "b1", "c", "c1");

Optional<String> firstElement = elements.stream()
        .filter(s -> s.startsWith("b")).findFirst();

System.out.println("findFirst: " + firstElement.get()); // findFirst: b

Match 매칭 관련 - anyMatch, allMatch, noneMatch

참고 : https://cornswrold.tistory.com/300

자바스크립트로 생각해보면 some과 every 정도 된다고 보면 될 듯.

anyMatch() 메서드 == some

  • 스트림에서 특정 조건을 만족하는 요소가 하나라도 있는 경우 true를 반환하고 더 이상 실행되지 않습니다.

allMatch() 메서드 == every

  • 스트림에서 모든 요소가 특정 조건을 만족하는 경우 true를 반환합니다. 특정 조건을 만족하지 않는다면, false를 반환하고 더 이상 실행되지 않습니다.

noneMatch() 메서드 == !every

  • 스트림에서 특정 조건을 만족하는 요소가 하나라도 있는 경우 false를 반환하고 더 이상 실행되지 않습니다.

이를 응용하면 아래와 같은 코드 작성도 가능하다. - 프로그래머스 "없는 숫자 더하기" 문제 참고.

import java.util.Arrays;
import java.util.stream.IntStream;

class Solution {
    public int solution(int[] numbers) {
        return IntStream.range(0, 10).filter(i -> 
							Arrays.stream(numbers).noneMatch(num -> i == num)).sum();
    }
}

Optional

Optional 생성 - Optional.of()

Optional은 어떤 객체를 wrapping하는 객체입니다. 즉, 어떤 객체를 포함할 수 있고 또는 null 객체를 포함할 수 있습니다. 자바의 null 처리를 유연하게 하고자 도입된 것 같습니다. 아래에서 보듯이 String 객체를 생성하고 Optional 객체는 단순히 내부에 String 객체를 감싸는 wrapper 객체라는 것을 알 수 있다.

String string = "a string in optional";
Optional<String> opString = Optional.of(string);
System.out.println(opString.get());

get()은 Optional안의 값을 반환한다. 하지만 사용을 권장하지 않는다. 왜냐하면 Optional이 null일 경우 런타임 에러가 나기 때문이다. 하지만 위 경우 Optional.of(null)은 허용되지 않기 때문에 상관은 없지만...

Optional 생성 - Optional.ofNullable()

만약 null을 wrapping하는 Optional 객체를 만드려면 어떻게 해야 할까요? Optional.ofNullable()은 객체 생성 시 null을 허용합니다. 이를 이용하면 null을 포함하는 Optional 객체를 만들 수 있습니다.

String nullString = null;
Optional<String> nullOpString = Optional.ofNullable(nullString);
// Optional<String> emptyOptional = Optional.empty();

try {
    System.out.println(nullOpString.get());
} catch (NoSuchElementException e) {
    System.out.println("No such element");
}

다만 null인데 get을 했다가는 NoSuchElementException 예외가 발생됩니다.

isPresent - null 여부 체크

예외가 발생하지 않으려면 Null인지 미리 체크하고 사용해야 한다. isPresent()는 내부 객체가 null인지 알려줌.

Optional<String> opString = Optional.of("a string in optional");
Optional<String> nullOpString = Optional.ofNullable(nullString);

if (opString.isPresent()) {
    System.out.println("opString: " + opString.get());
}
if (nullOpString.isPresent()) { 
	 // null인경우 실행되지 않음.
    System.out.println("nullOpString: " + nullOpString.get());
}

혹은 아래처럼 사용도 가능하다.

opString.ifPresent(s - > System.out.println(s)); // 출력함
nullOpString.ifPresent(s - > System.out.println(s)); // 출력하지 않음 *null이면 실행되지 않는다.

orElse - null인 경우 대신 실행

만약 null일 때 다른 값을 갖도록 설정할 수 없을까요? orElse를 사용하면 null인 경우 다른 값을 출력하도록 할 수 있다.

Optional<String> opString = Optional.of("a string in optional");
Optional<String> nullOpString = Optional.ofNullable(nullString);

String str = opString.orElse("new string from orElse");
System.out.println(str); // "a string in optional"

String str2 = nullOpString.orElse("new string from orElse");
System.out.println(str2); // "new string from orElse"

참고. Stream을 활용하는 코테 문제 예시

profile
velog ckstn0777 부계정 블로그 입니다. 프론트 개발 이외의 공부 내용을 기록합니다. 취업준비 공부 내용 정리도 합니다.

0개의 댓글

관련 채용 정보