美文网首页
第20讲.java8的新特性

第20讲.java8的新特性

作者: 祥祺 | 来源:发表于2019-09-29 20:17 被阅读0次

Java8的新特性

Java的概述

1995年Sun公司推出的Java语言,从第一个版本诞生到现在已经有二十多年的了。
时间若白驹过隙,转瞬即逝。二十多年来IT技术更新换代,编程语言层出不穷。
就像自然界遵循优胜劣汰的法则,编程语言也是一样,很多老牌的编程语言被新兴的编程语言替代,逐渐没落甚至退出历史舞台,
然而Java作为一门有着二十多年历史的编程语言却越发显得生机勃勃!宝刀未老!
究其原因,其中很重要的一点就是Java语言不断进行版本迭代推出一系列符合技术发展趋势的新特性!
目前JavaEE最新定位于处理高并发,微服务架构等

升级Java语言走向新高潮存在以下几个大障碍:
1:接口升级需要重新去改动该接口所有的实现类
2:提高对数据的处理效率
3:简化设计模式
4:java不能用于js
等等...
而Java8的改革就是为Java语法走向新高潮奠定基础

Java8的新特性简介

Java 8可谓是自Java 5以来最具革命性的版本了,她在语言、编译器、类库、开发工具以及Java虚拟机等方面都带来了不少新特性:
(其中最为核心的为Lambda 表达式与Stream API)
★ Lambda表达式
Lambda表达式可以说是Java 8最大的卖点,她将函数式编程引入了Java。Lambda允许把函数作为一个方法的参数,或者把代码看成数据。
★ Stream API
Stream API是把真正的函数式编程风格引入到Java中。其实简单来说可以把Stream理解为MapReduce。从语法上看,也很像linux的管道、或者链式编程,代码写起来简洁明了,非常酷帅!
● 接口的默认方法与静态方法
我们可以在接口中定义默认方法,使用default关键字,并提供默认的实现。所有实现这个接口的类都会接受默认方法的实现,除非子类提供的自己的实现。
● 方法引用
通常与Lambda表达式联合使用,可以直接引用已有Java类或对象的方法。
● 重复注解
Java 8引入重复注解,相同的注解在同一地方也可以声明多次。
● 扩展注解的支持
Java 8扩展了注解的上下文,几乎可以为任何东西添加注解,包括局部变量、泛型类、父类与接口的实现,连方法的异常也能添加注解。
● Optional
Java 8引入Optional类来防止空指针异常,使用Optional类我们就不用显式进行空指针检查了。

为何要学习java8新特性

通过文档我们可以看到Java8添加了很多API用于对函数式编程的支持,可以看出Java8对函数式编程的重视程度。
Java8之所以费这么大功夫引入函数式编程,原因有二:
1.代码简洁,函数式编程写出的代码简洁且意图明确,比如使用stream接口让你告别for循环。
2.多核友好,Java函数式编程使得编写并行程序如此简单,只需要调用一下parallel()方法即可。
“对于习惯了面向对象编程的开发者来说,抽象的概念并不陌生。面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。现实世界中,数据和行为并存,程序也是如此,因此这两种编程方式我们都得学。
函数式编程这种新的抽象方式还有其他好处,例如:
不是所有人都在编写性能优先的代码,对于这些人来说,函数式编程带来的好处尤为明显。程序员能编写出更容易阅读的代码——这种代码更多地表达了业务逻辑的意图,而不是它的实现机制。易读的代码也易于维护、更可靠、更不容易出错。比如,在写回调函数和事件处理程序时,程序员不必再纠缠于匿名内部类的冗繁和可读性,函数式编程让事件处理系统变得更加简单。能将函数方便地传递也让编写惰性代码变得容易,惰性代码在真正需要时才初始化变量的值。
总而言之,Java 已经不是祖辈们当年使用的Java 了,嗯, 这不是件坏事。”
--摘自《Java8 Lambdas Exercises》

面向对象编程的束缚

面向对象的束缚
在正式讲Lambda表达式和函数式编程之前我们先看一下,分析一下面向对象编程的不足!
需求:启动一个线程并输出一句话 。 如图:

1.png

