[Java] JAVA8 변경 사항

JD_S·2022년 11월 15일
0

Java

목록 보기
5/21

Java8

2014년에 발표된 자바의 최신 버전인 JavaSE 8버전에서는 많은 사항이 변경되거나 새롭게 추가되었다. 아래 글들을 보자. (그것들이 어떻게 작동되며 왜 나왔는지에 포커스를 두자)

람다 표현식(Lambda Expression)

문법 : (매개변수목록) -> {함수 몸체}

람다 표현식은 간단히 말해 메서드를 하나의 식으로 표현한 것이다.
즉, 식별자 없이 실행할 수 있는 함수 표현식을 의미하며, 익명 함수(anonymous function)라고도 부른다.

메서드를 람다 표현식으로 작성하면 클래스를 만들고 객체를 생성하지 않아도 메서드를 사용할 수 있다. 또한, 람다 표현식은 메서드의 매개변수로 전달될 수 있고, 메서드의 결괏값을 반환될 수도 있다. 이러한 람다 표현식은 기존의 불필요한 코드를 줄여주고, 작성된 코드의 가독성을 높이는 데 목적이 있다.

JavaSE 8버전부터 람다 표현식을 사용하여 자바에서도 함수형 프로그래밍을 할 수 있게 되었다.

new Thread(new Runnable() {
  public void run() {
    System.out.println("쓰레드 생성(일반적인 방법)");
  }
}).start();

new Thread(() -> {
  System.out.println("쓰레드 생성(람다 표현식 사용)");
}).start();

람다 표현식 작성시 유의사항

  1. 매개변수의 타입을 추론할 수 있는 경우에는 타입을 생략할 수 있다.
  2. 매개변수가 하나인 경우에는 괄호(())를 생략할 수 있다.
  3. 함수의 몸체가 하나의 명령문으로만 이루어진 경우 중괄호({})를 생략할 수 있다. (이때 세미콜론은 붙이지 않는다)
  4. 함수의 몸체가 하나의 return 문으로만 이루어진 경우에는 중괄호({})를 생략할 수 없다.
  5. return(생략가능) 문 대신 표현식을 사용할 수 있으며, 이때 반환값은 표현식의 결괏값이 된다.(이때 세미콜론은 붙이지 않는다)

함수형 인터페이스(functional interface)

람다 표현식을 사용할 때는 람다 표현식을 저장하기 위한 참조 변수의 타입을 결정해야만 한다.

문법 : 참조변수의타입 참조변수의이름 = 람다 표현식

위처럼 람다 표현식을 하나의 변수에 대입할 때 사용하는 참조 변수의 타입을 함수형 인터페이스라고 한다.

함수형 인터페이스는 추상 클래스와는 달리 단 하나의 추상 메서드만을 가져야 한다. 또한, 다음과 같은 어노테이션을 사용하여 함수형 인터페이스임을 명시할 수 있다.

문법 : @FunctionalInterface

위와 같이 어노테이션을 인터페이스의 선언 앞에 붙이면, 컴파일러는 해당 인터페이스를 함수형 인터페이스로 인식한다. 자바 컴파일러는 이렇게 명시된 함수형 인터페이스에 두 개 이상의 메서드가 선언되면 오류를 발생시킨다.

@FunctionalInterface
interface Calc {//함수형 인터페이스 선언
  public int min(int x, int y);
}

public class Lambda02 {
  public static void main(String[] args) {
    Calc minNum = (x, y) -> x < y ? x : y;	//추상 메서드의 구현
    System.out.println(minNum.min(3, 4));	//함수형 인터페이스의 사용
  }
}
결과 : 3

스트림 API(stream API)

자바에서는 많은 양의 데이터를 저장하기 위해서 배열이나 컬렉션을 사용한다.
또한, 이렇게 저장된 데이터에 접근하기 위해서 반복문이나 반복자(iterator)를 사용하여 매번 코드를 작성해야 했다. 하지만 이렇게 작성된 코드는 길이가 길고 가독성이 떨어지며, 코드의 재사용이 거의 불가능하다. 또한, 데이터베이스의 쿼리와 같이 정형화된 처리 패턴을 가지지 못했기에 데이터마다 다른 방법으로 접근해야만 했다. 이러한 문제점을 극복하기 위해 JavaSE 8버전 부터 도입된 방법이 stream API이다. 따라서 스트림 API를 이용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방법으로 다룰 수 있다.

String[] arr = new String[]{"넷", "둘", "셋", "하나"};

//배열에서 스트림 생성
Stream<String> stream1 = Arrays.stream(arr);
stream.forEach(e -> System.out.print(e + " "));
System.out.println();

//배열의 특정 부분만을 이용한 스트림 생성
Stream<String> stream2 = Arrays.stream(arr, 1, 3);
stream2.forEach(e -> System.out.print(e + " "));

결과
넷 둘 셋 하나
둘 셋

