> Java > java지도 시간 > 본문

람다 표현식이란 무엇입니까? 무슨 소용이 있나요?

零下一度
풀어 주다: 2017-07-20 14:16:58
원래의
3291명이 탐색했습니다.

이 섹션에서는 먼저 람다 표현에 대해 논의합니다. 람다 표현이란 무엇입니까? 무슨 소용이 있나요?

Lambda 표현식은 Java 8에 도입된 새로운 구문입니다. 코드를 전송하는 간단한 방법입니다. 이름은 학계의 람다 계산에서 따왔습니다.

람다 표현식을 이해하려면 먼저 인터페이스, 익명 내부 클래스 및 코드 전달을 검토해 보겠습니다.

인터페이스를 통해 코드 전송

섹션 19에서 인터페이스와 인터페이스 지향 프로그래밍을 소개했습니다. 특정 유형이 아닌 인터페이스용 프로그래밍을 사용하면 프로그램의 결합을 줄이고 유연성을 높이며 재사용성을 높일 수 있습니다. 인터페이스는 코드를 전송하는 데 자주 사용됩니다. 예를 들어 섹션 59에서는 File의 다음 메서드를 소개했습니다.

public String[] list(FilenameFilter filter)public File[] listFiles(FilenameFilter filter)
로그인 후 복사

list 및 listFiles에는 실제로 FilenameFilter 개체가 필요하지 않지만 여기에 포함된 다음 메서드는 :

boolean accept(File dir, String name);
로그인 후 복사

즉, list와 listFiles는 메소드 코드를 매개변수로 받기를 희망하지만, 메소드 코드 자체를 직접 전달할 수 있는 방법은 없고, 인터페이스만 전달할 수 있습니다.

또 다른 예를 들어, 섹션 53에서 컬렉션의 일부 알고리즘을 소개했습니다. 메소드:

public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c)public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)public static <T> void sort(List<T> list, Comparator<? super T> c)
로그인 후 복사
그러나 메소드를 직접 전달할 수 있는 방법은 없고 인터페이스만 전달할 수 있습니다.

섹션 77에서 비동기 작업 실행 서비스인 ExecutorService를 소개했습니다. 작업을 제출하는 방법은 다음과 같습니다.
int compare(T o1, T o2);
로그인 후 복사

Callable 및 Runnable 인터페이스는 작업 코드를 전송하는 데에도 사용됩니다.

인터페이스를 통해 동작 코드를 전달하려면 인터페이스를 구현하는 인스턴스 객체를 전달해야 합니다. 이전 장에서 가장 간결한 방법은 다음과 같은 익명 내부 클래스를 사용하는 것입니다.
<T> Future<T> submit(Callable<T> task);<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
로그인 후 복사

파일 이름별로 파일 정렬 , 코드를 사용하여 가장 간단한 작업 제출:

//列出当前目录下的所有后缀为.txt的文件File f = new File(".");
File[] files = f.listFiles(new FilenameFilter(){
    @Overridepublic boolean accept(File dir, String name) {if(name.endsWith(".txt")){return true;
        }return false;
    }
});
로그인 후 복사
코드를 사용하여 가장 간단한 작업 제출:

Arrays.sort(files, new Comparator<File>() {

    @Overridepublic int compare(File f1, File f2) {return f1.getName().compareTo(f2.getName());
    }
});
로그인 후 복사

Lambda 표현식

Syntax

Java 8은 새로운 컴팩트 패스를 제공합니다. 구문 코드 - 람다 표현식. 위에 나열된 파일의 예에서는 코드를 다음과 같이 변경할 수 있습니다.

ExecutorService executor = Executors.newFixedThreadPool(100);
executor.submit(new Runnable() {
    @Overridepublic void run() {
        System.out.println("hello world");
    }
});
로그인 후 복사

