람다식

박민수·2023년 2월 14일
0

자바의 정석

목록 보기
15/17
post-thumbnail

1. 람다식이란?

메서드를 하나의 '식'으로 표현한 것
람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.
함수를 람다식으로 표현하면 메소드의 이름이 필요 없기 때문에, 람다식은 *익명 함수(Anonymous Function)의 한 종류라고 볼 수 있다.

람다를 지원하기 전의 자바는 완전한 *명령형 프로그래밍이었다.

명령형 프로그래밍 : 클래스에서 메서드를 정의하고, 필요할 때 그 메서드를 호출하는 명령하여 동작한다.

명령형 프로그래밍을 기반으로 개발했던 개발자들은 개발하는 소프트웨어의 크기가 커짐에 따라, 복잡하게 엉켜있는 스파게티 코드를 유지보수하는 것이 매우 힘들다는 것을 깨닫게 되었다.

그리고 이를 해결하기 위해 함수형 프로그래밍이라는 프로그래밍 패러다임에 관심을 갖게 되었다.

함수형 프로그래밍은 거의 모든 것을 순수 함수로 나누어 문제를 해결하는 기법으로, 작은 문제를 해결하기 위한 함수를 작성하여 가독성을 높이고 유지보수를 용이하게 해준다.

JDK1.8부터 추가된 람다식은 자바가 객체지향언어인 동시에 함수형 언어의 기능을 갖추게 해주었다.

원래 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들고 객체도 생성해야만 호출할 수 있었다.

람다식은 이런 과정을 간략화시켜 호출할 수 있게 해준다.

익명함수란
함수의 이름이 없는 함수로, 자바에서는 람다식을 말한다.
익명함수들은 공통으로 *일급객체(First Class citizen)라는 특징을 가지고 있다.
프로그램에서 일시적으로 한번만 사용되고 버려지는 객체를 의미한다.

  • 프로그램 내에서 일시적으로(단발성으로) 한번만 사용되어야 하는 객체일 경우
    -> UI 이벤트처리, 스레드 객체 등 (단발성 이벤트 처리)
  • 재사용성이 없고, 확장성을 활용하는 것이 유지보수에서 더 불리할 때 사용한다.

일급객체란
함수형 프로그래밍의 특징 중 하나로 일반적으로 다른 객체들에 적용 가능한 연산을 모두 지원하는 개체를 말한다.
함수형 프로그래밍이란 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다.
함수형 프로그래밍의 특징으로는 다음과 같은 것이 있다.

  • 선언적 프로그래밍이다.
  • 불변성
  • 참조 투명성
  • 일급 함수 (일급 객체)
  • 게으른 평가
    그 중 일급 객체의 특징은 아래와 같다.
  • 변수나 데이터 구조 안에 담을 수 있다.
  • 파라미터로 전달 할 수 있다.
  • 반환값으로 사용할 수 있다.
  • 할당에 사용된 이름과 무관하게 고유한 구별이 가능하다.
    함수형 프로그래밍의 이해
    선언적 프로그래밍 람다식

2. 람다의 장단점

장점

(1) 코드의 간결성
람다를 사용하면 불필요한 반복문의 삭제가 가능하며 복잡한 식을 단순하게 표현할 수 있다.

(2) 지연연산 수행
람다는 지연연상을 수행 함으로써 불필요한 연산을 최소화 할 수 있다.

(3) 병렬처리 가능
멀티쓰레드를 활용하여 병렬처리를 사용 할 수 있다.

단점

(1) 람다식의 호출이 까다롭다.

(2) 람다 stream 사용 시 단순 for문 혹은 while문 사용 시 성능이 떨어진다.

(3) 불필요하게 너무 사용하게 되면 오히려 가독성을 떨어 뜨릴 수 있다.

3. 람다식 작성

람다식은 '익명 함수'답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 ->를 추가한다.

int max(int a, int b) {
	return a > b ? a : b;
    }
    	↓
    람다식 변환 (1)
    
(int a, int b) -> {
	return a > b ? a : b;
    }
    	↓
    람다식 변환 (2)
    
(int a, int b) -> a > b ? a : b 
    // 반환값이 있는 메서드의 경우, return문 대신 '식'으로 대신할 수 있다.
    // 문장이 아닌 '식'이므로 끝에 ;를 붙이지 않는다.
    	↓
    람다식 변환 (3)
(a, b) -> a > b ? a : b    
   // 매개변수의 타입을 추론할 수 있는 경우 생략 가능
   // 대부분의 경우 생략 가능

추가적으로 매개변수가 하나뿐인 경우에는 괄호()를 생략할 수 있다.

단 매개변수의 타입이 있으면 생략할 수 없다.

괄호{}안의 문장이 하나일 때도 괄호{} 생략 가능하다.

단 return 문일결우 괄호{} 생략할 수 없다.

(a) -> a * a는 a -> a * a로 생략 가능

(int a) -> a * a는 괄호 생략 불가능

(int a, int b) -> { return a > b ? a : b;} 괄호{} 생략 불가능 

다른 예시들

1. void test1(String name, int i){
	System.out.println(name + i);
}
			↓
		람다식 변환
 (name , i) -> System.out.println(name + i)

