Java Lambda 표현식에 대한 예비 연구
서문
이 기사는 JavaOne 2016에서 Trisha Gee의 Refactoring to Java 8 기조 연설에서 영감을 받았습니다.
Java 8이 출시된 지 2년이 넘었지만 아직도 많은 사람들이 JDK7을 사용하고 있습니다. 기업의 경우 기술에 주의하는 것이 반드시 나쁜 것은 아니지만 개인 학습의 경우 새로운 기술을 배우지 않으면 버림받을 가능성이 높습니다. Java 8의 중요한 변화는 람다 표현의 도입입니다. 람다 표현이 무엇인지는 모르지만 여전히 멋지다고 생각합니다. 걱정하지 마십시오. 언어 수준에서 Lambda 표현은 새로운 구문일 뿐이므로 Java는 함수형 프로그래밍의 문을 열어줄 것입니다.
람다 표현식이 필요한 이유
람다 표현식이 무엇인지, 함수형 프로그래밍이 무엇인지 걱정하지 마세요. 먼저 Java 8의 새로운 구문 기능이 제공하는 편리함을 살펴보겠습니다. 여러분도 이를 기억하실 것이라 믿습니다.
람다 표현식이 있기 전에 새 스레드를 생성하려면 다음과 같이 작성해야 합니다.
new Thread(new Runnable(){ @Override public void run(){ System.out.println("Thread run()"); } }).start();
람다 표현식이 있으면 다음과 같이 작성할 수 있습니다.
new Thread( () -> System.out.println("Thread run()") ).start();
보시다시피 이전에 쓸모없던 템플릿 코드가 사라졌습니다! 위에 표시된 것처럼 Lambda 표현식의 일반적인 용도는 (일부) 익명의 내부 클래스를 대체하는 것이지만 Lambda 표현식의 역할은 이에 국한되지 않습니다.
람다 표현식의 원리
람다 표현식을 처음 사용하는 경우 클래스나 메서드의 이름을 선언하지 않고 함수를 직접 정의할 수 있다는 사실이 마법처럼 느껴질 수 있습니다. 그러나 사실 이는 컴파일러가 제공하는 약간의 트릭일 뿐이며, 그 뒤에 숨은 원리는 이해하기 어렵지 않습니다. 다음은 Lambda 표현식의 몇 가지 가능한 작성 형식입니다.
Runnable run = () -> System.out.println("Hello World");// 1ActionListener listener = event -> System.out.println("button clicked");// 2Runnable multiLine = () -> {// 3 System.out.println("Hello "); System.out.println("World"); }; BinaryOperator<Long> add = (Long x, Long y) -> x + y;// 4BinaryOperator<Long> addImplicit = (x, y) -> x + y;// 5
위의 예에서 다음을 찾을 수 있습니다.
Lambda 표현식에는 유형이 있으며 할당 작업의 왼쪽은 유형. 람다 식의 유형은 실제로 해당 인터페이스의 유형입니다.
Lambda 표현식에는 여러 줄의 코드가 포함될 수 있으며 함수 본문을 작성하는 것처럼 중괄호를 사용하여 코드 블록을 묶어야 합니다.
목록 2와 5에 표시된 것처럼 대부분의 경우 람다 표현식의 매개변수 목록에서 유형을 생략할 수 있습니다. 이는 javac의 유형 파생 메커니즘으로 인해 컴파일러가 컨텍스트를 기반으로 유형 정보를 추론할 수 있기 때문입니다.
사실 각 람다 표현식은 기능적 인터페이스(Functional Interface)를 구현하는 원래 익명 내부 클래스의 약어입니다. 소위 기능적 인터페이스는 @FunctionalInterface 주석이 추가되고 내부에 하나의 인터페이스 함수만 있는 인터페이스를 말합니다. Java는 명시적으로 지정되었는지 여부에 관계없이 각 변수와 개체가 명시적으로 지정되지 않은 경우 컴파일러에서 유형을 결정하려고 시도합니다. 람다 식의 유형은 해당 함수 인터페이스의 유형입니다.
Lambda 표현식과 Stream
Lambda 표현식의 또 다른 중요한 용도는 Stream과 함께 사용하는 것입니다. 스트림은 순차 및 병렬 집계 작업을 지원하는 일련의 요소입니다. Stream은 이러한 요소에 대한 다양한 작업을 지원하는 일련의 요소이며 이러한 작업은 Lambda 표현식을 통해 지정됩니다. 반복자가 컨테이너의 뷰인 것처럼 스트림을 Java 컬렉션의 뷰로 생각할 수 있습니다(그러나 스트림은 컨테이너의 내용을 수정하지 않습니다). 다음 예에서는 Stream의 일반적인 사용법을 보여줍니다.
예제 1
문자열 목록에서 숫자로 시작하는 문자열을 선택하여 출력해야 한다고 가정해 보겠습니다. Java 7 이전에는 다음과 같이 작성해야 했습니다.
List<String> list = Arrays.asList("1one", "two", "three", "4four");for(String str : list){ if(Character.isDigit(str.charAt(0))){ System.out.println(str); } }
Java 8은 다음과 같이 작성할 수 있습니다.
List<String> list = Arrays.asList("1one", "two", "three", "4four"); list.stream()// 1.得到容器的Steam .filter(str -> Character.isDigit(str.charAt(0)))// 2.选出以数字开头的字符串 .forEach(str -> System.out.println(str));// 3.输出字符串
위 코드를 먼저 1. List.stream() 메서드를 호출하여 컨테이너의 스트림을 가져옵니다. 2. 그런 다음 필터( ) 메서드를 사용하여 숫자로 시작하는 문자열을 필터링합니다. 3. 마지막으로 forEach() 메서드를 호출하여 결과를 출력합니다.
Stream을 사용하면 두 가지 확실한 이점이 있습니다.
템플릿 코드가 줄어들고 Lambda 표현식만 사용하여 필요한 작업을 지정하는 것이 더 명확하고 읽기 쉽습니다.
외부 반복을 Stream의 내부 반복으로 변경하면 JVM 자체가 반복 프로세스를 최적화할 수 있습니다(예: 병렬로 반복 가능).
예제 2
문자열 목록에서 숫자로 시작하지 않는 모든 문자열을 선택하여 대문자로 변환하고 그 결과를 새 컬렉션에 넣어야 한다고 가정해 보겠습니다. Java 8로 작성된 코드는 다음과 같습니다.
List<String> list = Arrays.asList("1one", "two", "three", "4four"); Set<String> newList = list.stream()// 1.得到容器的Stream .filter(str -> !Character.isDigit(str.charAt(0)))// 2.选出不以数字开头的字符串 .map(String::toUpperCase)// 3.转换成大写形式 .collect(Collectors.toSet());// 4.生成结果集
上述代码首先1. 调用List.stream()方法得到容器的Stream,2. 然后调用filter()方法选出不以数字开头的字符串,3. 之后调用map()方法将字符串转换成大写形式,4. 最后调用collect()方法将结果转换成Set。这个例子还向我们展示了方法引用(method references,代码中标号3处)以及收集器(Collector,代码中标号4处)的用法,这里不再展开说明。
通过这个例子我们看到了Stream链式操作,即多个操作可以连成一串。不用担心这会导致对容器的多次迭代,因为不是每个Stream的操作都会立即执行。Stream的操作分成两类,一类是中间操作(intermediate operations),另一类是结束操作(terminal operation),只有结束操作才会导致真正的代码执行,中间操作只会做一些标记,表示需要对Stream进行某种操作。这意味着可以在Stream上通过关联多种操作,但最终只需要一次迭代。如果你熟悉Spark RDD,对此应该并不陌生。
结语
Java 8引入Lambda表达式,从此打开了函数式编程的大门。如果你之前不了解函数式编程,不必纠结于这个概念。编程过程中简洁明了的书写形式以及强大的Stream API会让你很快熟悉Lambda表达式的。
本文只对Java Lambda表达式的基本介绍,希望能够激发读者对Java函数式编程的兴趣。如果本文能够让你觉得Lambda表达式很好玩,函数式编程很有趣,并产生了进一步学习的欲望,那就再好不过了。文末参考文献中列出了一些有用的资源。
致谢
非常感谢阿里巴巴“西行游学计划”给予我们的支持,感谢阿里中间件团队资助我们远美国赴旧金山参加Java语言的顶级技术会议——2016 JaveOne大会。借此我们有机会跟国际顶尖大牛面对面的交流,并第一时间了解到Java语言的最新动态。同样感谢伴我一起游学的两位队友,有ta们在异国他乡的陪伴和关照,我的西行之旅才更加丰富多彩。这次游学给我们带来的惊喜,将深深留存在我们的记忆当中。
希望更多的同学能够关注并参与到阿里巴巴天池大赛当中,也希望阿里能够继续坚持对青年学生的支持和培养!
参考文献
http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/Lambda-QuickStart/index.html
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html
http://www.slideshare.net/trishagee/refactoring-to-java-8-devoxx-uk
《Java 8函数式编程 [英]沃伯顿》
https://www.oracle.com/javaone/speakers.html#gee