[Java] 애너테이션, 람다, 스트림

Nakjoo·2023년 1월 6일
0

[SEB_BE_43]

목록 보기
15/29
post-thumbnail

1. 애너테이션

에너테이션은 소스 코드가 컴파일되거나 실행될 때에 컴파일러 및 프로그램에게 필요한 정보를 전달해주는 문법 요소이다.

애너테이션은 @Override 처럼 @로 시작하며, 클래스, 인터페이스, 필드, 메서드 등에 붙여서 사용할 수 있다.

JDK에서 제공하는 애너테이션의 종류에는 두 가지가 있다.

  • 표준 애너테이션 : JDK에 내장되어 있는 일반적인 애너테이션
  • 메타 애너테이션 : 다른 애너테이션을 정의할 때 사용하는 애너테이션

1.1. 표준 에너테이션

1.1.1. @Override

@Override는 메서드 앞에만 붙일 수 있는 애너테이션으로, 선언한 상위 클래스의 메서드를 오버라이딩하거나 추상 메서드를 구현하는 메서드라는 것을 컴파일러에게 알려주는 역할을 수행한다.

예를 들어, 아래와 같이 SuperClass의 example()를 SubClass에서 오버라이딩할 때에, @Override를 붙여주면, 컴파일러는 SubClass의 example()이 상위 클래스의 메서드를 오버라이딩한 것으로 간주한다.

class SuperClass {
	public void example() {
    	System.out.println("example() of SuperClass");
    }
}

class SubClass extends SuperClass {
	
    @Override
    public void example() {
    	System.out.println("example() of SubClass");
    }
}

1.1.2. @Deprecated

@Deprecated는 기존에 사용하던 기술이 다른 기술로 대체되어 기존 기술을 적용한 코드를 더 이상 사용하지 않도록 유도하는 경우에 사용한다.

1.1.3. @SuppressWarnings

@SuppressWarnings 애너테이션은 컴파일 경고 메시지가 나타나지 않도록 한다. 경우에 따라서 경고가 발생할 것이 충분히 예상됨에도 묵인해야 할 때 주로 사용한다.

아래와 같이 @SuppressWarnings 뒤에 괄호를 붙이고 그 안에 억제학자 하는 경고메시지를 지정해줄 수 있다.

애너테이션설명
@SuppressWarnings("all")모든 경고를 억제
@SuppressWarnings("deprecation")Deprecated 메서드를 사용한 경우에 발생하는 경고를 억제
@SuppressWarnings("fallthrough")switch문에서 break 구문이 없을 때 발생하는 경고를 억제
@SuppressWarnings("finally")finally와 관련되 경고를 억제
@SuppressWarnings("null")null과 관련된 경고를 억제
@SuppressWarnings("unchecked")검증되지 않은 연산자와 관련된 경고를 억제
@SuppressWarnings("unused")사용하지 않는 코드와 관련된 경고를 억제

1.1.4. @FunctionalInterface

@FunctionalInterface 애너테이션은 함수형 인터페이스를 선언할 때, 컴파일러가 함수형 인터페이스의 선언이 바르게 선언되었는지 확인하도록 한다. 만약 바르게 선언되지 않은 경우, 에러를 발생시킨다.

참고록, 함수형 인터페이스는 단 하나의 추상 메서드만을 가져야하는 제약이 있다.

@FunctionalInterface
public interface ExampleInterface {
	public abstract void example(); // 단 하나의 추상 메서드
}

1.2. 메타 애너테이션

메타 애너테이션(meta-annotation)은 애너테이션을 정의하는 데에 사용되는 애너테이션으로, 애너테이션의 적용 대상 및 유지 기간을 지정하는 데에 사용된다.

1.2.1. @Target

@Target 애너테이션은 이름 그대로 애너테이션을 적용할 "대상"을 지정하는 데 사용된다.

다음의 표에 나와있는 내용이 @Target 애너테이션을 사용하여 지정할 수 있는 대상의 타입이며, 모두 java.lang.annotation.ElementType 이라는 열거형에 정의되어 있다.

