자바 : 애너테이션과 람다, 그리고 스트림
애너테이션(annotation)
- 정보 전달을 위한 목적
- 애너테이션은 다른 프로그램에게 정보를 전달
- 소스 코드가 컴파일되거나 실행될 때에 컴파일러 및 다른 프로그램에게 필요한 정보를 전달해주는 문법 요소
ex) @Override
애너테이션은 @로 시작하며, 클래스, 인터페이스, 필드, 메서드 등에 붙여서 사용
~> 컴파일러에게 알려주는 역할
JDK에서 기본적으로 제공하는 애너테이션
표준 애너테이션 : JDK에 내장되어 있는 일반적인 애너테이션
~> @Override와 같이 다른 문법 요소에 붙여서 사용하는 일반적인 애너테이션을 의미
메타 애너테이션 : 다른 애너테이션을 정의할 때 사용하는 애너테이션
~> 애너테이션을 직접 정의해서 사용할 때에 사용하는 애너테이션
! 붙이는 이유
@Override를 예로
상속 관계 일때 개발자의 실수로 오버라이드 하려는 메소드 명이 잘못 정의되면
컴파일 할때 새로운 메소드를 정의한 것으로 간주하고 컴파일 에러가 발생하지 않음
~> 이런 실수가 있을 경우 @Override가 붙어 있으면 이 메소드는 상위 객체의 오버라이드니까 문제 있으면 알려달라는 것
== 추상 메소드 구현도 마찬가지임
표준 애너테이션
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Practice_Override {
public static void main(String[] args) {
Example ex = new power1();
ex.exam();
}
}
interface Example{
void exam();
}
class power1 implements Example{
@Override // << 이게 애너테이션 / @로 시작하며 클래스, 인터페이스, 필드, 메서드 등에 붙여서 사용
// 이거 추상 메소드 구현 오버라이드 입니다~하고 컴파일러에게 알려주는 역할
public void exam() {
System.out.println("애너테이션 연습");
}
@Deprecated
기존에 사용하던 기술이 다른 기술로 대체되어 기존 기술을 적용한 코드를 더 이상 사용하지 않도록 유도하는 경우에 사용
기존의 코드를 다른 코드와의 호환성 문제로 삭제하기 곤란해 남겨두어야만 하지만 더 이상 사용하는 것을 권장하지 않을 때에 @Deprecated를 사용
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Practice_Deprecated {
public static void main(String[] args) {
OldClass oldClass = new OldClass();
System.out.println(oldClass.getOldField()); // << 밑줄로 경고해줌
}
}
class OldClass {
@Deprecated // 삭제하기 곤란해 남겨야하지만 쓰는 거 권장하지 않음의 뜻
private int oldField;
@Deprecated
public int getOldField() {
return oldField;
}
}
@SuppressWarnings
컴파일 경고 메시지가 나타나지 않도록 함
경고가 발생할 것이 충분히 예상됨에도 묵인해야 할 때 주로 사용
@SuppressWarnings 뒤에 괄호를 붙이고 그 안에 억제하고자 하는 경고메세지를 지정해줄 수 있음
ex) @SuppressWarnings({“deprecation”, “unused”, “null”})
ex)
1
2
3
4
5
6
7
8
class Anno {
@SuppressWarnings("all")
public void aa1() {
if(0==0) { // << always true 라는 경고가 출력 안됨
System.out.println("무조건 true임");
} else System.out.println("이건 무조건 안나옴");
}
}
@FunctionalInterface
함수형 인터페이스를 선언할 때, 컴파일러가 함수형 인터페이스의 선언이 바르게 선언되었는지 확인하도록
함수형 인터페이스는 단 하나의 추상 메서드만을 가져야하는 제약
메타 애너테이션
애너테이션을 정의하는 데에 사용되는 애너테이션
애너테이션의 적용 대상 및 유지 기간을 지정하는 데에 사용
@Target
애너테이션을 적용할 “대상”을 지정하는 데 사용
ex)
1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
public class Practice_Target {
}
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.TYPE_USE}) // 얘네를 담고 있는
@interface Custom{} // 커스텀 애노테이션 생성
@Custom // Type 대상에 적용
class main {
@Custom // FIELD 대상에 적용
int i;
}
@Documented
javadoc으로 작성한 문서에 포함되도록 하는 애너테이션 설정
@Inherited
하위 클래스가 애너테이션을 상속받도록
상위 클래스에 붙이면, 하위 클래스도 상위 클래스에 붙은 애너테이션들이 동일하게 적용
@Retention
특정 애너테이션의 지속 시간을 결정하는 데 사용
@Repeatable
애너테이션을 여러 번 붙일 수 있도록 허용한다는 의미
참고 : 사용자 정의 애너테이션
ex)
1
2
3
@interface 애너테이션명 { // 인터페이스 앞에 @기호만 붙이면 애너테이션을 정의할 수 있습니다.
타입 요소명(); // 애너테이션 요소를 선언
}
~> java.lang.annotation 인터페이스를 상속받기 때문에 다른 클래스나 인터페이스를 상속 받을 수 없다
람다식(Lambda Expression)
- 함수형 프로그래밍 기법을 지원하는 자바의 문법요소
- 메서드를 하나의 ‘식(expression)’으로 표현한 것
코드를 매우 간결하면서 명확하게 표현할 수 있다는 큰 장점
- 익명 자식 객체 (= 상속)
=> 상위 클래스를 상속 받아서 생성
=> 상위 타입의 참조변수에 할당 가능
=> 익명 하위 객체를 만들 때 , 중괄호 블록 내에는 보통 상위 클래스의 메소드를 재정의하는 코드 작성
익명 구현 객체 (= 인터페이스)
=> 인터페이스를 구현해서 생성
=> 인터페이스 타입의 참조 변수에 할당 가능
=> 익명 구현 객체를 만들 때, 중괄호 블록 내에는 보통 인터페이스의 추상 메소드를 구현하는 코드 작성
! 람다식은 인터페이스에서만 사용 가능
람다식의 기본 문법
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//기존 메서드 표현 방식
void sayhello() {
System.out.println("HELLO!")
}
//위의 코드를 람다식으로 표현한 식
() -> System.out.println("HELLO!")
~> 반환타입과 이름을 생략
따라서 람다함수를 종종 이름이 없는 함수, 즉 익명 함수(anonymous function)라 부르기도 함
변환 ex)1
int sum(int num1, int num2) {
return num1 + num2;
}
// ↓
(int num1, int num2) -> { // 반환타입과 메서드명 제거 + 화살표 추가
return num1 + num2;
}
ex)2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 기존 방식
void example1() {
System.out.println(5);
}
// ↓
() -> {System.out.println(5);}
ex)3
// 기존 방식
int example2() {
return 10;
}
// ↓
() -> {return 10;}
ex)4
// 기존 방식
void example3(String str) {
System.out.println(str);
}
// ↓
(String str) -> {System.out.println(str)};
! 특정 조건이 충족되면 람다식을 더욱 축약하여 표현
실행문이 하나만 존재할 경우 중괄호, ;, return문 생략 가능
매개변수의 타입을 함수형 인터페이스를 통해 유추할 수 있는 경우에 매개변수의 타입까지 생략 가능
ex)
1
2
3
4
5
6
7
8
// 기존 방식
int sum(int num1, int num2) {
return num1 + num2;
}
// ↓
(num1, num2) -> num1 + num2
함수형 인터페이스
- 람다식 = 객체 ~> 이름이 없기 때문에 일회용 익명 객체
- 익명 객체는 익명 클래스를 통해 만들 수 있음
- 익명 클래스 = 객체의 선언과 생성을 동시에 하여 오직 하나의 객체를 생성하고, 단 한번만 사용되는 일회용 클래스
~> 여기서 람다식이 객체면 이 객체에 접근하고 사용하기 위한 참조변수가 필요함
ex)
1
2
3
4
5
6
7
new Object() {
int sum(int num1, int num2) {
return num1 + num2;
}
};
// 여기서는 Object 클래스에 sum 이라는 메소드가 없어서
// Object 타입의 참조변수에 할당해도 sum 메소드를 쓸 수 없음
↓
이걸 메인 메소드에 인스턴스화 해 출력해도
java: cannot find symbol
symbol: method sum(int,int)
location: variable o of type java.lang.Object
이런 결과가 나옴
↓
해결하기 위한 방법이 함수형 인터페이스(Functional Interface)임
즉, 함수형 인터페이스는 기존의 인터페이스 문법을 활용하여 람다식을 다루는 것
단, 함수형 인터페이스에는 단 하나의 추상 메소드만 선언할 수 있음
람다식과 1:1로 매칭되어야 하기 때문
메서드 레퍼런스
- 불필요한 매개변수를 제거할 때 주로 사용
- 즉, 객체를 더욱더 간단하게 사용
- 입력값과 출력값의 반환타입을 쉽게 유추할 수 있기 때문에 입력값과 출력값을 일일이 적어주는 게 크게 중요하지 않음
ex)
1
2
3
4
(left, right) -> Math.max(left, right)
// 이걸 더 간편하게 만들면 ↓
Math :: max
// Math 클래스의 max() 메소드를 참조한다는 뜻임
내 경험 ex)
1
2
3
4
5
Functional functional = (num1, num2) -> num1 + num2;
// 참고로 이거 num 에 int 타입 들어갈거고 래퍼 클래스인 Integer 에
// 더하기 메소드가 있음 그래서 변형 가능
// 그래서 Functional functional = Integer::sum; 로 변형 가능
// Intger 클래스의 sum() 메소드를 참조한다는 것
! 메소드 참조의 원리도 람다식과 똑같이 인터페이스의 익명 구현 객체로 생성되는 것으로
인터페이스의 추상 메서드가 어떤 매개 변수를 가지고, 리턴 타입이 무엇인가에 따라 달라짐
정적 메소드와 인스턴스 메소드 참조
정적 메소드 ~> 클래스 :: 메소드
인스턴스 메소드 ~> 참조변수 :: 메소드
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Method_reference {
public static void main(String[] args) {
A a;
a = Integer :: sum;
System.out.println(a.ac(1,2));
a = Integer :: max;
System.out.println(a.ac(5,1));
a = Integer :: min;
System.out.println(a.ac(5,1));
// 기존 함수형 인터페이스 참조 메소드
a = Plus :: s; // 클래스명 :: 메소드명
System.out.println(a.ac(7,1));
// 정적 메소드 참조
Plus plus = new Plus();
a = plus :: i; // 인스턴스명 :: 메소드명
System.out.println(a.ac(7,1));
// 인스턴스 메소드 참조
}
}
interface A { // 함수형 인터페이스
int ac(int x, int y);
}
class Plus {
public static int s(int x, int y){ // 정적 메소드
return x + y;
}
public int i(int x, int y) { // 인스턴스 메소드
return x * y;
}
}
생성자 참조
- 생성자를 참조한다는 것은 객체 생성을 의미
- 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치 가능
- 클래스 이름 뒤에 :: 기호를 붙이고 new 연산자를 기술
ex)
1
2
3
4
5
//기존 람다식
(a,b) -> new 클래스(a,b)
//생성자 메소드 참조
클래스 :: new
생성자가 오버로딩 되어 여러 개가 있을 경우
컴파일러는 함수형 인터페이스의 추상 메서드와 동일한 매개 변수 타입과 개수를 가지고 있는 생성자를 찾아 실행
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Constructor_reference {
public static void main(String[] args) {
Function<String,Person> function = Person::new;
Person person = function.apply("");
BiFunction<String,String,Person> biFunction = Person::new;
Person person1 = biFunction.apply("","");
}
}
class Person {
private String name;
private String id;
public Person(String name) {
this.name = name;
System.out.println("이름만");
}
public Person(String name, String id) {
this.name = name;
this.id = id;
System.out.println("이름 아이디");
}
public String getName() {
return name;
}
public String getId() {
return id;
}
}
!
이거 근데 잘 이해 안감
Function 이랑 BiFunction 들어가는 이유랑
왜 써야하는지?
람다식이 맞는지? 맞다면 인터페이스는 어따 팔아먹었는지?
이건 그냥 메소드인거고
! 람다식은 인터페이스에서만 사용 가능
스트림(Stream)
- 배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자
- List, Set, Map, 배열 등 다양한 데이터 소스로부터 스트림을 만들 수 있고, 이를 표준화된 방법으로 다룰 수 있음
- 다량의 데이터에 복잡한 연산을 수행하면서도, 가독성과 재사용성이 높은 코드를 작성할 수 있음
스트림의 핵심 개념과 특징
- 자바8부터 도입된 문법
- 배열 및 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 하는 반복자
- 자바에서의 스트림은 “데이터의 흐름”을 의미
~> 각 데이터를 흐름에 따라 우리가 원하는 결과로 가공하고 처리하는 일련의 과정
스트림(Stream)의 도입 배경
기존에는 >
배열과 컬렉션
~> 저장된 데이터들에 반복적으로 접근 & 가공
~> for문 과 Iterator 을 활용
근데 이게 문제가 코드가 길고 복잡해질 수 있음
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Basic {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
// 기존의 Iterator 방식 순회
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
System.out.print(it.next());
}
System.out.println();
// 스트림 사용 순회
Stream<Integer> stream = list.stream();
stream.forEach(System.out::print);
}
}
명령형 프로그래밍(Imperative Programming)
- “어떻게” 코드를 작성할 지
- 기존에 하던 방식
선언형 프로그래밍(Declarative Programming)
- “무엇”에 집중하여 코드를 작성할 지 // 어떻게는 “추상화”됨
- 스트림 도입 방식
- 데이터 소스가 무엇이냐에 관계없이 같은 방식으로 데이터를 가공/처리
- 열이냐 컬렉션이냐에 관계없이 하나의 통합된 방식으로 데이터를 다룰 수 있게 되었음
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Declarative_Imperative {
public static void main(String[] args) {
List<Integer> list = List.of(1,3,5,7,9,11);
int sum = 0;
// 명령형 프로그래밍
for (int x : list) {
if (x > 4){
sum+=x;
}
}
System.out.println("명령형 프로그래밍 : "+sum);
// 기존에 하던대로 for-each 문으로 순회 후 합계 계산
// 선언형 프로그래밍
int sum1 =
list.stream()
.filter(x -> x > 4)
.mapToInt(x -> x)
.sum();
System.out.println("선언형 프로그래밍 : "+sum1);
// stream 을 이용한 합계 계산
// 전부 메소드 + 람다식 이라 이해하기 훨씬 직관적이긴 함
// 아직 코드를 짧게 넣어서 간결한지는 몰?루겠음
List<String> fruitList = List.of("바나나 ", "사과 ", "오렌지");
String[] fruits = {"오렌지 ", "사과 ", "바나나"};
// 리스트랑 배열 생성
Stream<String> str = fruitList.stream();
// 리스트 스트림 적용
Stream<String> str2 = Arrays.stream(fruits);
// 배열 스트림 적용
str.forEach(System.out::print); // 리스트 순회
System.out.println();
str2.forEach(System.out::print); // 배열 순회
}
}
스트림의 특징
- 스트림 처리 과정은 생성, 중간 연산, 최종 연산 세 단계의 파이프라인으로 구성
- 스트림은 원본 데이터 소스를 변경하지 않는다(read-only)
- 스트림은 일회용이다(onetime-only)
- 스트림은 내부 반복자
1.
스트림 파이프라인 3단계
1) 스트림의 생성
배열, 컬렉션, 임의의 수 등 다양한 데이터 소스를 일원화하여 스트림으로 작업하기 위해서 스트림을 생성
2) 중간 연산
필터링, 매핑, 정렬 등의 작업이 포함
중간 연산의 결과는 또 다른 스트림이기 때문에 계속 연결해서 연산을 수행할 수 있음
3) 최종 연산
중간 연산이 완료된 스트림을 최종적으로 처리하는 최종 연산(총합, 평균, 카운팅 등)을 끝으로 스트림은 닫히고 모든 데이터 처리가 완료
최종적으로 단 한번의 연산만 가능 ~> 다시 데이터를 처리하고 싶다면, 다시 스트림을 생성해야함
2.
원본이 되는 데이터 소스의 데이터들을 변경하지 않음
오직 데이터를 읽어올 수 있고, 데이터에 대한 변경과 처리는 생성된 스트림 안에서면 수행
원본 데이터가 스트림에 의해 임의로 변경되거나 데이터가 손상되는 일을 방지하기 위함
3.
스트림이 생성되고 여러 중간 연산을 거쳐 마지막 최종 연산이 수행되고 난 후에는 스트림은 닫히고 다시 사용 불가능
4.
외부 반복자(External Iterator) = 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 의미
ex) 인덱스를 사용하는 for문 , Iterator 를 사용하는 while문
내부 반복자(Internal Iterator) = 외부 반복자 반대 개념
컬렉션 내부에 데이터 요소 처리 방법(람다식)을 주입시켜서 요소를 반복처리하는 방식
스트림의 생성
- 스트림으로 데이터를 처리하기 위해서는 가장 먼저 스트림을 생성
- 가장 많이 쓰이는 배열, 컬렉션, 그리고 임의의 수로 스트림을 생성하는 방법
배열 스트림 생성
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Create_ArrayStream {
public static void main(String[] args) {
//배열 스트림 생성
int[] arr = new int[]{1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(arr);
// int 배열을 int 형 스트림으로 생성 후 intStream 에 할당
intStream.forEach(System.out::print);
System.out.println();
double[] arr2 = new double[]{1.1, 1.2, 1.3, 1.4, 1.5};
// double 배열을 double 형 스트림으로 생성
DoubleStream doubleStream = Arrays.stream(arr2);
// double 배열을 double 형 스트림으로 생성
doubleStream.forEach(System.out::print);
System.out.println();
String[] arr1 = new String[]{"Hello", " Java", " World", " !"};
// Stream<String> stream = Arrays.stream(arr1);
// String 배열을 String 형 스트림으로 생성
Stream<String> stream = Stream.of(arr1);
// 문자열은 스트림 클래스에서 불러온 메소드인 of() 도 가능
stream.forEach(System.out::print);
System.out.println();
char[] arr3 = new char[]{'a','b','c','d','e'};
Stream<Character> stream1 = new String(arr3)
.chars().mapToObj(x -> (char) x);
// Character 는 특이하게 배열을 문자열로 생성한 다음에
// 다시 String 클래스의 메소드 들로 나누고
// 해당 클래스의 stream 객체의 메소드인 mapToObj 를 사용해서
// 다시 다운캐스팅 해 줘야함
stream1.forEach(System.out::print);
}
}
컬렉션 스트림 생성
- 최상위 클래스인 Collection 에 정의된 stream() 메서드를 사용하여 스트림을 생성할 수 있음
- 따라서 Collection 으로부터 확장된 하위클래스 List 와 Set 을 구현한 컬렉션 클래스들은 모두 stream() 메서드를 사용하여 스트림을 생성할 수 있음
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Create_CollectionStream {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> stream = list.stream();
// List 클래스를 참조하고 있는 변수 list 에서 곧바로 stream() 메소드를 호출 가능함
// 이미 Collection 클래스에 stream() 메소드가 정의되어있기 때문
// 그래서 생성이 간편함
stream.forEach(System.out::print);
System.out.println();
// 무한 스트림
// IntStream ints = new Random().ints();
// ints.forEach(System.out::println);
// 난수 무한 생성
// 횟수 제한을 두고 싶다면
// IntStream in = new Random().ints(3);
IntStream in = new Random().ints().limit(3);
in.forEach(System.out::println);
// 2가지의 제한 두는 방법
// 범위는?
IntStream out = IntStream.rangeClosed(1,10);
out.forEach(System.out::println);
}
}
스트림의 중간 연산
중간 연산자(Intermediate Operation)
- 스트림의 중간 연산자의 결과는 스트림을 반환하기 때문에 여러 개의 연산자를 연결하여 우리가 원하는 데이터 처리를 수행할 수 있다
- 가장 빈번하게 사용되는 필터링(filtering), 매핑(maping), 정렬(sorting)
필터링(filter() , distinct() )
- 조건에 맞는 데이터들만을 정제하는 역할을 하는 중간 연산자
distinct() : 중복을 제거하기 위해 사용
filter() :
조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어냄
매개값으로 조건(Predicate)이 주어지고, 조건이 참이 되는 요소만 필터링
조건은 람다식을 사용하여 정의
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
//필터링
List<String> ns = Arrays.asList("뭐","요","인마","어쩌","라구요","라구요");
// List 니까 stream 바로 사용 가능
ns.stream()
.distinct() // 중복 제거
.forEach(System.out::print);
System.out.println();
ns.stream()
.distinct()
.filter(e -> e.startsWith("a")) // a 로 시작하는 문자열 거르기
.forEach(System.out::print);
}
매핑(map())
- 원하는 필드만 추출하거나 특정 형태로 변환할 때 사용하는 중간연산자
- 조건을 람다식으로 정의
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ns.stream()
.map(e -> e.toUpperCase()) // 대문자 전환
.forEach(System.out::print); // 순회하면서 대문자로 전환 가능한거 전환해줌
System.out.println();
ns.stream()
.map(e -> e + "끼얏호우 ") // List 요소에 문자 추가
.forEach(System.out::print);
System.out.println();
// 이중 배열인 경우
String[][] arr = new String[][] { {"a ","b ","c "},{"1 ","2 ","3"} };
// 기존에 쓰던
Arrays.stream(arr) // 배열로 스트림 바로 쓰기!
.map(e -> Arrays.stream(e))
.forEach(System.out::print);
// 주소값이 나와벌임
System.out.println();
//변형해서
Arrays.stream(arr)
.map(e -> Arrays.stream(e))
.forEach(r -> r.forEach(System.out::print));
//forEach 메소드에 람다형 조건식으로 넣어줌으로 해결
System.out.println();
// 근데 불편하잖음 그럴때 쓰는게 flatMap 메소드
Arrays.stream(arr)
.flatMap(Arrays::stream) // 중첩을 제거하고 Stream<String> 으로 캐스팅 해줌 ~> flattening 이라함
.forEach(System.out::print);
}
정렬(sorted())
- 정렬을 할 때 사용하는 중간 연산자
- 괄호 안에 아무 값도 넣지 않은 상태로 호출하면 기본 정렬(오름차순)로 정렬
ex)
1
2
3
4
5
6
7
8
List<String> alphabet = Arrays.asList("v ","f ","s ","d ","b ","a ");
alphabet.stream().sorted().forEach(System.out::print);
// 정렬됨
System.out.println();
// 역순으로?
alphabet.stream().sorted(Comparator.reverseOrder()).forEach(System.out::print);
// Comparator 클래스의 reverseOrder 메소드를 사용하여 내림차순으로 할 수 있음
System.out.println();
skip()
- 스트림의 일부 요소들을 건너뜀
ex)
1
2
3
4
// skip
IntStream intStream = IntStream.rangeClosed(1,5);
intStream.skip(3).forEach(System.out::print);
// skip 메소드로 앞의 3글자 건너뛰기
limit()
- 스트림의 일부를 자름
ex)
1
2
3
4
// limit
IntStream intStream1 = IntStream.rangeClosed(1,5);
intStream1.limit(2).forEach(System.out::print);
// 앞의 2개만 출력
peek()
- forEach() 와 마찬가지로, 요소들을 순회하며 특정 작업을 수행
- forEach() 와의 차이는 peek는 중간 연산자라 여러번 사용할 수 있지만
- forEach()는 최종 연산자라 쓰면 스트림 닫힘
ex)
1
2
3
4
5
6
7
// peek
IntStream intStream2 = IntStream.of(1, 2,2,3,4,4,5,5,6,7,8,8,9,10);
int sum = intStream2.filter(e -> e % 2 == 0)
.peek(System.out::println)
.sum();
System.out.println("짝수의 합계 : " +sum);
// peek로 순회하면서 filter에 걸린 값을 출력, 이후에 sum 메소드로 해당 숫자 합침
스트림의 최종 연산
- 스트림 파이프라인에서 최종 연산자가 최종적으로 사용되고 나면, 해당 스트림은 닫히고 모든 연산이 종료됨
- 지연된 연산(lazy evaluation) : 중간 연산은 최종 연산자가 수행될 때야 비로소 스트림의 요소들이 중간 연산을 거쳐 가공된 후에 최종 연산에서 소모됨. 즉, 런타임 시 최종 연산자가 수행되면 그제서야 스트림이 생성되고 중간 연산을 거쳐 가공되고 출력된다는 뜻
기본 집계(sum() , count() , average(), max() , min())
- 숫자와 관련된 기본적인 집계의 경우에는 대부분 최종 연산자
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Terminal_Operation {
public static void main(String[] args) {
// 기본 집계 ~> 숫자 관련 메소드는 대부분 최종 연산자로 작동
int[] num = new int[]{1,2,3,4,5};
// 개수
long count = Arrays.stream(num).count(); // count 메소드가 long 타입만 받아들이나봄
System.out.println("개수 : "+count);
//합계
int sum = Arrays.stream(num).sum();
System.out.println("개수 : "+sum);
// 평균
double average = Arrays.stream(num).average().getAsDouble(); // getAsDouble 로 값 더블로 받음
System.out.println("평균 : "+average);
// 최대값
int max = Arrays.stream(num).max().getAsInt(); // 인트로 값 받기
System.out.println("최대값 " + max);
// 최소값
int min = Arrays.stream(num).min().getAsInt();
System.out.println("최소값 " + min);
// 배열의 첫 번째 요소
int first = Arrays.stream(num).findFirst().getAsInt();
System.out.println("배열의 첫번째 요소 " + first);
// 근데 최종 연산자로 끝냈는데 get? 어떻게 들어가지
// ~>
// // 평균값을 구해 Optional 객체로 반환
// OptionalDouble average = Arrays.stream(intArr).average();
// System.out.println(average);
//
// // 기본형으로 변환
// double result = average.getAsDouble();
// System.out.println("전체 요소의 평균값 " + result);
// 이게 풀어 쓴거
// 즉 OptionalDouble 클래스는 래퍼 클래스로 null 값이 나와서 에러가 발생하는 현상을
// 방지하기 위함
// 그래서 캐스팅이 한번 더 필요한 것
// 즉, 객체로 반환된 값을 다시 기본형으로 변환하기 위한 것이라 스트림 파이프라인과는 관계 없음
매칭(allMatch(), anyMatch(), noneMatch() )
- 조건식 람다 Predicate 를 매개변수로 넘겨 스트림의 각 데이터 요소들이 특정한 조건을 충족하는 지 만족시키지 않는 지 검사하여, 그 결과를 boolean 값으로 반환
- allMatch() : 모든 요소들이 조건을 만족하는 지 여부를 판단
- noneMatch() : 모든 요소들이 조건을 만족하지 않는 지 여부를 판단
- anyMatch() : 하나라도 조건을 만족하는 요소가 있는 지 여부를 판단
ex)
1
2
3
4
5
6
7
boolean result = Arrays.stream(num).allMatch(e -> e % 2 == 0);
System.out.println(result);
result = Arrays.stream(num).anyMatch(e -> e % 3 == 0);
System.out.println(result);
result = Arrays.stream(num).noneMatch(e -> e % 3 == 0);
System.out.println(result);
// boolean 값으로 조건 결과 출력
요소 소모(reduce())
- 스트림의 요소를 줄여나가면서 연산을 수행하고 최종적인 결과를 반환
- 첫 번째와 두 번째 요소를 가지고 연산을 수행하고, 그 결과와 다음 세 번째 요소를 가지고 또다시 연산을 수행하는 식으로 연산이 끝날 때까지 반복
- BinaryOperator
로 정의 - reduce() 메서드는 최대 3개까지 매개변수를 받을 수 있음
ex) T reduce(T identity, BinaryOperator
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// reduce
long sum1 = Arrays.stream(num).sum();
System.out.println("전체 요소 합 : "+sum1);
int sum2 = Arrays.stream(num)
.reduce((a,b) -> a + b)
.getAsInt();
System.out.println("초기 값이 없는 reduce : "+sum2);
int sum3 = Arrays.stream(num)
.reduce(5,(a,b) -> a + b);
System.out.println("초기 값 5가 있는 reduce : "+sum3);
// a = 누적 값 , b = 새롭게 더해질 값
// 최초 연산 시에 1+2 -> a:3, b:3
// 마지막 6+4 -> a: 10, b: 5
// 그리고 나서 초기값이 더 해짐
요소 수집(collect())
- 중간 연산을 통한 요소들의 데이터 가공 후 요소들을 수집하는 최종 처리 메서드
- Collector 인터페이스 타입의 인자를 받아서 처리
ex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class collectMethod {
public static void main(String[] args) {
List<Person> list = Arrays.asList(
new Person("김씨",32,Person.Gender.Male,"aa"),
new Person("이씨",28,Person.Gender.Female,"bb"),
new Person("박씨",33,Person.Gender.Male,"cc"),
new Person("최씨",38,Person.Gender.Male,"dd")
);
Map<String,Integer> male = list.stream()
.filter(e -> e.getGender() == Person.Gender.Male)
.collect(Collectors.toMap(
p -> p.getName(),
p -> p.getAge()
));
System.out.println(male);
// Person 클래스에 객체 생성 후 List 에 때려박고
// 이름과 나이 두 가지를 받아오기 위해
// 키와 값으로 나뉘는 Map 으로 반환
// 이후 Map 에 filer 로 객체의 enum 이 Male 인 객체만 거름
// collect 로 list 의 배열을 Map 형태로 변환 후 Name 과 Age 만 getter 로 읽어옴
// 그럼 collect 로 주서 담은 객체는 male 로 들어감
// 그래서 male 을 출력하면 걸러진 결과가 나옴
// 한 박스에 객체가 두 개 라 가능한듯? 성별은 enum 으로 넣었고
// Person 에 멤버 몇 개를 만들던 상관은 없는데
// collector 로 담는건 Map 타입이라 두 개 밖에 안되네
// 흠.. 근데 어떻게 쓰는지 정확히 모르는듯 Collection 으로 한 객체만 뽑아와보려고했는데 흠...
}
}
class Person {
public enum Gender {Male,Female}
private String name;
private int age;
private Gender gender;
private String middleName;
public Person(String name, int age, Gender gender, String middleName) {
this.name = name;
this.age = age;
this.gender = gender;
this.middleName = middleName;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Gender getGender() {
return gender;
}
public String getMiddleName() {
return middleName;
}
}
이전 블로그에서 옮겨짐