半年前,因为工作需要,统计总结了一些关于Java9的新特性。而那时的我还是学生,现在却变成了一个社会人儿(手动滑稽)。Java10都发布几个月了,业也毕了,是时候拾起博客,继续坚定不移地向前走了,这次统计归纳一下Java8新特性中与程序猿息息相关的语言部分,下一篇将介绍其他特性。
英语好的童鞋可直接观看[官网文档],这可能是最全和最正确的介绍。(http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html)
为了支持函数式编程(函数式编程不明白的看这个),Java 8引入了Lambda表达式。与Java9中的模块化和Java10中的var一样,感觉是本次特性中最重要的,因为其的诞生导致一系列语法的变化。
Lambda表达式能够封装单个行为单元并将其传递给其他代码。如果您想要在一个集合的每个元素上执行某个动作,当一个进程完成,或者当一个进程遇到错误时,您可以使用lambda表达式。
Lambda表达式的诞生也由以下特性支持:
这些特性在下文将介绍。首先我们先看看什么是Lambda表达式
public static void main(String[] args) {
List list = Arrays.asList( "a", "b", "c" );
list.forEach((String e) -> System.out.println(e));
}
他的运行结果是:
a
b
c
(String e) -> System.out.println(e)
就是一个Lambda表达式,他还可以这样写:list.forEach( e -> System.out.println(e))
,String 可以省略,它的类型可以由编译器推理得出。
Map<String,String> map = new HashMap<>();
map.put("a", "1");
map.put("b", "2");
map.put("c", "3");
map.merge("a", "abc", (String a,String b)->a+b);
System.out.println(map);
运行结果为:
{a=1abc, b=2, c=3}
merge意思是取出map中a对应key的值,然后对值进行处理,再put(“a”,新值),我们采用lambda表达式,将key和value相加变为value,重新放入map。也可以变为语句块同样的道理map.merge("a", "abc", (String a,String b)->{b = b ;return b;});
。我们因为只有一句可以省略了return关键字和块。
总结一下,Java中Lambda的基本语法为:
(parameters) -> expression
或(parameters) ->{ statements; }
上文中的lambda只能赋值给函数化接口,或者直接使用,也不能赋予Object。函数化接口在Java中的实际意义就是只含有1
个抽象方法的接口,在Java中这些接口已经被@FunctionalInterface
打上注解。
如我们最常见的Runnable:
package java.lang;
//...
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
实例代码如下:
Runnable runable = ()->System.out.println("hello");
Thread thread = new Thread(runable);
thread.start();//hello
我们自己定义函数化接口时(使用@FunctionalInterface
注解),当接口中不止一个抽象方法时,编译器将会报错。
@FunctionalInterface
interface TestInterface {
public void run();
public void run2();
}
接口 | 参数 | 返回类型 | 描述 |
Predicate |
T | boolean | 用于判别一个对象。如list.removeIf(e->e==null); 将去除list中空值。 |
Consumer |
T | void | 用于接收一个对象进行处理并不需要返回值,如之前list.forEach((String e) -> System.out.println(e)); ,将遍历所有list中字符串并输出。 |
Function |
T | R | 转换一个对象为不同类型的对象 |
Supplier |
None | T | 提供一个对象 |
UnaryOperator |
T | T | 接收对象并返回同类型的对象 |
BinaryOperator |
(T,T) | T | 接收两个同类型的对象,并返回一个原类型对象 |
在我们之前的merge例子中可以用方法引用替代,如: map.merge("a", "abc", String::concat);
String::concat就是方法引用,实际上相当于:map.merge("a", "abc", (String a,String b)->return a.concat(b)););
方法引用不能独立存在,方法引用必须被转为函数式接口,否则编译器将报错。
类的静态方法引用 | ContainingClass::staticMethodName |
实例对象的实例方法引用 | containingObject::instanceMethodName |
类型上的实例方法引用 | ContainingType::methodName |
构造方法引用 | ClassName::new |
BinaryOperator binaryOperator = Math::pow;
System.out.println(binaryOperator.apply(2.0, 2.0));//4.0
Math::pow将接受两个参数,返回第一个参数的第二个参数次方。
list.forEach(System.out::println);
list.forEach(System.out::println);
相当于list.forEach(e-> System.out.println(e));
,将会输出e。
Arrays.sort(strings, String::compareTo);
String::compareTo
相当于(x,y)->x.compareTo(y)
,实则为一个Comparator,用来比较两者大小从而排序。
Stream<Person> stream = list.stream().map(Person::new);
map方法会为list中的每个元素调用Person的构造方法,生成一堆Person
注:如果有多个同名的重载方法,编译器将通过上下文判断参数类型和个数来确定使用哪个方法。lambda表达式中不能有和外界同名局部变量,使用的外界变量内部和外部都不可以修改(相当于final,中间过程中这个变量不会改变),否则编译器都将报错。lambda表达式中的this就是外界所说的this。
Java 8以前的接口中只存在抽象方法和常量,现在多了两种方法:默认方法和静态方法。默认方法可以使不强制那些实现了该接口的类也同时拥有这个新加的方法。比如java.util.Collection接口中的新方法,如stream()、parallelStream()、forEach()和removeIf()等,都可以在list,set中得到体现。
默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写,而静态方法可以直接通过接口调用。代码如下:
@FunctionalInterface
interface Test {
public abstract void run();
default void hello() {
System.out.println("hello");;
}
static String hi() {
return "hi";
}
}
Java 5开始引入注解,但是在同一个地方不能多次使用同一个注解。而Java 8引入了重复注解的概念,允许在同一个地方多次使用同一个注解。使用官网中的一个例子介绍:例如,您正在编写代码来使用一个计时器服务,使您能够运行一个方法在特定的时间或特定的时间表。现在你想设置一个定时器运行一个方法,doPeriodicCleanup在每月的最后一天,在每个星期五晚上11点左右。 设置定时器来运行,创建一个@Schedule注释,在doPeriodicCleanup方法上应用两次。 第一个使用指定本月的最后一天,第二个指定周五晚上11点。 以下代码示例所示:
@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }
这个注解相当于以下代码:
@Target( ElementType.METHOD )
@Retention( RetentionPolicy.RUNTIME )
public @interface Schedules{
Schedule[] value();
}
@Target( ElementType.METHOD )
@Retention( RetentionPolicy.RUNTIME )
@Repeatable( Schedule.class )
public @interface Schedule{
...
};
实际上是一个语法糖,语言底层并没有改变,Schedules是存放Schedule注解的容器,而Schedule类使用@Repeatable(Schedule.class)注解修饰,这样可以拥有多个相同注解。
Java编译器可以利用目标类型来推断泛型方法调用的类型参数。表达式的目标类型是Java编译器期望的数据类型,这取决于表达式出现的位置。例如,你可以在Java SE 7中使用赋值语句的目标类型来进行类型推断。然而,在Java SE 8中,你可以在更多的上下文中使用目标类型来进行类型推断。最突出的例子是使用方法调用的目标类型来推断其参数的数据类型。比如:
List<String> stringList = new ArrayList<>();
stringList.add("A");
stringList.addAll(Arrays.asList());
来自Java SE 7和更早版本的编译器不接受这段代码,因为它们不使用目标类型来推断方法调用参数的类型。例如,Java SE 7编译器生成一个类似于以下的错误消息:
error: no suitable method found for addAll(List ...
method List.addAll(Collection extends String>) is not applicable (actual argument List
在Java SE 7中Java编译器不能推断类型,必须显式地为其指定类型。例如:
List<String> stringList = new ArrayList<>();
stringList.add("A");
stringList.addAll(Arrays.<String>asList());
注解可以应用于场景:类、字段、方法和其他程序元素。在Java SE 8中,注释也可以应用于一些类型上。下面是一些例子:
类的实例化:
new @Interned MyObject();
类型转换:
myString = (@NonNull String) str;
实现语句:
class UnmodifiableList<T> implements
@Readonly List<@Readonly T> { ... }
判处异常声明:
void monitorTemperature() throws
@Critical TemperatureException { ... }
Java8中的ElementType(@Target()中的参数)多了TYPE_PARAMETER(类型参数声明)和TYPE_USE(使用的类型)两种。
可以用方法java.lang.reflect.executable.getparameters获得任何方法或构造函数的参数的名称。虽然,class文件默认不存储正式的参数名。为了在特定的.class文件中存储正式的参数名,从而使反射API能够获取正式的参数名,用javac编译器的-parameters
选项编译源文件,详情看这篇文章。