Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

개발자

[JAVA] 함수형 프로그래밍/익명 클래스/람다/함수형 인터페이스 본문

개발자/JAVA

[JAVA] 함수형 프로그래밍/익명 클래스/람다/함수형 인터페이스

GoGo개발 2023. 3. 21. 17:21
함수형 프로그래밍이란

 

함수형 프로그래밍은 프로그래밍의 패러다임이다. 마치 절차지향 프로그래밍, 객체지향 프로그래밍처럼.

 

함수형 프로그래밍은 선언적 프로그래밍이다. 이와 대조적으로 람다를 지원하기 전의 자바는 완전한 명령형 프로그래밍이었다.

  • 명령형 프로그래밍 : 클래스에서 메서드를 정의하고, 필요할 때 그 메서드를 호출하는 명령하여 동작.
  • 선언적 프로그래밍 : 데이터가 입력으로 주어지고, 데이터가 처리되는 과정(흐름)을 정의하는 것으로 동작.

 

함수형 프로그래밍의 조건

 

1. 순수 함수

같은 입력 시 같은 출력을 보장한다. 부수 효과(Side Effect)가 없다.

멀티쓰레드에서도 안전하다.

(Side effect는 반환 값 이외에, 호출 된 함수 밖에서 관찰할 수 있는 어플리케이션의 상태 변경이다.)

 

2. 고차 함수

일급 함수의 특징을 만족해야 한다.

  • 함수의 인자로 함수를 전달할 수 있다.
  • 함수의 리턴값으로 함수를 사용할 수 있다

3. 익명 함수

이름이 없는 함수이다. 자바에서 람다식을 말한다.

 

4. 합성 함수

새로운 함수를 생성하거나 어떤 계산을 수행하기 위해 둘 이상의 함수를 결합하는 것이다.

자바에서는 메서드 체이닝을 통해 구현된다.

 

함수형 프로그래밍의 특징

1. 불변성

상태를 변경하지 않는 것.

상태를 변경하게 되면, 부수 효과가 생기게 되어 순수함수의 조건을 만족하지 못한다.

순수함수를 사용하는 함수형 프로그래밍은 불변성을 가진다.

 

2. 참조 투명성

프로그램의 변경 없이도 어떤 표현식을 값으로 대체할 수 있다.

 

3. 일급 함수

일급 함수는 다음과 같다.

  • 함수를 함수의 매개변수로 넘길 수 있다.
  • 함수를 함수의 반환값으로 돌려줄 수 있다.
  • 함수를 변수나 자료구조에 담을 수 있다.

일급 함수를 포함하는 일급 시민 (First-class Citizen)

  • 대상을 함수의 매개변수로 넘길 수 있다.
  • 대상을 함수의 반환값으로 돌려줄 수 있다.
  • 대상을 변수나 자료구조에 담을 수 있다.

 

익명 클래스

 

익명클래스란 이름이 없는 클래스로, 객체 사용시에 클래스의 선언과 객체 생성이 동시에 이루어집니다. 
일회성으로 딱 하나의 객체만 필요할 경우 사용됩니다. 

즉,익명 클래스는 인터페이스를 구현한 클래스의 인스턴스를 생성하는 방식으로 사용됩니다. 익명 클래스는 인터페이스의 메소드를 오버라이드하고 그 메소드를 호출할 때 사용됩니다. 이를 통해 콜백이나 이벤트 핸들링 등의 작업을 처리할 수 있습니다.

 

new 인터페이스이름() {
    // 인터페이스의 메소드 구현
};

 

람다식

 

람다식은 메서드를 하나의 식으로 표현하는 선언적 프로그래밍의 방법이다.
원래의 자바는 익명 클래스를 이용하여 익명 구현 객체를 사용할 수 있었다.
 
이를 함수형 프로그래밍을 도입하면서 간단하게 표현할 수 있는 방법이 람다식이다.

즉, 람다는 익명 함수를 생성하는 방식으로 사용됩니다. 람다는 함수형 인터페이스를 구현하고 그 인터페이스의 메소드를 호출할 때 사용됩니다. 람다는 익명 클래스와 비교해서 문법이 간결하며, 람다식의 실행 결과를 반환하는 경우에는 return 키워드를 생략할 수 있습니다

람다는 컴파일러가 함수형 인터페이스를 구현한 클래스의 인스턴스를 생성하지 않고, 람다식을 바로 사용합니다.

 

(매개변수) -> {실행코드};

 

특징

  • 익명
    • 보통의 메서드와 달리 이름이 없다.
  • 함수
    • 보통의 메서드와 달리 메서드가 아닌 함수이다.
    • 메서드는 클래스에 종속적인 것을 메서드라 하지만, 함수는 어느곳에 종속적이지 않다.
  • 일급 시민
    • 매개변수의 인자가 될 수 있고, 반환값이 될 수 있고, 자료구조에 담길 수 있다.

 

익명 클래스와 람다식의 차이

 

구현방식 : 익명 클래스는 클래스를 정의하고 인스턴스를 생성하는 과정이 필요하다. 람다는 익명 클래스를 간략화한 표현식이기 때문에 클래스를 정의하고 인스턴스를 생성하는 과정이 필요하지 않다.

