|
람다 표현식(Lambda Expression)
java 8에 람다 표현식이 도입되면서 함수형 언어 비슷한 코드를 작성하는 것이 가능해 졌다. 람다 표현식은 이름이 없는 익명 함수를 의미한다. 람다에 관심을 둬야 하는 몇 가지 이유는 다음과 같다.
- OOP 언어인 Java에서 함수형 코드를 작성 가능.
- 람다 표현식을 이용함으로써 간결하고 명확한 코드로 그 의도를 표현 가능.
- Collection filtering, Iteration , Extraction 등에서 코드 생산성 향상.
람다는 다음과 같은 특징을 갖고 있다.
- 익명 : 보통의 메서드와 달리 이름이 없으므로 익명이라 표현하며, 코드를 간결하게 작성함으로 인하여 구현해야 할 코드에 대한 걱정거리가 줄어든다.
- 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
- 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 없다.
- 간결성 : 익명 클래스에서와 같은 자질구레한 코드를 구현할 필요가 없다.
람다는 무명의 메서드(Anonymous Method)이며, 일반적으로 자바에서의 람다는 구현해야 하는 메서드가 하나인 함수형 인터페이스(Functional Interface)를 구현할 경우에 람다를 통해 몸체를 다 기술하지 않고, 간단하게 구현할 수 있다. 이런 이유로 Interface를 이용하여 람다 함수를 만듦으로써 Interface의 함수가 단 1개만 있어야 람다식을 사용할 수 있다는 제약이 있다. 사실 자바에서 Interface를 이용하여 람다를 표현한다고 함은 내부적으로 Proxy 객체를 생성하여 그 안에 람다 표현을 메서드로 만든다.
참고 : 프록시(Proxy) 객체는 다른 객체에 대한 인터페이스 역할을 하는 객체다. 프록시는 실제 객체에 대한 접근을 제어하거나, 실제 객체에 대한 작업을 보조하거나, 실제 객체에 대한 추가적인 기능을 제공하는 래퍼(wrapper) 역할을 한다.
프록시는 다양한 상황에서 사용될 수 있다. 일반적으로는 다음과 같은 경우에 사용된다.
- 보안 제어: 프록시는 실제 객체에 대한 접근을 제어하여 보안 검사를 수행할 수 있다. 예를 들어, 특정 사용자가 특정 객체에 접근할 권한이 있는지 확인할 수 있다.
- 원격 호출(Remote Invocation): 프록시는 클라이언트와 서버 간의 통신을 위해 사용될 수 있다. 클라이언트는 프록시를 통해 서버에 요청을 보내고, 프록시는 이 요청을 실제 서버 객체에 전달하여 처리한다.
- 로깅 및 감시: 프록시는 메서드 호출을 가로채어 로깅하거나 감시하는 데 사용될 수 있다. 이를 통해 어떤 메서드가 호출되었는지, 어떤 인자가 전달되었는지 등을 추적할 수 있다.
- 지연 로딩(Lazy Loading): 프록시는 실제 객체를 생성하는 시점을 지연시킴으로써 성능을 향상시킬 수 있다. 예를 들어, 데이터베이스에서 데이터를 가져오는 경우 프록시는 실제 데이터를 가져오는 시점을 호출 시점으로 미룰 수 있다.
프록시 패턴은 객체 지향 디자인 패턴 중 하나이며, 다양한 프로그래밍 언어와 환경에서 사용된다. 자바에서는 다양한 용도로 프록시를 사용할 수 있으며, 예를 들어 동적 프록시를 사용하여 런타임에 프록시 객체를 생성하고 사용할 수 있다.
기본적인 람다 표현식에 대한 문법을 확인해 보자.
람다 표현식의 일반적인 구문 : (parameter ,,,) -> { body }
: 0개 이상의 매개변수를 실행하고자 하는 함수 body로 전달한다는 의미.
람다는 크게 세 부분으로 이루어져 있는데
- 파라미터 리스트 : xxx메서드의 파라미터() : (int a1, int a2)
- 화살표 : 화살표 ( -> )는 람다의 파라미터 리스트와 바디를 구분한다
- 람다의 바디 : 람다의 반환 값에 해당하는 표현식이다.
간단한 예를 작성해 보도록 하겠다. 먼저 인자가 없는 메소드를 갖는 인터페이스를 만들어 둔다.
MyInter.java ---
public interface MyInter {
void abc();
}
이번에는 인자가 있는 메소드를 갖는 인터페이스를 만들어 둔다.
MyInterArg.java ---
public interface MyInterArg {
void def(int a, int b);
}
다음으로 프로그램 실행을 위한 클래스를 작성하면 되겠다.
Main.java ---
public class Main {
public static void main(String[] args) {
//1. 인자가 없는 추상 메소드 처리
MyInter inter = new MyInter() {
@Override
public void abc() {
System.out.println("일반적인 익명 클래스의 메소드 오버라이딩");
}
};
inter.abc(); //무명 클래스의 메소드 호출
//람다식으로 표현
MyInter inter2 = ()-> System.out.println("람다식으로 표현");
inter2.abc(); //람다로 표현된 메소드 호출
System.out.println();
//2. 인자가 있는 추상 메소드 처리
MyInterArg interArg = new MyInterArg() {
@Override
public void def(int a, int b) {
System.out.println("두 수의 합은 " + (a + b));
}
};
interArg.def(3, 4); // 인자가 있는 무명 클래스의 메소드 호출
//람다식으로 표현
MyInterArg interArg2 = (a, b)->System.out.println("람다식으로 표현:" + (a + b));
interArg2.def(3, 4); // 인자가 있는 람다로 표현된 메소드 호출
}
}
* 이벤트 핸들러 메소드에서 사용되는 람다 표현식
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ButtonProcess extends JFrame{
public ButtonProcess(){
super("lambda test");
setBounds(100, 100, 300, 300);
setVisible(true);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(null);
JButton btn = new JButton("button1");
btn.setBounds(10, 50, 100, 50);
add(btn);
btn.addActionListener(new ActionListener() { //전통적 방법
@Override
public void actionPerformed(ActionEvent e) {
setTitle("첫번째 버튼");
}
});
JButton btn2 = new JButton("button2");
btn2.setBounds(10, 150, 100, 50);
add(btn2);
btn2.addActionListener(e -> setTitle("2번째 버튼")); //람다 방법
}
public static void main(String[] args) {
new ButtonProcess();
}
}
*** 다양한 람다 처리 방법 ***
- 불린 표현식 : (List<String> list) ->list.isEmpty()
- 객체 생성 : () -> new Apple(10)
- 객체에서 소비 : (Apple a) -> { System.out.println(a.getWeight()); }
- 객체에서 선택/추출 : (String s) ->s.length()
- 두 값을 조합 : (int a, int b) -> a * b
- 두 객체 비교 : (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
* 메서드 레퍼런스는 람다의 축약형이다.
람다와 메서드 레퍼런스 단축 표현 예제
- (Apple a) ->a.getWeight() = > Apple::getWeight()
- () ->Thread.currentThread().dumpStack() => Thread.currentThread()::dumpStack
- (str, i) ->str.substring(i) => String::substring
- (String s) ->System.out.println(s) => System.out::println
*** 람다 표현식은 매개변수와 실행 코드에 따라서 조금씩 다른 형태를 가질 수 있다.
// 문법에 따른 람다 예제 -------------------------
// 1) 매개변수가 없는 경우
() -> System.out.println("Hello, Lambda!");
// 2) 매개변수가 하나인 경우, 괄호 생략 가능
name -> System.out.println("Hello, " + name);
// 3) 매개변수가 두 개 이상인 경우, 괄호 필요
(int x, int y) -> { return x + y; }
// 4) 실행문이 반환 값을 가지는 표현식인 경우
(x, y) -> x + y; // 'return'과 중괄호 생략 가능
람다 표현식은 주로 함수형 인터페이스(functional interface)에서 사용된다. 함수형 인터페이스는 단 하나의 추상 메소드를 가진 인터페이스로, 람다 표현식을 통해 이 추상 메소드의 구현을 제공할 수 있다. java.util.function 패키지를 통해 다양한 함수형 인터페이스를 제공한다.
ex1) 람다 표현식을 사용하는 간단한 예제를 만들어 보겠다. 여기서는 리스트의 각 요소를 출력하는 예제를 만들어 본다. 먼저, 문자열 리스트를 생성하고, 이를 출력하는 전통적인 방법과 람다 표현식을 사용하는 방법을 비교해 보겠다.
// 전통적인 방법 (Java 7 이하)
import java.util.Arrays;
import java.util.List;
public class LambdaExample {
public static void main(String[] args) {
List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
// 전통적인 for-loop 사용
for (String item : list) {
System.out.println(item);
}
}
}
// 람다 표현식을 사용하는 방법 (Java 8 이상)
import java.util.Arrays;
import java.util.List;
public class LambdaExample {
public static void main(String[] args) {
List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
// 람다 표현식을 사용하여 forEach 메소드로 각 요소 출력
list.forEach(item -> System.out.println(item));
}
}
위 예제에서는 List 인터페이스의 forEach 메소드를 사용하여 리스트의 각 요소를 출력한다.
forEach 메소드는 Consumer 함수형 인터페이스의 인스턴스를 매개변수로 받으며, 여기에 람다 표현식 item -> System.out.println(item)을 전달하여 각 요소를 출력하도록 한다.
이렇게 람다 표현식을 사용하면 코드가 더 간결해지고, 함수형 프로그래밍의 장점을 활용할 수 있다.
ex2) 조금 더 실무적인 코드를 작성해 보자
실무에서 람다 표현식을 활용하는 보다 구체적인 예를 들면, Java의 스트림 API와 람다 표현식을 결합하여 컬렉션을 처리하는 경우를 들 수 있다. 여기서는 제품 객체의 리스트를 처리하여 조건에 맞는 제품을 필터링하고, 그 결과를 정렬 및 출력하는 예제를 만들어 보겠다.
-- Product 클래스 정의
public class Product {
private String name;
private double price;
private int quantity;
public Product(String name, double price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public int getQuantity() {
return quantity;
}
@Override
public String toString() {
return "Product{name='" + name + "', price=" + price + ", quantity=" + quantity + '}';
}
}
// 실무적인 람다 표현식 사용 예제
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class LambdaExample {
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("Laptop", 1200.00, 10),
new Product("Smartphone", 600.00, 50),
new Product("Tablet", 300.00, 30),
new Product("Monitor", 150.00, 15)
);
// 가격이 300 이상인 제품을 필터링하고, 가격으로 정렬한 후 결과를 출력
List<Product> filteredProducts = products.stream()
.filter(product -> product.getPrice() >= 300)
.sorted((p1, p2) -> Double.compare(p1.getPrice(), p2.getPrice()))
.collect(Collectors.toList());
System.out.println(filteredProducts);
// 결과 출력
filteredProducts.forEach(product -> System.out.println(product));
System.out.println();
filteredProducts.forEach(product -> System.out.println(product.getName() +
" " + product.getPrice() + " " + product.getQuantity()));
}
}
위 예제에서는 스트림 API의 filter, sorted, forEach 메소드를 사용하여 제품 리스트를 처리한다.
filter 메소드에서는 람다 표현식 product -> product.getPrice() >= 300을 사용하여 가격이 300 이상인 제품을 필터링한다.
sorted 메소드에서는 두 제품의 가격을 비교하는 람다 표현식을 사용하여 제품을 정렬한다. 마지막으로 forEach 메소드를 사용하여 필터링 및 정렬된 제품 리스트를 출력한다.