当我们需要启动一个线程去完成一个任务的时候,通常会通过 Runnable接口来定义任务内容并通过 Thread类来启动该线程。
本着“万物皆对象”的思想,创建一个Runnable接口的匿名内部类对象(线程对象)来指定任务内容,再启动该线程。这种做法是Ok的,但是……
我们真的想创建一个匿名内部类对象吗?
不。我们只是为了做这件事情而不得不创建一个对象。
我们真正希望做的事情是:将 run方法方法体内的代码传递给Thread类。
传递并执行这一段代码,才是我们真正的目的。
而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。
那,有没有更加简单的办法?
如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

函数式编程的解放

函数式编程思想让我们更加关注我们的目标也就是:“做什么”

2.png

这段代码和刚才的执行效果是完全一样的。
从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
瞬间感觉得到了解放!不再有“不得不创建 Runnable接口对象”的束缚,不再有“覆盖重写抽象方法”的负担,就是这么简单!
总结:
Java语言一大特点与优势就是面向对象,但是随着技术的发展,我们会发现面向对象思想在某些场景下并非是最优的思想。能够简单快速满足需求、达到目标效果的思想有时候可能更加适合。这种思想就是函数式编程思想!Java8中的Lambda表达式就可以让我们把这种思想发挥的淋漓尽致!
感觉不过瘾?那我们再来几个案例对比下!

java8 lambda语法揭秘

3.png 4.png

结合代码图解分析:两个红色框: 用r变量指向一个接口对象,
两个绿色框:把变量r传入Thread构造器中,
两个蓝色框:通过打印结果发现,两个蓝色框的功能是一样的即两者是等价的,所以分析得知右图。
lambda的语法:有三部分组成:函数,->,方法体
和传统方法对比:
()相当于run方法,只是没有方法名称
->是语法的固定写法
方法体都是一样的,方法的实现

什么是函数式编程

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型。这个定义还是太模糊的,让我们从以下几个关键词慢慢道来。
简洁——无需像匿名类那样写很多模板代码。
匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而做得多!
函数——我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方法一样Lambda有参数列表、函数主体、返回类型。
传递——Lambda表达式可以作为参数传递给方法或存储在变量中。

那到这里我们已经清楚了Lambda表达式的基本语法格式,那再继续思考一个问题:

是不是所有的接口类型作为参数传递(传统写法就是匿名内部类)都可以使用Lambda表达式呢?

答案是不可以!

lambda 表达式:只有接口中只有一个抽象方法的时候,Lambda表达式才可以正确的“猜测”到你传递的任务实际上是在实现接口中的唯一的那一个抽象方法!
那么我们把这种只有一个抽象方法的接口称作为“函数式接口”,只有函数式接口才可以使用Lambda表达式进行函数式编程!

5.png

注解@FunctionInterface的作用

函数式接口的定义:只要确保接口中有且仅有一个抽象方法即可
格式:

修饰符 interface 接口名称 {
[public abstract] 返回值类型 方法名称(可选参数信息);
// 其他
}

为了便于区分并便于编译器进行语法校验,JDK8中还引入了一个新的注解:
@FunctionalInterface
该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。
需要注意的是,即使不使用该注解,只要满足函数式接口的定义,该接口仍然是一个函数式接口,使用起来都一样。(可以类比下@Override 注解的作用)
我们可以去看下Runnable接口的源码,发现它就是一个函数式接口

那么我们也可以来定义一个简单的函数式接口:

@FunctionalInterface
public interface MyFunctionalInterface {
    void myMethod();
}

那到这里我们已经知道了只要参数是函数式接口我们就可以使用Lambda表达式讲我们要完成的任务作为参数直接传递,但是接口中的方法是否有参数,有几个参数,是否有返回值情况可分为好多种呢!那我们就来具体看一下吧!

6.png

lambda的写法上的特征

// 以下是lambda表达式的语法特征:
// 1.可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。

7.png

// 2.可选的参数圆括号():一个参数无需定义圆括号,但多个参数需要定义圆括号()。

8.png

// 3.可选的大括号{}:如果主体包含了一个语句,就不需要使用{}。

9.png

