Java - 람다와 스트림

Dasole Kwon·2023년 1월 25일
0

람다식

람다식이란?

: 메서드를 하나의 식(expression)으로 표현한 것

  • 객체 지향 언어보다는 함수 지향 언어에 가깝다.
  • 함수를 간략하면서도 명확한 식으로 표현 할 수 있도록 해준다.
  • 메서드를 람다식으로 표현하면 메서드의 이름 및 반환 값이 없어지므로 익명 함수라고도 한다.
  • 람다식의 형태는 매개 변수를 가진 코드 블록이지만 런타임 시에는 익명 구현 객체를 생성한다.

람다식 작성하기

(타입 매개변수) -> { 실행문; ... }
public int sum(int a, int b) {
    return a + b;
}

//람다식 변환
(a, b) -> a + b;
  • 반환 값이 있는 메서드의 경우 return 대신 expression으로 대신 할 수 있다. (expression인 경우 ;를 붙이지 않는다.)
  • 람다식에 선언 된 매개변수 타입은 추론이 가능한 경우 생략 가능(대부분 생략 가능)
  • 매개 변수가 하나인 경우 ()를 생략 할 수 있다.
  • {} 안 문장이 하나인 경우 생략 할 수 있다.

함수형 인터페이스(Functional Interface)

함수형 인터페이스는 람다식을 다루기 위한 인터페이스로 하나의 추상 메서드만 정의되어 있어야한다. 단, static 메서드와 default 메서드의 개수에는 제약이 없다.

  • 함수형 인터페이스 타입의 매개변수 및 반환 타입이 함수형 인터페이스 타입이라면 람다식을 참조하는 참조변수를 매개변수로 저장하고 람다식을 가리키는 참조변수를 반환하거나 또는 람다식 자체를 반환 할 수 있다.
  • 람다식은 Object 타입으로 형변환 할 수 없으며, 오직 함수형 인터페이스로만 형변환이 가능하다.
  • 람다식 내에서 참조하는 지역변수는 final이 붙어있지 않아도 상수로 간주되며, 외부 지역변수와 같은 이름의 매개변수를 허용하지 않는다.
  • 함수형 인터페이스는 @FunctionalInterface라는 어노테이션을 붙을 수 있다. (컴파일러에서 추상메서드를 갖춘 인터페이스인지 검사, javadoc 페이지에서 해당 인터페이스가 함수형 인터페이스임을 알 수 있도록 한다.)

스트림(Stream)

스트림이란?
:다양한 데이터 소스를 표준화 된 방법으로 다루기 위한 라이브러리이다. 스트림은 데이터 소스를 추상화하고, 데이터를 다루는 데 자주 사용되는 메서드들을 정의해 놓았다.

스트림을 이용하면, 배열이나 컬렉션 뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

String[] strArr = {"aaa", "ddd", "ccc"};
List<String> strList = Arrys.asList(strArr);

문자열 배열 'strArr'과 같은 내용의 문자열을 저장하는 리스트 'strList'가 있을 때, 각각의 데이터를 정렬하고 출력하는 방법

1.스트림x

Arrays.sort(strArr);
Collections.sort(strList);

for(String str : strArr) 
  System.out.println(str);

for(String str : strList)
  System.out.println(str);

2.스트림 활용한 방법

Stream<String> strStreamArr = Arrays.stream(strArr);
Stream<String> strStreamList = strList.stream();

strStremaArr.sorted().forEach(System.out::println);
strStreamList.sorted().forEach(System.out::println);

스트림을 사용한 코드가 더 간결하고 이해하기 쉬우면서 재사용성이 높다.

스트림의 연산

스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단하게 처리할 수 있다.

  1. 중간연산: 연산결과가 스트림. 연속에서 수행 가능.
  2. 최종연산: 연산결과가 스트림이 아님. 스트림의 요소를 소모하기 때문에 단 한번만 가능.

java.util.funtion패키지

대부분의 메서드는 타입이 비슷하고 지네릭 메서드로 정의하면 반환 타입이 달라도 문제가 되지 않는다. java.util.function 패키지에는 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해놓았으며 매번 함수형 인터페이스를 정의하기 보다는 가능하면 이 패키지의 인터페이스를 활용한다.

  • 메서드 이름 통일
  • 재사용성
  • 유지보수

Function의 합성과 Predicate의 결합

  • Function의 합성 andThen: a.andThen(b): a함수 적용 후 b함수 적용 compose:a.compose(b): b함수 적용 후 a함수 적용 identity:항등 함수(잘 사용되지 않는 편이나 map()으로 변환 작업 할 때 변환없이 그대로 처리하고자 할때 사용)
  • Predicate의 결합 and(): and조건 or(): or 조건 negate(): not isEqual(): 두 대상 비교

메서드 참조

메서드를 참조해서 매개변수의 정보 및 리턴 타입을 알아내어 람다식에서 불필요한 매개 변수를 제거하는 것이 목적. 람다식의 매개 변수는 메서드의 매개값을 전달하는 역할만 하기 때문에 메서드 참조를 이용하면 깔끔하게 처리 할 수 있따.

