Java14 都来了,你还不会用 Java8吗?

1,115 阅读9分钟

Java 8 于2014年3月18日发布,并且成为主流的 Java,如今,虽然 Java 14 都已经发布了,但是 开发者和公司选择的版本依旧是经久不衰的 Java 8 版本,如果你还不了解这些新特性,是时候学习一下了。

Java 8 更新的一些重要功能一览

  • Iterable 接口中的 forEach() 方法
  • 接口中的默认方法和静态方法
  • 功能接口和 Lambda 表达式
  • 用于集合上批量数据操作的 Java Stream API
  • Java 时间 API
  • 集合 API 的改进
  • 并发 API 改进
  • Java IO改进
  • 其他核心 API 改进

下面来简要了解一下这些Java 8功能。我将提供一些代码片段以更好地理解,因此,如果要在Java 8中运行程序,则必须按照以下步骤设置Java 8环境。

下载并安装JDK8。像其他Java版本一样,安装也很简单,运行下面的示例,必须要安装 JDK 环境。

下载最新的 IDEA 开发环境,这里我不推荐使用 Eclipse ,但是如果你有使用 Eclipse 习惯,那我在这里推荐你可以尝试 IDEA,因为它真的太棒啦~

Iterable 接口中的 forEach()方法

在 Java 8 以前,每当需要遍历 Collection 时,就需要创建一个 Iterator 来进行迭代 Collection 对象,然后针对 Collection 中 的每个元素将业务逻辑循环在一起。如果迭代器使用不正确,可能会抛出 ConcurrentModificationException。

Java 8 在接口中引入了forEach方法,java.lang.Iterable因此在编写代码时,我们仅关注业务逻辑。forEach方法将java.util.function.Consumer对象作为参数,因此有助于将我们的业务逻辑放在一个可以重用的单独位置。让我们通过简单的示例查看forEach用法。


package com.journaldev.java8.foreach;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.lang.Integer;

public class Java8ForEachExample {

	public static void main(String[] args) {
		
		//creating sample Collection
		List<Integer> myList = new ArrayList<Integer>();
		for(int i=0; i<10; i++) myList.add(i);
		
		//traversing using Iterator
		Iterator<Integer> it = myList.iterator();
		while(it.hasNext()){
			Integer i = it.next();
			System.out.println("Iterator Value::"+i);
		}
		
		//traversing through forEach method of Iterable with anonymous class
		myList.forEach(new Consumer<Integer>() {

			public void accept(Integer t) {
				System.out.println("forEach anonymous class Value::"+t);
			}

		});
		
		//traversing with Consumer interface implementation
		MyConsumer action = new MyConsumer();
		myList.forEach(action);
		
	}

}

//Consumer implementation that can be reused
class MyConsumer implements Consumer<Integer>{

	public void accept(Integer t) {
		System.out.println("Consumer impl Value::"+t);
	}


}

代码的行数可能会增加,但是forEach方法有助于将迭代逻辑和业务逻辑放在不同的位置,从而使关注点和代码更清晰地分离。

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

如果仔细阅读forEach方法的详细信息,会注意到它是在Iterable接口中定义的,但我们知道接口不能具有方法主体。从Java 8开始,接口已增强为具有实现的方法。我们可以使用default和static关键字来创建带有方法实现的接口。Iterable接口中的forEach方法实现为:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

我们知道Java不会在Class中提供多重继承,因为它会导致Diamond问题。由于接口现在类似于抽象类,因此现在如何使用接口处理它。解决方案是在这种情况下编译器将引发异常,我们将不得不在实现接口的类中提供实现逻辑。

package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface1 {

	void method1(String str);
	
	default void log(String str){
		System.out.println("I1 logging::"+str);
	}
	
	static void print(String str){
		System.out.println("Printing "+str);
	}
	
	//trying to override Object method gives compile-time error as
	//"A default method cannot override a method from java.lang.Object"
	
//	default String toString(){
//		return "i1";
//	}
	
}
package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface2 {

	void method2();
	
	default void log(String str){
		System.out.println("I2 logging::"+str);
	}

}

