TIL_230306_자바의 정석 복습_람다와 스트림 1

창고·2023년 3월 6일
0

Chapter 14. 람다와 스트림

1. 람다식(Lambda Expression)

(1) 람다식이란?

  • 메서드를 하나의 식으로 표현한 것
  • 람다식은 함수를 간략하면서 명확한 식으로 표현할 수 있게 해짐
  • 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로 '익명 함수' 라고도 함
// 람다식 미적용
int method() {
	return (int) (Math.random() * 5) * 1;
}

// 람다식 적용
int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int) (Math.random() * 5) +1);
  • 메서드는 클래스에 포함되어야 하고 객체도 생성해야 메서드를 호출할 수 있으나, 람다식은 이 모든 과정 없이 오직 람다식 자체만으로도 메서드의 역할을 대신할 수 있음
  • 또한 람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고 메서드의 결과로 반환될 수도 있음. 즉, 메서드를 변수처럼 다루는 것이 가능해짐
  • 메서드와 함수의 차이
    • 객체지향 개념에서는 함수(function) 대신 메서드라는 용어를 사용
    • 메서드와 함수는 같은 의미이지만 메서드는 특정 클래스에 반드시 속해야 한다는 제약이 있음
    • 그러나 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 이 경우엔 함수라는 용어를 사용할 수 있음

(2) 람다식 작성하기

  • 익명 함수답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통 사이에 ->를 추가
// 메서드
반환타입 메서드이름 (매개변수 선언) {
	문장들
}

// 람다식
(매개변수 선언) -> {
	문장들
}
  • 반환값이 있는 메서드의 경우 return 문 대신 식으로 대신할 수 있음. 이 경우 ;를 붙이지 않음
// 메서드
int max(int a, int b) {
	return a > b ? a : b;
}

// 람다식
(int a, int b) -> a > b ? a : b
  • 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략이 가능하며 대부분 생략이 가능. 반환 타입이 없는 이유도 항상 추론이 가능하기 때문. 대신, 생략을 하려면 모든 타입을 생략해야 함 (한 타입만 생략 x)
// 람다식
(int a, int b) -> a > b ? a : b

// 매개변수 타입 추론이 가능하여 생략
(a, b) -> a > b ? a : b
  • 선언된 매개변수가 하나뿐인 경우에는 괄호 생략이 가능. 단, 매개변수의 타입이 있을 경우 괄호를 생략할 수는 없음
(a) -> a * a // a - > a * a 가능
(int a) -> a * a // int a -> a * a 불가능
  • {} 안의 문장이 하나인 경우 {}를 생략할 수 있음. ;를 붙이지 말아야 함. 단, {} 안의 문장이 return 문일 경우엔 생략이 불가
(String name, int i) -> {
	System.out.println(name + "=" + i);
}

// {} 생략
(String name, int i) ->
	System.out.println(name + "=" + i) // ; 붙이지 말 것

// {} 생략 불가
(int a, int b) -> {
	return a > b ? a : b; // return 문이 있어 {} 생략 불가
}

// {} 생략 가능
(int a, int b) -> a > b ? a : b // return 문을 생략하였으므로

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

  • 람다식이 메서드와 동등한 것처럼 설명해왔으나 사실 익명 클래스의 객체와 동등
// 람다식
(int a, int b) -> a > b ? a : b

// 익명 클래스의 객체
new Object() {
	int max(int a, int b) {
    	return a > b ? a : b;
    }
}
  • 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출 할 수 있는가?
    • 참조 변수가 있어야 객체의 메서드를 호출할 수 있는데 참조 변수의 타입은 클래스 또는 인터페이스가 가능하고 람다식과 동등한 메서드가 정의되어 있어야 함 -> 인터페이스가 적합
interface MyFunction {
	public abstract int max(int a, int b);
}

// 위의 인터페이스를 구현한 익명 클래스의 객체
MyFunction f = new MyFunction() {
					public int max(int a, int b) {
                    	return a > b ? a : b;
                    }
				};
                
int big = f.max(5, 3); // 익명 객체의 메서드를 호출
  • 인터페이스에 정의된 메서드 max()는 람다식 (int a, b) -> a > b ? a : b과 메서드의 선언부가 일치, 따라서 위의 코드의 익명 객체를 람다식으로 대체할 수 있음
MyFunction f = (int a, int b) -> a > b ? a : b; // 익명 객체를 람다식으로 대체
int big = f.max(5, 3); // 익명 객체의 메서드를 호출
  • 인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유
    • 람다식도 실제로는 익명 객체이고 인터페이스를 구현한 익명 객체의 메서드와 람다식의 매개변수의 타입과 개수, 반환값이 일치하기 때문
    • 하나의 메서드가 선언된 인터페이스를 정의해 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스러움
    • 따라서 인터페이스를 통해 람다식을 다루기로 결정됨
  • 함수형 인터페이스 : 람다식을 다루기 위한 인터페이스
    • 오직 하나의 추상 메서드만 정의되어 있어야 하는 제약이 있음
    • static, default 메서드의 개수에는 제약이 없음
    • @FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해줌
