본문 바로가기
TIL/Java | Spring Boot

[Java] 스터디 8주차_람다와 스트림(Lambda & Stream)

by yeon_zoo 2022. 7. 16.

1. 람다식

메서드를 하나의 식으로 표현한 것. 익명함수라고도 함. 

※메서드와 함수의 차이 : 메서드는 함수와 같은 의미지만 특정 클래스에 반드시 속해야 한다는 제약이 있음(객체지향 개념에서 사용)

 

1.1 람다식 작성하기

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

  • 반환값이 있는 메서드의 경우 return 문 대신 식(expression)으로 대신할 수 있다. 식의 연산결과가 자동적으로 반환값이 된다. 이 때는 문장이 아닌 식이므로 끝에 ;를 붙이지 않는다. 
  • 람다식에 선선된 매개변수의 타입은 추론이 가능한 경우 생략할 수 있다 (반환타입이 없는 이유도 추론이 가능하기 때문)
  • 선언된 매개변수가 하나뿐인 경우에는 괄호를 생각할 수 있다. 단, 매개변수 타입이 있으면 생략 불가
  • 괄호{} 안의 문장이 하나일 때는 괄호도 생략할 수 있다. 문장의 끝에 ; 를 붙이지 않도록 주의
  • 단, 괄호{} 안의 문장이 return문인 경우 괄호{}를 생략할 수 없다.

1.2 함수형 인터페이스

자바에서는 모든 메서드가 클래스 내에 포함된다. 람다식은 메서드와 동일한 것처럼 보이지만 실제로는 익명 클래스의 객체와 동등하다. 

MyFunction이라는 인터페이스가 다음과 같이 정의되어 있다고 하자. 

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(3, 5);

만약 람다를 쓴다면 다음과 같이 사용할 수 있다. 

MyFunction f = (int a, int b) -> a > b ? a : b; //익명 객체를 람다식으로 대체
int big = f.max(3, 5); // 익명 객체의 메서드를 호출

이게 가능한 이유는 람다식도 실제로는 익명 객체이고 MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수의 타입과 개수, 그리고 반환값이 일치하기 때문이다. 이렇게 인터페이스를 통해 람다식을 다루기로 결정되었으며 람다식을 다루기 위한 인터페이스를 함수형 인터페이스라고 부르기로 했다. 

@FuntionalInterface
interface MyFunction { // 함수형 인터페이스 MyFunction을 정의
	public abstract int max(int a, int b);
}
  • 함수형 인터페이스에서는 오직 하나의 추상 메서드만 정의되어 있어야 한다. 그래야 람다식과 1:1로 연결될 수 있기 때문. 
  • 반면 static 메서드와 default 메서드의 개수에는 제약이 없다. 
  • @FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해 준다. 

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

어떤 메서드의 매개변수가 함수형 인터페이스 타입이면 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다. 

@FunctionalInterface
interface MyFunciont {
	void myMethod();
}

void aMethod(Myfuntion f) {	// 매개변수의 타입이 함수형 인터페이스
	f.myMethod();			// MyFunction에 정의된 메서드 호출
}