대상 타입적용 범위
ANNOTATION_TYPE애너테이션
CONSTRUCTOR생성자
FIELD필드(멤버변수, 열거형 상수)
LOCAL_VARIABLE지역변수
METHOD메서드
PACKAGE패키지
PARAMETER매개변수
TYPE타입(클래스, 인터페이스, 열거형)
TYPE_PARAMETER타입 매개변수
TYPE_USE타입이 사용되는 모든 대상

1.2.2. @Documented

@Documented 애너테이션은 애너테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 하는 애너테이션 설정이다.

자바에서 제공하는 표준 애너테이션과 메타 애너테이션 중 @Override@SuppressWarnings를 제외하고 모두 @Documented가 적용되어 있다.

@Documented
@Target(ElementType.Type)
public @interface CustomAnnotation { }

1.2.3. @Inherited

@Inherited 애너테이션은 이름에서도 알 수 있듯이 하위 클래스가 애너테이션을 상속받도록 한다. @Inherited 애너테이션을 상위 클래스에 붙이면, 하위 클래스도 상위 클래스에 붙은 애너테이션들이 동일하게 적용된다.

@Inherited // @SuperAnnotation이 하위 클래스까지 적용
@interface SuperAnnotation{}

@SuperAnnotation
class Super {}

class Sub extends Super{} // Sub에 애너테이션이 붙은 것으로 인식

1.2.4. @Retention

@Retention 애너테이션도 이름 그대로 특정 애너테이션의 지속시간을 결정하는 데 사용한다. 애너테이션과 관련한 유지 정책(retention policy)의 종류에는 다음의 세 가지가 있다.

유지 정책이란 애너테이션이 유지되는 기간을 지정하는 속성이다.

유지 정책설명
SOURCE소스 파일에 존재, 클래스 파일에는 존재하지 않음
CLASS클래스 파일에 존재, 실행시에 사용 불가, 기본값
RUNTIME클래스 파일에 존재, 실행 시에 사용 가능
@Target(Element.METHOD)
@Retention(RetentionPolicy.SOURCE)
// 오버라이딩이 제대로 되었는지 컴파일러가 확인하는 용도
// 클래스 파일에 남길 필요 없이 컴파일 시에만 확인하고 사라짐
public @interface Override() {}

예를 들면, 위의 예제에서 Override 애너테이션은 컴파일러가 사용하면 끝나기 때문에, 실행 시에는 더이상 사용되지 않음을 의미한다.

1.2.5. @Repeatable

@Repeatable 애너테이션은 애너테이션을 여러 번 붙일 수 있도록 허용한다는 의미를 갖고 있다.

@interface Works {
	Work[] value(); // 여러 개의 Work 애너테이션을 담을 컨테이너 애너테이션 Works
}

@Repeatable(Works.class) // Work 애너테이션을 여러 번 반복해서 쓸 수 있게 한다.
@interface Work{
	String value();
}

이렇게 사용자 타입의 애너테이션 Work를 정의하고, @Repeatable 애너테이션을 사용해 여러 번 사용할 수 있도록 만들었다.

@Work("코드 업데이트")
@Work("메서드 오버라이딩")
class Main {
	...
}

이런 식으로 Work 애너테이션을 하나의 대상에 여러 번 적용할 수 있게 된다.

2. 람다

람다식(Lambda Expression)은 함수형 프로그래밍 기법을 지원하는 자바의 문법요소이다.

람다식은 간단히 말해서 메서드를 하나의 '식(expression)'으로 표현한 것으로, 코드를 매우 간결하면서 명확하게 표현할 수 있다는 큰 장점이 있다.

람다식은 기존의 메서드에서 반환타입과 이름을 생략하고 코드 블럭 사이에 화살표를 추가해서 만들 수 있다.

int sum(int num1, int num2) {
	return num1 + num2;
} // 기본 메서드

(int num1, int num2) -> {
	return num1 + num2;
} // 람다식

만약 메서드 바디에 문장이 실행문 하나만 존재할 때, 중괄호와 return 문까지 생략 가능하다. 이 경우, 세미콜론(;)까지 생략해야 한다.

(int num1, int num2) -> num1 + num2

