OOP - 자바의 함수프로그래밍방식 람다(Lamda)

devdo·2022년 1월 8일
0

Java

목록 보기
30/59

람다를 이해하기 위해서는 함수형 프로그래밍 방식을 이해 해야 한다. 람다는 함수형 프로그래밍 방식을 위한 자바 기술이다.

관련블로그는 여기를 참고하길
: https://velog.io/@mooh2jj/왜-함수형-프로그래밍인가

람다란?

람다는 λ라고 표기하며 원래는 수학기호로 1930년대 수학자 알론소 처치의 람다 계산식에서 시작되었다고 한다.

최신의 프로그램언어에서 사용되는 람다식 혹은 람다함수는 함수형 언어의 특징에서 나온것으로 나중에 한번이상 실행할 수 있는 코드블록을 말하며 실제 구현에는 익명 함수형태로 사용한다.

자바에서 쓰는 람다는 함수형 인터페이스와 함수형 프로그래밍이 적용된 새로운 자바 API를 이해하고 사용해야 한다.


특징

1) 익명: 보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다. 구현해야 할 코드에 대한 코드 걱정거리가 하나 줄어든 것이다.

2) 함수: 람다는 특정 클래스에 종속되지 않기에 메서드가 아니라 함수라 불린다.
하지만 메서드처럼 파라미터 리스트, 바디, 반환형식, 가능한 예외 리스트를 포함한다.

3) 전달: 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.

4) 간결성: 익명 클래스처럼 많은 코드를 구현할 필요가 없다.


장점

  • 효율적인 람다 함수의 사용을 통해 불필요한 루프문의 삭제가 가능하며, 함수의 재활용이 용이함.
  • 필요한 정보만을 사용하는 방식을 통한 성능 향상.
  • 일반적으로 다중 cpu를 활용하는 형태로 구현되어 병렬처리에 유리.(단, 동시성 이슈가 있어 병렬처리방식을 잘 사용하지 않음.)

단점

  • 이론상 단순하게 모든 원소를 전부 순회하는 경우 람다식이 조금 느릴 수 있음.
  • 디버깅시 함수 콜스택 추적이 다소 어려움.
  • 지나치게 남발하면 코드가 이해하기 어려워짐.

최신언어 지원현황

  • 최신지원언어: Kotlin, Swift, JavaScript(ES6)
  • C, Fortran, Pascal: 지원 안함
  • Java: JDK8 이상
  • C++: 버전 11 이상(현재17)
  • .Net Framework(C#): 3.5 이상

람다식 표현

(매개변수, ...) -> { 실행문 }

람다식 예)

  • 기존 코드
public int sum(int a, int b) {
	return a + b;
}
  • 람다코드
(a, b) -> { a + b }; // 중괄호 빼도 된다.

✔ 자바에서는 람다는 사실 명확하게 보면 익명 클래스 로 생각할 수 있다. 위에서 봤던 람다식은 다음 익명 클래스와 동일하게 생각할 수 있다.

new Object() {
	int sum(int a, int b) {
    	return a + b;
    }
}

✔ 추상메서드가 하나인 인터페이스에서(대표적으로 함수형 인터페이스, Runnable 인터페이스)
람다식이 유용하다.

  • 기존코드
Thread thread = new Thread(new Runnable() {

    @Override
    public void run() {
          System.out.println("Start Thread");
          Thread.sleep(1000);
          System.out.println("End Thread");
   }
});
  • 람다코드
Thread thread = new Thread(() -> {
          System.out.println("Start Thread");
          Thread.sleep(1000);
          System.out.println("End Thread");
});

메서드/생성자 레퍼런스

람다는 기본적으로 함수의 구조로 되어 있으며 자바에서는 ->와 같이 화살표 형태의 기호를 이용해 매개변수를 함수 바디로 전달하는 형태를 취한다.

( parameters ) -> expression body       // 인자가 여러개 이고 하나의 문장으로 구성
( parameters ) -> { expression body }   // 인자가 여러개 이고 여러 문장으로 구성
() -> { expression body }               // 인자가 없고 여러 문장으로 구성
() -> expression body                   // 인자가 없고 하나의 문장으로 구성

하지만 람다는 매개변수만 전달하지 않는다. 메서드, 생성자도 전달할 수 있다.

메서드/생성자 레퍼런스는 이 함수형 인터페이스를 구현하는 더 간결한 방법이다.

  • 메서드 레퍼런스

::을 사용해 메서드 레퍼런스를 사용할 수 있다.

클래스 :: 정적메서드
클래스 :: 인스턴스메서드
인터페이스 :: 인스턴스메서드

print(msg -> System.out.println(msg));
print(System.out::println);
  • 생성자 레퍼런스

메서드 레퍼런스와 유사하지만 ::new 를 사용한다.

기본적으로는 메서드 레퍼런스와 같이 메서드에 대한 참조를 전달하는 것이지만 단순히 메서드에 대한 참조가 아니라 새로운 객체를 생성한다는 점에서 차이가 있다.