스트림 API의 특징

  1. 스트림은 외부 반복을 통해 작업하는 컬렉션과는 달리 내부 반복(internal iteration)을 통해 작업을 수행한다.
  2. 스트림은 재사용이 가능한 컬렉션과는 달리 단 한 번만 사용할 수 있다.
  3. 스트림은 원본 데이터를 변경하지 않는다.
  4. 스트림의 연산은 필터-맵(filter-map) 기반의 API를 사용하여 지연(lazy)연산을 통해 성능을 최적화합니다.
  5. 스트림은 parallelStream()메서드를 통한 손쉬운 별렬 처리를 지원한다.

컬렉션(or 컬렉션 프레임워크) : 다수의 데이터를 쉽고 효과적으로 처리할 수 있는 표준화된 방법을 제공하는 클래스의 집합. 즉, 데이터를 저장하는 자료 구조와 데이터를 처리하는 알고리즘을 구조화하여 클래스로 구현해 놓은 것

스트림 API의 동작 흐름

  1. 스트림의 생성
  2. 스트림의 중개 연산 (스트림의 변환)
  3. 스트림의 최종 연산 (스트림의 사용)

다음 그림은 자바 스트림API의 동작 흐름을 나타낸 것입니다.

스트림의 생성

컬렉션

자바에서 제공하는 모든 컬렉션의 최고 상위 조상인 Collection인터페이스에는 stream()메서드가 정의되어 있다. (이게 바로 우리가 스트림을 사용할 수 있었던 이유이다.) 따라서 Collection인터페이스를 구현한 모든 ListSet컬렉션 클래스에서도 stream()메서드로 스트림을 생성할 수 있다. 또한, parallelStream()메서드를 사용하면 병렬 처리가 가능한 스트림을 생성할 수 있다.

//List로부터 스트림을 생성
List<Integer> list = Arrays.asList(1,2,3); //가변인자
Stream<Integer> stream = list.stream(); //list를 소스로 하는 스트림 생성
ArrayList<Integer> list = new ArrayList<Integer>();

list.add(4);
list.add(2);
list.add(3);
list.add(1);

//컬렉션에서 스트림 생성
Stream<Integer> stream = list.stream();

//forEach()메서드를 이용한 스트림 요소의 순차 접근
stream.forEach(System.out::println);

결과
4
2
3
1

Stream클래스의 forEach()메서드는 해당 스트림의 요소를 하나씩 소모해가며 순차적으로 요소에 접근하는 메서드이다. 따라서 같은 스트림으로는 forEach()메서드를 한 번밖에 호출할 수 없다. 단, 원본 데이터의 요소를 소모하는 것은 아니므로, 같은 데이터에서 또 다른 스트림을 생성하여 forEach()메서드를 호출하는 것은 가능하다.

배열

배열에 관한 스트림을 생성하기 위해 Arrays클래스에는 다양한 형태의 stream()메서드가 클래스 메서드로 정의되어 있다. 또한, 기본 타입인 int, long, double형을 저장할 수 있는 배열에 관한 스트림이 별도로 정의되어 있다. 이러한 스트림은 java.util.stream패키지의 IntStream, LongStream, DoubleStream인터페이스로 각각 제공된다.

String[] arr = new String[]{"넷", "둘", "셋", "하나"};

//배열에서 스트림 생성
Stream<String> stream1 = Arrays.stream(arr);
stream1.forEach(e -> System.out.print(e + " "));
System.out.println();

//배열의 특정 부분만을 이용한 스트림 생성
Stream<String> stream2 = Arrays.stream(arr, 1, 3);
stream2.forEach(e -> System.out.print(e + " "));

결과
넷 둘 셋 하나
둘 셋

Arrays클래스의 stream()메서드는 전체 배열뿐만 아니라 배열의 특정 부분만을 이용하여 스트림을 생성할 수도 있다.

가변 매개변수

Stream클래스의 of()메서드를 사용하면 가변 매개변수(variable parameter)를 전달받아 스트림을 생성할 수 있다.

//가변 매개변수에서 스트림 생성
Stream<Double> stream = Stream.of(4.2, 2.5, 3.1, 1.9);
stream.forEach(System.out::println);

결과
4.2
2.5
3.1
1.9

지정된 범위의 연속된 정수

지정된 범위의 연속된 정수를 스트림으로 생성하기 위해 IntStreamLongStream인터페이스에는 range()rangeClosed()메서드가 정의되어 있다. range()메서드는 명시된 시작 정수를 포함하지만, 명시된 마지막 정수는 포함하지 않는 스트림을 생성한다. rangeClosed()메서드는 명시된 시작 정수뿐만 아니라 명시된 마지막 정수까지도 포함하는 스트림을 생성한다.

//지정된 범위의 연속된 정수에서 스트림 생성
IntStream stream1 = IntStream.range(1, 4);
stream1.forEach(e -> System.out.print(e + " "));
System.out.println(); //줄바꿈