심지어, 매개변수 타입을 함수형 인터페이스를 통해 유추할 수 있는 경우에는 매개변수의 타입까지 생략 가능하다.

(num1, num2) -> num1 + num2

2.1. 함수형 인터페이스

함수형 인터페이스에는 단 하나의 추상 메서드만 선언될 수 있는데, 이는 람다식과 인터페이스의 메서드가 1:1로 매칭되어야 하기 때문이다.

public class LambdaExample1 {
	public static void main(String[] args) {
    	ExampleFunction exampleFunction = (num1, num2) -> num1 + num2;
        System.out.println(exampleFunction.sum(10,15));
    }
}

@FunctionalInterface
interface ExampleFunction {
	int sum(int num1, int num2);
}

// 출력값
25

이처럼, 함수형 인터페이스를 사용하면 참조변수의 타입으로 함수형 인터페이스를 사용해 원하는 메서드에 접근 가능하다.

람다식을 선언할 때는 함수형 인터페이스에 선언된 메서드 시그니처와 완전히 똑같은 타입과 매개변수를 가져야한다. 그리고 리턴 타입이 있다면 return 문도 선언해줘야 한다.

2.2. 메서드 레퍼런스

메서드 참조는 람다식에서 불필요한 매개변수를 제거할 때 주로 사용한다.

입력값과 출력값의 반환타입을 쉽게 유추할 수 있을 때는 입력값과 출력값을 생략할 수 있다.

(left, right) -> Math.max(left, righ)
// 위의 람다식을 아래 처럼 확 줄일 수 있다.

// 클래스 이름 :: 메서드 이름
Math :: max

메서드 참조도 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 인터페이스의 추상 메서드가 어떤 매개 변수를 가지고, 리턴 타입이 무엇인가에 따라 달라진다.

IntBinaryOperator 인터페이스는 두 개의 int 매개값을 받아 int 값을 리턴하므로, Math::max 메서드 참조를 대입할 수 있다.

IntBinaryOperator operator = Math :: max;

2.2.1. 정적 메서드와 인스턴스 메서드 참조

정적 메서드를 참조할 경우에는 클래스 이름 뒤에 :: 기호를 붙이고 정적 메서드 이름을 기술하면 된다.

인스턴스 메서드의 경우에는 먼저 객체를 생성한 뒤 참조 변수 뒤에 :: 기호를 붙이고 인스턴스 메서드 이름을 기술하면 된다.

//Calculator.java
public class Calculator {
  public static int staticMethod(int x, int y) {
                        return x + y;
  }

  public int instanceMethod(int x, int y) {
   return x * y;
  }
}
import java.util.function.IntBinaryOperator;

public class MethodReferences {
  public static void main(String[] args) throws Exception {
    IntBinaryOperator operator;

    /*정적 메서드
		클래스이름::메서드이름
	*/
    operator = Calculator::staticMethod;
    System.out.println("정적메서드 결과 : " + operator.applyAsInt(3, 5));

    /*인스턴스 메서드
		인스턴스명::메서드명
	*/
		
    Calculator calculator = new Calculator();
    operator = calculator::instanceMethod;
    System.out.println("인스턴스 메서드 결과 : "+ operator.applyAsInt(3, 5));
  }
}
/*
정적메서드 결과 : 8
인스턴스 메서드 결과 : 15
*/

2.2.2. 생성자 참조

메서드 참조는 생성자 참조도 포함된다.

생성자를 참조한다는 것은 객체 생성을 의미한다.

이 경우 생성자 참조로 표현하려면 클래스 이름 뒤에 :: 기호를 붙이고 new 연산자를 기술하면 된다.

생성자가 오버로딩 되어 여러 개가 있을 경우 컴파일러는 함수형 인터페이스의 추상 메서드와 동일한 매개 변수 타입과 개수를 가지고 있는 생성자를 찾아 실행한다.

만약 해당 생성자가 존재하지 않으면 컴파일 오류가 발생한다.

//Member.java
public class Member {
  private String name;
  private String id;

  public Member() {
    System.out.println("Member() 실행");
  }

  public Member(String id) {
    System.out.println("Member(String id) 실행");
    this.id = id;
  }