List<String> labels = ...
Stream<Button> stream = labels.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());

Stream 연산(중간연산/최종연산)

1,2,3 세개의 값을 가지고 있는 배열의 각 원소값을 가져와 제곱값을 출력하는 코드다.
기존의 for문을 이용하지 않고 람다를 사용하면 한줄로 쭈욱 처리할 수 있다.

위의 스트림(stream)데이터의 흐름으로서 원본 데이터가 변하지 않는 특성으로 람다를 활용한 자바8의 기술이다.

컬렉션 순회를 많이 쓰는 자바는 Stream()을 써서 Stream의 메서드들은 중간연산들의 산출물을 매개변수삼아 람다를 많이 사용한다.

자세한 내용은 이 블로그에 정리했었다.
Java Stream, Collector 왜 쓰는 거야?

스트림에서는 게으른(Lazy) 특성으로 2 가지로 나누어진다. 중간연산, 최종연산이다.

  • 중간 연산: 중간 연산의 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지 아무 연산도 수행하지 않느다. 즉, 리턴값이 없다. (filter, map, limit, sorted, distinct 등), 그냥 지나가는 통로라 생각하면 된다.
  • 최종 연산: 스트림 파이프라인에서 결과를 도출한다. (forEach, collect, count 등)

중간연산은 선택적이다. 안 쓸 수도 있고 쓸 수도 있다.


예제코드

Arrays.asList(1,2,3).stream()
	.map(i -> i*i)
	.forEach(System.out::println);

결과

1 4 9

함수형 인터페이스란?

함수형 프로그램을 구현하다 보면 구조적으로 비슷한 유형의 함수형 인터페이스가 필요한 경우가 많이 발생 합니다.

함수형 인터페이스의 특징은 추상 메서드가 하나 정의되어 있다는 것입니다.

이 경우 java.util.function 패키지에 정의된 함수형 인터페이스들을 사용할 수 있다.

특히 다음에 배우게 될 Stream API 를 제대로 이해하기 위해서는 다음의 함수형 인터페이스를 잘 알아 두어야 한다.


함수형 인터페이스 종류


※ 이미지 출처 : https://dinfree.com/lecture/language/112_java_9.html

이들 함수형 인터페이스는 타입파라미터를 가지는데 각각의 의미는 다음과 같다.

T: 첫번째 인자 Type
U: 두번째 인자 Type
R: 리턴 타입

1) Function

전형적인 함수를 지원한다. 하나의 인자와 하나의 리턴을 가진다.

method는 apply() 가 있고
default method는 andThen() (Consumer 와 동일 기능) 과
compose() (andThen()과 순서만 다름) 가 있습니다.
static method는 파라메터를 그대로 리턴하는 identity()가 있습니다

Function<String, Integer> f = str -> Integer.parseInt(str);
Integer result = f.apply("10");

2) Consumer

인자를 받아 소모하고 리턴이 없는 함수형 인터페이스.
accept() 메서드를 추상 메소드로 선언한 함수형 인터페이스다.

가장많이 쓰이는 Stream 인터페이스의 forEach 메서드는 void forEach(Consumer<? super T> action)로 정의되어 있으며 Cosumer 함수형 인터페이스를 인자로 가지고 있다.

Arrays.stream(strArr).sorted().forEach(str -> System.out.println(str));

3) Supplier

인자가 없으며 리턴타입만 존재하는 형태. 순수함수에서 결과를 바꾸는건 오직 입력 뿐인데 입력이 없다는 것은 내부에서 랜덤 함수 같은것을 쓰는게 아닌이상 항상 같은 것을 리턴한다는 것을 알 수 있다.

get() 메서드를 추상 메소드로 선언한 함수형 인터페이스다.

Supplier<String> s = () -> "msg from supplier";
String result = s.get();

4) Operator

거의 안쓰임
??? 추후 정리

5) Predicate

하나의 인자와 boolean 타입의 리턴값을 가지고 있고
test() 메소드를 추상 메소드로 선언한 함수형 인터페이스다.

and(), or(), negate(), isEqual(), not() 등 중개연산 default 메소드를 제공하며 이를 사용하여 복수의 조건을 추가할 수 있다.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    // ...
}
Predicate<String> p = str -> str.contains("msg");
boolean result = p.test("hello"); // false


Predicate<String> strContain = (str) -> str.contains("aljja");
Predicate<String> strStartsWidth = (str) -> str.startsWith("http");
Predicate<String> strEndsWidth = (str) -> str.endsWith("com");

String url = "aljjabaegi.tistory.com";
boolean result = strContain.or(strStartsWidth).and(strEndsWidth).negate().test(url);
System.out.println(result); /** false */


참고

https://dinfree.com/lecture/language/112_java_9.html

profile
배운 것을 기록합니다.

0개의 댓글