IntStream stream2 = IntStream.rangeClosed(1, 4);
stream2.forEach(e -> System.out.print(e + " "));

결과
1 2 3
1 2 3 4

특정 타입의 난수들

특정 타입의 난수로 이루어진 스트림을 생성하기 위해 Random클래스에는 ints(), longs(), doubles()와 같은 메서드가 정의되어 있다. 이 메서드들은 매개변수로 스트림의 크기를 long타입으로 전달받을 수 있다. 이 메서드들은 만약 매개변수를 전달받지 않으면 크기가 정해지지 않은 무한 스트림(infinite stream)을 반환한다. 이때 limit()메서드를 사용하여 따로 스트림의 크기를 제한해야 한다.

//특정 타입의 난수로 이루어진 스트림 생성
IntStream stream = new Random().ints(4);
stream.forEach(System.out::println);

결과
1072176871
-649065206
133298431
-616174137

람다 표현식

람다 표현식을 매개변수로 전달받아 해당 람다 표현식에 의해 반환되는 값을 요소로 하는 무한 스트림을 생성하기 위해 Stream클래스에는 iterate()generate()메서드가 정의되어 있다. iterate()메서드는 seed(씨앗값)로 명시된 값을 람다 표현식에 사용하여 반환된 값을 다시 seed로 사용하는 방식으로 무한 스트림을 생성한다. 반면에 generate()메서드는 매개변수가 없는 람다 표현식을 사용하여 반환된 값으로 무한 스트림을 생성한다.

iterate()generate()에 의해 생성된 스트림은 기본형 스트림 타입의 참조변수로 다룰 수 없다.

아래 코드는 iterate()메서드를 이용하여 홀수만으로 이루어진 무한 스트림을 생성하는 예제이다.

Stream<Integer> stream = Stream.iterate(2, n -> n + 2); //2, 4, 6, 8, 10, ... (o)
IntStream stream = Stream.iterate(2, n -> n + 2); //2, 4, 6, 8, 10, ... (x)

Stream<Double> randomStream = Stream.generate(Math::random); //0이상 1미만의 값들이 랜덤으로 나온다.(o)
DoubleStream randomStream = Stream.generate(Math::random); //0이상 1미만의 값들이 랜덤으로 나온다.(x)

seed : 컴퓨터는 난수를 간단히 만들 수 없다. 컴퓨터는 사람과 달리 무의식적인 선택, 또는 우연에 의하는 선택을 할 수 없기에 기본적으로 정해진 입력에 따라 정해진 값을 낼 뿐이다. 그래서 난수표를 여러 개 만들어 놓고 매번 다른 난수표를 읽히는 것이다. 이 난수표를 선택하는 것을 'seed(시드)'라고 한다.

파일

파일의 한 행을 요소로 하는 스트림을 생성하기 위해 java.nio.file.Files클래스에는 lines()메서드가 정의되어 있다. 또한, java.io.BufferedReader클래스의 lines()메서드를 사용하면 파일뿐만 아니라 다른 입력으로부터도 데이터를 행단위로 읽어 올 수 있다.

Stream<String> stream = Files.lines(Path path);

빈 스트림

아무 요소도 가지지 않는 빈 스트림은 Stream클래스의 empty()메서드를 사용하여 생성할 수 있다.

Stream<Object> stream = Stream.empty();
System.out.println(stream.count()); //스트림의 요소의 총 개수를 출력

결과
0

스트림의 중간연산(intermediate operation)

스트림API에 의해 생성된 초기 스트림은 중간 연산을 통해 또 다른 스트림으로 변환된다. 이러한 중간 연산은 스트림을 전달받아 스트림을 반환하므로, 중간 연산은 연속으로 연결해서 사용할 수 있다. 또한, 스트림의 중간 연산은 필터-맵(filter-map) 기반의 API를 사용함으로 지연연산을 통해 성능을 최적화할 수 있다.

스트림API에서 사용할 수 있는 대표적인 중간 연산과 그에 따른 메서드는 다음과 같다.

  1. 스트림 필터링 : filter(), distinct()
  2. 스트림 변환 : map(), flatMap()
  3. 스트림 제한 : limit(), skip()
  4. 스트림 정렬 : sorted()
  5. 스트림 연산 결과 확인 : peek()

스트림 필터링

filter()메서드는 해당 스트림에서 주어진 조건(predicate)에 맞는 요소만으로 구성된 새로운 스트림을 반환한다. 또한, distinct()메서드는 해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 반환한다. distinct()메서드는 내부적으로 Object클래스의 equals()메서드를 사용하여 요소의 중복을 비교한다.

IntStream stream1 = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
IntStream stream2 = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);

//스트림에서 중복된 요소를 제거함
stream1.distinct().forEach(e -> System.out.print(e + " "));
System.out.println();