  public Member(String name, String id) {
    System.out.println("Member(String name, String id) 실행");
    this.id = id;
    this.name = name;
  }

  public String getName() {
    return name;
  }

public String getId() {
    return id;
  }
}
import java.util.function.BiFunction;
import java.util.function.Function;

public class ConstructorRef {
  public static void main(String[] args) throws Exception {
    Function<String, Member> function1 = Member::new;
    Member member1 = function1.apply("kimcoding");

    BiFunction<String, String, Member> function2 = Member::new;
    Member member2 = function2.apply("kimcoding", "김코딩");
  }
}

/*
Member(String id) 실행
Member(String name, String id) 실행
*/

위의 코드 예제는 생성자 참조를 이용해서 두 가지 방법으로 Member 객체를 생성하고 있다.

하나는 Function<String, Member> 함수형 인터페이스의 Member apply(String) 메서드를 이용해서 Member 객체를 생성하고, 다른 하나는 BiFunction<String, String, Member> 함수형 인터페이스의 Member 객체를 생성한다.

이때 생성자 참조는 두 가지 방법 모두 동일하지만, 실행되는 Member 생성자가 다른 것을 볼 수 있다.

3. 스트림

스트림(Stream)이란 배열 및 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 하는 반복자이다.

스트림은 선언형 프로그래밍(Declarative Programming) 방식으로 내부의 동작 원리를 모르더라도 어떤 코드가 어떤 역할을 하는 지 직관적으로 이해할 수 있다.

스트림의 핵심적인 특징으로 네 가지가 있다.

  1. 스트림 처리 과정은 생성, 중간 연산, 최종 연산 세 단계의 파이프라인으로 구성될 수 있다.
  2. 스트림 원본 데이터 소스를 변경하지 않는다.(read-only)
  3. 스트림은 일회용이다.(onetime-only)
  4. 스트림은 내부 반복자이다.

3.1. 스트림 생성

3.1.1. 배열 스트림 생성

배열을 데이터 소스로 하는 스트림 생성은 Arrays 클래스의 stream() 메서드 또는 Stream 클래스의 of() 메서드를 사용할 수 있다.

  • Arrays.stream()
 public class StreamCreator {
   
       public static void main(String[] args) {
           // 문자열 배열 선언 및 할당
           String[] arr = new String[]{"apple", "banana", "orange"};
   
           // 문자열 스트림 생성
           Stream<String> stream = Arrays.stream(arr);
   
           // 출력
           stream.forEach(System.out::println);
   
       }
   }
   
   // 출력값
   apple
   banana
   orange
  • Stream.of()
import java.util.stream.Stream;

public class StreamCreator {

    public static void main(String[] args) {
        // 문자열 배열 선언 및 할당
        String[] arr = new String[]{"apple", "banana", "orange"};

        // 문자열 스트림 생성
        Stream<String> stream = Stream.of(arr);

        // 출력
        stream.forEach(System.out::println);

    }
}

// 출력값
apple
banana
orange

추가적으로, Arrays 클래스에는 int, long, double 과 같은 기본형 배열을 데이터 소스로 스트림을 생성하는 IntStream, LongStream, DoubleStream 메서드가 있다.

3.1.2. 컬렉션 스트림

컬렉션 타입(List, Set 등)의 경우, 컬렉션의 최상위 클래스인 Collection에 정의된 stream() 메서드를 사용하여 스트림을 생성할 수 있다.

  • 컬렉션 스트림 생성
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamCreator {

    public static void main(String[] args) {
				// 요소들을 리스트
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        Stream<Integer> stream = list.stream();

        stream.forEach(System.out::print);
    }
}

//출력값
1234567

3.1.3. 임의의 수 스트림 생성

난수를 생성하는 자바의 기본 내장 클래스 Random 클래스 안에는 해당 타입의 난수들을 반환하는 스트림을 생성하는 메서드들이 정의되어 있다.

import java.util.Random;
import java.util.stream.IntStream;

public class StreamCreator {

    public static void main(String[] args) {
			
			// 스트림 생성의 범위를 5개로 제한
        IntStream ints = new Random().ints(5); // 범위를 지정해주지 않으면 무한대로 생성된다.
		IntStream ints = new Random().ints().limit(5); // 
        ints.forEach(System.out::println);
    }
}