익명 내부 클래스에 비해 코드 전달이 더 직관적이라는 것을 알 수 있습니다. 인터페이스를 구현하는 데 더 이상 템플릿 코드가 없습니다. 더 이상 선언된 메소드가 없습니다. 이름은 없지만 메소드의 구현 코드가 직접 제공됩니다. 람다 표현식은 ->에 의해 두 부분으로 나누어집니다. 첫 번째 부분은 메서드의 매개변수이고 다음의 {}는 메서드의 코드입니다.

위 코드는 다음과 같이 단순화할 수 있습니다.
File f = new File(".");
File[] files = f.listFiles((File dir, String name) -> {if (name.endsWith(".txt")) {return true;
    }return false;
});
로그인 후 복사

본문 코드에 명령문이 하나만 있는 경우 대괄호와 반환 문도 생략할 수 있으며 위 코드는 다음과 같이 될 수 있습니다.

File[] files = f.listFiles((File dir, String name) -> {return name.endsWith(".txt");
});
로그인 후 복사
괄호가 없으면 본문 코드입니다. 이 표현식의 값은 함수의 반환 값이며 끝에 세미콜론이나 return 문이 있을 수 없습니다.

메서드의 매개변수 유형 선언도 생략할 수 있습니다. 위 코드는 다음과 같이 더욱 단순화할 수 있습니다.
File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));
로그인 후 복사

메서드의 매개변수 유형을 생략할 수 있는 이유는 Java가 이를 자동으로 추론할 수 있기 때문입니다. listFiles에서 허용하는 매개변수 유형은 FilenameFilter이고 이 인터페이스에는 accept 메소드가 하나만 있으며 이 메소드의 두 매개변수 유형은 File 및 String입니다.

이러한 단순화를 통해 코드가 훨씬 더 간단해지고 명확해졌나요?
정렬 코드는 람다 표현식을 사용하여 작성할 수 있습니다.

File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));
로그인 후 복사

작업 제출 코드는 람다 표현식을 사용하여 작성할 수 있습니다.

Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
로그인 후 복사
로그인 후 복사
매개변수 비어있는 부분은 ()로 표기합니다.

매개변수가 하나만 있는 경우 매개변수 부분의 괄호는 생략할 수 있습니다. 예를 들어 File에는 다음과 같은 메서드도 있습니다.
executor.submit(()->System.out.println("hello"));
로그인 후 복사

FileFilter는 다음과 같이 정의됩니다.

public File[] listFiles(FileFilter filter)
로그인 후 복사
FileFilter를 사용하여 위 내용을 다시 작성합니다. 파일 열거의 예, 코드는 다음과 같습니다.

public interface FileFilter {boolean accept(File pathname);
}
로그인 후 복사

변수 참조

익명 내부 클래스와 유사하게 Lambda 표현식은 기본 코드 외부에 정의된 변수에도 액세스할 수 있지만 로컬 변수의 경우 최종 유형에만 액세스할 수 있습니다. 변수, 익명 내부 클래스와의 차이점은 변수를 final로 선언할 필요가 없지만 실제로 변수를 재할당할 수 없다는 것입니다. 예를 들어

File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));
로그인 후 복사

는 지역 변수 msg에 액세스할 수 있지만 msg를 재할당할 수는 없습니다.

String msg = "hello world";
executor.submit(()->System.out.println(msg));
로그인 후 복사

Java编译器会提示错误。

这个原因与匿名内部类是一样的,Java会将msg的值作为参数传递给Lambda表达式,为Lambda表达式建立一个副本,它的代码访问的是这个副本,而不是外部声明的msg变量。如果允许msg被修改,则程序员可能会误以为Lambda表达式会读到修改后的值,引起更多的混淆。

为什么非要建副本,直接访问外部的msg变量不行吗?不行,因为msg定义在栈中,当Lambda表达式被执行的时候,msg可能早已被释放了。如果希望能够修改值,可以将变量定义为实例变量,或者,将变量定义为数组,比如:

String[] msg = new String[]{"hello world"};
msg[0] = "good morning";
executor.submit(()->System.out.println(msg[0]));
로그인 후 복사