//스트림에서 홀수만을 골라냄
stream2.filter(n -> n % 2 != 0).forEach(e -> System.out.print(e + " "));

결과
7 5 2 1 3 4 6
7 5 5 1 3 5

스트림 변환

map()메서드는 해당 스트림의 요소들을 주어진 함수에 인수로 전달하여, 그 반환값들로 이루어진 새로운 스트림을 반환한다. 만약 해당 스트림의 요소가 배열이라면, flatMap()메서드를 사용하여 각 배열의 각 요소의 반환값을 하나로 합친 새로운 스트림을 얻을 수 있다.

아래 코드는 문자열로 이루어진 스트림을 map()메서드를 이용하여 각 문자열의 길이로 이루어진 스트림을 변환하는 코드이다.

Stream<String> stream = Stream.of("HTML", "CSS", "JAVA", "JAVASCRIPT");
stream.map(s -> s.length()).forEach(System.out::println);

결과
4
3
4
10

아래 코드는 여러 문자열이 저장된 배열을 각 문자열에 포함된 단어로 이루어진 스트림으로 반환하는 코드이다.

String[] arr = {"I study hard", "You study JAVA", "I am hungry"};

Stream<String> stream = Arrays.stream(arr);
stream.flatMap(s -> Stream.of(s.split(" +"))).forEach(System.out::println);

결과
I
study
hard
You
study
JAVA
I
am
hungry

스트림 제한

limit()메서드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 반환한다. skip()메서드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 반환한다.

IntStream stream1 = IntStream.range(0, 10);
IntStream stream2 = IntStream.range(0, 10);
IntStream stream3 = IntStream.range(0, 10);

stream1.skip(4).forEach(n -> System.out.print(n + " "));
System.out.println();

stream2.limit(5).forEach(n -> System.out.print(n + " "));
System.out.println();

stream3.skip(3).limit(5).forEach(n -> System.out.print(n + " "));

결과
4 5 6 7 8 9
0 1 2 3 4
3 4 5 6 7

스트림 정렬

sorted()메서드는 해당 스트림을 주어진 비교자(Comparator)를 이용하여 정렬한다. 이때 비교자를 전달하지 않으면 기본적으로 사전 편찬 순(natural order)으로 정렬하게 된다.

Stream<String> stream1 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");
Stream<String> stream2 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");

stream1.sorted().forEach(s -> System.out.print(s + " "));
System.out.println();

stream2.sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s + " "));

결과
CSS HTML JAVA JAVASCRIPT 
JAVASCRIPT JAVA HTML CSS 

스트림 연산 결과 확인

peek()메서드는 결과 스트림으로부터 요소를 소모하여 추가로 명시된 동작을 수행한다. 이 메서드는 원본 스트림에서 요소를 소모하지 않으므로, 주로 연산과 연산 사이에 결과를 확인하고 싶을 때 사용한다. 따라서 개발자가 디버깅 용도로 많이 사용한다.

IntStream stream = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);

stream.peek(s -> System.out.println("원본 스트림 : " + s))
  .skip(2)
  .peek(s -> System.out.println("skip(2) 실행 후 : " + s))
  .limit(5)
  .peek(s -> System.out.println("limit(5) 실행 후 : " + s))
  .sorted()
  .peek(s -> System.out.println("sorted() 실행 후 : " + s))
  .forEach(n -> System.out.println(n));
  
결과
원본 스트림 : 7
원본 스트림 : 5
원본 스트림 : 5
skip(2) 실행 후 : 5
limit(5) 실행 후 : 5
원본 스트림 : 2
skip(2) 실행 후 : 2
limit(5) 실행 후 : 2
원본 스트림 : 1
skip(2) 실행 후 : 1
limit(5) 실행 후 : 1
원본 스트림 : 2
skip(2) 실행 후 : 2
limit(5) 실행 후 : 2
원본 스트림 : 3
skip(2) 실행 후 : 3
limit(5) 실행 후 : 3
sorted() 실행 후 : 1
1
sorted() 실행 후 : 2
2
sorted() 실행 후 : 2
2
sorted() 실행 후 : 3
3
sorted() 실행 후 : 5
5

위 코드에서 첫 번째 요소인 7과 두 번째 요소인 5는 skip()메서드에 의해 삭제되므로, 원본 스트림에서만 나타난다. 하지만 세 번째 요소인 5는 skip()메서드와 limit()메서드가 실행된 후에도 존재하므로 모두 나타난다. 이렇게 peek()메서드는 스트림의 각 요소가 해당 중간 연산후에 어떻게 변화하는지를 보여준다.

대표적인 중간 연산 메서드

스트림의 최종연산