추가적으로 IntStreamLongStream에 정의된 range()rangeClosed를 사용하면 특정 범위의 정수값을 스트림으로 생성해서 반환할 수 있다.

import java.util.stream.IntStream;

public class StreamCreator {

    public static void main(String[] args) {
        //특정 범위의 정수
        IntStream intStream = IntStream.rangeClosed(1, 10);
        intStream.forEach(System.out::println);
    }
}

//출력값
12345678910

rangeClosed()range() 의 차이는 두 번째로 전달되는 매개 변수가 범위 안에 포함되는 지 여부에 따라 구분될 수 있다. rangeClosed() 는 끝번호가 포함되어 위의 코드 예제처럼 1~10까지의 숫자가 출력되는 반면, range() 의 경우에는 끝번호가 포함되지 않아 1~9까지의 숫자가 출력된다.

3.2. 스트림의 중간 연산

스트림의 중간 연산자의 결과는 스트림을 반환하기 때문에 여러 개의 연산자를 연결하여 원하는 데이터 처리를 수행 할 수 있다.

다양한 중간 연산자들이 있지만 그 중에서 가장 빈번하게 사용되는 필터링(filtering), 매핑(mapping), 정렬(sorting) 등을 알아보겠다.

3.2.1. 필터링

필터링은 이름 그대로 필요에 따라 조건에 맞는 데이터들만을 정제하는 역할을 하는 중간 연산자를 가리킨다.

  • distinct() : Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 사용한다.
  • filter() : Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어낸다. filter() 메서드에는 매개값으로 조건(Predicate)이 주어지고, 조건이 참이 되는 요소만 필터링한다. 여기서 조건은 람다식을 사용하여 정의할 수 있다.
import java.util.Arrays;
import java.util.List;

public class FilteringExample {
    public static void main(String[] args) throws Exception {

        List<String> names = Arrays.asList("김코딩", "이자바", "박해커", "김코딩", "박해커");

        names.stream()
                .distinct() //중복 제거
                .forEach(element -> System.out.println(element));
        System.out.println();

        names.stream()
                .filter(element -> element.startsWith("김")) // 김씨 성을 가진 요소만 필터링 
                .forEach(element -> System.out.println(element));
        System.out.println();

        names.stream()
                .distinct() //중복제거
                .filter(element -> element.startsWith("김")) // 김씨 성을 가진 요소만 필터링 
                .forEach(element -> System.out.println(element));
    }
}

// 출력값
김코딩
이자바
박해커

김코딩
김코딩

김코딩

3.2.2. 매핑

매핑은 스트림 내 요소들에서 원하는 필드만 추출하거나 특정 형태로 변환할 때 사용하는 중간연산자다. 위의 filter 메서드와 마찬가지로 값을 변환하기 위한 조건을 람다식으로 정의한다.

  • map()
import java.util.Arrays;
import java.util.List;

public class IntermediateOperationExample {
    public static void main(String[] args) {
        List<String> fruits = Arrays.asList("apple", "banana", "orange", "strawberry");
        fruits.stream()
                .map(element -> element.toUpperCase()) // 요소들을 하나씩 대문자로 변환
                .forEach(element->System.out.println(element));
    }
}

// 출력값
APPLE
BANANA
ORANGE
STARWBERRY

위의 예제는 리스트 타입의 참조변수 fruits 안에 정의되어 있는 각 요소들을 순회하면서 소문자 이름을 대문자로 변환 값들이 담긴 스트림으로 반환하는 연산 과정을 보여준다.

또한, 다음과 같이 각 요소들에 어떤 연산을 실행하고 난 후의 값을 반환받을 수 있다.

import java.util.Arrays;
import java.util.List;

public class IntermediateOperationExample {
    public static void main(String[] args)
    {
        List<Integer> list = Arrays.asList(1, 3, 6, 9);

        // 각 요소에 3을 곱한 값을 반환
        list.stream().map(number -> number * 3).forEach(System.out::println);
    }

}

// 출력값
3
9
18
27
  • flatMap()