与匿名内部类比较

从以上内容可以看出,Lambda表达式与匿名内部类很像,主要就是简化了语法,那它是不是语法糖,内部实现其实就是内部类呢?答案是否定的,Java会为每个匿名内部类生成一个类,但Lambda表达式不会,Lambda表达式通常比较短,为每个表达式生成一个类会生成大量的类,性能会受到影响。

Java利用了Java 7引入的为支持动态类型语言引入的invokedynamic指令、方法句柄(method handle)等,具体实现比较复杂,我们就不探讨了,感兴趣可以参看~briangoetz/lambda/lambda-translation.html,我们需要知道的是,Java的实现是非常高效的,不用担心生成太多类的问题。

Lambda表达式不是匿名内部类,那它的类型到底是什么呢?是函数式接口。

函数式接口

Java 8引入了函数式接口的概念,函数式接口也是接口,但只能有一个抽象方法,前面提及的接口都只有一个抽象方法,都是函数式接口。之所以强调是"抽象"方法,是因为Java 8中还允许定义其他方法,我们待会会谈到。Lambda表达式可以赋值给函数式接口,比如:

FileFilter filter = path -> path.getName().endsWith(".txt");
FilenameFilter fileNameFilter = (dir, name) -> name.endsWith(".txt");
Comparator<File> comparator = (f1, f2) -> f1.getName().compareTo(f2.getName());
Runnable task = () -> System.out.println("hello world");
로그인 후 복사

如果看这些接口的定义,会发现它们都有一个注解@FunctionalInterface,比如:

@FunctionalInterfacepublic interface Runnable {public abstract void run();
}
로그인 후 복사

@FunctionalInterface用于清晰地告知使用者,这是一个函数式接口,不过,这个注解不是必需的,不加,只要只有一个抽象方法,也是函数式接口。但如果加了,而又定义了超过一个抽象方法,Java编译器会报错,这类似于我们在85节介绍的Override注解。

预定义的函数式接口

接口列表

Java 8定义了大量的预定义函数式接口,用于常见类型的代码传递,这些函数定义在包java.util.function下,主要的有:

对于基本类型boolean, int, long和double,为避免装箱/拆箱,Java 8提供了一些专门的函数,比如,int相关的主要函数有:

这些函数有什么用呢?它们被大量使用于Java 8的函数式数据处理Stream相关的类中,关于Stream,我们下节介绍。

即使不使用Stream,也可以在自己的代码中直接使用这些预定义的函数,我们看一些简单的示例。

Predicate示例

为便于举例,我们先定义一个简单的学生类Student,有name和score两个属性,如下所示,我们省略了getter/setter方法。

static class Student {
    String name;double score;    public Student(String name, double score) {this.name = name;this.score = score;
    }
}
로그인 후 복사

有一个学生列表:

List<Student> students = Arrays.asList(new Student[] {new Student("zhangsan", 89d),new Student("lisi", 89d),new Student("wangwu", 98d) });
로그인 후 복사

在日常开发中,列表处理的一个常见需求是过滤,列表的类型经常不一样,过滤的条件也经常变化,但主体逻辑都是类似的,可以借助Predicate写一个通用的方法,如下所示:

public static <E> List<E> filter(List<E> list, Predicate<E> pred) {
    List<E> retList = new ArrayList<>();for (E e : list) {if (pred.test(e)) {
            retList.add(e);
        }
    }return retList;
}
로그인 후 복사

这个方法可以这么用:

// 过滤90分以上的students = filter(students, t -> t.getScore() > 90);
로그인 후 복사

Function示例

列表处理的另一个常见需求是转换,比如,给定一个学生列表,需要返回名称列表,或者将名称转换为大写返回,可以借助Function写一个通用的方法,如下所示:

public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
    List<R> retList = new ArrayList<>(list.size());for (T e : list) {
        retList.add(mapper.apply(e));
    }return retList;
}
로그인 후 복사

