- Java8新特性你知道多少?
- Java8新特性你用过多少?
- Java8新特性你理解多少?
Java8所有的新特性
Lambda表达式、 函数式接口、接口默认方法、接口静态方法、扩展注解、重复注解、Optional、Stream、时间/日期API、方法引用、 参数名字保留在字节码中、并行数组、CompletableFuture
Lambda表达式
在JDK8之前,一个方法能接受的参数都是变量,例如:object.method(Object o)
那么,如果需要传入一个动作呢?比如回调。
那么你可能会想到匿名内部类。
例如:
匿名内部类是需要依赖接口的,所以需要先定义个接口
@FunctionalInterface
public interface PersonCallback {
void callback(Person person);
}
复制代码
Person类:
public class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
// 创建一个Person后,进行回调
public static void create(Integer id, String name, PersonCallback personCallback) {
Person person = new Person(id, name);
personCallback.callback(person);
}
}
复制代码
public static void main(String[] args) {
Person.create(1, "周瑜", new PersonCallback() {
public void callback(Person person) {
System.out.println("去注册...");
}
});
Person.create(2, "老师", new PersonCallback() {
public void callback(Person person) {
System.out.println("去登陆...");
}
});
}
复制代码
上面的PersonCallback其实就是一种动作,但是我们真正关心的只有callback方法里的内容而已,我们用Lambda表示,可以将上面的代码就可以优化成:
Person.create(1, "周瑜", (Person person) -> {System.out.println("去注册...");})
复制代码
有没有发现特别简单了...,但是我们发现Person.create这个方法其实接收的对象依然是PersonCallback这个接口,但是现在传的是一个Lambda表达式,那么难道Lambda表达式也实现了这个接口?。问题也放这,我们先看一下Lambda表达式的接口
Lambda允许把函数作为一个方法的参数,一个lambda由用逗号分隔的参数列表、–>符号、函数体三部分表示。对于上面的表达式
- **(Person person)为Lambda表达式的入参,{System.out.println("去注册...");}**为函数体
- 重点是这个表达式是没有名字的。 我们知道,当我们实现一个接口的时候,肯定要实现接口里面的方法,那么现在一个Lambda表达式应该也要遵循这一个基本准则,那么一个Lambda表达式它实现了接口里的什么方法呢? 答案是:一个Lambda表达式实现了接口里的有且仅有的唯一一个抽象方法。那么对于这种接口就叫做函数式接口。 Lambda表达式其实完成了实现接口并且实现接口里的方法这一功能,也可以认为Lambda表达式代表一种动作,我们可以直接把这种特殊的动作进行传递。
当然,对于上面的Lambda表达式你可以简化:
Person.create(1, "周瑜", person -> System.out.println("去注册..."));
复制代码
这归功于Java8的类型推导机制。因为现在接口里只有一个方法,那么现在这个Lambda表达式肯定是对应实现了这个方法,既然是唯一的对应关系,那么入参肯定是Person类,所以可以简写,并且方法体只有唯一的一条语句,所以也可以简写,以达到表达式简洁的效果。
函数式接口
函数式接口是新增的一种接口定义。 用**@FunctionalInterface修饰的接口叫做函数式接口**,或者,函数式接口就是一个只具有一个抽象方法的普通接口,@FunctionalInterface可以起到校验的作用。 下面的接口只有一个抽象方法能编译正确:
@FunctionalInterface
public interface TestFunctionalInterface {
void test1();
}
复制代码
下面的接口有多个抽象方法会编译错误:
@FunctionalInterface
public interface TestFunctionalInterface {
void test1();
void test2();
}
复制代码
在JDK7中其实就已经有一些函数式接口了,比如Runnable、Callable、FileFilter等等。 在JDK8中也增加了很多函数式接口,比如java.util.function包。 比如这四个常用的接口:
接口 | 描述 |
---|---|
Supplier | 无参数,返回一个结果 |
Function<T,R> | 接受一个输入参数,返回一个结果 |
Consumer | 接受一个输入参数,无返回结果 |
Predicate | 接受一个输入参数,返回一个布尔值结果。 |
那么Java8中给我们加了这么多函数式接口有什么作用?
上文我们分析到,一个Lambda表达式其实也可以理解为一个函数式接口的实现者,但是作为表达式,它的写法其实是多种多样的,比如
- () -> {return 0;},没有传入参数,有返回值
- (int i) -> {return 0;},传入一个参数,有返回值
- (int i) -> {System.out.println(i)},传入一个int类型的参数,但是没有返回值
- (int i, int j) -> {System.out.println(i)},传入两个int类型的参数,但是没有返回值
- (int i, int j) -> {return i+j;},传入两个int类型的参数,返回一个int值
- (int i, int j) -> {return i>j;},传入两个int类型的参数,返回一个boolean值
等等,还有许多许多种情况。那么这每种表达式的写法其实都应该是某个函数式接口的实现类,需要特定函数式接口进行对应,比如上面的四种情况就分别对应
Supplier<T>
,Function<T,R>
,Consumer<T>
,BiConsumer<T, U>
,BiFunction<T, U, R>
,BiPredicate<T, U>
。
答案已经明显了,Java8中提供给我们这么多函数式接口就是为了让我们写Lambda表达式更加方便,当然遇到特殊情况,你还是需要定义你自己的函数式接口然后才能写对应的Lambda表达式。
总的来说,如果没有函数式接口,就不能写Lambda表达式.
接口的默认方法与静态方法
在JDK7中,如果想对接口Collection新增一个方法,那么你需要修改它所有的实现类源码(这是非常恐怖的),在那么Java8之前是怎么设计来解决这个问题的呢,用的是抽象类,比如: 现在有一个接口PersonInterface接口,里面有1个抽象方法:
public interface PersonInterface {
void getName();
}
复制代码
有三个实现类:
public class YellowPerson implements PersonInterface {
@Override
public void getName() {
System.out.println("yellow");
}
}
public class WhitePerson implements PersonInterface {
@Override
public void getName() {
System.out.println("white");
}
}
public class BlackPerson implements PersonInterface {
@Override
public void getName() {
System.out.println("black");
}
}
复制代码
现在我需要在PersonInterface接口中新增一个方法,那么势必它的三个实现类都需要做相应改动才能编译通过,这里我就不进行演示了,那么我们在最开始设计的时候,其实可以增加一个抽象类PersonAbstract,三个实现类改为继承这个抽象类,按照这种设计方法,对PersonInterface接口中新增一个方法是,其实只需要改动PersonAbstract类去实现新增的方法就好了,其他实现类不需要改动了:
public interface PersonInterface {
void getName();
void walk();
}
public abstract class PersonAbstract implements PersonInterface {
@Override
public void walk() {
System.out.println("walk");
}
}
public class BlackPerson extends PersonAbstract {
@Override
public void getName() {
System.out.println("black");
}
}
public class WhitePerson extends PersonAbstract {
@Override
public void getName() {
System.out.println("white");
}
}
public class YellowPerson extends PersonAbstract {
@Override
public void getName() {
System.out.println("yellow");
}
}
复制代码
那么在Java8中支持直接在接口中添加已经实现了的方法,一种是Default方法(默认方法),一种Static方法(静态方法)。
接口的默认方法
在接口中用default修饰的方法称为默认方法。 接口中的默认方法一定要有默认实现(方法体),接口实现者可以继承它,也可以覆盖它。
default void testDefault(){
System.out.println("default");
};
复制代码
接口的静态方法
在接口中用static修饰的方法称为静态方法。
static void testStatic(){
System.out.println("static");
};
复制代码
调用方式:
TestInterface.testStatic();
复制代码
因为有了默认方法和静态方法,所以你不用去修改它的实现类了,可以进行直接调用。
重复注解
假设,现在有一个服务我们需要定时运行,就像Linux中的cron一样,假设我们需要它在每周三的12点运行一次,那我们可能会定义一个注解,有两个代表时间的属性。
public @interface Schedule {
int dayOfWeek() default 1; // 周几
int hour() default 0; // 几点
}
复制代码
所以我们可以给对应的服务方法上使用该注解,代表运行的时间:
public class ScheduleService {
// 每周3的12点运行
@Schedule(dayOfWeek = 3, hour = 12)
public void start() {
// 执行服务
}
}
复制代码
那么如果我们需要这个服务在每周四的13点也需要运行一下,如果是JDK8之前,那么...尴尬了!你不能像下面的代码,会编译错误
public class ScheduleService {
// jdk中两个相同的注解会编译报错
@Schedule(dayOfWeek = 3, hour = 12)
@Schedule(dayOfWeek = 4, hour = 13)
public void start() {
// 执行服务
}
}
复制代码
那么如果是JDK8,你可以改一下注解的代码,在自定义注解上加上@Repeatable元注解,并且指定重复注解的存储注解(其实就是需要需要数组来存储重复注解),这样就可以解决上面的编译报错问题。
@Repeatable(value = Schedule.Schedules.class)
public @interface Schedule {
int dayOfWeek() default 1;
int hour() default 0;
@interface Schedules {
Schedule[] value();
}
}
复制代码
同时,反射相关的API提供了新的函数getAnnotationsByType()来返回重复注解的类型。 添加main方法:
public static void main(String[] args) {
try {
Method method = ScheduleService.class.getMethod("start");
for (Annotation annotation : method.getAnnotations()) {
System.out.println(annotation);
}
for (Schedule s : method.getAnnotationsByType(Schedule.class)) {
System.out.println(s.dayOfWeek() + "|" + s.hour());
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
复制代码
输出:
@repeatannotation.Schedule$Schedules(value=[@repeatannotation.Schedule(hour=12, dayOfWeek=3), @repeatannotation.Schedule(hour=13, dayOfWeek=4)])
3|12
4|13
复制代码
获取方法参数的名字
在Java8之前,我们如果想获取方法参数的名字是非常困难的,需要使用ASM、javassist等技术来实现,现在,在Java8中则可以直接在Method对象中就可以获取了。
public class ParameterNames {
public void test(String p1, String p2) {
}
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod("test", String.class, String.class);
for (Parameter parameter : method.getParameters()) {
System.out.println(parameter.getName());
}
System.out.println(method.getParameterCount());
}
}
复制代码
输出:
arg0
arg1
2
复制代码
从结果可以看出输出的参数个数正确,但是名字不正确!需要在编译时增加**–parameters**参数后再运行。 在Maven中增加:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerArgument>-parameters</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
复制代码
输出结果则变为:
p1
p2
2
复制代码
CompletableFuture
当我们Javer说异步调用时,我们自然会想到Future,比如:
public class FutureDemo {
/**
* 异步进行一个计算
* @param args
*/
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> result = executor.submit(new Callable<Integer>() {
public Integer call() throws Exception {
int sum=0;
System.out.println("正在计算...");
for (int i=0; i<100; i++) {
sum = sum + i;
}
Thread.sleep(TimeUnit.SECONDS.toSeconds(3));
System.out.println("算完了!");
return sum;
}
});
System.out.println("做其他事情...");
try {
System.out.println("result:" + result.get());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("事情都做完了...");
executor.shutdown();
}
}
复制代码
那么现在如果想实现异步计算完成之后,立马能拿到这个结果继续异步做其他事情呢?这个问题就是一个线程依赖另外一个线程,这个时候Future就不方便,我们来看一下CompletableFuture的实现:
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture result = CompletableFuture.supplyAsync(() -> {
int sum=0;
System.out.println("正在计算...");
for (int i=0; i<100; i++) {
sum = sum + i;
}
try {
Thread.sleep(TimeUnit.SECONDS.toSeconds(3));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"算完了!");
return sum;
}, executor).thenApplyAsync(sum -> {
System.out.println(Thread.currentThread().getName()+"打印"+sum);
return sum;
}, executor);
System.out.println("做其他事情...");
try {
System.out.println("result:" + result.get());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("事情都做完了...");
executor.shutdown();
}
复制代码
结果:
正在计算...
做其他事情...
pool-1-thread-1算完了!
pool-1-thread-2打印4950
result:4950
事情都做完了...
复制代码
只需要简单的使用thenApplyAsync就可以实现了。 当然CompletableFuture还有很多其他的特性,我们下次单独开个专题来讲解。
Java8的特性还有Stream和Optional,这两个也是用的特别多的,相信很多同学早有耳闻,并且已经对这两个的特性有所了解,所以本片博客就不进行讲解了,有机会再单独讲解,可讲的内容还是非常之多的 对于Java8,新增的特性还是非常之多的,就是目前Java11已经出了,但是Java8中的特性肯定会一直在后续的版本中保留的,至于这篇文章的这些新特性我们估计用的比较少,所以特已此篇来进行一个普及,希望都有所收货。