인터페이스의 제한 : 익명 클래스는 여러 개의 메소드를 구현할 수 있다. 람다는 함수형 인터페이스의 추상 메소드 하나만 구현할 수 있다.

 

예제 1

Runnable 인터페이스를 구현하는 익명 클래스와 람다를 비교해보겠다.

 

 

익명 클래스를 사용한 경우:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("익명 클래스를 사용한 경우");
    }
};
new Thread(runnable).start();

익명 클래스를 사용한 경우에는 Runnable 인터페이스를 구현한 익명 클래스를 생성하고, 그 안에서 run() 메소드를 구현합니다. 이는 기존에 자바에서 함수형 프로그래밍을 지원하지 않았을 때 사용되던 방식입니다.

 

람다를 사용한 경우:

Runnable runnable = () -> System.out.println("람다를 사용한 경우");
new Thread(runnable).start();

 

예제2

 

익명 클래스 사용

interface Calculator {
    int calculate(int x, int y);
}

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator() {
            @Override
            public int calculate(int x, int y) {
                return x + y;
            }
        };
        int result = calculator.calculate(3, 4);
        System.out.println(result);
    }
}

 

람다 사용

 

interface Calculator {
    int calculate(int x, int y);
}

public class Main {
    public static void main(String[] args) {
        Calculator calculator = (x, y) -> x + y;
        int result = calculator.calculate(3, 4);
        System.out.println(result);
    }
}

 

 

 

 

위의 코드에서는 Runnable 인터페이스를 구현하는 객체를 생성한 다음, Thread 클래스의 생성자로 전달하여 새로운 스레드를 생성하고 실행합니다.

 

람다를 사용한 경우에는 람다 표현식으로 Runnable 객체를 생성합니다. 람다 표현식은 람다 파라미터와 -> 연산자, 그리고 람다 바디로 이루어져 있습니다. 여기서는 Runnable 객체의 run() 메소드의 내용을 간단하게 표현하기 위해 람다 바디에 출력문을 작성했습니다.

 

익명 클래스를 사용하면 구현해야 할 메소드가 많을 때, 코드가 길어지고 가독성이 떨어질 수 있습니다. 람다를 사용하면 코드가 간결해지고 가독성이 높아질 수 있습니다. 그러나 람다는 인터페이스의 메소드가 하나만 있을 때 사용할 수 있기 때문에, 구현해야 할 메소드가 여러 개인 경우에는 익명 클래스를 사용해야 합니다.

 

예제3

 

익명 클래스 사용 : 

public static void main(String[] args) {
        Comparator<Integer> comp = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1.compareTo(o2); // o1 은 Integer 이고, Integer 은 Comparable 을 구현하고,
            }				 // Comparable 에 compareTo 메서드가 있다.
        };

        System.out.println(comp.compare(2, 1));
    }

 

람다 사용 :

public static void main(String[] args) {
    Comparator<Integer> comp = (o1, o2) -> (o1.compareTo(o2));

    System.out.println(comp.compare(2, 1));
}

 

예제 4

 

import dto.SampleDto;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Basic {
    private void test() {
        List<SampleDto> sampleDtoList = new ArrayList<>();
        
        // 익명클래스 사용
        sampleDtoList.sort(new Comparator<SampleDto>() {
            @Override
            public int compare(SampleDto sampleDto1, SampleDto sampleDto2) {
                return sampleDto1.getName().compareTo(sampleDto2.getName());
            }
        });

        // 람다식 사용
        sampleDtoList.sort(
                (SampleDto sampleDto1, SampleDto sampleDto2) -> sampleDto1.getName().compareTo(sampleDto2.getName())
        );
    }
}

 

List.java 의 sort() 를 호출하였다. sort() 메서드는 다음과 같다.

 

.
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

 

인자로 Comparator 인터페이스를 받는다. 이 뜻은 Comparator 인터페이스를 구현한 객체를 받는다는 의미다. 익명클래스는 Comparator 인터페이스를 구현한 클래스를 가지지않고도 로직 안에서 객체를 생성하여 파라미터로 넘길 수 있다

 

 

람다표현식은 아래의 장점을 가진다.

 

1) 이름 없는 함수 선언 가능

2) 소스코드의 분량을 줄일 수 있고, 반복 코드 관리에 유용하다.

3) 코드를 파라미터로 전달이 가능하여, 동작을 정의하여 메서드에 전달할때 편리하게 사용이 가능하다.

 

함수형 인터페이스

 

위 예제 코드를 따라오면서 한가지 의문점이 들 수 있다. 람다표현식은 메서드 명도 없고, 단순히 구현 로직을 명시한다. 그렇다면 만약 여러개의 추상메서드가 있는 인터페이스의 경우에는 어떻게 처리될까?
 
정답을 말하자면, 위 상황에서 람다표현식의 사용은 불가능하다. 람다표현식은 함수형 인터페이스의 경우에만 가능하다. 위에서 예제로 설명했던 Comparator 인터페이스를 보자.

 