// 4.可选的返回关键字return:如果主体只有一个表达式返回值则可以省略return和{}

10.png

函数式编程的魅力

准备的数据:

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Product {
private Long id; //商品编号
private String name; //商品名称
private Double price;; //商品价格
}

public class MagicLambda {
private List<Product> products = new ArrayList<>();
@Before
public void init(){
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));
}
}

需求:
根据用户提出的不同条件筛选出满足相应条件的商品,比如:

筛选出所有名称包含手机的商品

筛选出所有价格大于1000的商品

方案一: 采用传统的方式

11.png 12.png 13.png

存在的问题: 存在硬编码,代码不灵活

方案二:策略模式的方式

14.png 15.png

之前的传统模式根据条件过滤商品,存在很严重的弊端(当随着条件的增多,
就要相应的增加很多方法,一个方法只能处理一类条件的查询,存在硬编码,代码不灵活)
所以我们优化的方案就是把查询条件变成动态的(有外部去控制,而不是写死在代码当中)
策略模式的定义:
定义一系列的算法,把它们一个个封装起来,并且使它们可互相替换。本模式使得算法可独立于使用它的客户而变化。
策略模式的本质:分离算法,选择实现

方案三:
对于策略模式的方式,我们发现一个问题,具体的实现类我们只调用了一次,但是我new 出来一个对象,创建一个对象就要在内存中开辟堆栈空间,浪费资源,所以我们引入匿名内部类的方式来优化上述代码。

16.png 17.png

方案三:发现匿名内部类的方式有点不足就是代码过于臃肿,所以使用java8的新特性的方式进行瘦身。

18.png 19.png

常用接口

20.png

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function包中被提供。

函数式接口 参数类型 返回类型 说明
// 函数式接口类型 参数类型 返回类型 说明
// Consumer<T>消费型接口 T void 对类型为T的对象操作,方法:void accept(T t)
// 需求1:编写shop方法输出消费多少元

21.png

// 函数式接口类型 参数类型 返回类型 说明
// Supplier<T>供给型接口 无 T 返回类型为T的对象,方法:T get();可用作工厂
// 需求2:编写getCode方法返回指定位数的随机验证码字符串

22.png

// 函数式接口类型 参数类型 返回类型 说明
// Function<T, R>函数型接口T R 对类型为T的对象操作,并返回结果是R类型的对象。方法:R apply(T t);
// 需求3:编写getStringRealLength方法返回字符串真实长度

23.png

// 函数式接口类型 参数类型 返回类型 说明
// Predicate<T>断言型接口 T boolean 判断类型为T的对象是否满足条件,并返回boolean 值。方法boolean test(T t);
// 需求4:编写getString方法返回长度大于5的字符串的集合

24.png 25.png

遇到只有传入参数,没有输出结果的,选择Consummer接口

遇到对传入的对象进行修改的改变类型的(对传入的内容进行加工的)就选择Function

遇到判断的传入的参数是否满足条件的 就选择Predicate接口
遇到只有输出结果,没有传入参数的,选择Supplier接口

方法引用

List<Integer> list = Arrays.asList(2, 4, 6, 9, 1);

list.forEach(x->System.out.println(x));

/**

  • 有上面的匿名内部类演化而来
  • lambda 表达式 从语法上分析 x 是一个变量 在这里代表 集合(aList)里面的每一项,
  • 通过 PrintStream 对象 调用了 println 方法 进行输出 采用了java 中 对象.方法的方式
    */

/**

  • 对上述的方式,我们还可以使用lambda 给我们提供的调用方法的方式 用的是 对象::方法 的方式进行输出
    */
    list.forEach(System.out::println); 通过观察 x 变量省略了,和变量的类型 能省略的道理是一样的
    lambda 语法能推测出遍历的内容类型

注意
方法引用:

所引用的方法的参数列表与返回值类型,需要与函数式接口中抽象方法的参数列表和返回值类型保持一致!
格式: ClassName::MethodName

Lambda 的参数列表的第一个参数,是实例方法的调用者(有时候是类名,有时候是对象名),第二个参数(或无参)是方法

构造器引用:

格式:类名 :: new
构造器的参数列表,需要与函数式接口中参数列表保持一致!