注意,两个接口都有一个带有实现逻辑的通用方法log()。

package com.journaldev.java8.defaultmethod;

public class MyClass implements Interface1, Interface2 {

	@Override
	public void method2() {
	}

	@Override
	public void method1(String str) {
	}

	//MyClass won't compile without having it's own log() implementation
	@Override
	public void log(String str){
		System.out.println("MyClass logging::"+str);
		Interface1.print("abc");
	}
	
}

如你所见, Interface1 具有在 MyClass.log() 方法实现中使用的静态方法实现。Java 8 在 Collection API 中大量使用默认和静态方法,并且添加了默认方法,以便使 JDK 8 之前的代码保持向后兼容。

如果层次结构中的任何类都具有具有相同的方法,则默认方法将变得无关紧要。由于任何实现接口的类都已经具有 Object 作为超类,因此如果接口中具有 equals(),hashCode() 默认方法,它将变得无关紧要。这就是为什么为了更清楚起见,不允许接口具有Object默认方法。

有关Java 8接口特性的完整详细信息,请阅读 Java 8接口更改

功能接口和Lambda表达式

如果你注意到上述接口代码,则会注意到 @FunctionalInterface 注解。该功能接口是Java 8 中引入的新概念。具有一种抽象方法的接口就变成了功能接口。我们不需要使用 @FunctionalInterface 注解将接口标记为Functional Interface。@FunctionalInterface 注解是一种避免在功能接口中意外添加抽象方法的工具。您可以将其视为 @Override 批注,并且是使用它的最佳实践。java.lang.Runnable 使用单个抽象方法 run() 是功能接口的一个很好的例子。

功能接口的主要优点之一是可以使用 lambda 表达式实例化它们。在 Java 8 之前,可以用匿名类实例化一个接口,但是代码看起来很庞大。

Runnable r = new Runnable(){
	@Override
	public void run() {
            System.out.println("My Runnable");
	}};

由于功能接口只有一种方法,因此 lambda 表达式可以轻松提供该方法的实现。只需要提供方法参数和业务逻辑。例如,可以使用 lambda 表达式编写上面的实现:

Runnable r1 = () -> {
			System.out.println("My Runnable");
		};

如果方法实现中只有一条语句,那么我们也不需要花括号。例如,上面的Interface1匿名类可以使用lambda实例化,如下所示:

Interface1 i1 = (s) -> System.out.println(s);
		
i1.method1("abc");

因此,lambda 表达式是轻松创建功能接口的匿名类的一种方法。使用 lambda 表达式对于代码运行没有任何的影响,因此要谨慎谨慎使用它,因为我们并不介意编写一些额外的代码行。

新包装 java.util.function 添加了带有功能接口束的,以提供 lambda 表达式和方法引用的目标类型。Lambda 表达式是一个非常复杂的话题,后续我会编写一篇文章专门针对 lambda 表达式。

您可以在 Java 8 Lambda Expressions Tutorial 中阅读完整的教程。

用于集合上批量数据操作的Java Stream API

java.util.stream 是Java 8中添加的一个新内容,以对该集合执行类似过滤/映射/遍历的操作。Stream API 将允许顺序执行和并行执行。这对我来说是非常好用的一个功能,因为我经常处理 Collections,而且通常使用很多的数据进行过滤数据,遍历数据,stream 就完美的解决了这个问题。

Collection 接口已使用 stream() 和 parallelStream() 默认方法进行了扩展,以获取用于顺序执行和并行执行的 Stream ,用一个简单的例子看看它们的用法。

package com.journaldev.java8.stream;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {

	public static void main(String[] args) {
		
		List<Integer> myList = new ArrayList<>();
		for(int i=0; i<100; i++) myList.add(i);
		
		//sequential stream
		Stream<Integer> sequentialStream = myList.stream();
		
		//parallel stream
		Stream<Integer> parallelStream = myList.parallelStream();
		
		//using lambda with Stream API, filter example
		Stream<Integer> highNums = parallelStream.filter(p -> p > 90);
		//using lambda in forEach
		highNums.forEach(p -> System.out.println("High Nums parallel="+p));
		
		Stream<Integer> highNumsSeq = sequentialStream.filter(p -> p > 90);
		highNumsSeq.forEach(p -> System.out.println("High Nums sequential="+p));

	}

}