String[][] namesArray = new String[][]{{"박해커", "이자바"}, {"김코딩", "나박사"}};

Arrays.stream(namesArray)
			.flatMap(Arrays::stream)
            .forEach(System.out::println);

위의 예시처럼 flatMap()은 중첩 구조를 제거하고 단일 컬렉션(Stream<String>)으로 만들어주는 역할을 한다. 이를 요소들을 "평평하게"한다는 의미에서 플래트닝(flattening)이라고 한다.

3.2.3. 정렬

sorted() 메서드는 이름처럼 정렬을 할 때 사용하는 중간 연산자이다.

sorted() 메서드를 사용하여 정렬을 할 때에는 괄호(()) 안에 Comparator 라는 인터페이스에 정의 된 static 메서드와 디폴트 메서드를 사용해 간편하게 정렬 작업을 수행할 수 있다. 괄호 안에 아무 값도 넣지 않은 상태로 호출하면 기본 정렬(오름차순)로 정렬된다.

import java.util.Arrays;
import java.util.List;

public class IntermediateOperationExample {
    public static void main(String[] args) {
				// 동물들의 이름을 모아둔 리스트 
        List<String> animals = Arrays.asList("Tiger", "Lion", "Monkey", "Duck", "Horse", "Cow");
				
				// 인자값 없는 sort() 호출
        animals.stream().sorted().forEach(System.out::println);
    }
}

// 출력값
Cow
Duck
Horse
Lion
Monkey
Tiger
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class IntermediateOperationExample {
    public static void main(String[] args) {

        List<String> animals = Arrays.asList("Tiger", "Lion", "Monkey", "Duck", "Horse", "Cow");

				// 인자값에 Comparator 인터페이스에 규정된 메서드 사용
        animals.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);

    }
}

// 출력값
Tiger
Monkey
Lion
Horse
Duck
Cow

3.2.4. 기타

  • skip() - 스트림의 일부 요소들을 건너 뛴다.
import java.util.stream.IntStream;
    
public class IntermediateOperationExample {
    public static void main(String[] args) {
    
        // 1~10 범위의 정수로 구성된 스트림 생성
        IntStream intStream = IntStream.rangeClosed(1, 10);
    
        // 앞의 5개의 숫자를 건너뛰고 숫자 6부터 출력
        intStream.skip(5).forEach(System.out::println);
    }
}
    
// 출력값
6
7
8
9
10
  • limit() - 스트림의 일부를 자른다.
import java.util.stream.IntStream;
    
public class IntermediateOperationExample {
    public static void main(String[] args) {
    
        // 1~10 범위의 정수로 구성된 스트림 생성
        IntStream intStream = IntStream.rangeClosed(1, 10);
    
        // 앞에서부터 5개의 숫자만 출력
        intStream.limit(5).forEach(System.out::println);
    }
}
    
// 출력값
1
2
3
4
5
  • peek() - forEach() 와 마찬가지로, 요소들을 순회하며 특정 작업을 수행한다. forEach()와 다른 점은 peek()는 중산 연산자이기 때문에 여러번 사용할 수 있다.
import java.util.stream.IntStream;

public class IntermediateOperationExample {
    public static void main(String[] args) {

        // 요소들을 사용하여 IntStream 생성
        IntStream intStream3 = IntStream.of(1, 2, 2, 3, 3, 4, 5, 5, 7, 7, 7, 8);

        // 짝수만 필터링하여 합계 구하기
        int sum = intStream3.filter(element -> element % 2 == 0)
                .peek(System.out::println)
                .sum();

        System.out.println("합계 = " + sum);
    }
}

// 출력값
2
2
4
8
합계 = 16

3.3. 스트림의 최종 연산

최종 연산은 스트림 파이프라인에서 최종적으로 사용되고 나면, 해당 스트림은 닫히고 모든 연산이 종료된다는 의미이다.

또한, 중간 연산은 최종 연산자가 수행될 때야 비로소 스트림의 요소들이 중간 연산을 거쳐 가공된 후에 최종 연산에서 소모되는데 이를 좀 어려운 말로 “지연된 연산(lazy evaluation)”이라고 부른다.