2. int test2(int x){
	return x * x;
    }
    	↓
    람다식 변환
	x -> x * x
 
 3. sum(int [] arr){
 	int sum = 0;
    for(int i : arr)
    	sum += i;
    return sum;
    }
    	↓
    람다식 변환
    int [] arr -> {
 	int sum = 0;
    for(int i : arr)
    	sum += i;
    return sum;
	}

4. 함수형 인터페이스

위에 언급한 것처럼 모든 메서드는 클래스에 포함되어야 하고 객체를 생성해야만 호출할 수 있다.

람다식은 어떤 클래스에 포함되어 있을까 라는 의문이 생긴다.

사실 람다식은 익명 클래스의 객체와 동등하다.

(int a, int b) -> a > b ? a : b 를 다른 말로 풀면 아래와 동일하다.
		↓
new Object() {
int max(int a, int b){ //max는 임의로 넣은 것
	return a > b ? a : b;
	}
}

그리고 람다식으로 정의된 메서드를 호출할 때도 객체를 생성하여 호출해야한다.

이때 참조변수의 타입은 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다.(클래스나 인터페이스)

참조변수타입 f = (int a, int b) -> a > b ? a : b

여기서 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스'라고 한다.
자바의 람다 표현식은 함수형 인터페이스로만 사용 가능하다.

단 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어야 한다.

그래야만 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다.

그러나 static메서드와 defalut메서드의 개수에는 제약이 없다.

그리고 @FunctionalInterface 어노테이션을 사용하는데, 이 어노테이션은 해당 인터페이스가 함수형 인터페이스 조건에 맞는지 검사해준다.

@FunctionalInterface 어노테이션이 없어도 함수형 인터페이스로 동작하고 사용하는 데 문제는 없지만, 인터페이스 검증과 유지보수를 위해 붙여주는 게 좋다.

예를 들어 test라는 인터페이스가 정의되어 있다고 가정한다.

interface test {
	public abstract int max(int a , int b)
   }
   
   이 인터페이스를 구현한 익명 클래스의 객체는
   					↓
                    
 test f = new test() {
 				public int max(int a ,int b) {
                return a > b ? a : b;
                }
         };
int max = f.max(2, 3);         
   로 구현
   					↓
 	
    중복되는 부분을 람다식으로 대체하면
    
 test f = (int a , int b) -> a > b ? a : b; 
 int max = f.max(2, 3);  
 
 로 가능하다.
  

정리하면
함수형 인터페이스는 1개의 추상메서드를 가진 인터페이스이며, 사용하려는 람다식과 동등한 메서드로 정의된 것을 호출하여 사용할 수 있다.
왜 함수형 인터페이스를 사용하냐면 모든 메서드는 클래스에 포함되어야 하고 객체를 생성해야만 호출되어야 하는데 자바의 람다식은 함수형 인터페이스로만 접근이 되기 때문이다.
즉 함수형 인터페이스는 람다식으로 만든 객체에 접근하기 위해서이다.

5. java.util.function패키지

대부분의 메서드는 타입이 비슷하다.

  • 매개변수가 없거나
  • 한 개, 또는 두 개
  • 반환 값은 없거나 한 개

함수형 인터페이스는 1개의 추상 메서드를 갖고 있는 인터페이스를 말한다.

어차피 메서드의 형식은 다 비슷비슷하다.

람다식을 사용할 때마다 함수형 인터페이스를 매번 정의하기 불편하다.

따라서 자주 쓰이는 함수형 인터페이스를 정의해놓은 것이 java.util.funtion 패키지이다.

매번 새로운 함수형 인터페이스를 정의하기보다는, 이 패키지를 활용하는 것이 재사용성이나 유지보수 측면에서 좋다.

다음은 가장 기본적인 함수형 인터페이스 목록이다.

매개변수가 2개인 함수형 인터페이스

입력과 반환이 동일한 UnaryOperation, BinaryOperation

함수형 인터페이스가 적용된 CollectionFramework API

구현예제

public class Ex {

    public static void main(String[] args) {
        Supplier<Integer> s = () -> (int)(Math.random()*100)+1;// 1~100난수
        Consumer<Integer> c = i -> System.out.println(i+", ");
        Predicate<Integer> p = i -> i*2 == 0;// 짝수인지 검사
        Function<Integer, Integer> f = i -> i/10*10;// i의 일의 자리를 없앤다.

        List<Integer> list = new ArrayList<>();
        makeRandomList(s, list); //list를 random값으로 채운다.
        System.out.println(list);
        printEvenNum(p, c, list);//짝수를 출력
        List<Integer> newList = doSomething(f, list);
        System.out.println(newList);
    }

    static <T> List<T> doSomething(Function<T, T>f, List<T> list){
        List<T> newList = new ArrayList<>(list.size());
        for (T i : list) newList.add(f.apply(i));
        return newList;
    }

    static <T> void makeRandomList(Supplier<T> s, List<T> list) {
        for(int i=0; i<10; i++) list.add(s.get());//Supplier로부터 1~100난수 받아서 list에 추가
    }

    static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
        System.out.print("[");
        for (T i : list)
        	if(p.test(i)) c.accept(i);//짝수인지 검사하고 출력
        System.out.println("]");
    }
}

java.util.function 패키지
함수형 인터페이스

profile
쉽게 쉽게

0개의 댓글