-Comparator.java

 

package java.util;

import java.io.Serializable;
import java.util.function.Function;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.function.ToDoubleFunction;
import java.util.Comparators;

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);

    // Object의 public 함수이므로 추상 메서드로 속하지 않는다.
    // boolean equals(Object obj);

    ...
}

 

우선 다른 default 메서드는 제외하고 추상 메서드만 가져왔다.  compare 메서드 단 1개만을 갖고있다. 즉, 함수형 인터페이스란 하나의 추상메서드만을 갖고있는 인터페이스로, 이러한 함수형 인터페이스의 경우에만 람다표현식 사용이 가능하다.

 

@FunctionalInterface

 

함수형 인터페이스라는 의미를 어노테이션으로 명시 가능하다. 생략이 가능하지만, 명시적으로 사용해주는 것을 권장한다. 이유는 해당 어노테이션의 선언으로 혹시라도 인터페이스에 추상 메서드가 2개 이상으로 선언되면 컴파일 에러가 발생하여 오류를 방지할 수 있기 때문이다.

 

Object 의 public 메서드

위 Comparator 인터페이스에서 아래 코드를 짚고 넘어가보자.

 

- 함수형 인터페이스인 경우

int compare(T o1, T o2);

// Object의 public 함수이므로 추상 메서드로 속하지 않는다.
boolean equals(Object obj);

 

Comparator 인터페이스에 추상 메서드는 compare, equals 총 2개였는데 함수형 인터페이스가 가능했다. 이유는 equals 메서드는 Object 객체의 public 메서드이기 때문에 포함되지 않았기 때문이다. 

 

 함수형 인터페이스가 아닌 경우 

int compare(T o1, T o2);

// Object의 public 메서드가 아니므로 추상메서드로 포함된다.
Object clone();

만약 위의 clone 메서드가 선언되어 있었다면 그것은 함수형 인터페이스가 될 수 없었을 것이다. Object 클래스의 public 메서드일 경우에만 추상메서드 개수로 포함되지 않는 것이다. 

 

람다를 사용하지 않았을 경우 사용한 경우 비교

 

자바에서 람다를 사용하지 않았을 경우, 일반적으로 불필요한 클래스나 인터페이스를 선언하고, 별도의 메서드를 작성해야 합니다.

 

 

public class Example {
    public static void main(String[] args) {
        String[] names = {"Alice", "Bob", "Charlie", "Dave"};

        List<String> uppercaseNames = new ArrayList<>();
        for (String name : names) {
            uppercaseNames.add(name.toUpperCase());
        }

        System.out.println(uppercaseNames);
    }
}

 

위의 예제에서는 배열 "names"에 저장된 문자열을 대문자로 변환하여 "uppercaseNames" 리스트에 저장합니다. 이를 위해 "for" 루프를 사용하고, "toUpperCase()" 메서드를 호출하여 문자열을 대문자로 변환합니다.

반면에, 람다를 사용하면 코드를 간결하게 작성할 수 있습니다. 다음은 람다를 사용하여 같은 기능을 수행하는 예제입니다.

 

 

public class Example {
    public static void main(String[] args) {
        String[] names = {"Alice", "Bob", "Charlie", "Dave"};

        List<String> uppercaseNames = Arrays.stream(names)
                                           .map(String::toUpperCase)
                                           .collect(Collectors.toList());

        System.out.println(uppercaseNames);
    }
}

 

위의 예제에서는 "Arrays.stream()" 메서드를 사용하여 배열을 스트림으로 변환한 후, "map()" 메서드를 사용하여 문자열을 대문자로 변환합니다. 마지막으로 "collect()" 메서드를 사용하여 대문자로 변환된 문자열을 리스트에 저장합니다.

 

이러한 방식으로 람다를 사용하면 코드가 간결하고 가독성이 좋아집니다. 또한, 자바 8부터 추가된 스트림 API와 함께 사용하면, 함수형 프로그래밍을 더욱 쉽게 구현할 수 있습니다.

 

 

 

참고 블로그

https://devfunny.tistory.com/691

 

익명클래스 vs 람다식 비교

람다표현식 메서드를 하나의 식(expression)으로 표현한 것이다. 메서드를 람다식으로 표현하면 메서드 이름과 반환값이 존재하지 않기 때문에 이를 '익명함수' 라고도 한다. 람다표현식의 뜻을 글

devfunny.tistory.com

https://alkhwa-113.tistory.com/entry/%EB%9E%8C%EB%8B%A4%EC%8B%9Dfeat-%EC%9D%B5%EB%AA%85-%EA%B5%AC%ED%98%84-%ED%81%B4%EB%9E%98%EC%8A%A4-vs-%EB%9E%8C%EB%8B%A4%EC%8B%9D

 

람다식(feat. 익명 구현 클래스 vs 람다식)

선장님과 함께하는 마지막 자바 스터디입니다. (ㅜ) 자바 스터디 Github github.com/whiteship/live-study whiteship/live-study 온라인 스터디. Contribute to whiteship/live-study development by creating an account on GitHub. github.c

alkhwa-113.tistory.com