Java8 新特性简介.
Java8 是自Java1.0 以来最具革命性的版本,在语法特点、编译器、类库、开发工具以及 JVM 等方面都带来了不少新特性,其中最为核心的为
Lambda 表达式与 Stream API。
☆Lambda 表达式
Lambda 表达式是 Java8 语法新增最大的卖点,将函数式编程引入了Java,允许把行为(代码) 看成数据,作为一个方法的实参。
☆Stream API
Stream API 是把函数式编程风格引入到 Java 自带 API 中。从语法上看比较像链式编程,代码写起来简洁明了,可以根据需求对数据进行层层处理,非常酷帅!
☆ 其他优势
代码简洁 :函数式编程写出的代码简洁且意图明确,比如使用 Stream 接口代替复杂的 for 循环。
多核友好 :Java 函数式编程自带编写并行程序的方法,只需要调用 parallel() 方法即可实现并行程序开发。
2.函数式编程
2.1 面向对象的束缚
需求: 启动一个线程来执行 输出一句话的 代码
代码如下:
@Test
public void testOOP() throws Exception { new Thread(new Runnable() { public void run() {
System.out.println("开一个子线程处理一个耗时操
作");
}
}).start();
}
为了完成需求,需创建一个 Runnable 接口的匿名内部类对象来给 Thread 对象指定任务内容,再启动该线程。
问题:创建匿名内部类对象不是我们的本意,只是面向对象的思想让我们不得不创建一个匿名内部类对象来完成咱们的需求。
需求:给 Thread 对象传递要执行的代码(输出一句话的代码)并执行。
所以如果咱们将关注点从“怎么做”回归到“做什么”的本质上,会更加的简单,只要能够达到目的,如果有更简单的方式,何乐而不为?
有没有更好更简单的方式?函数式编程
2.2 函数式编程的使用
函数式编程思想让咱们更加关注咱们的需求,也就是 “做什么”
@Test
public void testLambda() throws Exception {
new Thread(()->System.out.println("开一个子线程处理一个耗时操作")).start();
}
这段代码和刚才的执行效果是完全一样的。
从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
瞬间感觉得到了解放!不再有“不得不创建 Runnable接口对象”的束缚,不再有“覆盖重写抽象方法”的负担,就是这么简单!
2.3 结论
函数式编程思想可以简单快速的满足需求、达到目标效果。而随着技术的发展,在某些场景下,这种思想更能得到程序员的青睐,所以 Java8 中就加入该思想,并在 Lambda 表达式和 Stream 中发挥的淋漓尽致!
3. 体验
根据用户提出的不同条件筛选出满足相应条件的商品
需求1: 筛选出所有名称包含手机的商品
需求2: 筛选出所有价格大于1000的商品
3.1 准备数据
public class HelloJava8{
private static List<Product> products = new ArrayList<> ();
static {
products.add(new Product(1L, "小狼手机", 8888.88,"手机"));
products.add(new Product(2L, "狼码手机", 6666.66,"手机"));
products.add(new Product(3L, "小狼笔记本", 7777.77,"电脑"));
products.add(new Product(4L, "机械键盘", 999.99,"键盘"));
products.add(new Product(5L, "狼码鼠标", 222.22,"鼠标"));
}
}
public class Product {
private Long id; //商品编号
private String name; //商品名称
private Double price; //商品价格
private String brand; //品类
//省略 setter/getter方法
//省略 toString方法
}
3.2 传统方式:
public static void main(String[] args) {
//需求1: 筛选出所有名称包含手机的商品
List<Product> list = findPhoneByName(products); for (Product product : list) {
System.out.println("product = " + product);
}
//需求2: 筛选出所有价格大于1000的商品
List<Product> productList=findPhoneByPrice(products);
for (Product product : productList) {
System.out.println("product = " + product);
}
}
private static List<Product> findPhoneByPrice(List<Product> products) {
List<Product> list = new ArrayList<>();
for (Product product : products) {
if (product.getPrice()>1000) {
list.add(product);
}
}
return list;
}
private static List<Product> findPhoneByName(List<Product> products) {
List<Product> list = new ArrayList<>(); f
or (Product product : products) {
if (product.getName().contains("手机")) {
list.add(product);
}
}
return list;
}
3.3 匿名内部类改造:
public static void main(String[] args) {
//需求1: 筛选出所有名称包含手机的商品
List<Product> productList = findPhoneByCondition(products, new MyPredicate() {
public boolean test(Product product) {
return product.getName().contains("手机");
}
});
for (Product product : productList) {
System.out.println(product);
}
//需求2: 筛选出所有价格大于1000的商品
List<Product> productList1 = findPhoneByCondition(products, new MyPredicate() {
public boolean test(Product product) {
return product.getPrice()>1000;
}
});
for (Product product : productList1) {
System.out.println("product = " + product);
}
}
//定义通用型方法,根据传入的条件动态的筛查
private static List<Product> findPhoneByCondition(List<Product> products, MyPredicate predicate){
List<Product> productList = new ArrayList<>();
for (Product product : products) {
if (predicate.test(product)) {
productList.add(product);
}
}
return productList;
}
//定义一个接口,用来判断
public interface MyPredicate {
boolean test(Product product);
}
3.4 Lambda表达式改造:
public static void main(String[] args) {
//需求1: 筛选出所有名称包含手机的商品
List<Product> productList = findPhoneByCondition(products, (p)->p.getName().contains("手机"));
for (Product product : productList) {
System.out.println(product);
}
//需求2: 筛选出所有价格大于1000的商品
List<Product> productList1 = findPhoneByCondition(products,(p)->p.getPrice()>1000);
for (Product product : productList1) {
System.out.println("product = " + product);
}
}
private static List<Product> findPhoneByCondition(List<Product> products, MyPredicate predicate){
List<Product> productList = new ArrayList<>();
for (Product product : products) {
if (predicate.test(product)) {
productList.add(product);
}
}
return productList;
}
public interface MyPredicate {
boolean test(Product product);
}
3.5 Stream改造:
public static void main(String[] args) {
//需求1: 筛选出所有名称包含手机的商品
products.stream().filter(p->p.getName().contains("手机")).forEach(System.out::println);
System.out.println("---------------------");
//需求2: 筛选出所有价格大于1000的商品
products.stream().filter(p->p.getPrice()>1000).forEach(System.out::println);
}
4. Lambda语法
4.1 lambda语法图解
在上图中,Thread类的构造器中需要传入一个Runnable接口的实现类,而Runnable接口中只有一个run方法(实现类中需要实现该方法并将任务代码写该方法中)
那既然咱们知道Thread需要一个Runnable,并且只需要重写一个run方法,并且run方法中就是咱们要写的任务代码,那咱们为何不直接把咱们要完成的任务代码传递给Thread呢?
所以咱们就可以使用 Lambda 语法,将咱们要完成的任务直接传递给
Thread:
()->System.out.println("使用Lambda方式开启线程")
这种新的语法就是: “->”
“->”被称为 Lambda 操作符或箭头操作符。----固定语法
它将Lambda 分为两个部分:
左侧:指定了Lambda 表达式需要的所有参数 ---参数run方法不需要参数,所以可以直接写()
右侧:指定了Lambda 体,即Lambda 表达式要执行的任务、功能或者说是行为。---方法体
我们要完成的功能就是在run方法中输出一句话,所以直接写System.out.println("使用Lambda方式开启线程")
5.什么是函数式接口?
5.1 定义
只要确保接口中有且仅有一个抽象方法即可
5.2 格式
修饰符 interface 接口名称 {
[public abstract] 返回值类型 方法名称(可选参数信息);
☆其他
}
为了便于区分并方便编译器进行语法校验, JDK8 中还引入了一个新的注解:
@FunctionalInterface
该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,该接口仍然是一个函数式接口,使用起来都一样。(可以类比下@Override注解的作用)
我们可以去看下 Runnable 接口的源码,发现它就是一个函数式接口那么我们也可以来定义一个简单的函数式接口:
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
5.3 特征
1.可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
2.可选的参数圆括号():一个参数无需定义圆括号,但多个参数需要定义圆括号()。
3.可选的大括号{}:如果主体包含了一个语句,就不需要使用{}。
4.可选的返回关键字return:如果主体只有一个表达式返回值则可以省略return和{}
public static void main(String[] args) {
List<String> list = Arrays.asList("a","c","b"); Map<String,String> map = new HashMap<>(){
{
this.put("手机","小米");
this.put("笔记本","苹果笔记本"); this.put("鼠标","罗技鼠标");
}
};
1.可选类型声明:不需要声明参数类型,编译器可以统一识别参数值
list.forEach((String x)->System.out.println(x));
list.forEach((x)->System.out.println(x)); System.out.println("-------------------------");
2.可选的参数圆括号():一个参数无需定义圆括号,但多个参数需要定义圆括号()
list.forEach((x)->System.out.println(x));
list.forEach(x->System.out.println(x));
map.forEach((x,y)->System.out.println(x+":" +y));
System.out.println("-------------------------");
3.可选的大括号{}:如果主体包含了一个语句,就不需要使用{}
list.forEach(x->System.out.println(x));
list.forEach(x->{
System.out.println(x);
System.out.println(x);
});
System.out.println("-------------------------");
4.可选的返回关键字return:如果主体只有一个表达式返回值则可以省略return和{}
list.stream().sorted((s1,s2)->s2.compareTo(s1)).forEach(x->System.out.println(x));
System.out.println("-------------------------");
}
总结
Lambda表达式极大的丰富了Java语言的表现力,简化了Java代码的编写,但是语法细节我们或许很难记住,那么只需要记住一条:可推断则可省略!能省则省
注意
在某些情况下,类型信息可以帮助阅读者理解代码,这时需要手动声明类型,让代码便于阅读。有时省略类型信息更加简洁明了,还可以减少干扰,让阅读者关注业务逻辑,这时就可以省略;所以在开发中建议大家根据自己或项目组的习惯结合实际情况,进行类型声明或省略。
3.常见的函数式接口
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function包中被提供。
上面用到过的Consumer接口就是其中之一,接下来给大家介绍下其他的常用函数式接口
7.方法引用
在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑呢?
7.1 之前方法引用存在的问题
a.不够精简的Lambda
看一下我们之前写的代码
Arrays.asList("a", "b", "c").forEach((x) -> System.out.print(x));
b.问题分析
这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明
已经有了现成的实现,那就是System.out对象中的println(String) 方法。
既然Lambda希望做的事情就是调用println(String) 方法,那何必自己手动调用呢?
能否省去Lambda的语法格式呢(尽管它已经相当简洁)?
c.问题解决
只要“引用”过去就好了!
请注意其中的双冒号:: 写法,这被称为“方法引用”,而双冒号是一种新的语法。
Arrays.asList("a", "b", "c").forEach(System.out::println);
d.语法分析
双冒号:: 为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
下面两种写法的执行效果完全一样,而方法二方法引用的写法复用了已有方案,更加简洁。
方法一:Lambda表达式:s -> System.out.println(s);
拿到参数之后经Lambda之手,继而传递给System.out.println 方法去处理。
方法二:方法引用: System.out::println
直接让System.out 中的println 方法来取代Lambda。
7.2 方法引用的语法
(1)对象的引用 :: 实例方法名
(2)类名 :: 静态方法名
(3)类名 :: 实例方法名
(4)类名 :: new (构造器引用)
public static void main(String[] args) {
1.对象的引用 :: 实例方法名
Arrays.asList("a","b","c").forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
Arrays.asList("a","b","c").forEach(s->System.out.println(s)); // 前
Arrays.asList("a","b","c").forEach(System.out::println );//后
System.out.println("-----------------------");
2.类名 :: 静态方法名
Supplier<Double> sup = new Supplier<Double>() { @Override
public Double get() {
return Math.random();
}
};
Supplier<Double> supp = ()->Math.random();// 前
Supplier<Double> suppp = Math::random;// 后
System.out.println(suppp.get());
System.out.println("-----------------------");
3.类名 :: 实例方法名
Function<Product,String> fun = new Function<Product, String>() {
@Override
public String apply(Product product) { return product.getName();
}
};
Function<Product,String> funn = (p)->p.getName(); // 前
Function<Product,String> funnn = Product::getName; // 后
String productName = funnn.apply(new Product(2L,"小米手机",3200d));
System.out.println("productName = " + productName);
System.out.println("-----------------------");
4.类名 :: new (构造器引用)
Supplier<Product> snew = new Supplier<Product>
() {
@Override
public Product get() {
return new Product();
}
};
Supplier<Product> snew1 = ()->new Product(); //
前
Supplier<Product> snew2 = Product::new; // 后
System.out.println(snew2.get());
System.out.println("-----------------------");
;
}
8.Lambda表达式的延迟执行
到这里我们已经学会了使用Lambda表达式以及它的孪生兄弟方法引用来简化我们代码的编写,但是Lambda表达式的强大可不仅仅只是简化语法哦!
8.1 记录日志时的性能浪费
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:
public static void main(String[] args) {
String name = "张无忌";
String action="夜店消费";
String money = "20000元";
☆立即拼接字符串
logMethod("info",name + action + money);
}
private static void logMethod(String info, String s) {
if ("info".equals(info)) {
System.out.println(s);
}
}
这段代码存在问题:无论级别是否满足要求,作为log 方法的第二个参数,三个字符串一定会首先被拼接并传入方
法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。
备注:SLF4J(应用非常广泛的日志框架)在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进行字符串拼接。例如: LOGGER.debug("变量{}的取值为{}。", "os", "windows") ,其中的大括号{} 为占位符。如果满足日志级别要求,则会将“os”和“windows”两个字符串依次拼接到大括号的位置;否则不会进行字符串拼接。这也是一种可行解决方案,但Lambda可以做到更好。
8.2 使用Lambda拒绝性能浪费
定义一个函数式接口并对log方法进行改造
这样一来,只有当级别满足要求的时候,才会进行三个字符串的拼接;否则三个字符串将不会进行拼接。
public static void main(String[] args) {
String name = "张无忌";
String action="夜店消费";
String money = "20000元";
//延迟拼写字符串
logLambdaMethod("info",()->{ System.out.println("come in"); return name + action+ money;
});
}
private static void logLambdaMethod(String level, Supplier<String> sup) {
if ("info".equals(level)){
System.out.println("come in");
System.out.println(sup.get());
}
}
9.更强大的接口
在JDK 1.7及更老的版本中,接口中只能包含常量与抽象方法两种内容,不允许包含其他。但是这种情况在JDK 1.8中已经改变:接口中允许包含default方法和static方法并指定方法体的具体实现。
9.1 默认方法
从JDK 1.8开始,可以在接口名称不变的情况下,追加新的方法定义,而对已有的若干实现类不产生任何影响。这种新添加的方法需要使用default关键字进行修饰并指定方法体实现,被称为“默认方法”。例如例中的Callable接口,如果添加一个autoCall方法:
interface Callable{
void call();
default void autoCall(){
System.out.println("自动拨号");
}
}
而IPhone和Galaxy两个实现类不需要任何修改,即可继续使用,这是保证接口向后兼容性的更优手段。
9.1.1 问题:接口冲突
考虑一种场景,如果有两个接口中都指定了default方法且签名一样,那么当一个实现类同时实现了两个接口时会怎么样?例如
接口A:
接口B:
实现类(编译时报错):
//Duplicate default methods named method with the parameters ()
and ()
//are inherited from the types InterfaceB and InterfaceA public class InterfaceImpl implements InterfaceA, InterfaceB { }
这段代码是错误的,编译错误将会发生在实现类中。
interface A{
default void print(){
System.out.println("A");
}
}
interface B{
default void print(){
System.out.println("B");
}
}
class C implements A,B{
}
9.1.2 解决:方法覆盖
当同时实现的多个接口中存在签名一样的若干个方法体实现(无论内容是否完全相同)时,实现类必须进行覆盖重写以解决冲突。
interface A{
default void print(){
System.out.println("A");
}
}
interface B{
default void print(){
System.out.println("B");
}
}
class C implements A,B{
@Override
public void print() {
System.out.println("这个print方法必须被覆写");
}
}
总结:
为什么接口中可以有具体方法?
接口中的default方法是一个无奈之举,在Java 7及之前要想在定义好的接口中加入新的抽象方法是很困难甚至不可能的,因为所有实现了该接口的类都要重新实现。default方法就是用来解决这个尴尬问题的,直接在接口中实现新加入的方法。既然已经引入了default方法,为何不再加入static方法来避免专门的工具类呢!
9.2 静态方法
Collection和Collections、File和Files……这些带有s后缀的类从名称上
约定为对应的接口或类的工具类。然而这是一种妥协方案,并不是最优方案。
考虑以下几个问题:
1)对于多个对象的公共特征,如何复用?提高其抽象层次。
2)为什么优先实现接口而不是继承父类?因为Java有单继承的限制。
3)对于不依托于对象的内容,应该如何设计?使用静态。
结合这三个问题,JDK 1.8中允许在接口中定义静态方法并指定方法体实现。例如:
interface Callable{
void call();
default void autoCall(){
System.out.println("自动拨号");
}
static void videoCall(){
System.out.println("视频通话");
}
}
这样我们就不再需要Collections和Files这样单独的工具类了。
当然,为了保证向后兼容性,它们也被保留。
并且在Java9已经在java.util.List接口中 加入了大量的static静态方法of的重载以设计出更优的API:直接返回本接口对象。
当然在Java8中很多API接口中也加入了静态方法比如:Comparator接口