람다식이 코드 줄을 저장하는 것 외에 다른 용도로 사용됩니까?


120

람다식이 코드 줄을 저장하는 것 외에 다른 용도로 사용됩니까?

해결하기 쉽지 않은 문제를 해결 한 람다가 제공하는 특별한 기능이 있습니까? 내가 본 일반적인 사용법은 다음과 같이 작성하는 것입니다.

Comparator<Developer> byName = new Comparator<Developer>() {
  @Override
  public int compare(Developer o1, Developer o2) {
    return o1.getName().compareTo(o2.getName());
  }
};

람다 식을 사용하여 코드를 줄일 수 있습니다.

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());

27
더 짧을 수 있습니다. (o1, o2) -> o1.getName().compareTo(o2.getName())
Thorbjørn Ravn Andersen

88
또는 정지 짧은 :Comparator.comparing(Developer::getName)
토마스 Kläger

31
나는 그 혜택을 과소 평가하지 않을 것입니다. 일이 훨씬 쉬워지면 "올바른"방식으로 일을 할 가능성이 더 높아집니다. 장기적으로 유지하기 쉬운 코드를 작성하는 것과 단기적으로 적어두기 쉬운 코드를 작성하는 것 사이에는 종종 긴장이 있습니다. Lambda는 이러한 긴장을 줄여 주므로 더 깨끗한 코드를 작성할 수 있습니다.
yshavit

5
Lambda는 클로저이므로 이제 매개 변수화 된 생성자, 필드, 할당 코드 등을 대체 클래스에 추가 할 필요가 없기 때문에 공간 절약이 훨씬 더 커집니다.
CodesInChaos

답변:


116

Lambda 표현식은 일반적으로 Java로 해결할 수있는 문제 집합을 변경하지 않지만, 더 이상 어셈블리 언어로 프로그래밍하지 않는 것과 같은 이유로 특정 문제를 쉽게 해결할 수 있도록합니다. 프로그래머의 작업에서 중복 된 작업을 제거하면 삶이 더 쉬워지고, (수동으로) 생성해야하는 코드의 양만큼만 손대지 않았던 작업을 수행 할 수 있습니다.

그러나 람다 식은 단순히 코드 줄을 절약하는 것이 아닙니다. Lambda 표현식을 사용하면 이전에 익명 내부 클래스를 해결 방법으로 사용할 수 있었던 함수 를 정의 할 수 있습니다. 그렇기 때문에 이러한 경우 익명 내부 클래스를 대체 할 수 있지만 일반적으로는 그렇지 않습니다.

특히, 람다 식은 변환 될 기능 인터페이스에 독립적으로 정의되므로 액세스 할 수있는 상속 된 멤버가 없으며 기능 인터페이스를 구현하는 유형의 인스턴스에 액세스 할 수 없습니다. 람다 식 내 thissuper주변의 상황에서와 같은 의미를 가지고, 또한 볼 이 대답 . 또한 주변 컨텍스트의 지역 변수를 숨기는 새 지역 변수를 만들 수 없습니다. 함수를 정의하는 의도 된 작업의 경우 많은 오류 소스를 제거하지만 다른 사용 사례의 경우 기능 인터페이스를 구현하더라도 람다 식으로 변환 할 수없는 익명 내부 클래스가있을 수 있음을 의미합니다.

또한,이 구조 new Type() { … }new항상 그렇듯이 새로운 고유 한 인스턴스를 생성하도록 보장합니다 . 익명의 내부 클래스 인스턴스는 비 static컨텍스트 에서 생성 된 경우 항상 외부 인스턴스에 대한 참조를 유지합니다 . 대조적으로, 람다 식은 this필요할 때 (예 : 액세스 this또는 비 static멤버)에 대한 참조 만 캡처합니다 . 또한 의도적으로 지정되지 않은 ID의 인스턴스를 생성하여 구현시 기존 인스턴스를 재사용할지 여부를 런타임에 결정할 수 있습니다 (“ 람다식이 실행될 때마다 힙에 개체를 생성합니까? ”참조).

이러한 차이점은 귀하의 예에 적용됩니다. 익명의 내부 클래스 구성은 항상 새 인스턴스를 생성하고 외부 인스턴스에 대한 참조를 캡처 할 수있는 반면, (Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())일반적인 구현에서 싱글 톤으로 평가되는 비 캡처 람다 표현식입니다. 또한 .class하드 드라이브에 파일을 생성하지 않습니다 .

