본문 바로가기

자바(Java)

Java - 람다식

반응형

1. 기초 지식


람다식 이전에 사용되던 “익명 클래스”에 대해 선행하며, 람다식에 대한 이해를 향상시키고자 한다.

1) 익명 클래스 (Anonymous Class)

자바에서 익명 클래스(Anonymous Class)는 이름이 없는 클래스로, 주로 인터페이스나 추상 클래스의 객체를 생성할 때 사용된다. 익명 클래스는 한 번만 사용할 목적으로 간단한 클래스를 정의하고 인스턴스화할 때 유용하다. 주로 이벤트 핸들링, 스레드 생성 등에서 활용된다.

기본 구조>

인터페이스명/클래스명 변수명 = new 인터페이스명/클래스명() {
    // 멤버 변수 및 메서드 구현
};


Button 클래스의 클릭 이벤트 처리를 위한 익명 클래스 사용 예시>

public class Main {
    public static void main(String[] args) {
        Button button = new Button();

        // 익명 클래스를 사용하여 버튼 클릭 이벤트 처리
        button.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick() {
                System.out.println("버튼이 클릭되었습니다.");
            }
        });

        // 버튼이 클릭되면 익명 클래스의 onClick 메서드가 호출됨
        button.click();
    }
}

interface OnClickListener {
    void onClick();
}

class Button {
    private OnClickListener clickListener;

    public void setOnClickListener(OnClickListener listener) {
        this.clickListener = listener;
    }

    public void click() {
        if (clickListener != null) {
            clickListener.onClick();
        }
    }
}

위 코드에서 OnClickListener 인터페이스는 클릭 이벤트를 처리하기 위한 onClick() 메서드를 선언한다. Button 클래스는 이벤트 리스너를 등록하고, 클릭 시 등록된 리스너의 onClick() 메서드를 호출한다. Main 클래스에서는 익명 클래스를 사용하여 실제 이벤트 핸들링 로직을 정의한다.

익명 클래스는 OnClickListener 인터페이스를 구현하는 클래스의 이름이 없지만, 인터페이스의 메서드를 재정의하여 원하는 동작을 구현할 수 있다. 이를 통해 이벤트 핸들링과 같은 간단한 기능을 쉽게 구현할 수 있다.

2. 람다식 정의


자바 8부터 도입된 람다식은 함수형 프로그래밍 스타일을 지원하기 위한 기능이다. 람다식은 익명 함수(anonymous function)로도 알려져 있으며, 다른 언어에서는 클로저(closure)라고도 불린다.

람다식은 메서드를 하나의 식으로 표현하는 방법이다. 기존에는 메서드를 정의하고 호출하는 과정이 필요했지만, 람다식을 사용하면 메서드의 이름과 반환 타입을 생략하고 간결하게 표현할 수 있다. 이로써 코드의 가독성을 높일 수 있다.

기본 문법>

(parameter1, parameter2, ...) -> {
    // 코드 블록(메서드 구현 내용 기입)
}

- parameter1, parameter2, ...: 메서드의 매개변수를 나타낸다. 타입은 생략 가능하며, 인자가 없을 경우 괄호를 비워둔다.
- ->: 람다식의 화살표 연산자로, 매개변수와 코드 블록을 분리한다.
- {}: 람다식의 코드 블록을 감싸는 중괄호로, 메서드의 구현 내용을 작성한다.

예시 코드>

int sum = (a, b) -> a + b;
// **a, b:** 매개변수, **a + b:** 람다식의 구현 내용, 반환값: **a**와 **b**를 더한 값
// 연산 결과가 남으면, return을 별도로 명시하지 않아도 반환 대상이 된다.

@FunctionalInterface // 인터페이스가 함수형 인터페이스임을 나타내기 위해 사용
// 특징: 함수형 인터페이스는 단 하나의 추상 메서드만을 가지며, 람다식과 같은 함수형 프로그래밍 기법을 사용할 수 있도록 한다.
interface Calculate <T> {
	T cal(T a, T b);
}
// Calculate Interface: T type으로 반환하는 cal(T a, T b) 추상 메소드 정의

class LambdaGeneric { // LambdaGeneric 클래스 생성
   public static void main(String[] args) {
      Calculate<Integer> ci = (a, b) -> a + b;
			// 해석1 - ci 인스턴스 생성(T type: Integer)
			// 해석2 - ci 메소드 구체화 (람다식 이용하여 Calculate 인터페이스의 cal 메소드 사용)
			// 해석3 - Integer 타입의 a, b 인자를 넣어 람다식의 구현 내용인 (a+b)를 실행
			// 해석4 - 반환값: int type의 a+b
      System.out.println(ci.cal(4, 3));
			// 출력값: 7

      Calculate<Double> cd = (a, b) -> a + b;
			// 해석1 - cd 인스턴스 생성 (T type: Double) 이하 동일
      System.out.println(cd.cal(4.32, 3.45));
			// 출력값: 7.77
   }
}

