Java 8 已经发行好几年了,作为一名Java程序员,再不应用它的美好的新特性肯定要被社会淘汰了。这篇文章,我作为一名Java8新手用代码实践Java8新特性,来探究它的美好。
1. Lambda表达式与Functional接口
Lambda表达式(也称为闭包),它允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据,这一特性和scala语言很像。
在最简单的形式中,一个lambda可以由用逗号分隔的参数列表、–>符号与函数体三部分表示。例如:
Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );//e的类型是由编译器推测出来的
某些情况下lambda的函数体会是多条语句,这时可以把函数体放到在一对花括号中,就像在Java中定义普通函数一样。例如:
Arrays.asList( "a", "b", "d" ).forEach( e -> {
System.out.print( e );
System.out.print( e );
} );
Lambda可以引用类的成员变量与局部变量(如果这些变量不是final的话,它们会被隐含的转为final,这样效率更高)。例如,下面两个代码片段是等价的:
String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
Lambda可能会返回一个值。返回值的类型也是由编译器推测出来的。如果lambda的函数体只有一行的话,那么没有必要显式使用return语句。下面两个代码片段是等价的:
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
int result = e1.compareTo( e2 );
return result;
} );
如何使现有的函数友好地支持lambda。方法是:增加函数式接口的概念。函数式接口就是一个具有一个方法的普通接口。像这样的接口,可以被隐式转换为lambda表达式。java.lang.Runnable与java.util.concurrent.Callable是函数式接口最典型的两个例子。在实际使用过程中,函数式接口是容易出错的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。为了克服函数式接口的这种脆弱性并且能够明确声明接口作为函数式接口的意图,Java 8增加了一种特殊的注解@FunctionalInterface(Java 8中所有类库的已有接口都添加了@FunctionalInterface注解)。让我们看一下这种函数式接口的定义:
@FunctionalInterface
public interface Functional {
void method();
}
需要记住的一件事是:默认方法与静态方法并不影响函数式接口的契约,可以任意使用:
@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() {
}
}
其实,lambda表达式本质就是匿名内部类。
new Thread(()->{
for (int i = 0; i <10 ; i++) {
System.out.println("这是"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
lambda表达式常见形式
//无参数单语句
Runnable rundemo=()-> System.out.println("无参数单语句");
//单参数单语句,event为javase中的事件类就是ActionListener的实现类
ActionListener al= event-> System.out.println("单参数单语句");
//无参数多语句
Runnable r=()->{
System.out.println("第一个");
System.out.println("无参数多语句");
};
//多参数单语句
BinaryOperator add=(x,y)->x+y;
//或者
BinaryOperator add2=(Integer x,Integer y)->x+y;
2. 接口的默认方法与静态方法
Java 8用默认方法与静态方法来扩展接口的声明。默认方法具有方法体,但与传统的接口又有些不一样,它允许在已有的接口中添加新方法,而同时又保持了与旧版本代码的兼容性。
默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。相反,每个接口都必须提供一个所谓的默认实现,这样所有的接口实现者将会默认继承它(如果有必要的话,可以覆盖这个默认实现)。让我们看看下面的例子:
private interface Defaulable {
default String notRequired() {
return "Default implementation";
}
}
private static class DefaultableImpl implements Defaulable {
//该实现类默认继承了默认方法
}
private static class OverridableImpl implements Defaulable {
//该实现类用自己的方法覆盖了默认方法。
@Override
public String notRequired() {
return "Overridden implementation";
}
}
Java 8带来的另一个有趣的特性是接口可以声明(并且可以提供实现)静态方法。例如:
private interface DefaulableFactory {
// Interfaces now allow static methods
static Defaulable create( Supplier< Defaulable > supplier ) {
return supplier.get();
}
}
下面的一小段代码片段把上面的默认方法与静态方法黏合到一起。
public static void main( String[] args ) {
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
System.out.println( defaulable.notRequired() );
defaulable = DefaulableFactory.create( OverridableImpl::new );
System.out.println( defaulable.notRequired() );
}
在JVM中,大量的默认方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……,这些默认方法非常高效。
3. Java 类库的新特性
Java 8 通过增加大量新类,扩展已有类的功能的方式来改善对并发编程、函数式编程、日期/时间相关操作以及其他更多方面的支持。
3.1 Optional
空指针异常是导致Java应用程序失败的最常见原因。为了解决空指针异常,引入了Optional类,Optional类已经成为Java 8类库的一部分。
Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。更多详情请参考官方文档。
Optional
Optional someValue = someMethod();
if (someValue.isPresent()) { // check
someValue.get().someOtherMethod(); // retrieve and call
}
可以将Optional看成是需要使用某个T值的方法之间某种中间人或者协调者Mediator,而不只是一个普通对象的包装器。如果你有一个值返回类型T,你有一个方法需要使用这个值,那么你可以让 Optional
下面这个案例涉及到Lambda表达式 方法引用,是将单词流中第一个以"L"开始单词取出,作为返回结果是一个Optional
使用ifPresent()
Stream names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
Optional longest = names
.filter(name -> name.startsWith("L"))
.findFirst();
longest.ifPresent(name -> {
String s = name.toUpperCase();
System.out.println("The longest name is "+ s);
});
这里ifPresent() 是将一个Lambda表达式作为输入,T值如果不为空将传入这个lambda。那么这个lambda将不为空的单词转为大写输出显示。在前面names单词流寻找结果中,有可能找不到开始字母为L的单词,返回为空,也可能找到不为空,这两种情况都传入lambda中,无需我们打开盒子自己编写代码来判断,它自动帮助我们完成了,无需人工干预。
使用map()
Stream names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
Optional longest = names
.filter(name -> name.startsWith("L"))
.findFirst();
Optional lNameInCaps = longest.map(String::toUpperCase);
使用Optional
使用orElse()
Stream names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
Optional longest = names
.filter(name -> name.startsWith("Q"))
.findFirst();
String alternate = longest.orElse("Nimrod");
System.out.println(alternate); //prints out "Nimrod"
如果在T可能空时你需要一个值的话,那么可以使用 orElse(),它能在T值存在的情况下返回这个值,否则返回输入值。
使用orElseGet()
Stream names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
Optional longest = names
.filter(name -> name.startsWith("Q"))
.findFirst();
String alternate = longest.orElseGet(() -> {
// perform some interesting code operation
// then return the alternate value.
return "Nimrod";
});
System.out.println(alternate);
使用 orElseThrow()
//orElseThrow()是在当遭遇Null时,决定抛出哪个Exception时使用
Stream names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
Optional longest = names
.filter(name -> name.startsWith("Q"))
.findFirst();
longest.orElseThrow(NoSuchElementStartingWithQException::new);
总结,你能创建下面三种类型的Optional
Optional getSomeValue() {
// 返回一个空的Optional类型;
return Optional.empty();
}
Optional getSomeValue() {
SomeType value = ...;
// 使用这个方法,值不可以为空,否则抛exception
return Optional.of(value);
}
Optional getSomeValue() {
SomeType value = ...;
// 使用这个方法,值可以为空,如果为空返回Optional.empty
return Optional.ofNullable(value);
// usage
Optional someType = getSomeValue();
3.2Stream
Stream API极大简化了集合框架的处理(但它的处理的范围不仅仅限于集合框架的处理)。让我们以一个简单的Task类为例进行介绍:
public class Streams {
private enum Status {
OPEN, CLOSED
};
private static final class Task {
private final Status status;
private final Integer points;
Task( final Status status, final Integer points ) {
this.status = status;
this.points = points;
}
public Integer getPoints() {
return points;
}
public Status getStatus() {
return status;
}
@Override
public String toString() {
return String.format( "[%s, %d]", status, points );
}
}
}
Task类有一个分数的概念(或者说是伪复杂度),其次是还有一个值可以为OPEN或CLOSED的状态.
final Collection< Task > tasks = Arrays.asList(
new Task( Status.OPEN, 5 ),
new Task( Status.OPEN, 13 ),
new Task( Status.CLOSED, 8 )
);
第一个问题是所有状态为OPEN的任务一共有多少分数?在Java 8以前,一般的解决方式用foreach循环,但是在Java 8里面我们可以使用stream:一串支持连续、并行聚集操作的元素。
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();
System.out.println( "Total points: " + totalPointsOfOpenTasks );
几个注意事项:
第一,task集合被转换化为其相应的stream表示。然后,filter操作过滤掉状态为CLOSED的task。下一步,mapToInt操作通过Task::getPoints这种方式调用每个task实例的getPoints方法把Task的stream转化为Integer的stream。最后,用sum函数把所有的分数加起来,得到最终的结果。
Stream操作被分成了中间操作与最终操作这两种:
中间操作返回一个新的stream对象。中间操作总是采用惰性求值方式,运行一个像filter这样的中间操作实际上没有进行任何过滤,相反它在遍历元素时会产生了一个新的stream对象,这个新的stream对象包含原始stream中符合给定谓词的所有元素。
像forEach、sum这样的最终操作可能直接遍历stream,产生一个结果或副作用。当最终操作执行结束之后,stream管道被认为已经被消耗了,没有可能再被使用了。在大多数情况下,最终操作都是采用及早求值方式,及早完成底层数据源的遍历。
Stream另一个有价值的地方是能够原生支持并行处理。
final Map< Status, List< Task > > map = tasks
.stream()
.collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
//控制台输出
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
计算整个集合中每个task分数(或权重)的平均值:
final Collection< String > result = tasks
.stream() // Stream< String >
.mapToInt( Task::getPoints ) // IntStream
.asLongStream() // LongStream
.mapToDouble( points -> points / totalPoints ) // DoubleStream
.boxed() // Stream< Double >
.mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
.mapToObj( percentage -> percentage + "%" ) // Stream< String>
.collect( Collectors.toList() ); // List< String >
System.out.println( result );
//控制台输出
[19%, 50%, 30%]
注意:Stream API不仅仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操作也很适合用Stream API来处理。下面用一个例子来应证这一点。
final Path path = new File( filename ).toPath();
try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}
对一个stream对象调用onClose方法会返回一个在原有功能基础上新增了关闭功能的stream对象,当对stream对象调用close()方法时,与关闭相关的处理器就会执行。
3.4. 一些新的操作方式
ForEach
//map新的遍历方式
Set>> entries = collect.entrySet();
entries.forEach(map-> System.out.println(map.getKey()+"====="+map.getValue()));
4.并行(parallel)数组
Java 8增加了大量的新方法来对数组进行并行处理。可以说,最重要的是parallelSort()方法,因为它可以在多核机器上极大提高数组排序的速度。下面的例子展示了新方法(parallelXxx)的使用。
public class ParallelArrays {
public static void main(String[] args) {
long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
System.out.print("unsorted: ");
Arrays.stream( arrayOfLong ).limit( 20).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
System.out.print("sorted: ");
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 20 ).forEach(
i -> System.out.print( i + " " ) );
}
}
打印结果如下:
unsorted: 391090 629642 229354 60426 771326 736856 463120 191727 58792 597735 881800 139717 606935 934037 971280 322328 959308 211004 685634 326106
sorted: 128 161 217 245 308 331 335 602 608 637 671 678 716 770 773 909 919 926 954 1069
parallelSetAll()方法来对一个有20000个元素的数组进行随机赋值。然后调用parallelSort方法对其进行排序。
以上便是我刚进行的Java8实践,还有很多要学习的,待续!