总结
如果使用Lambda,那么根据“可推断则可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。
// 1.对象的引用 :: 实例方法名 (System.out::println)

26.png

// 2.类名 :: 静态方法名 (Supplier<Double> s = Math::random;)

27.png

// 3.类名 :: 实例方法名 (Function<Product,String> f = Product::getName;)

28.png

// 4.类名 :: new (构造器引用) (Supplier<Product> sp = Product::new;)

29.png

// 5.类型[] :: new (数组引用) (Function<Integer,String[]> f2 = String[]::new;)

30.png

接口的扩展API

从JDK 1.8开始,可以在接口名称不变的情况下,追加新的方法定义,而对已有的若干实现类不产生任何影响。这种新添加的方法需要使用default关键字进行修饰并指定方法体实现,被称为“默认方法”。

31.png

Stream 流 概述

什么是Stream--是操作数据的一套工具
Java8中有两大最为重要的特性。
1)Lambda 表达式,已经学习过了
2)Stream API (java.util.stream.*包下)

说到Stream便容易想到I/O Stream,而实际上我们这里讲的Stream它是Java8中对数据处理的一种抽象描述;
我们可以把它理解为数据的管道,我们可以通过这条管道提供给我们的API很方便的对里面的数据进行复杂的操作!比如查找、过滤和映射(类似于使用SQL);
更厉害的是可以使用Stream API 来并行执行操作;
简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式,解决了已有集合类库操作上的弊端。
注意:
1.请暂时忘记对传统IO流的固有印象!
2.Stream接口继承关系

32.png

stream接口继承自BaseStream,其中IntStream, LongStream, DoubleStream对应三种基本类型(int, long, double,注意不是包装类型),Stream对应所有剩余类型的stream视图。为不同数据类型设置不同stream接口,可以提高性能,并针对不同数据类型提供不同方法实现。那什么不把IntStream等设计成Stream的子接口?毕竟这接口中的方法名大部分是一样的。答案是这些方法的名字虽然相同,但是返回类型不同,如果设计成父子接口关系,这些方法将不能共存,因为Java不允许只有返回类型不同的重载。

流操作

使用Stream API操作数据可以分为以下几个步骤:

1)创建流:
通过数据源(如:集合、数组)获取流

2)处理流:(中的数据)
对流中的数据进行处理(处理是延迟执行的)

3)收集流:(中的数据)
通过调用收集方法,真正执行处理操作,并产生结果

创建一个流非常简单,有以下几种常用的方式:
1)Collection的默认方法stream()和parallelStream()
2)Arrays.stream()
3)Stream.of()

33.png

处理Stream流-----filter

filter(Predicate<T> p):过滤(根据传入的Lambda返回的ture/false 从流中过滤掉某些数据(筛选出某些数据))

34.png

需求:按照条件过滤 1)找出姓马的; 2)再从姓马的中找到名字长度等于3的。
数据:
List<String> list = Arrays.asList("马云","马三立","马化腾","马莉","张国立","袁立","沈腾");

35.png

处理Stream流-----map

map(Function<T, R> f):接收一个函数作为参数,该函数会被应用到流中的每个元素上,并将其映射成一个新的元素。

36.png

需求把int 类型的集合 变成 String 类型的集合 打印
数据:
List<Integer> list =Arrays.asList(2,4,6,7);

37.png

处理Stream流-----sorted

排序
sorted():自然排序使用Comparable<T>的int compareTo(T o)方法
sorted(Comparator<T> com):定制排序使用Comparator的int compare(T o1, T o2)方法
// 需求: 把int 类型是集合排序输出
List<Integer> list = Arrays.asList(2,4,6,1,3,7,5);

38.png

处理Stream流----reduce

归约
reduce(T identity, BinaryOperator) / reduce(BinaryOperator) :将流中元素挨个结合起来,得到一个值。

39.png

// 需求: 求int 类型的集合里面的和
List<Integer> list = Arrays.asList(2,4,6,1,3,7,5);

处理Stream流----groupBy

分组
Collectors.groupingBy()对元素做group操作。可以分为多组

40.png