스트림API에서 중간 연산을 통해 변환된 스트림은 마지막으로 최종 연산을 통해 각 요소를 소모하여 결과를 표시한다. 즉, 지연되었던 모든 중간 연산들이 최종 연산 시에 모두 수행된다. 이렇게 최종 연산 시에 모든 요소를 소모한 해당 스트림은 더는 사용할 수 없게 된다. 스트림API에서 사용할 수 있는 대표적인 최종 연산과 그에 따른 메서드는 다음과 같다.

  1. 요소의 출력 : forEach()
  2. 요소의 소모 : reduce()
  3. 요소의 검색 : findFirst(), findAny()
  4. 요소의 검사 : anyMatch(), allMatch(), noneMatch()
  5. 요소의 통계 : count(), min(), max()
  6. 요소의 연산 : sum(), average()
  7. 요소의 수집 : collect()

요소의 출력

앞선 수업에서 자주 사용한 forEach()메서드는 스트림의 각 요소를 소모하여 명시된 동작을 수행한다. 반환 타입이 void이므로 보통 스트림의 모든 요소를 출력하는 용도로 사용된다.

Stream<String> stream = Stream.of("넷", "둘", "셋", "하나");
stream.forEach(System.out::println);

결과
넷
둘
셋
하나

요소의 소모

스트림의 최종 연산은 모두 스트림의 각 요소를 소모하여 연산을 수행하게 된다. 하지만 reduce()메서드는 첫 번째와 두 번째 요소를 가지고 연산을 수행한 뒤, 그 결과와 세 번째 요소를 가지고 또다시 연산을 수행한다. 이런 식으로 해당 스트림의 모든 요소를 소모하여 연산을 수행하고, 그 결과를 반환한다. 또, 인수로 초깃값을 전달하면 초깃값과 해당 스트림의 첫 번째 요소와 연산을 시작하며, 그 결과와 두 번째 요소를 가지고 계속해서 연산을 수행하게 된다.

아래 코드는 스트림의 각 문자열 요소를 "++"기호로 연결하여 출력하는 코드이다.

Stream<String> stream1 = Stream.of("넷", "둘", "셋", "하나");
Stream<String> stream2 = Stream.of("넷", "둘", "셋", "하나");

Optional<String> result1 = stream1.reduce((s1, s2) -> s1 + "++" + s2);
result1.ifPresent(System.out::println);

String result2 = stream2.reduce("시작", (s1, s2) -> s1 + "++" + s2);
System.out.println(result2);

결과
넷++++++하나
시작++++++++하나

위 코드처럼 인수로 초깃값을 전달하는 reduce()메서드의 반환 타입은 Optional<T>가 아닌 T타입이다. 그 이유는 비어 있는 스트림과 reduce()연산을 할 경우 전달받은 초깃값을 그대로 반환해야 하기 때문이다.

요소의 검색

findFirst()findAny()메서드는 해당 스트림에서 첫 번째 요소를 참조하는 Optional객체를 반환한다. 두 메서드 모두 비어있는 스트림에서는 비어있는 Optional객체를 반환한다.

아래 코드는 스트림의 모든 요소를 정렬한 후, 첫 번째에 위치한 요소를 출력하는 코드이다.

IntStream stream1 = IntStream.of(4, 2, 7, 3, 5, 1, 6);
IntStream stream2 = IntStream.of(4, 2, 7, 3, 5, 1, 6);

OptionalInt result1 = stream1.sorted().findFirst();
System.out.println(result1.getAsInt());

OptionalInt result2 = stream2.sorted().findAny();
System.out.println(result2.getAsInt());

결과
1
1

위 코드에서 볼 수 있듯이 두 메서드의 결과는 같다. 하지만 병렬 스트림인 경우에는 findAny()메서드를 사용해야만 정확한 연산 결과를 반환할 수 있다.

요소의 검사

해당 스트림의 요소 중에서 특정 조건을 만족하는 요소가 있는지, 아니면 모두 만족하거나 모두 만족하지 않는지를 다음 메서드를 사용하여 확인할 수 있다.

  • anyMatch() : 해당 스트림의 일부 요소가 특정 조건을 만족할 경우에 true를 반환
  • allMatch() : 해당 스트림의 모든 요소가 특정 조건을 만족할 경우에 true를 반환
  • noneMatch() : 해당 스트림의 모든 요소가 특정 조건을 만족하지 않을 경우에 true를 반환

세 메서드 모두 인수로 Predicate객체를 전달받으며, 요소의 검사 결과는 boolean값으로 반환.

아래 코드는 스트림의 모든 요소를 검사하여 80보다 큰 값을 가지는 요소가 하나라도 존재하는지를 검사하는 코드다.

IntStream stream1 = IntStream.of(30, 90, 70, 10);
IntStream stream2 = IntStream.of(30, 90, 70, 10);

System.out.println(stream1.anyMatch(n -> n > 80));
System.out.println(stream2.allMatch(n -> n > 80));

결과
true
false