如果你运行示例代码,将获得如下输出:

High Nums parallel=91
High Nums parallel=96
High Nums parallel=93
High Nums parallel=98
High Nums parallel=94
High Nums parallel=95
High Nums parallel=97
High Nums parallel=92
High Nums parallel=99
High Nums sequential=91
High Nums sequential=92
High Nums sequential=93
High Nums sequential=94
High Nums sequential=95
High Nums sequential=96
High Nums sequential=97
High Nums sequential=98
High Nums sequential=99

请注意,并行处理的时候。值不按顺序排列,因此在处理庞大的集合时,并行处理将非常有帮助。

这篇文章无法涵盖有关Stream API的所有内容,您可以在 Java 8 Stream API Example Tutorial 中阅读有关Stream API的所有内容。

Java 8 时间API

在之前的 Java 中使用日期,时间和时区一直很困难。Java 中没有用于日期和时间的标准方法或 API。Java 8 有一个不错的附加功能是 java.time 软件包,它简化了 Java 中使用时间的过程。

仅查看 Java Time API 软件包,就可以感觉到它非常易于使用。它具有一些子包 java.time.format,这些子包提供用于打印和解析日期和时间的类,还有java.time.zone 提供对时区及其规则的支持。

新的 Time API 在整月的几个月和一周中的几天中都使用了枚举而不是整数常量。常用的类之一是 DateTimeFormatter 将 DateTime 对象转换为字符串。

有关完整的教程,请转到 Java日期时间API示例教程

集合API的改进

在上面的介绍已经看到了 forEach() 方法和用于集合的 Stream API。

Collection API 中添加的一些新方法是:

IteratorforEachRemaining(Consumer action) 在所有元素都已处理完毕或该动作引发异常之前,对其余每个元素执行给定操作的默认方法。

CollectionremoveIf(Predicate filter) 删除此集合中所有满足给定谓词的元素的默认方法。

Collection spliterator() 该方法返回Spliterator实例,该实例可用于顺序或并行遍历元素。 地图replaceAll(),compute(),merge()方法。

另外还有一些具有 hash 冲突 的HashMap 类的性能改进

并发API改进

一些重要的并发API增强功能包括:

ConcurrentHashMap compute(),forEach(),forEachEntry(),forEachKey(),forEachValue(),merge(),reduce()和search()方法。 CompletableFuture 可以明确完成(设置其值和状态)。 Executors newWorkStealingPool() 使用所有可用处理器作为目标并行度级别创建窃取线程池的方法。

Java IO改进

我知道的一些IO改进:

Files.list(Path dir) 返回一个延迟加载的 Stream,其元素是目录中的文件夹和文件列表。

Files.lines(Path path) 返回一个读取指定文件所有行的文件流。

Files.find() 返回一个根据指定目录搜索指定文件的文件列表流。

BufferedReader.lines() 返回一个Stream,其元素是从此 BufferedReader 中读取的行。

其他核心API改进

一些杂项API改进在某些特殊情况可能会派上用场:

  • ThreadLocal 静态方法可以使用 withInitial 方法创建实例。

  • 比较器接口已扩展了许多默认和静态方法,用于自然排序,反向排序等。

  • Integer,Long 和 Double 包装器类中增加了 min(),max() 和 sum() 方法。

  • Boolean 类中的 logicalAnd() ,logicalOr() 和 logicalXor() 方法。

  • ZipFile.stream() 方法获取 ZIP 文件条目上的有序Stream,并以压缩时的顺序出现在 Stream 中。

  • Math 类中增加了几种实用方法。

  • jjs 添加命令以调用 Nashorn Engine。

  • jdeps 添加命令以分析类文件

  • JDBC-ODBC 桥已被删除。

  • PermGen 内存空间已被删除