// 함수 호출 시
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);
// 아니면
aMethod(() -> System.out.println("myMethod()");

그리고 메서드의 반환타입이 함수형 인터페이스타입이라면 이 함수형 인터페이스의 추상 메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다. 

MyFuntion myMethod(){
	return () -> {};
}

람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것이다. 즉 변수처럼 메서드를 주고 받는 것이 가능해진 것이다. 

 

람다식의 타입과 형변환

람다식은 익명 객체이고 익명 객체는 타입이 없다. 그래서 대입 연산자의 양변의 타입을 일치시키기 위해 형변환이 필요할 수 있다. 

MyFunction f = (MyFunction) (() -> {}); // 형변환 생략 가능

하지만 Object 타입으로 형변환은 할 수 없고 함수형 인터페이스로만 형변환이 가능하다. 굳이 Object 타입으로 변환하고 싶다면 먼저 함수형 인터페이스로 변환해야 한다. 

 

외부 변수를 참조하는 람다식

람다식도 익명 클래스의 인스턴스이므로 외부에 선언된 변수에 접근하는 규칙은 익명 클래스의 규칙과 동일하다. 람다식 내에서 참조하는 지역변수는 final이 없어도 상수로 간주된다. 그리고 외부 지역변수와 같은 이름의 람다식 매개변수는 허용되지 않는다. 

 

1.3 java.util.function 패키지

이 패키지에는 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 두었다. 매번 새로운 함수형 인터페이스를 정의하지 않고 가능하면 이 패키지의 인터페이스를 활용하는 것이 좋다. 네 가지가 있는데, java.lang.Runnable -> void run() / Supplier<T> -> T get() / Consumer<T> / Function<T, R> / Predicate<T> dlek. 

함수형 인터페이스 메서드 설명
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

 

조건식의 표현에 사용되는 Predicate

Function의 변형으로 반환타입이 boolean이다. Predicate는 조건식을 람다식으로 표현하는데 사용된다.

Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";

if (isEmptyStr.test(s)) // if s.length() == 0
	System.out.println("This is an Empty String.");

 

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

함수형 인터페이스 메서드 설명
BiConsumer<T, U> void accept(T t, U u) 두 개의 매개변수가 있고 반환값 없음
BiPredicate<T, U> boolean test (T t, U u) 조건식을 표현하는데 사용됨.
매개변수는 둘, 반환값은 boolean
BiFunction<T, U, R> R apply(T t, U u) 두 개의 매개변수를 받아서 하나이 결과를 반환

※Supplier는 매개변수는 없고 반환값만 존재하는 것인데 메서드는 두 개의 값을 반환할 수 없어서 BiSupplier가 없는 것이다. 

3개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 선언해서 사용하면 된다.

 

컬렉션 프레임웍과 함수형 인터페이스

컬렉션 프레임웍의 인터페이스에 다수의 디폴트 메서드가 추가되었는데 그 중의 일부는 함수형 인터페이스를 사용한다. 

 

1.4 메서드 참조

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

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

이 메서드 참조에서 람다식의 일부가 생략되었지만 컴파일러는 생략된 부분을 우변의 parseInt메서드의 선언부로부터, 또는 좌변의 Function 인터페이스에 지정된 지네릭 타입으로부터 쉽게 알아낼 수 있다. 또 다른 예를 보면 다음과 같다. 

BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);
BiFunction<String, String, Boolean> f = String::equals;

두 개의 String을 받아서 Boolean을 반환하는 equals 라는 이름의 메서드는 다른 클래스에도 존재할 수 있기 때문에 equals 앞에 클래스 이름은 반드시 필요하다. 

메서드 참조가 가능한 마지막 경우는 이미 생성된 객체의 메서드를 람다식에서 사용한 경우에는 클래스 이름 대신 그 객체의 참조변수를 적어줘야 한다. 

MyClass obj = new MyClass();
Function<String, Boolean> f2 = obj::equals;

 

생성자의 메서드 참조

Supplier<MyClass> s = () -> new MyClass(); // 람다식
Supplier<MyClass> s = MyClass::new;

Function<Integer, int[]> f = x -> new int[x];
Function<Integer, int[]> f = int[]::new;

위의 예시처럼 생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다. 

 

2. 스트림(Stream)

2.1 스트림이란

데이터소스를 추상화하고 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다. 데이터 소스를 추상화하였다는 것은 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.

Collections.sort(strList);

for (String str : strList)
	System.out.println(str);
->
Stream<String> strStream1 = strList.stream();
strStream1.sorted().forEach(System.out::println);

 

스트림의 특징

  • 스트림은 데이터 소스를 변경하지 않는다. 
  • 스트림은 일회용이다. 
  • 스트림은 작업을 내부 반복으로 처리한다. 
  • 지연된 연산 : 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않음. 

댓글