요소의 통계

  • count() : 해당 스트림의 요소의 총 개수를 long타입의 값으로 반환
  • max() : 해당 스트림의 요소 중에서 가장 큰 값을 가지는 요소를 참조하는 Optional객체를 얻을 수 있다.
  • min() : 해당 스트림의 요소 중에서 가장 작은 값을 가지는 요소를 참조하는 Optional객체를 얻을 수 있다.
IntStream stream1 = IntStream.of(30, 90, 70, 10);
IntStream stream2 = IntStream.of(30, 90, 70, 10);

System.out.println(stream1.count());
System.out.println(stream2.max().getAsInt());

결과
4
90

요소의 연산

  • sum() : IntStream이나 DoubleStream과 같은 기본 타입 스트림의 모든 요소의 합을 반환한다.
  • average() : IntStream이나 DoubleStream과 같은 기본 타입 스트림의 모든 요소의 평균을 Optional객체로 반환한다.
IntStream stream1 = IntStream.of(30, 90, 70, 10);
DoubleStream stream2 = DoubleStream.of(30.3, 90.9, 70.7, 10.1);

System.out.println(stream1.sum());
System.out.println(stream2.average().getAsDouble());

결과
200
50.5

요소의 수집

collect()메서드는 인수로 전달되는 Collectors객체에 구현된 방법대로 스트림의 요소를 수집한다. 또한, Collectors클래스에는 미리 정의된 다양한 방법이 클래스 메서드로 정의되어 있다. 이외에도 사용자가 직접 Collector인터페이스를 구현하여 자신만의 수집 방법을 정의할 수도 있다.

스트림 요소의 수집 용도별 사용할 수 있는 Collectors메서드는 다음과 같다.

  1. 스트림을 배열이나 컬렉션으로 반환 : toArray(), toCollection(), toList(), toSet(), toMap()
  2. 요소의 통계와 연산 메서드와 같은 동작을 수행 : counting(), maxBy(), minBy(), summingInt(), averagingInt()
  3. 요소의 소모와 같은 동작을 수행 : reducing(), joining()
  4. 요소의 그룹화와 분할 : groupingBy(), partitioningBy()

아래 코드는 collect()메서드를 이용하여 해당 스트림을 리스트로 반환하는 코드이다.

Stream<String> stream = Stream.of("넷", "둘", "하나", "셋");

List<String> list = stream.collect(Collectors.toList());
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
  System.out.print(iter.next() + " ");
}

결과
넷 둘 하나 셋

아래 코드는 Collectors클래스의 partitioningBy()메서드를 이용하여 해당 스트림의 각 요소별 글자 수에 따라 홀수와 짝수로 나누어 저장하는 코드이다.

Stream<String> stream = Stream.of("HTML", "CSS", "JAVA", "PHP");

Map<Boolean, List<String>> patition = stream.collect(Collectors.partitioningBy(s -> (s.length() % 2) == 0));

List<String> oddLengthList = patition.get(false);
System.out.println(oddLengthList);

List<String> evenLengthList = patition.get(true);
System.out.println(evenLengthList);

결과
[CSS, PHP]
[HTML, JAVA]

대표적인 최종 연산 메서드

스트림API에서 사용할 수 있는 대표적인 최종 연산을 위한 메서드는 다음과 같다.

java.time 패키지

자바에서의 날짜 및 시간 처리

JDK1.0에서는 Date클래스를 사용하여 날짜에 관한 처리를 수행했다. 하지만 Date클래스는 현재 대부분의 메서드가 사용을 권장하지 않고 있다.(deprecated)

JDK1.1부터 새롭게 제공된 Calendar클래스는 날짜와 시간에 대한 정보를 손쉽게 얻을 수 있다. 하지만 Calendar클래스는 다음과 같은 문제점을 가지고 있다.

  1. Calendar인스턴스는 불변 객체(immutable object)가 아니라서 값이 수정될 수 있다.
  2. 윤초(leap second)와 같은 특별한 상황은 고려하지 않는다.
  3. Calendar클래스에서는 월을 나타낼 때 1월부터 12월을 0부터 11까지 표현해야 하는 불편함이 있다.

윤초 : 협정 세계시에서 기준으로 삼고 있는 세슘 원자시계와 실제 지구의 자전, 공전 속도를 기준으로 한 태양시의 차이로 인해 발생한 오차를 보정하기 위하여 추가하는 1초이다. 12월 31일의 마지막에 추가하거나, 혹은 6월 30일의 마지막에 추가한다.

따라서 많은 자바 개발자들은 Calendar클래스뿐만 아니라 더 나은 성능의 Joda-Time이라는 라이브러리를 함께 사용해 왔다. 버전에서는 이러한 Joda-Time라이브러리를 발전시킨 새로운 날짜와 시간API인 java.time패키지를 제공한다. java.time패키지는 위와 같은 문제점을 모두 해결했으며, 다양한 기능을 지원하는 다수의 하위 패키지를 포함하고 있다.

java.time 패키지란