의미론과 성능의 차이를 감안할 때, 람다 식은 프로그래머가 미래에 특정 문제를 해결하는 방식을 바꿀 수 있습니다. 물론 새로운 언어 기능을 활용하는 함수형 프로그래밍의 아이디어를 수용하는 새로운 API 덕분입니다. Java 8 람다 표현식 및 일류 값을 참조하십시오 .


1
기본적으로 람다 식은 함수형 프로그래밍의 한 유형입니다. 함수형 프로그래밍 이나 명령형 프로그래밍으로 모든 작업을 수행 할 수 있다는 점을 감안할 때 가독성 및 유지 관리 성 외에는 다른 이점보다 더 큰 이점이 없습니다 .
Draco18s은 더 이상 SE 신뢰하지

1
@Trilarion : 기능 의 정의로 책 전체를 채울 수 있습니다 . 목표 또는 Java의 람다 표현식이 지원하는 측면에 초점을 맞출 것인지도 결정해야합니다 . 내 대답의 마지막 링크 ( this one )는 이미 Java의 맥락에서 이러한 측면 중 일부를 논의하고 있습니다. 그러나 내 대답을 이해하려면 함수가 클래스와 다른 개념이라는 것을 이해하는 것으로 이미 충분합니다. 자세한 내용은 이미 기존 리소스와 Q & A가 있습니다…
Holger

1
@Trilarion : 둘 다, 코드 크기 / 복잡성 또는 다른 솔루션에 필요한 해결 방법의 양을 허용 할 수없는 것으로 간주하고 (첫 번째 단락에서 언급했듯이) 이미 함수를 정의하는 방법이있는 경우를 제외하고는 더 많은 문제를 해결할 수 없습니다. , 말했듯이 내부 클래스는 함수를 정의하는 더 좋은 방법이없는 경우 해결 방법으로 사용할 수 있습니다 (그러나 내부 클래스는 훨씬 더 많은 용도를 제공 할 수 있으므로 함수가 아닙니다). OOP와 동일합니다. 어셈블리로는 할 수없는 작업을 할 수 없지만 여전히 어셈블리로 작성된 Eclipse와 같은 애플리케이션을 이미지화 할 수 있습니까?
Holger 2011

3
@EricDuminil : 나에게 Turing Completeness는 너무 이론적입니다. 예를 들어 Java가 Turing 완료 여부에 관계없이 Java로 Windows 장치 드라이버를 작성할 수 없습니다. (또는 Turing이 완성 된 PostScript에서) Java에이를 허용하는 기능을 추가하면 해결할 수있는 일련의 문제가 변경 될 수 있지만 그렇게되지는 않습니다. 그래서 저는 조립 지점을 선호합니다. 할 수있는 모든 것이 실행 가능한 CPU 명령어로 구현되기 때문에 모든 CPU 명령어를 지원하는 Assembly에서는 할 수없는 작업이 없습니다 (튜링 머신 형식보다 이해하기 쉽습니다).
Holger

2
@abelito는 모든 상위 수준의 프로그래밍 구조 에서 일반적으로 "실제 진행 상황에 대한 가시성" 을 낮추고 , 세부 사항을 줄이고 실제 프로그래밍 문제에 더 집중합니다. 코드를 더 좋게 또는 더 나쁘게 만드는 데 사용할 수 있습니다. 그것은 단지 도구 일뿐입니다. 투표 기능처럼; 게시물의 실제 품질에 대한 근거있는 판단을 제공하기 위해 의도 한 방식으로 사용할 수있는 도구입니다. 또는 람다를 싫어한다는 이유만으로 정교하고 합리적인 답변에 반대표를 던지는 데 사용합니다.
Holger

52

프로그래밍 언어는 기계가 실행할 수 없습니다.

프로그래머가 생각 하기위한 입니다.

언어는 우리의 생각을 기계가 실행할 수있는 것으로 바꾸는 컴파일러와의 대화입니다. 다른 언어에서 온 (또는 사람에서 자바에 대한 주요 불만 중 하나 남겨위해 이 있음으로 사용되는 다른 언어) 강제 프로그래머의 특정 정신적 모델을 (즉, 모든 클래스)입니다.