3.3.1. 기본 집계(sum(), count(), average(), max(), min())

숫자와 관련된 기본적인 집계의 경우에는 대부분 최종 연산자라고 생각해도 크게 틀리지 않는다.

import java.util.Arrays;

public class TerminalOperationExample {
    public static void main(String[] args) {
        // int형 배열 생성
        int[] intArray = {1,2,3,4,5};

        // 카운팅
        long count = Arrays.stream(intArray).count();
        System.out.println("intArr의 전체 요소 개수 " + count);

        // 합계
        long sum = Arrays.stream(intArray).sum();
        System.out.println("intArr의 전체 요소 합 " + sum);

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

        // 최대값
        int max = Arrays.stream(intArray).max().getAsInt();
        System.out.println("최대값 " + max);

        // 최소값
        int min = Arrays.stream(intArray).min().getAsInt();
        System.out.println("최소값 " + min);

        // 배열의 첫 번째 요소 
        int first = Arrays.stream(intArray).findFirst().getAsInt();
        System.out.println("배열의 첫번째 요소 " + first);
    }
}

// 출력값
intArr의 전체 요소 개수 5
intArr의 전체 요소 합 15
전체 요소의 평균값 3.0
최대값 5
최소값 1
배열의 첫번째 요소 1

위의 예제에서 최종 연산 뒤에 getAsInt()getAsDouble이 붙는 이유는

만약 getAsInt()getAsDouble을 사용하지 않는 다면 반환값의 타입이 OptionalIntOptionalDouble이라는 래퍼 클래스 객체로 반환되기 때문에 반환되는 값을 다시 기본형으로 변환하기 위해서 getAsInt()getAsDouble이 사용된다.

3.3.2. 매칭

match() 메서드를 사용하면 조건식 람다 Predicate를 매개변수로 넘겨 스트림의 각 데이터 요소들이 특정한 조건을 충족하는 지 만족시키지 않는 지 검사하여, 그 결과를 boolean 값으로 반환한다.

match() 메서드는 크게 세 가지 종류가 있다.

  • allMatch() - 모든 요소들이 조건을 만족하는 지 여부를 판단

  • noneMatch() - 모든 요소들이 조건을 만족하지 않는 지 여부를 판단

  • anyMatch() - 하나라도 조건을 만족하는 요소가 있는 지 여부를 판단

  • 매칭

import java.util.Arrays;

public class TerminalOperationExample {
    public static void main(String[] args) throws Exception {
        // int형 배열 생성
        int[] intArray = {2,4,6};

        // allMatch()
        boolean result = Arrays.stream(intArray).allMatch(element-> element % 2 == 0);
        System.out.println("요소 모두 2의 배수인가요? " + result);

        // anyMatch()
        result = Arrays.stream(intArray).anyMatch(element-> element % 3 == 0);
        System.out.println("요소 중 하나라도 3의 배수가 있나요? " + result);

        // noneMatch()
        result = Arrays.stream(intArray).noneMatch(element -> element % 3 == 0);
        System.out.println("요소 중 3의 배수가 하나도 없나요? " + result);
    }

}

// 출력값
요소 모두 2의 배수인가요? true
요소 중 하나라도 3의 배수가 있나요? true
요소 중 3의 배수가 하나도 없나요? false

3.3.3. 요소 소모

reduce() 최종 연산자는 스트림의 요소를 줄여나가면서 연산을 수행하고 최종적인 결과를 반환한다.

연산하는 방법은 첫 번째와 두 번째 요소를 가지고 연산을 수행하고, 그 결과와 다음 세 번째 요소를 가지고 또 다시 연산을 수행하는 식으로 연산이 끝날 때까지 반복한다.

그렇기 때문에 reduce() 메서드의 매개변수 타입은 BinaryOperater<T>로 정의되어 있다.

T reduce(T identity, BinaryOperator<T> accumulator)

위에서 첫 번째 매개변수 identity는 특정 연산을 시작할 때 설정되는 초기값을 의미한다. 두 번째 accumulator는 각 요소들을 연산하여 나온 누적된 결과값을 생성하는 데 사용하는 조건식이다.

  • reduce()
import java.util.Arrays;