JavaSE 8부터 제공되는 java.time패키지에는 자바에서 날짜와 시간을 다루는 데 사용되는 필수 클래스들이 포함되어 있다. 또한, 다음과 같은 다양한 기능을 하는 하위 패키지를 포함하고 있다.

  1. java.time.chrono : ISO-8601에 정의된 표준 달력 이외의 달력 시스템을 사용할 때 필요한 클래스들
  2. java.time.format : 날짜와 시간에 대한 데이터를 구문분석하고 형식화하는 데 사용되는 클래스들
  3. java.time.temporal : 날짜와 시간에 대한 데이터를 연산하는 데 사용되는 보조 클래스들
  4. java.time.zone : 타임 존과 관련된 클래스들

java.time패키지는 기존에 사용되던 Calendar클래스의 단점을 보완했다. 따라서 해당 패키지에 속하는 모든 클래스의 인스턴스는 불변 객체로 생성된다. 즉, java.time패키지에 포함되는 클래스의 메서드들은 모두 새로운 객체를 생성하여 반환하고 있다.

java.time 패키지의 구성 클래스

기존의 Calendar클래스는 날짜와 시간을 한 번에 표현했지만, java.time패키지에서는 별도로 구분하여 처리한다. LocalDate클래스는 날짜를 표현할 때 사용하며, LocalTime클래스는 시간을 표현할 때 사용한다. 또한, 기존의 Calendar클래스처럼 날짜와 시간을 한 번에 표현하고 싶을 때는 LocalDateTime클래스를 사용한다. ZonedDateTime클래스는 특정 타임 존에 해당하는 날짜와 시간을 다루는 데 사용한다.
또한, 기존의 Date클래스와 비슷한 용도로 사용되는 Instant클래스가 있다. Instant클래스는 특정 시점의 날짜와 시간을 나노초단위로 표현하는 타임스탬프를 다루는 데 사용된다. Period클래스는 두 날짜 사이의 차이를 표현하는 데 사용되며, Duration클래스는 두 시각 사이의 차이를 표현하는 데 사용된다.

LocalDate vs LocalTime

LocalDate 클래스와 LocalTime 클래스

LocalDate클래스는 날짜를 표현하는 데 사용되며, LocalTime클래스는 시간을 표현하는데 사용된다. java.time패키지에 포함된 대부분의 클래스들은 이 두 클래스를 확장한 것이 많으므로, 우선 이 두 클래스를 잘 이해해야 한다.

날짜와 시간 객체의 생성

LocalDateLocalTime클래스는 객체를 생성하기 위해서 now()of()메서드라는 클래스 메서드를 제공한다. now()메서드는 현재의 날짜와 시간을 이용하여 새로운 객체를 생성하여 반환한다. 하지만 of()메서드는 전달된 인수를 가지고 특정 날짜와 시간을 표현하는 새로운 객체를 생성하여 반환한다.

LocalDate today = LocalDate.now();
LocalTime present = LocalTime.now();
System.out.println(today + " " + present);

//static LocalDate of(int year, int month, int dayOfMonth)
LocalDate birthDay = LocalDate.of(1982, 02, 19);

//static LocalTime of(int hour, int minute, int second, int nanaOfSecond)
LocalTime birthTime = LocalTime.of(02, 02, 00, 100000000);
System.out.println(birthDay + " " + birthTime);

결과
2022-11-16 11:38:15.087
1982-02-19 02:02:00.100

of()메서드는 위의 코드에서 사용된 메서드 시그니처 이외에도 다양한 형태가 오버로딩되어 제공된다.

날짜와 시간 객체에 접근하기

LocalDateLocalTime클래스는 특정 필드의 값을 가져오기 위해서 다음과 같이 다양한 getter메서드를 제공한다.

LocalDate클래스에서 제공하는 대표적인 getter메서드

기존의 Calendar클래스에서는 1월을 0으로 표현하여 월의 범위가 0~11이었으며, 요일은 일요일부터 1로 표현했다. 하지만 java.time패키지에서는 1월을 1로 표현하여 월의 범위가 1~12가 되었으며, 요일은 월요일부터 1로 표현하도록 변경되었다.

Calendar클래스와 java.time패키지의 클래스를 같이 사용할 때에는 특히 위와 같은 차이점에 주의해야 한다.

LocalDate today = LocalDate.now();

System.out.println("올해는 " + today.getYear() + "년입니다.");
System.out.println("이번달은 " + today.getMonthValue() + "월입니다.");
System.out.println("오늘은 " + today.getDayOfWeek() + "입니다.");
System.out.println("오늘은 1년 중 " + today.get(ChronoField.DAY_OF_YEAR) + "일째 날입니다.");

결과
올해는 2022년입니다.
이번달은 11월입니다.
오늘은 WEDNESDAY입니다.
오늘은 1년 중 320일째 날입니다.

LocalTime클래스에서 제공하는 대표적인 getter메서드