나는 그것이 좋은지 나쁜지에 대해서는 무게를 두지 않을 것입니다. 모든 것이 절충안입니다. 그러나 Java 8 람다는 프로그래머가 이전에는 Java에서 할 수 없었던 기능 측면 에서 생각할 수 있도록합니다 .

Java에 올 때 클래스 측면 에서 생각 하는 법을 배우는 절차 적 프로그래머와 똑같습니다 . 합리적인 OO 디자인 (mea culpa)과 더 유사합니다.

익명의 내부 클래스를 표현하는 더 짧은 방법으로 생각한다면 아마도 위의 절차 적 프로그래머가 클래스가 크게 향상되었다고 생각하지 않았던 것과 같은 방식으로 매우 인상적이지 않을 것입니다.


5
그래도 조심하세요. OOP가 전부라고 생각했는데 엔터티-컴포넌트 시스템 아키텍처와 끔찍하게 혼란 스러웠습니다. OOP 객체가
아닙니까

3
즉, * o f * 클래스를 다른 도구로 생각하는 것보다 OOP 에서 생각 하는 것이 내 생각을 나쁜 방식으로 제한했습니다.
user253751

2
@immibis : "OOP에서 생각"하는 방법에는 여러 가지가 있습니다. 따라서 "OOP에서 생각하기"와 "수업에서 생각하기"를 혼동하는 일반적인 실수 외에도 비생산적인 방식으로 생각하는 방법이 많이 있습니다. 불행히도 책과 튜토리얼조차도 열등한 것들을 가르치고, 예에서 반 패턴을 보여주고 그 생각을 퍼 뜨리기 때문에 처음부터 너무 자주 발생합니다. 무엇이든 "각종 클래스를 가질"필요가 있다고 가정하는 것은 하나의 예일뿐, 추상화의 개념과 모순되는 것입니다. 마찬가지로 "OOP는 가능한 한 많은 상용구를 작성하는 것을 의미합니다"라는 신화 등이있는 것 같습니다
Holger

@immibis는 그 경우에 도움이되지 않았지만 그 일화는 실제로 요점을 뒷받침합니다. 언어와 패러다임은 생각을 구조화하고 디자인으로 전환하는 프레임 워크를 제공합니다. 귀하의 경우에는 프레임 워크의 가장자리에 부딪 혔지만 다른 상황에서 그렇게하면 큰 진흙 덩어리로 빠져 나가 못생긴 디자인을 만드는 데 도움이 될 수 있습니다. 따라서 나는 "모든 것이 절충안"이라고 가정한다. 후자의 이점을 유지하면서 전자 문제를 피하는 것은 언어를 얼마나 "큰"으로 만들 것인지 결정할 때 중요한 질문입니다.
Leushenko 2017

@immibis 그래서 많은 패러다임을 배우는 것이 중요합니다 . 각 은 다른 사람들의 부적절함에 대해 당신에게 도달합니다.
Jared Smith

36

코드 줄을 저장하는 것은 다른 사람이 읽고 이해하는 데 더 적은 시간이 걸리는 더 짧고 명확한 방식으로 상당한 논리 청크를 작성할 수있는 경우 새로운 기능으로 볼 수 있습니다.

람다 식 (및 / 또는 메서드 참조)이 없으면 Stream파이프 라인의 가독성이 훨씬 떨어졌습니다.

예를 들어 Stream각 람다 식을 익명의 클래스 인스턴스로 바꾼 경우 다음 파이프 라인이 어떻게 생겼을 지 생각해보십시오 .

List<String> names =
    people.stream()
          .filter(p -> p.getAge() > 21)
          .map(p -> p.getName())
          .sorted((n1,n2) -> n1.compareToIgnoreCase(n2))
          .collect(Collectors.toList());

다음과 같습니다.

List<String> names =
    people.stream()
          .filter(new Predicate<Person>() {
              @Override
              public boolean test(Person p) {
                  return p.getAge() > 21;
              }
          })
          .map(new Function<Person,String>() {
              @Override
              public String apply(Person p) {
                  return p.getName();
              }
          })
          .sorted(new Comparator<String>() {
              @Override
              public int compare(String n1, String n2) {
                  return n1.compareToIgnoreCase(n2);
              }
          })
          .collect(Collectors.toList());

이것은 람다 식을 사용하는 버전보다 작성하기가 훨씬 더 어렵고 오류가 발생하기 쉽습니다. 이해하기도 더 어렵습니다.

