[자바의 정석 기초편] 람다식

JEREGIM·2023년 3월 14일
0

자바의 정석 기초편

목록 보기
20/23

📌람다식(Lambda Expression)

: 함수(메서드)를 간단한 식(expression)으로 표현하는 방법

람다식은 익명 함수(이름이 없는 함수)

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

람다식으로 표현🔽

(int a, int b) -> {
	return a > b ? a : b;
}
  • 반환타입과 함수 이름을 생략한다.

함수와 메서드의 차이

  • 근본적으로 동일하지만 함수는 일반적인 용어, 메서드는 객체지향개념 용어이다.
  • 함수는 클래스에 독립적이지만 메서드는 클래스에 종속적이다.
  • 자바는 모든 메서드가 클래스 안에서만 정의될 수 있기 때문에 메서드라고 부른다.

람다식 작성하기

  1. 메서드의 이름과 반환타입을 제거하고 "->" 추가
(int a, int b) -> {
	return a > b ? a : b;
}
  1. 반환값이 있는 경우, 식이나 값만 적고 return 문 생략(;도 생략)
    (int a, int b) -> a > b ? a : b

  2. 매개변수의 타입이 추론 가능하면 생략 가능(대부분 생략 가능)
    (a, b) -> a > b ? a : b

작성 시 주의사항

  1. 매개변수가 하나인 경우 괄호 생략 가능
    a -> a * a

  2. 블록 안의 문장이 하나 뿐일 때 중괄호 생략 가능(;도 생략)
    i -> System.out.println(i)

람다식 예시

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

람다식🔽
(a, b) -> a > b ? a : b

int square(int x) {
	return x * x;
}

람다식🔽
x -> x * x

int roll() {
	return (int)(Math.random() * 6);
}

람다식🔽
() -> (int)(Math.random() * 6)

  • 매개변수 없을 때 괄호만 써준다. 괄호 생략 불가

    람다식은 익명 객체

    람다식은 익명 함수가 아니라 익명 객체이다.

  • 람다식은 메서드를 간단하게 표현한 식이다. 그런데 자바는 메서드만 단독으로 쓰일 수 없고 항상 클래스 안에 있어야 하기 때문에 익명 객체로 람다식을 감싼 것이고 따라서 람다식을 익명 개체라고 하는 것이다.

 (a, b) -> a > b ? a : b

// 익명 클래스
new Object() {
	int max(int a, int b) {
    	return a > b ? a : b ;
    }
}

// 참조변수에 대입
Object obj = new Object() {
	int max(int a, int b) {
    	return a > b ? a : b ;
    }
};
...
int value = obj.max(3,5); // 에러
  • 람다식은 익명 객체지만 Object 타입의 참조변수에 대입하면 에러가 난다. Object 타입에는 max() 라는 메서드가 정의되어 있지 않기 때문이다.
    그래서 람다식을 다루기 위해 만들어진 참조타입이 함수형 인터페이스이다.

📌함수형 인터페이스

단 하나의 추상 메서드만 선언된 인터페이스

@FunctionalInterface
interface MyFunction {
	int max(int a, int b);
}

public class Ex14_0 {
	public static void main(String[] args) {
    	MyFunction f = (a, b) -> a > b ? a : b;
        int value = f.max(3, 5);
        System.out.println(value);
    }
}

5

  • 람다식을 다루기 위해 참조타입을 함수형 인터페이스로 선언한다.
  • @FunctionalInterface : 함수형 인터페이스라는 것을 알리는 애너테이션. 이 애너테이션이 붙은 인터페이스에 2개 이상의 추상 메서드를 작성하면 에러가 난다. -> 하나의 추상 메서드만 작성 가능

익명 객체를 람다식으로 대체

List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd");
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        return o2.compareTo(o1);
    }
});
  • 내림차순으로 정렬하는 기준을 익명 객체로 구현

람다식으로 대체🔽

Collections.sort(list,(s1, s2) -> s2.compareTo(s1));
  • Comparator<T>compare(T o1, T o2) 추상 메서드 하나만을 가진 함수형 인터페이스이다.

함수형 인터페이스 타입의 매개변수, 반환타입

매개변수

@FunctionalInterface
interface MyFunction {
	void myMethod() ;
}

...
void aMethod(MyFunction f) {
	f.myMethod();
}
...
Myfunction f = () -> System.out.println("myMethod()");
aMethod(f);
  • 함수형 인터페이스 타입의 매개변수는 매개변수로 람다식을 넣는다. 그럼 이 메서드가 람다식을 호출하는 것이다.

  • aMethod(() -> System.out.println("myMEthod()")); 한 줄로 줄일 수 있다.

반환타입