根据学生列表返回名称列表的代码可以为:

List<String> names = map(students, t -> t.getName());
로그인 후 복사
로그인 후 복사

将学生名称转换为大写的代码可以为:

students = map(students, t -> new Student(t.getName().toUpperCase(), t.getScore()));
로그인 후 복사

Consumer示例

在上面转换学生名称为大写的例子中,我们为每个学生创建了一个新的对象,另一种常见的情况是直接修改原对象,具体怎么修改通过代码传递,这时,可以用Consumer写一个通用的方法,比如:

public static <E> void foreach(List<E> list, Consumer<E> consumer) {for (E e : list) {
        consumer.accept(e);
    }
}
로그인 후 복사

上面转换为大写的例子可以改为:

foreach(students, t -> t.setName(t.getName().toUpperCase()));
로그인 후 복사

以上这些示例主要用于演示函数式接口的基本概念,实际中应该使用下节介绍的流API。

方法引用

基本用法
Lambda表达式经常就是调用对象的某个方法,比如:

List<String> names = map(students, t -> t.getName());
로그인 후 복사
로그인 후 복사

这时,它可以进一步简化,如下所示:

List<String> names = map(students, Student::getName);
로그인 후 복사

Student::getName这种写法,是Java 8引入的一种新语法,称之为方法引用,它是Lambda表达式的一种简写方法,由::分隔为两部分,前面是类名或变量名,后面是方法名。方法可以是实例方法,也可以是静态方法,但含义不同。

我们看一些例子,还是以Student为例,先增加一个静态方法:

public static String getCollegeName(){return "Laoma School";
}
로그인 후 복사

静态方法

对于静态方法,如下语句:

Supplier<String> s = Student::getCollegeName;
로그인 후 복사

等价于:

Supplier<String> s = () -> Student.getCollegeName();
로그인 후 복사

它们的参数都是空,返回类型为String。

实例方法

而对于实例方法,它第一个参数就是该类型的实例,比如,如下语句:

Function<Student, String> f = Student::getName;
로그인 후 복사

等价于:

Function<Student, String> f = (Student t) -> t.getName();
로그인 후 복사

对于Student::setName,它是一个BiConsumer,即:

BiConsumer<Student, String> c = Student::setName;
로그인 후 복사

等价于:

BiConsumer<Student, String> c = (t, name) -> t.setName(name);
로그인 후 복사

通过变量引用方法

如果方法引用的第一部分是变量名,则相当于调用那个对象的方法,比如:

Student t = new Student("张三", 89d);
Supplier<String> s = t::getName;
로그인 후 복사

等价于:

Supplier<String> s = () -> t.getName();
로그인 후 복사

而:

Consumer<String> consumer = t::setName;
로그인 후 복사

等价于:

Consumer<String> consumer = (name) -> t.setName(name);
로그인 후 복사

构造方法

对于构造方法,方法引用的语法是<类名>::new,如Student::new,如下语句:

BiFunction<String, Double, Student> s = (name, score) -> new Student(name, score);
로그인 후 복사

等价于:

BiFunction<String, Double, Student> s = Student::new;
로그인 후 복사

函数的复合

在前面的例子中,函数式接口都用作方法的参数,其他部分通过Lambda表达式传递具体代码给它,函数式接口和Lambda表达式还可用作方法的返回值,传递代码回调用者,将这两种用法结合起来,可以构造复合的函数,使程序简洁易读。

下面我们会看一些例子,在介绍例子之前,我们先需要介绍Java 8对接口的增强。

接口的静态方法和默认方法

在Java 8之前,接口中的方法都是抽象方法,都没有实现体,Java 8允许在接口中定义两类新方法:静态方法和默认方法,它们有实现体,比如:

public interface IDemo {void hello();public static void test() {
        System.out.println("hello");
    }default void hi() {
        System.out.println("hi");
    }
}
로그인 후 복사