그리고 이것은 비교적 짧은 파이프 라인입니다.

람다 식 및 메서드 참조없이이를 읽을 수 있도록하려면 여기에서 사용되는 다양한 기능 인터페이스 인스턴스를 보유하는 변수를 정의해야했으며, 이는 파이프 라인의 논리를 분할하여 이해하기 어렵게 만들었습니다.


17
이것이 핵심 통찰력이라고 생각합니다. 구문 및인지 오버 헤드가 너무 커서 유용하지 않아 다른 접근 방식이 더 우월 할 경우 프로그래밍 언어에서 무언가를 수행하는 것이 사실상 불가능하다고 주장 할 수 있습니다. 따라서 구문 및인지 오버 헤드 (예 : 익명 클래스에 비해 람다)를 줄임으로써 위와 같은 파이프 라인이 가능해집니다. 좀 더 혀를 내밀고, 코드를 작성하는 것이 직장에서 해고되면 불가능하다고 말할 수 있습니다.
Sebastian Redl

Nitpick : 어쨌든 자연스러운 순서로 비교하기 때문에 실제로 Comparatorfor 가 필요하지 않습니다 sorted. 예제를 더 좋게 만들기 위해 문자열의 길이를 비교하거나 유사한 것으로 변경할 수 있습니까?
tobias_k nov.

@tobias_k 문제 없습니다. 나는 compareToIgnoreCase그것을와 다르게 작동하도록 변경했습니다 sorted().
에 란

1
compareToIgnoreCase단순히 기존 String.CASE_INSENSITIVE_ORDER비교기 를 사용할 수 있으므로 여전히 나쁜 예 입니다. @tobias_k의 길이 (또는 내장 된 비교기가없는 다른 속성)로 비교하는 것이 더 적합합니다. SO Q & A 코드의 실시 예에 의해 판단 람다 ... 불필요한 비교기 구현 많이 발생
홀가

두 번째 예가 이해하기 더 쉽다고 생각하는 사람은 나뿐입니까?
Clark Battle

8

내부 반복

자바 컬렉션을 반복 할 때, 대부분의 개발자는 경향이 얻을 요소를 다음 처리 를. 즉, 해당 항목을 꺼내서 사용하거나 다시 삽입하는 등의 작업입니다. Java 8 이전 버전에서는 내부 클래스를 구현하고 다음과 같은 작업을 수행 할 수 있습니다.

numbers.forEach(new Consumer<Integer>() {
    public void accept(Integer value) {
        System.out.println(value);
    }
});

이제 Java 8을 사용하면 다음을 통해 더 나은 작업을 수행 할 수 있습니다.

numbers.forEach((Integer value) -> System.out.println(value));

이상

numbers.forEach(System.out::println);

인수로서의 행동

다음 경우를 추측하십시오.

public int sumAllEven(List<Integer> numbers) {
    int total = 0;

    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    } 
    return total;
}

Java 8 Predicate 인터페이스를 사용 하면 다음과 같이 더 잘할 수 있습니다.

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;

    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

그것을 다음과 같이 부르십시오.

sumAll(numbers, n -> n % 2 == 0);

출처 : DZone-Java에서 Lambda 표현식이 필요한 이유


아마도 첫 번째 경우는 몇 줄의 코드를 저장하는 방법입니다,하지만 쉽게 코드를 읽을 수 있습니다
alseether

4
내부 반복은 어떻게 람다 기능입니까? 사실 간단한 문제는 기술적으로 람다는 Java-8 이전에는 할 수 없었던 일을 할 수 없다는 것입니다. 코드를 전달 하는 더 간결한 방법 일뿐 입니다.
Ousmane D.

@ Aominè이 경우 numbers.forEach((Integer value) -> System.out.println(value));람다 식은 매개 변수를 나열하는 화살표 기호 (->) 의 왼쪽 에있는 부분 과 본문을 포함 하는 오른쪽 에있는 부분의 두 부분으로 구성 됩니다. 컴파일러는 람다 식에 Consumer 인터페이스의 구현되지 않은 유일한 메서드와 동일한 서명이 있음을 자동으로 파악합니다.
alseether

5
람다식이 무엇인지 묻지 않았습니다. 단지 내부 반복이 람다 식과 관련이 없다고 말하는 것입니다.
Ousmane D.