@FunctionalInterface
interface MyFunction {
	void myMethod(); // 추상 메서드, 단 1개만 정의할 수 있음
}
  • 함수형 인터페이스 타입의 매개변수와 반환 타입
    • 메서드의 매개변수가 함수형 인터페이스 타입일 경우 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 함
void aMethod(MyFunction f) { // 매개변수의 타입이 함수형 인터페이스
	f.myMethod(); // 인터페이스에 정의된 메서드 호출
}

...

MyFunction f = () -> System.out.println("myMethod()"); // 람다식을 참조하는 매개변수
aMethod(f);

// 또는 참조변수 없이 직접 람다식을 매개변수로 지정하는 것도 가능
aMethod(() -> System.out.println("myMethod()")); // 람다식을 매개변수로 지정
  • 메서드의 반환 타입이 함수형 인터페이스 타입일 경우, 함수형 인터페이스의 추상 메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있음
MyFunction myMethod() {
	MyFunction f = () -> {};
    return f;
    // 위의 두 줄을 한줄로 줄이면 return () -> {};
}
  • 람다식의 타입과 형변환
    • 함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아님. 람다식은 익명 객체이며 익명 객체는 컴파일러가 타입의 이름을 임의로 정하기 때문에 알 수 없음
    • 또한 Object 타입으로 형변환이 불가능하며 오직 함수형 인터페이스로만 형변환이 가능. 굳이 하려면 함수형 인터페이스로 먼저 변환을 하고 형변환을 진행해야 함
MyFunction f = (MyFunction) (() -> {}); // 양변의 타입이 다르므로 함수형 인터페이스로 형변환

Object obj = (Object) (() -> {}); // 에러, Object 형변환 불가
Object obj = (Object) (MyFunction) (() -> {}); // 함수형 인터페이스로 형변환 후 Object 형변환

(4) java.util.function 패키지

  • java.util.function 패키지
    • 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의
    • 매번 새로운 함수형 인터페이스를 정의하지 말고 해당 패키지의 인터페이스를 활용하는 것이 좋음
    • java.lang.Runnable - void run() : 매개변수도, 반환값도 없음
    • Supplier<T> - T get() -> T : 매개변수는 없고 반환값만 있음
    • Consumer<T> - T -> void accept(T t) : 매개변수만 있고 반환값은 없음
    • Function<T, R> - T -> R apply(T t) -> R : 일반적인 함수. 하나의 매개변수를 받아서 결과를 반환
    • Predicate<T> - T -> boolean test(T t) -> boolean : 조건식을 표현하는데 사용. 매개변수는 하나, 반환 타입은 boolean

(5) Function의 합성과 Predicate의 결합

  • Function의 합성
    • 수학에서 두 함수를 합성해 하나의 새로운 함수를 만들어내는 것 처럼 두 람다식을 합성해 새로운 람다식을 만들어낼 수 있음
    • 두 함수의 합성은 함수의 적용 순서에 따라 결과가 달라짐
      • A.andThen(B) : A 적용 후 B 적용
      • A.compose(B) : B 적용 후 A 적용
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);
Function<Integer, String> g = (i) -> Interger.toBinaryString(i);
Function<String, String> h = f.andThen(g); // 합성
  • Predicate의 결합
    • 여러 조건식을 논리 연산자인 &&(and), ||(or) 등으로 연결해서 하나의 식을 구성할 수 있는 것처럼 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있음
    • 두 대상을 비교할 때엔 isEqual()을 사용

(6) 메서드 참조

  • 메서드 참조(method reference) : 람다식이 하나의 메서드만 호출하는 경우 람다식을 더욱 더 간결히 작성할 수 있음
    • 클래스이름::메서드이름 또는 참조변수::메서드이름 으로 바꿀 수 있음
// 메서드 참조 전
Function<String, Integer> f = (String s) -> Integer.parseInt(s);

// 메서드 참조 후
Function<String, Integer> f = Integer::parseInt;
  • 생성자의 메서드 참조
    • 생성자를 호출하는 람다식 역시 메서드 참조로 변환이 가능
// 메서드 참조 전
Supplier<MyClass> s = () -> new MyClass();

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

// 매개변수가 있는 생성자는?
Funtion<Integer, MyClass> f = (i) -> new MyClass(i); // 람다식
profile
공부했던 내용들을 모아둔 창고입니다.

0개의 댓글