public class TerminalOperationExample {
    public static void main(String[] args) throws Exception {
        int[] intArray = {1,2,3,4,5};

        // sum()
        long sum = Arrays.stream(intArray).sum();
        System.out.println("intArray 전체 요소 합: " + sum);

        // 초기값이 없는 reduce()
        int sum1 = Arrays.stream(intArray)
                .map(element -> element * 2)
	                .reduce((a , b) -> a + b)
                .getAsInt();
        System.out.println("초기값이 없는 reduce(): " + sum1);

        // 초기값이 있는 reduce()
        int sum2= Arrays.stream(intArray)
                .map(element -> element * 2)
                .reduce(5, (a ,b) -> a + b);
        System.out.println("초기값이 있는 reduce(): " + sum2);
    }
}

// 출력값
intArray 전체 요소 합: 15
초기값이 없는 reduce(): 30
초기값이 있는 reduce(): 35

첫 번째 예제는 sum() 메서드를 활용하여 숫자 요소들의 총합을 도출하는 스트림 작업이다. 1+2+3+4+5 의 결과로 숫자 15 가 나왔다.

두 번째와 세 번째는 모두 reduce() 메서드를 사용하고 있지만, 세 번째의 경우에는 초기값으로 5 가 설정이 되어있기 때문에 최종적인 연산의 결과가 두 번째 예제보다 5 가 더 많은 35 가 출력되었다.

위의 배열의 값을 사용하여 조금 더 구체적인 흐름을 살펴보면 다음과 같다.

  1. accumulator: (a, b) -> a + b (a: 누적 값, b: 새롭게 더해질 값)
  2. 최초 연산 시 1+2 -> a : 3, b : 3
  3. 3+3 -> a : 6, b : 4
  4. 6+4 -> a : 10, b : 5
  5. 10+5 -> 최종 결과 : 15

참고로, count()sum() 과 같은 집계 메서드 또한 내부적으로 모두 reduce() 를 사용하여 연산을 수행한다.

3.3.4. 요소 수집

스트림은 중간 연산을 통한 요소들의 데이터 가공후 요소들을 수집하는 최종 처리 메서드인 collect() 를 지원한다. 좀 더 구체적으로, 스트림의 요소들을 List, Set, Map 등 다른 타입의 결과로 수집하고 싶은 경우에 collect() 메서드를 사용하면 된다.

  • collect()
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TerminalOperationExample {

    public static void main(String[] args) {
        // Student 객체로 구성된 배열 리스트 생성 
        List<Student> totalList = Arrays.asList(
                new Student("김코딩", 100, Student.Gender.Male),
                new Student("박해커", 80, Student.Gender.Male),
                new Student("이자바", 90, Student.Gender.Female),
                new Student("나미녀", 60, Student.Gender.Female)
        );
        
        // 스트림 연산 결과를 Map으로 반환
        Map<String, Integer> maleMap = totalList.stream()
                .filter(s -> s.getGender() == Student.Gender.Male)
                .collect(Collectors.toMap(
                        student -> student.getName(), // Key
                        student -> student.getScore() // Value
                ));

        // 출력
        System.out.println(maleMap);
    }
}

class Student {
    public enum Gender {Male, Female};
    private String name;
    private int score;
    private Gender gender;

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

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    public Gender getGender() {
        return gender;
    }
}

// 출력값
{김코딩=100, 박해커=80}

이렇게 리스트 배열로 이뤄진 데이터 요소들에서 남학생들의 이름과 점수만을 추출하여 이름과 점수를 각각 키(key)값(value)으로 하는 Map 타입의 결과를 수집하고자 할 때 collect() 메서드가 위의 코드처럼 유용하게 사용될 수 있다.

흐름을 간략하게 설명해보면, 먼저 리스트 배열에 스트링을 생성하고, 중간 연산자 filter() 메서드를 통해 성별이 남자인 학생들만 필터링 한 후, 마지막으로 최종 연산자 collect()Collectors 클래스 안에 정의된 정적 메서드(toMap())을 사용하면 아래 출력값과 같이 우리가 의도했던대로 Map 타입의 결과물을 받아볼 수 있다.

0개의 댓글