[JAVA] Lambda

sy·2023년 7월 3일
0

JAVA

목록 보기
4/8

Lambda Expression

  • JAVA 8에 추가 되었다.
  • 람다식 도입으로 인해 자바는 객체 지향 언어인 동시에 함수형 언어가 되었다.
  • 메서드를 하나의 '식(expression)'으로 표현한 것이다.
  • 메서드의 이름과 반환 값이 없어진다.
  • 익명 함수 (anonymous function) 라고도 한다.
int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int) (Math.random() * 5)+1);
  • 간결하면서 이해하기 쉽다.
  • 클래스를 만들고, 객체를 생성하는 과정없이 오직 람다식 자체만으로 메서드의 역할을 대신한다.
  • 람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다. 메서드를 변수처럼 다루는 것이 가능해진 것이다.

💡 method와 function 차이
객체지향개념에서는 함수 대신 객체의 행위나 동작을 의미하는 메서드라는 용어를 사용한다. 메서드는 함수와 같은 의미지만, 특정 클래스에 반드시 속해야 한다는 제약이 있기 때문에 메서드 라는 용어를 사용한다. 그러나 이제 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 함수라는 용어를 사용하게 되었다.

int max(int a, int b) {
	return a > b ? a : b;
}

// 람다식으로 변환
(int a, int b) -> {
	return a > b ? a : b;
}

// return 문에 '식(expression)'으로 대신 할 수 있음
// 식의 연산 결과가 자동적으로 반환값이 됨
// 문장이 아닌 식이므로 ; 붙이지 않음
(int a, int b) -> a > b ? a : b

// int 생략 가능 
(a, b) -> a > b ? a : b

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

  • 람다식을 다루기 위한 인터페이스
  • 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다.
    (static 메서드, default 메서드는 개수 제약 없음)

람다식으로 변환 과정

  1. max() 라는 메서드가 정의된 MyFunction 인터페이스 정의
@FuctionalInterface // 컴파일러가 함수형 인터페이스를 올바르게 정의 했는지 확인
interface MyFunction {
	public abstract int max(int a, int b);
}
  1. 위의 인터페이스를 구현한 익명 클래스
MyFunction f = new MyFunction() {
				public int max(int a, int b) {
                	return a > b ? a : b?
                }
			}
  1. 위 코드의 익명 객체를 람다식으로 대체
MyFunction f = (int a, int b) -> a > b ? a : b;

📌 대체 가능한 이유는?
람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문이다.

Collections.sort 람다식 예

  • 람다식으로 변경 전
List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd","aaa");

Collections.sort(list, new Comparator<String>() {
	@Override
	public int compare(String o1, String o2) {
		return o2.compareTo(o1);
	}
});

Intellij 에서는 Lambda 로 변경하라는 가이드가 나온다.

Replace with lambda 를 눌러주면 아래와 같이 변환된다.

  • 람다식으로 변경 후
Collections.sort(list, (o1, o2) -> o2.compareTo(o1));

// Intellij는 위의 코드를 아래와 같이 변경 추천
// Collections.sort(list, Comparator.reverseOrder());

매개 변수와 반환 타입

@FuctionalInterface // 컴파일러가 함수형 인터페이스를 올바르게 정의 했는지 확인
interface MyFunction {
	void run();
}

class LambdaEx1 {
	static void execute(MyFunction f) { // 매개변수의 타입이 MyFunction 인 메서드
    	f.run();
    }
    
    static MyFunction getMyFunction() { // 반환타입이 MyFunction인 메서드
    	MyFunction f = () -> System.out.println("f3.run()");
        return f;
    }
    
    public static void main(String[] args) {
    	// 람다식으로 구현
    	MyFunction f1 = () -> System.out.println("f1.run()");
        
        // 익명클래스로 구현
        MyFunction f2 = new MyFunction() {
        	pubic void run() { // 반드시 public 
	            System.out.println("f2.run()");
            }
        }
        
        MyFunction f3 = getMyFunction();
        
        f1.run();
        f2.run();
        f3.run();
        
        execute(f1);
        execute( () -> System.out.println("run()") );
    }
}

실행결과

f1.run()
f2.run()
f3.run()
f1.run()
run()
  • 람다식을 참조 변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미한다.
  • 즉, 변수처럼 메서드를 주고 받는 것이 가능하다. 사실상 메서드가 아니라 객체를 주고 받는 것

java.util.function

  • 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다.
  • 함수형 인터페이스에 정의된 메서드 이름도 통일되고, 재사용성이나 유지보수 측면에서도 좋다.

매개변수(parameter) 1개

함수형 인터페이스 메서드 설명
java.lang.Runnable void run() 매개변수 없고, 반환값도 없음
Supplier<T> T get() → 매개변수 없고, 반환값만 있음
Consumer<T> → void accept(T t) Supplier와 반대로 매개변수만 있고 반환값 없음
Function<T,R> → R apply(T t) → 일반적인 함수. 하나의 매개변수를 받아서 결과를 반환
Predicate<T> → boolean test(T t) → 조건식 표현에 사용. 매개변수는 하나. 반환타입 boolean

