본문 바로가기

Language/Java

JAVA - Lambda, Stream

 

 

자바 8부터 가장 큰 변화는 함수형 프로그래밍, 람다, 스트림이 생겼다는 점이다.

함수형 프로그래밍이란 '어떻게'가 아닌 '무엇에'에 집중하는 프로그래밍 방식이다.

스트림과 for,if문의 차이로 생각하면 쉽다.

 

이번 블로그에서는 람다에는 무엇이 있고 어떻게 사용하는지와 Stream을 사용하는 방법에 대해서 전체적인 정리를 해보고자 한다.

 

 

 

람다와 함수형 인터페이스

람다란 익명 클래스를 좀더 간소화하여 사용하는 방식이다.

또한, 람다를 사용하기 위해서 함수형 인터페이스를 보통 사용하곤 한다.

 

 

T : 클래스를 의미

R : 반환 값을 의미

기본적으로 import는 java.util.function

익명 클래스 new Class(){}

 

 

다음과 같은 클래스가 있다고 가정하자

아래 프로그램의 메서드는 모두 짝수를 반환하는 프로그램이다.

public static List<Integer> list = Arrays.asList(0, 1, 2, 3);


public static void Test1(Predicate<Integer> pr){
    for(int i : list){
        if(pr.test(i)){
            System.out.println(i);
        }
    }
}

public static void Test2(Consumer<Integer> co){
    for(int i : list){
        co.accept(i);
    }
}

public static void Test3(Function<Integer, Boolean> fu){
    for(int i : list){
        if(fu.apply(i)){
            System.out.println(i);
        }
    }
}

public static void Test4(Supplier<List<Integer>> su){
    List<Integer> supplies = su.get();
    for(int i : supplies){
        if(i % 2 == 0){
            System.out.println(i);
        }
    }
}

 

 

1. Predicate<T> : Stream의 Filtering에서 사용

- 메서드 : test()

- 반환값 : Boolean

방법 1
Predicate<Integer> option = i -> i % 2 == 0;
Test.Test(option);

방법 2
Test.Test1(arr -> arr % 2 == 0);

방법 3
Test.Test(new Predicate<Integer>() {
    @Override
    public boolean test(Integer integer) {
        if(integer % 2 == 0){
            return true;
        }
        else{
           return false;
        }
    }
});

 

 

2. Consumer<T> : Stream의 Peek에서 사용

- 메서드 : accept()

- 반환값 : void

방법 1
Consumer<Integer> option2 = arr -> {if(arr % 2 == 0){System.out.println(arr);} };
Test.Test2(option2);

방법 2
Test.Test2(arr -> {if(arr % 2 == 0){System.out.println(arr);} });

방법 3
Test.Test2(new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        if(integer % 2 == 0){
            System.out.println(integer);
        }
    }
});

 

 

3. Consumer<T, R> : Stream의 Mapping에서 사용

- 메서드 : apply()

- 반환값 : 지정 가능

방법 1
Function<Integer, Boolean> option3 = arr -> arr % 2 == 0;
Test.Test3(option3);

방법 2
Test.Test3(arr -> arr % 2 == 0);

방법 3
Test.Test3(new Function<Integer, Boolean>() {
    @Override
    public Boolean apply(Integer integer) {
        if(integer % 2 == 0){
            return true;
        }
        else{
            return false;
        }
    }
});

 

 

4. Supplier<T> : Stream 생성시 사용

메서드 : get()

반환 값 : T

- 특정 데이터를 전달하는 용도로 사용한다.

방법 1
Supplier<List<Integer>> option4 = () -> Arrays.asList(0, 1, 2, 3);
Test.Test4(option4);

방법 2
Test.Test4(() -> Arrays.asList(0, 1, 2, 3));

방법 3
Test.Test4(new Supplier<List<Integer>>() {
    List<Integer> list = Arrays.asList(0, 1, 2, 3);
    @Override
    public List<Integer> get() {
        return list;
    }
});

 

 

 

Stream

for문이나 if문 대신 Stream을 사용하면 코드에서 무엇을 하는지 쉽게 파악할 수 있기에 가독성이 좋아진다.

 

 

특징

  • 한번 생성하면 다른 코드에서 재사용이 불가능하다.
  • 원본 데이터를 수정하지 않는다.

 

 

장점

- Chat GPT는 Stream의 장점으로 아래내용을 명시합니다.

 

 

파이프라인

  • Stream에는 파이프라인이 존재한다.
  • Stream 생성 -> 중간 연산 (가공하기) -> 최종 연산(결과 도출)

 

 

 

 

 

1. 가공할 데이터 형식에 따른 Stream 생성

 

 

- 배열을 Stream으로 변경

String[] arr1 = {"1", "2", "3"};
Stream<String> stream1 = Arrays.stream(arr1);

 

- Collection을 Stream으로 변경

List<String> arr2 = Arrays.asList("1", "2", "3");
Stream<String> stream2 = arr2.stream();

 

- Builder를 통해 Stream 새로 만들기

Stream<String> stream3 = Stream.<String> builder().add("1").add("2").add("3").build();

 

- 람다 사용 (보통 Supplier 사용)

Stream<String> stream4 = Stream.generate(() -> "1").limit(1);

 

- 기본 타입형 스트림 : String, Integer 등 박싱형이 아닌 기본형을 의미

IntStream stream5 = IntStream.range(0, 5);

 

- 조건 적용  : For문 역할을 수행

Stream<Integer> stream6 = Stream.iterate(0, n->n+3).limit(3); // 0, 3, 6

 

- 병렬 스트림

Stream<Integer> stream7 = Stream.iterate(0, n->n+3).limit(3).parallel();
Stream<String> stream8 = arr2.parallelStream();

 

 