LocalTime present = LocalTime.now();
System.out.println("현재 시각은 " + present.getHour() + "시 " + present.getMinute() + "분입니다.");

결과
현재 시각은 1146분입니다.

TemporalField 인터페이스

TemporalField인터페이스는 월과 시와 같이 날짜와 시간과 관련된 필드를 정의해 놓은 인터페이스이다. 이 인터페이스를 구현하여 날짜와 시간을 나타낼 때 사용하는 열거체가 바로 ChronoField이다. java.time패키지를 구성하는 클래스의 메서드에서는 이 열거체를 이용하여 날짜와 시간을 처리하고 있다.

ChronoField열거체에 정의된 대표적인 열거체 상수는 다음과 같다.

LocalTime present = LocalTime.now();
String ampm;

if(present.get(ChronoField.AMPM_OF_DAY) == 0) {
    ampm = "오전";
} else {
    ampm = "오후";
}
System.out.println("지금은 " + ampm + " " + present.get(ChronoField.HOUR_OF_AMPM) + "시입니다.");

결과
지금은 오전 11시입니다.

날짜와 시간 객체의 필드값 변경

LocalDateLocalTime클래스는 날짜와 시간 객체에 접근하여 특정 필드의 값을 변경하기 위해서 with()메서드를 사용한다. with()메서드를 사용하면 값이 변경될 필드를 사용자가 직접 명시할 수 있다. 또한, 특정 필드의 값을 변경하기 위해 미리 정의되어 제공되는 다양한 with()메서드를 사용할 수도 있다.

LocalDate클래스에서 제공하는 with()메서드는 다음과 같다.

LocalDate today = LocalDate.now();
System.out.println("올해는 " + today.getYear() + "년입니다.");

LocalDate otherDay = today.withYear(1982);
System.out.println("올해는 " + otherDay.getYear() + "년입니다.");

결과
올해는 2022년입니다.
올해는 1982년입니다.

LocalTime클래스에서 제공하는 with()메서드는 다음과 같다.

LocalTime present = LocalTime.now();
System.out.println("현재 시각은 " + present.getHour() + "시입니다.");

LocalTime otherTime = present.withHour(8);
System.out.println("현재 시각은 " + otherTime.getHour() + "시입니다.");

결과
현재 시각은 11시입니다.
현재 시각은 8시입니다.

with()메서드 이외에도 특정 필드의 값을 더하거나 뺄 수 있는 다양한 plus(), minus()메서드도 제공된다.

LocalTime present = LocalTime.now();
System.out.println("현재 시각은 " + present.get(ChronoField.HOUR_OF_DAY) + "시입니다.");

LocalTime otherTime = present.plus(2, ChronoUnit.HOURS);
System.out.println("바뀐 시간은 " + otherTime.getHour() + "시입니다.");
 
LocalTime anotherTime = present.minus(6, ChronoUnit.HOURS);
System.out.println("바뀐 시간은 " + anotherTime.getHour() + "시입니다.");

결과
현재 시각은 11시입니다.
바뀐 시간은 13시입니다.
바뀐 시간은 5시입니다.

날짜와 시간 객체의 비교

LocalDateLocalTime클래스에도 객체를 비교할 수 있는 compareTo()메서드가 오버라이딩되어 있다. 하지만 더욱 편리하게 날짜와 시간 객체를 서로 비교할 수 있도록 다음과 같은 메서드를 제공한다.

  1. isEqual() : equals()메서드와는 달리 오직 날짜만을 비교함. (LocalDate클래스에서만 제공)
  2. isBefore() : 두 개의 날짜와 시간 객체를 비교하여 현재 객체가 명시된 객체보다 앞선 시간인지를 비교함
  3. isAfter() : 두 개의 날짜와 시간 객체를 비교하여 현재 객체가 명시된 객체보다 늦은 시간인지를 비교함
LocalDate today = LocalDate.now();
LocalDate otherDay = LocalDate.of(1982, 02, 19);

System.out.println(today.compareTo(otherDay));
System.out.println(today.isBefore(otherDay));
System.out.println(today.isEqual(otherDay));

결과
40
false
false

나즈혼(Nashorn)

지금까지 자바스크립트의 기본 엔진으로는 모질라의 리노(Rhino)가 사용되어 왔다. 모질라의 리노는 그 당시에는 훌륭한 스크립트 엔진이었다. 하지만 세월이 흐르면서 자바의 최신 개선 사항 등을 제대로 활용하지 못하는 등 노후화된 모습을 보여주게 된다. 그래서 JavaSE 8버전부터는 자바스크립트의 새로운 엔진으로 오라클의 나즈혼을 도입하게 된다. 나즈혼은 기존에 사용되어 온 리노에 비해 성능과 메모리 관리 면에서 크게 개선된 스크립트 엔진이다.

Reference

profile
Whatever does not destroy me makes me stronger.

0개의 댓글