💡 수학에서 결과로 true/false를 반환하는 함수를 predicate 라고 한다.

매개변수(parameter) 2개

  • 파라미터의 개수가 2개인 함수형 인터페이스는 이름 앞에 Bi 가 붙는다.
함수형 인터페이스 메서드 설명
BiConsumer<T,U> → R accept(T t, U u) 두 개의 매개변수만 있고 반환값 없음
BiPredicate<T,U> → R test(T t, U u) → 조건식 표현에 사용. 매개변수는 둘. 반환값 boolean
BiFunction<T,U,R> → R apply(T t, U u) → 두 개의 매개변수만 있고 반환값 없음
  • 3개 이상의 파라미터를 갖는 함수형 인터페이스가 필요하다면 아래와 같이 직접 만들어써야한다.
@FunctionalInterface
interface TriFunction<T,U,V,R> {
	R apply(T t, U u, V v);
}

UnaryOperator

  • Function의 자손, Function과 달리 매개변수와 결과의 타입이 같다.

BinaryOperator

  • BiFunction의 자손, BiFunction과 달리 매개변수와 결과의 타입이 같다.

컬렉션 프레임워크와 함수형 인터페이스

  • 컬렉션 프레임워크의 인터페이스에 다수의 디폴트 메서드가 추가되었는데, 그 중의 일부는 함수형 인터페이스를 사용한다.
함수형 인터페이스 메서드 설명
Collection boolean removeIf(Predicate filter) 조건에 맞는 요소를 삭제
List void replaceAll(UnaryOperator operator) 모든 요소를 변환하여 대체
Iterable void forEach(Consumer<T> action) 모든 요소에 action 수행
Map V compute(K key, BiFunction<K,V,V> f) 지정된 키의 값에 작업 f 수행
V computeIfAbsent(K key, Function<K,V> f) 키가 없으면 작업 f 수행 후 추가
V computeIfPresent(K key, BiFunction<K,V,V> f) 지정된 키가 있을 때, 작업 f 수행
V merge(K key, V value, BiFunction<V,V,V> f) 모든 요소에 병합작업 f를 수행
void forEach(BiConsumer<K,V> actioin) 모든 요소에 작업 action 수행
void replaceAll(BiFunction<K,V,V> f) 모든 요소에 치환 작업 f 수행

사용 예

list.forEach(i->System.out.println(i));
list.removeIf(x-> x%2 ==0);
list.replaceAll(i->i*10);

map.forEach((k, v) -> System.out.println(k, v));

기본형을 사용하는 함수형 인터페이스

함수형 인터페이스 메서드 설명
DoubleToIntFunction → int applyAsInt(double d) → AToBFunction은 입력=A타입, 출력=B타입
DoubleToIntFunction → int applyAsInt(T value) → ToBFunction은 출력=B타입, 입력=Generic 타입
intFunction → R apply(T t, U u) → AFunction은 입력=A타입, 출력=Generci타입
ObjIntConsumer<T> → void accept(T t, U u) → ObjAFunction은 입력=T,A타입, 출력은 없음

Function 합성과 Predicate 결합

class LambdaEx7 {
    public static void main(String[] args) {
        Function<String, Integer> f = (s -> Integer.parseInt(s , 16));
        Function<Integer, String > g = (i -> Integer.toBinaryString(i));

        Function<String, String> h = f.andThen(g);
        Function<Integer, Integer> h2 = f.compose(g);

        System.out.println(h.apply("FF")); // "FF" -> 255 -> "11111111"
        System.out.println(h2.apply(2)); // 2 -> "10" -> 16

        Function<String, String> f2 = x -> x;
        System.out.println(f2.apply("AAA"));

        Predicate<Integer> p = i -> i < 100;
        Predicate<Integer> q = i -> i < 200;
        Predicate<Integer> r = i -> i % 2 == 0;
        Predicate<Integer> notP = p.negate(); // i >= 100

        Predicate<Integer> all = notP.and(q.or(r));
        System.out.println(all.test(150)); // true

        String str1 = "abc";
        String str2 = "abc";

        // str1과 str2가 같은지 비교한 결과를 변환
        Predicate<String> p2 = Predicate.isEqual(str1);
        boolean result = p2.test(str2);
        System.out.println(result);
    }
}

메서드 참조

람다식이 하나의 메서드만 호출하는 경우에 '메서드 참조(mehotd reference)'라는 방법으로 람다식을 더욱 간결하게 표현할 수 있다.

Function<String, Integer> f = (String s) -> Integer.parseIn(s);
				↓
Function<String, Integer> f = Integer::parseInt; // 메서드 참조


BiFunction<String, String, Boolean> f2 = (s1, s2) -> sq.equals(s2);
				↓
BiFunction<String, String, Boolean> f2 = String::equals; // 메서드 참조

참조변수 f2의 타입만 봐도 람다식이 두 개의 String 타입의 파라미터를 받는다는 것을 알 수 있으므로 람다식의 파라미터들은 없어도 된다.

0개의 댓글