결과는 Stream<T> 나 IntStream, DoubleStream 등 기본타입 스트림으로 반환된다.

 

 

 

 

 

 

 

2. 중간 연산으로 데이터를 가공한다.

 

Filtering : IF문 역할

- IF문의 역할을 한다.

- Predicate<T>와 같은 Boolean 반환 람다를 넣을 수 있고 결과가 True인 값들만 다음 값에 파이프라인에 넘겨진다.

List<Integer> array = Arrays.asList(1, 2, 3, 4);
int sum = array.stream().filter(arr -> arr % 2 == 0) ...

 

 

Mapping  : 값 자체 또는 값의 타입을 변경

- 을 변경하거나 값의 형태를 변경

- Function<T, R>과 같이 반환값을 지정할 수 있는 람다가 들어가고 변경된 값이나 값의 형태가 다음 파이프라인에 넘겨진다.

List<ENTITY> entities = 엔터티 데이터
List<DTO> result = entities.stream()
		.map(entity -> DTO.builder().id(entity.getId()).build()) ...
- 클래스 메서드 사용해 변경
.map(클래스명 :: 메서드명)

- 기본 타입으로 변경
.mapToInt(Integer :: intValue)
.mapToInt(Integer :: valueOf)
.mapToDouble(Double :: valueOf)
.mapToLong(Long :: valueOf)

 

 

Sorting : 정렬

- 정렬 역할을 수행

- sorted() 사용시 클래스라면 내부에 compareTo 호출해 수행

- sorted()사용시 기본 타입은 기본타입 규칙 따름

- Comparator로 정렬 조건 전달 가능

List<Integer> array1 = new ArrayList<>();
array1.stream().sorted();
array1.stream().sorted(Comparator.comparing(arr -> -arr));

List<String> array2 = new ArrayList<>();
array2.stream().sorted();
array2.stream().sorted(Comparator.comparing(arr -> arr.length()));

 

 

추가

- 중복값 제거
.distinct()

- 앞에서부터 n개까지만 사용
.limit(2)
[1, 2, 3, 4] : 1, 2

- 앞에서부터 n개까지 제외
.skip(2)
[1, 2, 3, 4] : 3, 4

- void 반환
.peek(System.out :: println)

 

 

 

 

 

 

 

3. 최종연산으로 결과를 도출한다.

 

Collecting : 원하는 자료형으로 변경 : 결과 반환

1. List로 반환
.collect(Collectors.toList());


2. Set으로 반환
.collect(Collectors.toSet());


3. HashSet으로 반환
.collect(Collectors.toCollection(HashSet::new));


4. Map으로 반환
.collect(Collectors.toMap());


5. HashMap으로 반환
.collect(Collectors.toCollection(HashMap::new));


6. 결과를 Grouping하여 Map으로 반환
Map<Integer, List<String>> result = array.stream()
.collect(Collectors.groupingBy(String::length));


7. 결과를 HashMap으로 반환
Map<String, Short> result1 = stores.stream()
			    .collect(Collectors
                .toMap(StoreListResultDTO::getStoreName, StoreListResultDTO::getStoreId, 
                (existing, replacement) -> existing, HashMap::new));
-> existing : 겹치는 키에 대해서 새로운 값을 무시
-> replacement : 겹치는 키에 대해서 새로운 값으로 변경
-> Collectors.toMap(KEY만들기, VALUE만들기, (existing, replacement) -> existing, HashMap::new));


8. 결과를 합쳐서 전달
List<String> array = Arrays.asList("data1", "data2", "data3");
String result1 = array.stream().collect(Collectors.joining());
String result2 = array.stream().collect(Collectors.joining(delimeter = ", ", prefix = "<", suffix = ">"));
-> <data1, data2, data3>


9. 두개 이상의 작업
array.stream().collect(Collectors
	.collectingAndThen (Collectors.toSet(),Collections::unmodifiableSet));

 

 

Literating : 순환 : 출력, 반환값 없을 때 사용

.forEach(System.out :: println);

 

 

Finding  : 찾기  : 첫번째 결과 찾기  : 요소 반환

.findAny() -> 가장 먼저 찾은 요소 하나 반환
.findFirst() -> 결과에서 첫번째 요소 반환

 

 

Calculating  : 기본 연산  : 값 반환

.count(); -> 요소의 수
.sum(); -> 요소의 합, 기본타입 stream 만 가능
.min(); -> 요소의 최솟값, 기본타입 stream 만 가능
.max(); -> 요소의 최댓값, 기본타입 stream 만 가능
.average(); -> 요소의 평균값, 기본타입 stream 만 가능

 

 

Reduction : 누적 연산 : 값 반환

.reduce(100, (sum, now) -> sum + now);
-> 100에 계속 더해 줌
-> reduce(초기값, (누적값변수, 더해질값) -> 연산);

 

 

Matching : 해당 조건 충족을 하는지 여부 : True or False

.anyMatch(arr -> arr.length() >= 2); -> 길이가 2이상인게 있는지 True or False
.allMathch(arr -> arr.lengtth() >= 2); -> 모두 길이가 2이상인지 True or False
.noneMathch(arr -> arr.lengtth() >= 2); -> 아무것도 길이가 2이상이 없는지 True or False

 

 

 

'Language > Java' 카테고리의 다른 글

Jar 파일 만들고 사용해보자  (0) 2023.10.09
완전탐색 재귀호출에서 전역필드에 값 추가  (0) 2023.06.09
JAVA - BASIC  (0) 2023.05.18
JAVA - Grammar  (0) 2023.05.03
JAVA - Loop(for, iterator, stream)  (0) 2023.03.23