Stream API
: Java 8 버전에 소개된 데이터 모음을 다루는 방법.
✅ 원래 데이터의 모음이 있다면...?
데이터 모음의 데이터가 하나씩 나와서 특정 작업(함수)의 목록을 거쳐 결과 데이터를 만들어내는 방식으로 동작한다.
~> 어떤 작업을 진행할지를 전달하려면 함수를 인자로 전달한다.
- 데이터 모음을 Stream으로 변환, 해당 데이터에 할 작업들을 선택, 그 결과를 필요한 형태로 반환하는 과정..
3단계
1) Stream 만들기
2) Intermediate Operation
3. Terminal Operation 순서로 동작
> 훨씬 간결하게 코드 작성 가능.
작업을 가하는 것이 아니라 작업을 전달해주는 것!
List<String> namesWithA = nameList.stream() //Stream을 만들고
.filter(name -> name.contains("a")) //작업내역 전달
.sorted()
.toList(); // 데이터를 가공해서 결과 반환
// 같은 기능을 하는 stream 사용하지 않은 코드
List<String> nameList = new ArrayList<>();
...
List<String> namesWithA = new ArrayList<>();
for (String name : nameList) {
if (name.contains("a")) namesWithA.add(name);
}
namesWithA.sort(String.CASE_INSENSITIVE_ORDER);
System.out.println(namesWithA);
1. Functional Interface (함수형 인터페이스)
: 단 하나의 추상 메서드만 가지는 인터페이스.
- 만들어진 함수형 인터페이스를 바탕으로, 람다 함수(익명함수)를 만들 수 있음.
// 함수형 인터페이스
@FunctionalInterface
public interface NoArgRetFunction {
void noArgNoReturn();
}
1) 전통적 방식?
// 인터페이스를 상속받은 클래스 생성하여 main에서 호출
public class NoArgRetFunctionImpl implements NoArgRetFunction {
@Override
public void noArgNoReturn() {
System.out.println("Traditional Implementation");
}
}
2) 익명 내부 클래스 사용
- 한 번만 쓰고 말 클래스를 ~ > 메서드 안에다 만들어 버린다.
- 익명 내부 클래스 (anonymous inner class) : 메서드 내부에서 즉석으로 새로운 구현체를 만들어 사용할 수 있음.
// 익명 내부 클래스
NoArgRetFunction anonymousClass = new NoArgRetFunction() {
@Override
public void noArgNoReturn() {
System.out.println("anonymous inner class");
}
};
3) 람다 함수 사용
- 람다 표현식 : 매개변수를 ( )로, 함수 본문은 ->로 연결. 함수형 인터페이스는 어차피 구현할 메서드가 하나이므로 그 메서드만 표시하는 것.
NoArgRetFunction lambda = () -> System.out.println("Lambda Expression");
final int a = 10;
int b= 20;
// 여러 줄은 { } 활용
// 람다 내부에서는 final 변수만 사용 가능
// 함수의 기능이 예측이 불가해지기 때문에 변경 가능한 변수를 허용하지 않음.
NoArgRetFunction lambda = () -> {
System.out.println(a);
b=30; //에러
System.out.println("Hello Lambda!");
System.out.println("Multiline Lambda!");
};
lambda.noArgNoReturn();
- 메서드 직접 참조도 가능하다 (::)
public static void main(String[] args) {
useFunction(String::length);
}
public static void useFunction(StringArgFunction function) {
System.out.println(function.run("lorem ipsum"));
}
함수형 인터페이스 | 람다 표현식 | 실제 메서드 |
Function | T -> R | R apply(T t) |
Predicate | T -> boolean | boolean test(T t) |
Consumer | T -> void | void accept(T t) |
Supplier | () -> T | T get() |
Comparator | (T, T) -> int | int compare(T o1, T o2) |
2. Stream 만들기
- 배열이나 Collection을 활용하면 가장 편하다.
- 배열 : Arrays.stream() / 원시타입은 제공되는 별도의 클래스를 활용
- 문자열 : IntStream /CharStream은 없음.
- Collection : stream() 메서드
// String
String[] nameArray = {"Alex", "Dave", "Chad", "Brad"};
Stream<String> nameArrStream = Arrays.stream(nameArray);
// 원시타입 스트림
int[] numbers = {1, 23, 14, 53, 22};
IntStream intStream = Arrays.stream(numbers);
// 문자열 스트림 : CharStream은 없음
IntStream charStream = "hellojava".chars();
// Collection
List<String> nameList = new ArrayList<>();
nameList.add("Chad");
nameList.add("Alex");
Stream<String> nameListStream = nameList.stream();
3. Intermediate Operations (Stream에 적용할 작업 전달)
: 각 데이터에 중간 작업을 진행한다.
- 다른 데이터를 만들거나(map), 일부 데이터만 골라내거나(filter), 정렬(sort) 등의 작업 진행.
- 메서드의 결과로 새로운 Stream을 반환.
- 그러면 다시 다음 작업을 메서드로 선택할 수 있다. (메서드 체이닝 가능)
- 원본 데이터는 그대로 남아있다. (데이터 조작이 아니라 선별해서 사용하는 것!)
Stream<String> nameListStream1
= nameList.stream();
Stream<String> nameListStream2
= nameListStream1.filter(name -> name.contains("a"));
Stream<String> nameListStream3
= nameListStream2.sorted((o1, o2) -> o1.length() - o2.length());
- filter : boolean을 반환하는 함수를 인자로 받음. 해당 함수에 인자로 전달되었을 때, true가 나오는 데이터 선별
- map : 값을 인자로 받아 하나의 값을 반환. 반환값이 바뀌면 Stream 타입도 바뀜.
- sort : 데이터 정렬. (Comparable을 기준으로 정렬 | Comparator를 람다식으로 구현 가능 | 미리 정해진 Comparator도 사용가능)
// filter
Stream<String> nameListStream = nameList.stream()
.filter(name -> name.contains("a")); // a가 포함되면 true
// map
Stream<String> nameListStream = nameList.stream()
.filter(name -> name.contains("a"))
.map(name -> name.toUpperCase()); // name을 대문자로 변환
nameList.stream()
.map(name -> name.length()); //각 단어의 길이로 이루어진 Stream 반환
nameList.stream()
.mapToInt(name -> name.length()); //각 단어의 길이로 이루어진 IntStream으로 반환
// sort
nameList.stream()
.sorted(); // 오름차순 정렬
nameList.stream()
.sorted(Comparator.reverseOrder()); //역순정렬
nameList.stream()
.sorted((o1, o2) -> o2.length() - o1.length()) //임시 Comparator 정의 (둘의 길이를 비교해 더 긴게 앞으로 간다)
4. Terminal Operations (Stream 결과 반환)
: 중간 작업으로 선별/변환된 데이터를 (Stream) 최종적으로 활용하고 싶은 형태로 반환하는 작업.
- Stream에 남은 데이터를 계산하거나, 내부 데이터가 어떤 조건을 만족하거나, 다시 Collection과 같은 형태로 변환하는 작업
- 이미 종결작업(Terminal operations)이 호출된 Stream은 재사용 불가
- 기술적으로 최종작업이 선택되어야 Stream 전체 완료.
- forEach : 간단한 순회
- count : 총 갯수 조회
- toList : 결과를 리스트로 정리
- allMatch, anyMatch, noneMatch : 주어진 함수에 인자로 넣은 결과가 true인 애들을 기준으로 모든 원소가 통과하는지 (allMatch)/ 원소 하나라도 통과하는지 (anyMatch)/ 하나도 통과하지 않는지 (noneMatch)
- sum : IntStream과 같은 숫자 스트림은 집계 가능.
- max / min / average : 최댓값/ 최솟값/ 평균
//forEach
nameListStream.forEach(System.out::println);
// count
System.out.println(nameListStream.count());
// toList
List<String> result = nameListStream.toList();
// allMatch 모든 원소가 통과하는지
boolean allMatch = nameListStream.allMatch(name -> name.contains("a"));
System.out.println(allMatch);
// anyMatch 원소 하나라도 통과하는지
boolean anyMatch = nameListStream.anyMatch(name -> name.contains("e"));
System.out.println(anyMatch);
// noneMatch 하나도 통과하지 않는지
boolean noneMatch = nameListStream.noneMatch(name -> !name.contains("e"));
System.out.println(noneMatch);
// sum
System.out.println(intStream.sum());
// max, min, average
System.out.println(intStream.max());
System.out.println(intStream.min());
System.out.println(intStream.average());
'Programming > Java' 카테고리의 다른 글
String | StringBuffer | StringBuilder (1) | 2023.12.04 |
---|---|
File I/O (1) | 2023.12.03 |
제너릭 (Generics) (1) | 2023.11.29 |
예외처리 (Exception Handling) (1) | 2023.11.28 |
객체지향 프로그래밍 (1) | 2023.11.28 |