通过一些小代码段,看看Java8新的特性吧
Java8允许我们使用default 关键字来实现对非抽象方法的实现,例:
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
看,Formula定义了一个抽象方法calculate,然后又定义了一个默认方法sqrt,具体的类只要实现抽象方法calculate即可,默认方法可以直接使用。
Formula formula = new Formula() {
@Override
public double calculate(int a) {
return sqrt(a * 100);
}
};
formula.calculate(100); // 100.0
formula.sqrt(16); // 4.0
上面的这段代码中,formula是通过匿名对象实现的。当然,这段代码看起来过于繁琐,用6行代码实现一个小小的计算sqrt(a * 100),不过在下面我们会看到Java8用更加帅气的方式实现单个方法对象的做法,那就是
让我们先来一个小例子:看看在之前的Java版本中如何实现对String字符串进行排序
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});
Collections.sort这个方法的参数是一个列表和一个比较器,它可以对你给出的list进行排序。这样你就发现经常需要创造一些匿名的comparators来进行排序方法。
为了取代一直以来的创造匿名对象的方式,Java8推出了更加简洁的语法:lambda表达式:
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});
就像你看见的这段代码,它,更加的简洁,更加的清晰易读,但是,它还可以更加的简洁:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
这行代码,你可以省略掉{}和关键字return,但是还可以更简洁:
Collections.sort(names, (a, b) -> b.compareTo(a));
Java编译器可以自动判断参数类型,因此,类型也可以省略
感受到了简洁的语法后,我们需要深入了解一下,看看Lambda表达式怎样在实际工作中使用:
Lambda表达式是怎样匹配Java类型系统呢?通过接口指定,每一个lambda对应一个给定的类型。一个被称为函数式接口必须要包含一个准确的抽象方法的声明。这样每个lambda和它的对应类型将会匹配到个抽象方法。因为default method不是抽象的,所以可以自由的在函数式接口中添加default方法
只要这个接口包含唯一的一个抽象方法,我们就可以使用任意的接口。为了确定你的接口满足要求,需要添加一个@FunctionalInterface注解,这样,当你试图给这个接口添加第二个抽象方法声明的时候,编译器就会自动感知,会抛出一个编译期错误。
栗子:
@FunctionalInterface
interface Converter<F, T> {
T convert(F from);
}
<!-- lang:java -->
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted); // 123
记住:如果代码中没有写注解 @FunctionalInterface,那么也是错误的!
上面实例的代码通过使用静态函数引用可以进一步的简化
Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted); // 123
Java 8 允许你使用关键字 :: 来引用方法和构造器。上面的代码显示的是怎样引用一一个静态方法。但是我们同样,也可以引用对象方法。
class Something {
String startsWith(String s) {
return String.valueOf(s.charAt(0));
}
}
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"
让我们看看 :: 关键字对于构造方法是如何工作的。首先我们需要构造一个带有不同构造器的bean实例:
class Person {
String firstName;
String lastName;
Person() {}
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
然后我们来指定一个person 工厂接口,为创建person对象时使用:
interface PersonFactory<P extends Person> {
P create(String firstName, String lastName);
}
我们可以通过构造器引用来连接所有的东西,来取代手动的创建工厂
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
我们通过Person::new 创建了一个Person构造器的引用,Java编译器会通过PersonFactory.create的方法签名,匹配到正确的构造器方法。
从Lambda表达式中获取外部作用域变量的方式与匿名对象的方式非常相似,你可以获取final变量从局部外部变量中,跟实例域和静态变量一样。
获取局部变量
我们可以读取final局部变量从lambda表达式的外部作用域中
final int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
但是不同于匿名对象,变量num不需要必须设置为final,下面的代码同样有效:
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
然而,num是一个隐式的final局部变量,编译器才能通过。下面这段代码就不会编译通过:
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
num = 3;
在Lambda表达式内部修改num的值同样是不被允许的。你只要把这个变量看做final修饰的即可,只是final没有写出来罢了。
获取实体域和静态变量
不同于局部变量,我们在具有Lambda表达式的代码中,可以对实体域和静态变量进行读和写的权利。这种方式同样在匿名对象中为我们所熟知。
class Lambda4 {
static int outerStaticNum;
int outerNum;
void testScopes() {
Converter<Integer, String> stringConverter1 = (from) -> {
outerNum = 23;
return String.valueOf(from);
};
Converter<Integer, String> stringConverter2 = (from) -> {
outerStaticNum = 72;
return String.valueOf(from);
};
}
}
获取默认接口方法:
还记得开头讲述的formula 那个栗子吗?接口fomula定义个一个默认方法:sqrt,可以被包含匿名对象的每一个formula实例使用,这种机制跟lambda表达式无关
默认方法不能被lambda表达式使用,下面的代码不能被编译:
Formula formula = (a) -> sqrt( a * 100);