MyFunction getMyFunction() {
	return () -> System.out.println("myMethod");
}
  • 함수형 인터페이스 타입의 반환타입은 람다식을 반환하는 것이다.

실습 예제

@FunctionalInterface
interface MyFunction {
    void run();  // public abstract void run();
}

public class Ex14_1 {
    static void execute(MyFunction f) {
        f.run();
    }

    static MyFunction getMyFunction() {
		return () -> System.out.println("f3.run()");
    }

    public static void main(String[] args) {
        MyFunction f1 = () -> System.out.println("f1.run()");

        MyFunction f2 = getMyFunction(); // 함수형 인터페이스 타입의 반환타입

        f1.run();
        f2.run();

        execute(f1); // 함수형 인터페이스 타입의 매개변수
        execute(() -> System.out.println("run()"));
    }
}

f1.run()
f2.run()
f1.run()
run()

  • execute(f1); : 매개변수가 함수형 인터페이스 타입인 메서드 excute()는 매개변수로 람다식이 들어간다.

📌java.util.function 패키지

: 자주 사용되는 다양한 함수형 인터페이스를 제공하는 패키지이다.
-> 람다식을 쓸 때마다 새로운 함수형 인터페이스를 정의하는 것보다 이 패키지 안에 있는 이미 만들어진 함수형 인터페이스를 사용하는 것이 재사용성 / 유지보수 측면에서 좋고 표준화가 된다는 장점이 있다.

Quiz

빈칸에 들어갈 알맞은 함수형 인터페이스는?

( ① ) f = () -> (int)(Math.random()*100) + 1;
A. ① : Supplier<Integer>

  • 입력값은 없고 난수만 반환하기 때문에

( ② ) f = i -> System.out.print(i + ", ");
A. ② : Consumer<Integer>

  • 입력값(i)은 있는데 반환값은 없고 그냥 출력만 하기 때문에

( ③ ) f = i -> i % 2 == 0;
A. ③ : Predicate<Integer>

  • i % 2 == 0 이라는 조건식을 반환하기 때문에

( ④ ) f = i -> i / 10 * 10;
A. ④ : Function<Integer, Integer>

  • 입력값과 출력값이 모두 있기 때문에

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

  • BiSupplier는 없다. 함수는 반환값이 2개가 될 수 없기 때문에

매개변수의 타입과 반환타입이 일치하는 함수형 인터페이스

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
	static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}
  • identity() : 항등함수라고 한다. 입력값 t를 입력하면 그대로 t가 반환되는 함수이다.

실습 예제

import java.util.function.*;
import java.util.*;

class Ex14_2 {
    public static void main(String[] args) {
        Supplier<Integer> s = () -> (int) (Math.random() * 100) + 1; // 난수 생성
        Consumer<Integer> c = i -> System.out.print(i + ", "); // 입력값 출력
        Predicate<Integer> p = i -> i % 2 == 0; // 짝수인지 검사
        Function<Integer, Integer> f = i -> i / 10 * 10; // 일의 자리 버림

        List<Integer> list = new ArrayList<>();
        makeRandomList(s, list);
        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와 크기가 같은 newList를 새로 생성
        List<T> newList = new ArrayList<T>(list.size());
        // Function으로 일의 자리 버림한 수를 newList에 add
        for (T i : list) {
            newList.add(f.apply(i));
        }

        return newList;
    }

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

    static <T> void makeRandomList(Supplier<T> s, List<T> list) {
        // Supplier를 통해 생성된 난수 10개를 list에 저장
        for (int i = 0; i < 10; i++) {
            list.add(s.get());
        }
    }
}
  • makeRandomList(s, list); : Supplier를 통해 생성된 난수 10개를 list에 저장

  • printEvenNum(p, c, list); : Predicate로 list의 요소들을 짝수인지 검사하고 짝수가 맞으면 Consumer를 통해 출력

  • List<Integer> newList = doSomething(f, list); : Function을 통해 list의 값들을 일의 자리 버림한 수들을 newList에 저장


📌Predicate의 결합

and(), or(), negate()로 두 Predicate를 하나로 결합(default 메서드)

Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 0;

Predicate 결합🔽

Predicate<Integer> notP = p.ngeate();
Predicate<Integer> all = notP.and(q).or(r);
Predicate<Integer> all2 = notP.and(q.or(r));

notP = i >= 100
all = i >= 100 && i < 200 || i % 2 == 0
all2 = i >= 100 && (i < 200 || i % 2 == 0)

System.out.println(all.test(2));
System.out.println(all2.test(2));

true
false

all.test(2)

  • 2 >= 100 -> false
  • 2 < 200 -> true
  • 2 % 2 == 0 -> true
    -> false && true || true = false || true = true