3. 람다식 특징


람다식은 함수형 인터페이스(functional interface)와 함께 사용된다. 함수형 인터페이스는 하나의 추상 메서드를 가지는 인터페이스로, 람다식을 담을 수 있는 타입이다. 자바에서 제공하는 많은 함수형 인터페이스 중 일부는 java.util.function 패키지에서 찾을 수 있다.

예시 코드> 람다식을 사용하여 리스트의 각 요소를 출력하는 예제

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

names.forEach(name -> System.out.println(name));
// 해석1 - List names를 반복순회하면서, name(type: String)을 List 각 인덱스에 저장되어 있는 Value로 초기화 후 Print 진행
// 질문1 - name을 List names의 value값으로 초기화 가능한 이유는?
// 답변1 - 람다식은 기본적으로 함수형 인터페이스의 단일 추상 메소드의 구현을 표현한다. forEach메서드에 전달된 람다식에서 name 은 람다식의 매개변수를 의미한다. 이 람다식은 Consumer 인터페이스를 구현한 형태이며, 해당 인터페이스는 하나의 입력값을 받고 반환값이 없는 accept 메서드를 가지는 함수형 인터페이스이다. 때문에, forEach 메서드는 리스트의 각 요소를 인자로 받아 람다식의 매개변수에 전달한다. 즉, 리스트의 요소가 name 매개변수에 순차적으로 할당되는 구조이다. 이 후, 람다식의 내부에서 System.out.println(name)을 통해 name변수에 할당된 값을 출력한다.


예시 코드> 매개변수가 없는 람다식

interface Generator{
	int rand(); // int를 반환하는 rand() 메소드
}

class NoParamAndReturn{
	public static void main(String[] args){
		Generator gen = () => {
		// gen 인스턴스 생성 (람다식 인수 없음)
			Random rand = new Random();
			// Random() 메소드를 이용하여 rand 초기화
			return rand.nextInt(50);
		};
		System.out.println(gen.rand());
	}
}

4. 람다식 사용 전후 비교


아래의 예시는 사람 객체를 나이 순으로 정렬하는 메소드를 ‘람다식 전/후’로 비교한 것이다.


1) 사용 전

// 정렬 기준을 가지고 있는 인터페이스
interface Comparator<T> {
    int compare(T obj1, T obj2);
}

// 정렬 기준에 따라 사람 객체를 비교하는 클래스
class PersonComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return p1.getAge() - p2.getAge();
    }
}

// 사람 객체를 정렬하는 메서드
public void sortPeople(List<Person> people) {
    Collections.sort(people, new PersonComparator());
}


2) 사용 후

// 사람 객체를 정렬하는 메서드 (람다식 사용)
public void sortPeople(List<Person> people) {
    Collections.sort(people, (p1, p2) -> p1.getAge() - p2.getAge());
}


람다식 사용 전후 비교 분석>

람다식 사용 전의 경우에는 Comparator 인터페이스를 구현하는 별도의 클래스를 정의하고, 그 클래스의 인스턴스를 생성하여 정렬에 사용해야 한다. 이로 인해 코드가 번잡하고 길어진다.

람다식을 사용하여 Comparator 인터페이스의 compare 메서드를 직접 구현하였다. 이로 인해 별도의 클래스 정의와 인스턴스 생성이 필요하지 않으며, 코드가 훨씬 간결해진다. 또한, 코드의 가독성을 향상시킬 수 있다. 함수형 프로그래밍 스타일의 코드로 인해 메서드의 구현 내용이 명확하게 드러나며, 불필요한 부분을 줄일 수 있다. 그리고, 람다식은 메서드를 인자로 전달하거나 변수에 할당할 수 있으므로, 코드의 유연성과 재사용성을 높일 수 있다.

전체적으로 람다식을 사용하면 코드를 간결하고 명확하게 표현할 수 있으며, 자바의 기능을 활용하여 함수형 프로그래밍 스타일을 적용할 수 있게 된다.

5. 익명 클래스와 람다식 비교


익명 클래스와 람다식은 모두 자바에서 간결한 코드를 작성하고 인터페이스(혹은 추상 클래스)의 메서드를 구현하기 위한 방법이다. 둘 다 함수형 프로그래밍 스타일을 지원하며, 주로 이벤트 핸들링이나 간단한 작업에 활용된다. 하지만 두 가지의 사용법과 특성에는 몇 가지 중요한 차이점이 있다.