// 需求: 根据商品分类名称进行分组
private List<Product> products = new ArrayList<>();
@Before
public void init() {
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, "鼠标"));
}

41.png

收集流

汇总

reduce擅长的是生成一个值,如果想要从Stream生成一个集合或者Map等复杂的对象该怎么办呢?终极武器collect()横空出世!
collect(Collector<T, A, R>):将流转换为其他形式。
需求:
数据:Integer[] intArray = {3,4,5,2,1,6}

collect:将流转换为其他形式:list
collect:将流转换为其他形式:set

42.png

collect:将流转换为其他形式:map

43.png

collect:将流转换为其他形式:sum

44.png

collect:将流转换为其他形式:avg
collect:将流转换为其他形式:max

45.png

collect:将流转换为其他形式:min

46.png

其他API

47.png

深入理解Stream

那么,流到底是什么呢?简短的定义就是“从支持数据处理操作的源生成的元素序列”。
元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与 LinkedList)。但流的目的在于表达计算,比如filter、sorted和map。集合讲的是数据,流讲的是计算。
源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
此外,流操作有两个重要的特点。
流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。
内部迭代——与使用迭代器外部迭代的集合不同,流的迭代操作是在背后进行的。
--摘自《Java 8 In Action》

1.Stream vs Collection

虽然大部分情况下Stream是容器调用Collection.stream()方法得到的,但Stream和Collection有以下不同:
无存储。Stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,集合等。
不修改。对Stream的任何修改都不会修改背后的数据源,比如过滤操作并不会删除被过滤的元素,而是产生一个新Stream。
惰式执行。Stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
可消费性。Stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。
2.Stream分类
中间操作(intermediate operations)
返回值为Stream的大都是中间操作,中间操作支持链式调用,并且会惰式执行
终端操作(结束操作)(terminal operations)
返回值不为Stream 的为终端操作(立即求值),终端操作不支持链式调用,会触发实际计算

48.png 49.png

并行流

并发和并行

并发是多个任务共享时间段(由CPU切换执行,就像是在同时执行)
并行是多个任务发生在同一时刻(真真正正的同时执行)(必须在多核CPU下)
并行就像用更多的马车(CPU)来拉货(执行任务),货物总量(任务量)一定,那么花费的时间自然减少了。
所以并行可以缩短任务执行时间,提高多核CPU的利用率

数据并行化

任务可以并行化执行,同样的数据也可以并行化处理!

在Java8之前,当需要对存在于集合或数组中的若干元素进行并发操作时,简直就是噩梦!我们需要仔细考虑多线程环境下的原子性、竞争甚至锁问题。
使用Java5的java.util.concurrent.*并发库也还是要考虑诸多细节必须十分谨慎,
使用Java7的fork/join框架编码和调试对于一般程序员来说难度还是太大
而这一切对于Java8中的Stream,不过是小菜一碟!直接调用API方法就可以搞定!
(Stream API 可以声明性地通过parallel() 与sequential() 在并行流与顺序流之间进行切换)
1.转换为并行流
Stream 的父接口java.util.stream.BaseStream 中定义了一个parallel 方法:
只需要在流上调用一下无参数的parallel 方法,那么当前流即可变身成为支持并发操作的流,返回值仍然为
Stream 类型。例如:
Stream<Integer> stream = Stream.of(10, 20, 30, 40, 50).parallel();
2.直接获取并行流
在通过集合获取流时,也可以直接调用parallelStream 方法来直接获取支持并发操作的流。
代码为:
Stream<String> stream = new ArrayList<String>().parallelStream();
3.使用并行流
并行流后续操作的使用方式还是和以前一样,只是底层执行的时候会使用多CPU并行执行
比如多次执行下面这段代码,并行流的输出顺序在很大概率上是不一定的:
数据并行化是指将数据分成块,为每块数据分配单独的处理单元。也就是并行流

需求:演示并行流和顺序流之间的切换

50.png

需求:使用普通for和Stream和Parallel Stream 分别计算累加和并统计执行效率

51.png

相关文章

网友评论

      本文标题:第20讲.java8的新特性

      本文链接:https://www.haomeiwen.com/subject/qorpaqtx.html