자바(SE)

[람다표현식]Lambda 표현식

  • -

이번 포스트에서는 본격적으로 lambda 표현식을 작성해보자.

 

lambda 표현식

 

lambda를 이용한 정렬

이전 포스트(https://goodteacher.tistory.com/548)에서 이야기 했듯이 lambda 표현식이란 anonymous inner class를 이용한 처리 방식을 간결하게 처리하기 위한 것이다. 이를 위해 함수형 프로그래밍의 방식을 도입해서 객체를 전달하는데 클래스의 형태가 아닌 함수 블럭의 형태로 전달한다.

예를 들어 배열을 정렬하기 위해서는 Arrays.sort 메서드에 Comparator 타입의 객체가 필요하다.

public static <T> void sort(T[] a, Comparator<? super T> c) {
    . . .
}

 

다음과 같은 String [] 이 주어졌을 때 이 요소를 알파벳의 역순으로 정렬해보자.

String [] strs = {"this", "is", "java", "world"};

 

먼저 anonymous inner class를 이용한 형태이다. 실행가능한 코드 블럭 즉 compare 라는 메서드의 내용이 필요하지만 덕지덕지 부가적인 내용이 많이 필요하다.

Arrays.sort(strs, new Comparator<String>() {
	@Override
	public int compare(String o1, String o2) {
		return o1.compareTo(o2)*-1;
	}
});

 

이를 lambda 식으로 변경해서 처리해보자. 이제 거의 필요한 코드 블록만 전달되어 아주 깔끔한 구조가 되었다.

Arrays.sort(strs, (o1, o2) -> {return o1.compareTo(o2)*-1;});

 

타겟 타입과 @FunctionalInterface

lambda 식이 매우 간결해보이지만 모든 interface 구현을 lambda로 대체할 수는 없다. 위 코드에서 Comparator 처럼 lambda 식이 할당되는 인터페이스를 타겟 타입이라고 하는데 이 타겟 타입은 반드시 하나의 abstract method만 존재 해야 한다. 그리고 이 메서드의 구현부가 lambda 식으로 대체된다. abstract method가 2개 이상 존재하는 경우는 여전히 anonymous inner class를 사용해야 한다.

이처럼 lambda 식으로 처리될 수 있는 interface에는 @FunctionalInterface라는 annotation을 추가할 수 있다. @FunctionalInterface는 반드시 있어야 하는 내용은 아니지만 컴파일러에게 하나의 abstract method만 있음을 체크하도록 하고 2개 이상 존재하면 오류를 발생시켜서 안정적인 프로그래밍이 가능하게 한다. Comparator는 당연히 @FunctionalInterface의 하나이다.

@FunctionalInterface
public interface Comparator<T> {
    . . .
}

이렇게 하나의 메서드만 존재해야하는 이유는 런타임에 함수를 호출할 때 함수의 이름을 추정할 수 있어야 하기 때문이다.

즉 객체의 타입이야 메서드의 선언부를 보면 알 수 있지만 어떤 메서드를 호출해야할지는 알 수 없는데 메서드가 하나 뿐이라면 고민할 필요가 없을 것이다.

 

lambda 식 작성 방법

그럼 lambda 식 작성 방법에 대해 살펴보자. 람다식을 함수에 전달할 파라미터 목록과 함수에서 실행할 실행문으로 구성된다. 이 둘의 사이는 ->로 연결한다.

(type variable_name[,…]) -> {실행문};

기본 동작은 전달된 파라미터를 이용해서 실행문을 실행해라라는 정도로 이해하면 된다. 즉 파라미터는 실행문 블록에서 사용하기 위한 값을 제공한다.

(String msg) -> {System.out.println(msg);}

 

여기까지는 아주 간단한데 lambda 식의 마법은 아직 끝나지 않았다. lambda식은 다양한 형태에서 축약이 가능하다.

  • 파라미터의 타입은 런타임 시에 대입되는 값에 따라 자동으로 인식되므로 일반적으로 생략한다.
  • 파라미터가 하나이면 파라미터를 감싸는 ()를 생략할 수 있고 실행문이 하나이면 실행문을 감싸는 {}를 생략할 수 있다.
  • 메서드에서 return이 필요한 경우 return 키워드를 사용하지만 만약 return 문만 존재하는 경우는 return 키워드도 생략할 수 있다.
List<String> strs = Arrays.asList("Hello")	;

// 가장 기본적인 형태
strs.forEach((String msg) -> {System.out.println(msg);});
// 파라미터 타입 생략
strs.forEach((msg) -> {System.out.println(msg);});

// 파라미터, 실행문이 하나인 경우
strs.forEach(msg ->System.out.println(msg));

// return 타입 처리
strs.removeIf(str-> {return str.equals("Hello");});
strs.removeIf(str-> str.equals("Hello"));

 

lambda 실행 블록에서의 변수 참조

가끔 lambda 실행 블록에서 외부의 변수를 참조할 경우가 있는데 이때 변수의 종류에 따라 주의해야할 내용들이 있다.

  • 외부 객체의 member 변수: 접근제한자와 무관하게 제약 없이 사용 가능
  • 선언된 실행 블록의 local 변수: final 키워드가 추가된 것으로 동작. 따라서 read only로만 동작

또한 this의 용법도 다른데 this를 사용하면 외부 객체의 레퍼런스에 접근한다. 또는 anonymous inner class 처럼 외부클래스이름.this를 사용해서 외부 객체의 레퍼런스에 접근할 수도 있다.

사실 lambda 표현식에는 멤버를 선언할 수 있는 영역이 없기도 해서 람다식에 대한 객체를 내부에서 참조할 일이 없다.

interface FunctionalInterface4 {
  String methodB(String msg);
}

public class VariableUseTest {
  private int instanceMember = 10;

  public void useFIMethod(FunctionalInterface4 fi) {
    System.out.println(fi.methodB("홍길동"));
  }

  public void lambdaTestMethod() {
    int localVar = 20;
    useFIMethod((String msg) -> {
      System.out.println("this: " + this);
      System.out.println("외부클래스.this: " + VariableUseTest.this);
      System.out.println("변수 참조: " + localVar + " : " + instanceMember);
      // localVar++;// 로컬 변수 수정은 불가
      return "람다 표현식 이용 : " + msg + ", " + (instanceMember++);
    });
  }
}

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.