1
@ Aominè 나는 당신의 요점을 알았습니다, 그것은 람다 그 자체로 아무것도 가지고 있지 않지만 당신이 지적했듯이 그것은 더 깨끗한 글쓰기 방법과 같습니다. 내가 효율성의 관점에서 생각 사전-8 버전 좋은 같습니다
alseether

4

다음과 같이 내부 클래스 대신 람다를 사용하면 많은 이점이 있습니다.

  • 더 많은 언어 구문 의미를 도입하지 않고도 코드를 더 간결하고 표현력있게 만듭니다. 당신은 이미 질문에 예를 들었습니다.

  • 람다를 사용하면 컬렉션에 대한 맵 축소 변환과 같은 요소 스트림에 대한 함수 스타일 작업으로 프로그래밍 할 수 있습니다. java.util.functionjava.util.stream 패키지 문서를 참조하십시오 .

  • 컴파일러가 람다에 대해 생성 한 물리적 클래스 파일이 없습니다. 따라서 제공되는 애플리케이션을 더 작게 만듭니다. 메모리가 람다에 어떻게 할당됩니까?

  • 컴파일러는 람다가 범위 밖의 변수에 액세스하지 않는 경우 람다 생성을 최적화합니다. 즉, 람다 인스턴스는 JVM에 의해 한 번만 생성됩니다. 자세한 내용 은 Java 8에서 메서드 참조 캐싱이 좋은 아이디어입니까? 질문에 대한 @Holger의 답변을 참조하십시오 . .

  • Lambda는 기능 인터페이스 외에 다중 마커 인터페이스를 구현할 수 있지만 익명 내부 클래스는 더 많은 인터페이스를 구현할 수 없습니다. 예를 들면 다음과 같습니다.

    //                 v--- create the lambda locally.
    Consumer<Integer> action = (Consumer<Integer> & Serializable) it -> {/*TODO*/};

2

Lambda는 익명 클래스의 구문 설탕 일뿐입니다.

람다 이전에는 익명 클래스를 사용하여 동일한 작업을 수행 할 수 있습니다. 모든 람다 식은 익명 클래스로 변환 할 수 있습니다.

IntelliJ IDEA를 사용하는 경우 변환을 수행 할 수 있습니다.

  • 람다에 커서를 놓습니다.
  • Alt / 옵션 + Enter 누르기

여기에 이미지 설명 입력


3
... @StuartMarks가 람다의 통 사적 당도 (sp? gr?)를 이미 명확히했다고 생각 했습니까? ( stackoverflow.com/a/15241404/3475600 , softwareengineering.stackexchange.com/a/181743 )
srborlongan

2

귀하의 질문에 답하기 위해 사실 람다 Java-8 이전에는 할 수 없었던 작업을 허용하지 않고보다 간결한 코드 를 작성할 수 있습니다 . 이것의 장점은 코드가 더 명확하고 유연하다는 것입니다.


@Holger는 람다 효율성 측면에서 의심을 없앴습니다. 그것은뿐만 아니라 메이크 코드 청소기에 대한 방법,하지만 람다 값을 캡처하지 않는 경우, 그것은 (재사용) 싱글 톤 클래스 될 것입니다
alseether

자세히 살펴보면 모든 언어 기능에는 차이가 있습니다. 당면한 질문은 높은 수준이므로 높은 수준에서 내 대답을 썼습니다.
Ousmane D.

2

내가 아직 언급하지 않은 한 가지는 람다를 사용하면 사용되는 기능을 정의 할 수 있다는 것 입니다.

따라서 간단한 선택 기능이있는 경우 여러 상용구를 사용하여 별도의 위치에 배치 할 필요가없는 경우 간결하고 로컬에 관련된 람다를 작성하면됩니다.


1

네, 많은 장점이 있습니다.

  • 전체 클래스를 정의 할 필요가 없습니다. 함수 구현을 참조로 전달할 수 있습니다.
    • 내부적으로 클래스를 생성하면 .class 파일이 생성되지만 람다를 사용하면 람다에서 클래스 대신 함수 구현을 전달하기 때문에 컴파일러에서 클래스 생성을 피할 수 있습니다.
  • 코드 재사용 가능성이 이전보다 높습니다.
  • 그리고 당신이 말했듯이 코드는 정상적인 구현보다 짧습니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.