test()就是一个静态方法,可以通过IDemo.test()调用。在接口不能定义静态方法之前,相关的静态方法往往定义在单独的类中,比如,Collection接口有一个对应的单独的类Collections,在Java 8中,就可以直接写在接口中了,比如Comparator接口就定义了多个静态方法。

hi()是一个默认方法,由关键字default标识,默认方法与抽象方法都是接口的方法,不同在于,它有默认的实现,实现类可以改变它的实现,也可以不改变。引入默认方法主要是函数式数据处理的需求,是为了便于给接口增加功能。

在没有默认方法之前,Java是很难给接口增加功能的,比如List接口,因为有太多非Java JDK控制的代码实现了该接口,如果给接口增加一个方法,则那些接口的实现就无法在新版Java 上运行,必须改写代码,实现新的方法,这显然是无法接受的。函数式数据处理需要给一些接口增加一些新的方法,所以就有了默认方法的概念,接口增加了新方法,而接口现有的实现类也不需要必须实现它。

看一些例子,List接口增加了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);
    }
}
로그인 후 복사

Collection接口增加了stream方法,其定义为:

default Stream<E> stream() {return StreamSupport.stream(spliterator(), false);
}
로그인 후 복사

需要说明的是,即使能定义方法体了,接口与抽象类还是不一样的,接口中不能定义实例变量,而抽象类可以。

了解了静态方法和默认方法,我们看一些利用它们实现复合函数的例子。

Comparator中的复合方法

Comparator接口定义了如下静态方法:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
{
    Objects.requireNonNull(keyExtractor);return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
로그인 후 복사

这个方法是什么意思呢?它用于构建一个Comparator,比如,在前面的例子中,对文件按照文件名排序的代码为:

Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
로그인 후 복사
로그인 후 복사

使用comparing方法,代码可以简化为:

Arrays.sort(files, Comparator.comparing(File::getName));
로그인 후 복사

这样,代码的可读性是不是大大增强了?comparing方法为什么能达到这个效果呢?它构建并返回了一个符合Comparator接口的Lambda表达式,这个Comparator接受的参数类型是File,它使用了传递过来的函数代码keyExtractor将File转换为String进行比较。像comparing这样使用复合方式构建并传递代码并不容易阅读和理解,但调用者很方便,也很容易理解。

Comparator还有很多默认方法,我们看两个:

default Comparator<T> reversed() {return Collections.reverseOrder(this);
}default Comparator<T> thenComparing(Comparator<? super T> other) {
    Objects.requireNonNull(other);return (Comparator<T> & Serializable) (c1, c2) -> {int res = compare(c1, c2);return (res != 0) ? res : other.compare(c1, c2);
    };
}
로그인 후 복사

reversed返回一个新的Comparator,按原排序逆序排。thenComparing也是一个返回一个新的Comparator,在原排序认为两个元素排序相同的时候,使用提供的other Comparator进行比较。

看一个使用的例子,将学生列表按照分数倒序排(高分在前),分数一样的,按照名字进行排序,代码如下所示:

students.sort(Comparator.comparing(Student::getScore)
                        .reversed()
                        .thenComparing(Student::getName));
로그인 후 복사

这样,代码是不是很容易读?

java.util.function中的复合方法

在java.util.function包中的很多函数式接口里,都定义了一些复合方法,我们看一些例子。

Function接口有如下定义:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);return (T t) -> after.apply(apply(t));
}
로그인 후 복사

先将T类型的参数转化为类型R,再调用after将R转换为V,最后返回类型V。

还有如下定义:

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);return (V v) -> apply(before.apply(v));
}
로그인 후 복사

对V类型的参数,先调用before将V转换为T类型,再调用当前的apply方法转换为R类型返回。

Consumer, Predicate等都有一些复合方法,它们大量被用于下节介绍的函数式数据处理API中,具体我们就不探讨了。

위 내용은 람다 표현식이란 무엇입니까? 무슨 소용이 있나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!