all2.test(2)

  • 2 >= 100 -> false
  • 2 < 200 -> true
  • 2 % 2 == 0 -> true
    -> false && (true || true) = false && true = false

등가비교를 Predicate 작성 - isEqual() (static 메서드)

boolean result = Predicate.isEqual(str1).test(str2);
  • str1과 str2 비교

andThen() - Function 결합

2개 이상의 함수를 하나로 연결할 때 사용

// 입력값 : String / 반환값 : Integer
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16) 
// 입력값 : Integer / 반환값 : String
Function<Integer, String> g = (i) -> Integer.toBinaryString(i) 

Function<String, String> h = f.andThen(g); // 두 함수를 하나의 함수로 결합

System.out.println(h.apply("FF"));
  • Function<String, String> h = f.andThen(g)

    • h의 입력타입은 f의 입력타입과 같고 h의 반환타입은 g의 반환타입과 같다.

    • f의 반환타입과 g의 입력타입이 같아야 한다.

  • h.apply("FF")
    -> Integer.parseInt(FF, 16) : "FF"를 16진수로 변환 = 255
    -> Integer.toBinaryString(255) : 255를 2진수로 변환 = 11111111 출력


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

함수형 인터페이스를 사용하는 컬렉션 프레임워크의 메서드(와일드 카드 생략)

InterfaceMethod설명
Collectionboolean removeIf(Predicate<E> filter)조건에 맞는 요소 삭제
Listvoid replaceAll(UnaryOperator<E> operator)List 내 모든 요소 변환하여 대체
Iterablevoid forEach(Consumer<T> action)모든 요소에 작업(action) 수행
MapV 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> action)모든 요소에 작업(action) 수행
void replaceAll(BiFunction<K,V,V> f)모든 요소에 치환작업(f) 수행

실습 예제

import java.util.*;

class Ex14_4 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++)
            list.add(i);

		// list의 모든 요소 출력
        list.forEach(i -> System.out.print(i + ","));
        System.out.println();

		// list에서 2 또는 3의 배수 제거
        list.removeIf(x -> x % 2 == 0 || x % 3 == 0);
        System.out.println(list);

		// list의 모든 요소에 x10
        list.replaceAll(i -> i * 10);
        System.out.println(list);

        Map<String, String> map = new HashMap<>();
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        map.put("4", "4");

		// map의 모든 요소를 {k, v} 형식으로 출력
        map.forEach((k, v) -> System.out.print("{" + k + "," + v + "},"));
        System.out.println();
    }
}
  • map.forEach((k, v) -> System.out.print("{" + k + "," + v + "},")); : iterator() 반복자를 써서 컬렉션을 출력하는 대신 forEach() 메서드를 통해 코드를 간결하게 바꿀 수 있다.

📌메서드 참조(method reference)

: 하나의 메서드만 호출하는 람다식은 "메서드 참조"로 더욱 간단히 할 수 있다.

종류람다식메서드 참조
static 메서드 참조x -> ClassName.method(x)CalssName::method
인스턴스 메서드 참조(obj, x) -> obj.method(x)CalssName::method

static 메서드 참조

Integer method(string s) {
	return Integer.parseInt(s);
}

람다식🔽
Function<String, Integer> f = s -> Integer.parseInt(s);

메서드 참조🔽
Function<String, Integer> f = Integer::parseInt;

생성자 메서드 참조

매개변수가 없는 생성자

// 생성자 람다식
Supplier<MyClass> s = () -> new MyClass();
  • 매개변수가 없는 생성자는 입력값이 없기 때문에 Supplier로 작성

메서드 참조🔽
Supplier<MyClass> s = MyClass::new;

매개변수가 있는 생성자

Function<Integer, MyClass> f = (i) -> new MyClass(i); //람다식
Function<Integer, MyClass> f = MyClass::new; // 메서드 참조
  • 매개변수라는 입력값이 있기 때문에 Function을 사용한다.

매개변수가 2개 이상인 생성자

// 매개변수가 2개 이상인 생성자
BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s); // 람다식
BiFunction<Integer, String, MyClass> bf = MyClass::new; // 메서드 참조
  • BiFunction 사용

배열 메서드 참조

Function<Integer, int[]> arrF = x -> new int[x]; // 람다식
Function<Integer, int[]> arrF = int[]::new; // 메서드 참조 

// String 배열
Function<Integer, String[]> arrF2 = String[]::new ;
  • 배열을 생성할 땐 배열의 크기를 정해줘야 하므로 입력값이 있다. 따라서 Function을 이용한다.

어떤 함수형 인터페이스를 사용할지 모를땐 입력값과 반환값의 유무를 잘 생각해보면 된다.

0개의 댓글