1) 구문의 간결함
- 익명 클래스: 익명 클래스를 사용하면 클래스의 정의와 객체 생성이 분리되어 있어 코드가 더 길어진다. 중괄호와 메서드의 선언, 오버라이드할 메서드의 구현 등이 필요하다.
- 람다식: 람다식은 함수형 인터페이스를 구현하는 무명 함수의 형태로, 매우 간결한 형태의 코드를 작성할 수 있다. 람다식을 사용하면 메서드의 선언과 구현을 한 줄로 표현할 수 있어 코드가 더 간단해진다.

2) 타입 추론
- 익명 클래스: 익명 클래스를 사용할 때는 클래스 타입을 명시적으로 선언해야 한다.
- 람다식: 람다식은 컴파일러가 타입을 추론할 수 있으므로, 타입을 명시적으로 선언하지 않아도 된다. 이는 코드의 간결성을 높여준다.


3) 컨텍스트 참조
- 익명 클래스: 익명 클래스 내에서 this 키워드는 익명 클래스 자체를 가리킨다. 따라서 외부 클래스의 멤버 변수에 접근할 때는 '외부클래스.this.변수명'과 같이 명시적으로 참조해야 한다.
- 람다식: 람다식 내에서 this 키워드는 람다식을 감싸는 외부 클래스를 가리킨다. 이렇게 람다식에서 외부의 변수를 캡처할 수 있어 편리하다.

람다식은 주로 단일 메서드를 가지는 함수형 인터페이스를 구현하는 데 사용된다. 이벤트 핸들링이나 간단한 작업에서 람다식을 사용하면 코드가 더 간결하고 가독성이 좋아진다. 반면에 복잡한 클래스 구조를 가져야 하는 경우나 여러 메서드를 구현해야 할 때는 익명 클래스가 더 유용할 수 있다.

6. 함수형 인터페이스


1) Predicate<T>

Predicate는 하나의 인자를 받아서 참 또는 거짓 값을 반환하는 함수형 인터페이스이다. 주로 조건을 검사할 때 사용된다. 인터페이스에는 test(T t) 메서드가 선언되어 있으며, 이 메서드는 입력 값을 받아서 조건을 만족하면 true, 만족하지 않으면 false를 반환한다.

예시 코드> 정수를 받아서 해당 정수가 짝수인지 검사하는 Predicate

Predicate<Integer> isEven = num -> num % 2 == 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(5)); // false

2) Supplier<T>

Supplier는 인자를 받지 않고 결과만 반환하는 함수형 인터페이스이다. 주로 데이터를 생성하거나 초기화할 때 사용된다. 인터페이스에는 get() 메서드가 선언되어 있으며, 이 메서드는 결과 값을 계산하고 반환한다.


예시 코드> 랜덤한 정수를 생성하는 Supplier

Supplier<Integer> randomIntSupplier = () -> (int) (Math.random() * 100);
int randomValue = randomIntSupplier.get();
System.out.println(randomValue); // 랜덤한 정수 출력

3) Consumer<T>

Consumer는 하나의 인자를 받고 결과를 반환하지 않는 함수형 인터페이스이다. 주로 인자로 받은 데이터를 소비하고 처리하는 용도로 사용된다. 인터페이스에는 accept(T t) 메서드가 선언되어 있으며, 이 메서드는 입력 값을 받아서 처리한다.


예시 코드> 문자열을 받아서 그 문자열을 출력하는 Consumer

Consumer<String> printString = str -> System.out.println(str);
printString.accept("Hello, World!"); // "Hello, World!" 출력

4) Function<T, R>

Function은 하나의 인자를 받아서 다른 타입의 결과를 반환하는 함수형 인터페이스이다. 주로 데이터 변환 또는 매핑에 사용된다. 인터페이스에는 apply(T t) 메서드가 선언되어 있으며, 이 메서드는 입력 값을 받아서 변환하여 결과를 반환한다.


예시 코드> 문자열을 정수로 변환하는 Function

Function<String, Integer> stringToInt = str -> Integer.parseInt(str);
int number = stringToInt.apply("123");
System.out.println(number); // 123 출력
반응형

'자바(Java)' 카테고리의 다른 글

Java - Stream  (0) 2023.08.17
Java - 메소드 참조와 Optional 클래스  (0) 2023.08.11
Java Collection - 2  (0) 2023.07.19
Java Collection - 1  (0) 2023.07.19
Java Generic  (0) 2023.07.03