(a, b) -> Math.max(a,b);
Math::max
  • 하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름 또는 '참조변수::메서드이름'으로 바꿀 수 있다.

생성자의 메서드 참조

  • 생성자를 호출하는 람다식도 메서드 참조로 변환 할 수 있다.

스트림의 특징
특징1. 스트림은 데이터 소스를 변경하지 않는다.
스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터소스를 변경하지 않는다.
정렬된 결과가 필요할 경우, collect를 활용해서 컬렉션이나 배열에 담아 return할 수 있다.

List<String> sortedList = strStreamList.sorted().collect(Collectors.toList());

특징2. 스트림은 일회용이다.
스트림은 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야 한다.

특징3. 스트림은 작업을 내부 반복으로 처리한다.
내부 반복이란, 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다. forEach()는 스트림에 정의 된 메서드 중의 하나로 매개변수에 대입 된 람다식을 데이터소스의 모든 요소에 적용한다. 즉, forEach()는 메서드 안에 for문을 넣어버린 것이다.

//수행할 작업을 매개변수로 받는다.
strStreamArr.sorted().forEach(System.out::println);

특징4. 지연된 연산
스트림 연산에서는 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다. 스트림에 대해 sort()나 distinct()같은 중간 연산을 호출해도 즉각적으로 수행되지 않는다는 것이다. 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야 하는지를 지정해주는 것일 뿐이다. 최종 연산이 수행되어서야 스트림의 요소들이 중간연산을 거치고 최종연산에 소모된다.

특징5. 기본형 스트림
오토박싱, 언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 InStream, LongStream, DoubleStream이 제공된다.
일반적으로 Stream< Integer>대신 IntStream을 사용하는 것이 더 효율적이고, IntStream에는 int타입으로 작업하는데 유용한 메서드들이 포함되어 있다.

특징6. 병렬스트림
스트림은 내부적으로 framework를 이용해서 연산을 자동적으로 병렬로 수행한다.
parallel()메서드를 호출하면 병렬로 연산이 수행되고, sequential()메서드를 호출하면 병렬로 처리되지 않게된다. 모든 스트림은 기본적으로 병렬 스트림이 아니기 때문에 sequential()메서드는 parallel()를 취소할때만 사용한다.

int sum = strStream.parallel().mapToInt(s -> s.length()).sum();
Stream.of("3", "1", "4", "2", "5", "5") // 문자열 스트림 생성
        .map(Integer::parseInt) // 문자열 스트림을 정수형 스트림으로 변환
        .sorted() // 정렬
        .distinct() // 중복제거
        .limit(3) // 갯수를 3개로 제한
        .collect(Collectors.toList()) // 리스트로 변환 => {1, 2, 3} : 단말 연산
        .stream() // 다시 정수형 값을 갖는 스트림으로 변환
        .filter(x -> x > 1) // 1보다 큰 값만 갖도록 필터링함 {2, 3}
        .forEach(System.out::println); // 2와 3만 출력됨

스트림 만들기

자바 8 이후부터 추가된 stream은 for문을 사용한 반복문이 갖는 단점들을 해결해 줄 수 있다. for문이 갖는 단점으로는 쉽게 indent가 증가하고 if문 같은 분기문이 쉽게 발생한다는 점이다.

문법

stream().중개연산.단말연산

문법은 아래의 코드처럼 stream() 생성을 하고 중개 연산 결과들을 계속 반환하면서 이어가다 단말 연산을 이용해 멈춘다.

ArrayList<RacingCar> winners = new ArrayList<>();

for (RacingCar racingCar : racingCars) {
    if (racingCar.isSamePosition(racingCarOfMaxPosition)) {
        winners.add(racingCar);
    }
}
return winners;

// stream api를 활용한 식
private final List<RacingCar> racingCars = new ArrayList<>();

return racingCars.stream()
                .filter(racingCar -> racingCar.isSamePosition(racingCarOfMaxPosition))
                .collect(Collectors.toCollection(ArrayList::new));

람다식

Stream 클래스의 iterate(), generate() 는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 결과값들을 요소로 하는 무한 스트림을 생성한다.

iterate(): 이전 결과에 대해 종속적

Stream<Integer> evenStream = Stream.iterate(0, n->n+2);
// 0, 2, 4, 6, ...

generate(): 이전 결과에 대해 독립적

Stream<Double> randomStream = Stream.generate(Math::random);
  • 이전 결과를 이용해서 다음 요소를 계산하지 않는다.
    iterate()와 generate()에 의해 생성된 스트림은 아래와 같이 기본형 스트림 타입의 참조변수로 다룰 수 없다.
IntStream evenStream = Stream.iterate(0, n->n+2);  //error
DoubleStream randomStream = Stream.generate(Math::random);  //error

굳이 필요하다면, 아래와 같이 mapToInt()와 같은 메서드로 변환을 해야한다.(Stream, IntStream변환)

IntStream evenStream = Stream.iterate(0, n->n+2).mapToInt(Integer::valueOf);
Stream<Integer> stream = evenStream.boxed();

0개의 댓글

관련 채용 정보