本篇文章会介绍Java 8
中的一些新特性(不包括Lambda
表达式,因为在Java多线程这篇文章中介绍过了)。主要内容是Java 8
中新增的函数式接口以及Stream
流,还有方法引用。
常用函数式接口
函数式接口指的就是接口里面只含有一个抽象方法。这样我们就可以使用Lambda
表达式编程,这是一种函数式编程的思想,强调的是怎么做。Java 8
提供了很多的函数式接口,这里我们介绍常见的函数式接口。
Supplier\<T>
Consumer\<T>
Predicate\<T>
Functional\<T,R>
Supplier<T>
该接口里面有一个T get()
方法,按照字面意思,是提供者的意思,表示生产出一个与泛型类型T
相同的数据。下面我们来讲一个例子说明此接口的使用。假设有一个方法,需要返回一个字符串,该字符串由Supplier
接口的get()
方法产生,而产生什么样的字符串,则由程序员在调用该方法是传入Lambda
决定。如下方法传入一个Supplier
接口得到一个字符串
public static String getString(Supplier<String> supplier) { |
我们在main
方法中调用该方法
String str = getString(() -> { |
输出为
Hello World! |
Consumer<T>
与Supplier
接口不同的是,Consumer
接口是消费或者说处理一个与泛型类型相同数据类型的数据,它有一个accept(T t)
方法,该方法用来消费数据,假设有下面这一个方法
public static void handleString(String str, Consumer<String> consumer) { |
我们使用Consumer
来消费(处理)传入的这个字符串,而怎么消费,就取决与程序员在调用该方法时传入的Lambda
,这时对程序员来说,就是怎么做的问题,相当于传入一个方法去处理数据,这就是函数式编程,这里我们就简单的将传入的数据进行打印
handleString("Hello Again!", (String str) -> { |
输出为
Hello Again! |
Consumer
接口中有一个默认方法andThen(Consumer consumer)
,看下面的程序说明它的用处
con1.andThen(con2).accept(str); //con1和con2都是Consumer接口的实现类对象 |
假设有一个方法有需要传入两个Consumer
接口对数据进行消费
public static void handleInterger(Integer i, Consumer<Integer> con1, Consumer<Integer> con2) { |
在mian
方法中使用,con1
对数字进行+10
然后打印,con2
对数字进行*10
然后打印
handleInteger(10, (i) -> { |
输出为
20 |
Predicate<T>
Predicate
接口中有一个test(T t)
,它的作用是对某种数据类型进行判断,它返回一个boolean
值。假设有一个集合,我们对其中的元素进行判断,符合条件放入一个新的集合,看下面的方法。
public static HashMap<String, Integer> getMap(HashMap<String,Integer> map, |
现在我们在main
方法中进行调用
HashMap<String,Integer> map = new HashMap<>(); |
运行结果为
{佟丽娅=20, 迪丽热巴=18, 古力娜扎=19} |
Predicate
还有三个默认方法
and(Predicate\<T> pre)
- 与
or(Predicate\<T> pre)
- 或
negate()
- 非
假设对于上面的那个方法,我提出一个新的需求,要求不仅年龄要小超过20
岁,而且年龄要大于18
public static HashMap<String, Integer> getMap(HashMap<String,Integer> map, |
在main
方法中调用该方法
HashMap<String,Integer> resmap = getMap(map, (Integer i) -> { |
输出结果为
{佟丽娅=20, 古力娜扎=19} |
至于or()
和negate()
的使用方法同上。
Function<T,R>
该接口的作用是将T
这种数据类型转化为R
这种数据类型,它里面有一个R apply(T t)
方法。下面这个方法将一个字符串转化为一个整数
public static Integer StrToInt(String str, Function<String, Integer> fun) { |
在main()
方法中调用该方法
Integer integer = StrToInt("123", (String str) -> { |
输出为
123 |
Function
接口中还有一个默认方法andThen(Function<T,R> fun)
,这个方法与在上面介绍的Consumer
接口的andThen()
很像,但是有点不同,Consumer
接口的andThen
是两个对象消费同一个数据,而Function
接口的addThen()
是将第一个fun
处理后的结果拿给第二个fun
去处理,相当于apply(apply())
。比如现在我有一个需求,将一个字符串转化为数字,然后将这个数字,+10
然后再转化为字符串,这个方法可以这么写
public static String StrPlus(String str, |
我们在main
方法中调用该方法
String s = StrPlus("123", (String str) -> { |
输出为
133 |
Stream流
Stream
流是Java 8
引入的新特性,首先它跟I/O
流没有任何的关系,它主要是用来处理集合、数组问题的。要看Stream
流有什么用处,还是要看集合处理有什么缺点。
问题引出
有下面这么一个数组
String[] strings = {"张无忌", "张三丰", "赵敏", "张翠山", "小昭", "张良"}; |
现在我们有如下要求
- 筛选出以”张”字开头的字符串,放入一个
Arraylist
集合中 - 在
ArrayList
集合中筛选出字符串长度为3
的字符串,放入一个新的集合
ArrayList<String> list1 = new ArrayList<>(); |
输出为
[张无忌, 张三丰, 张翠山, 张良] |
现在我们使Stream
流的方式实现
Stream<String> stream = Stream.of(strings); |
输出为
张无忌 张三丰 张翠山 |
我们发现使用Stream
流的代码比遍历集合简单很多,因为使用集合直接遍历真正核心的代码就那么一两句,比如
for (String string : strings) { |
这些代码中核心的就是string.startsWith("张")
,而其他的代码是为了达到这个目的不得不写的代码。这就是集合相较于Stream
流的局限性所在,观察Stream
流的写法,根本没有什么遍历集合的代码,直接就是你想要干的事情。
获取Stream流的方法
获取Stream
流有两种方法
Collection
中新加的stream()
方法,该方法可以得到一个Stream
流,对于Map
集合,可以通过keySet(),values(),entrySet()
等方法得到Set
集合,然后通过Set
对象调用stream()
方法得到Stream
流Stream
流的静态方法of()
,该方法接收一个可变参数,所以可以传入一个数组
下面做一个演示
import java.util.*; |
Stream中的常见方法
Stream
流中的方法分为两类,一类叫做延迟方法,该方法返回的还是一个Stream
流对象,所以可以进行链式编程,如filter()
;另一类叫做终结方法,该方法不返回Stream
流对象,如forEach()
, count()
(终结方法只有这两个,其他的都是延迟方法)。
filter()
该方法需要传入的是一个Predicate\<T>
接口,这个接口我们在常用函数式接口讲过,它是对某中数据进行测试,而filter
的作用就是如果test(T t)
返回的是true
,那么就将这个数据加入到新的流中,遍历完流中所有的元素后返回。
//当字符串以迪开头时返回true,加入到新的流中,这个流会被返回 |
输出为
迪丽热巴 |
map()
该方法传入的是一个Function<T,R>
接口,所以它的作用是将一个类型的转转化为另一个类型的流。如下
//得到一个流,这个流是字符串的长度 |
这时结果报错了
这是因为这个stream
在调用上面的filter()
的时候已经使用过了,而流使用了一次就会关闭,不能在使用,这就是为什么会报错的原因,所以我们把代码改为
//得到一个流,这个流是字符串的长度 |
这时输出为
4 |
forEach
该方法传入的是一个Consumer\<T>
接口,是一个终结方法,该方法会遍历流中的元素,然后使用Consumer
接口中的accept()
方法对元素进行处理,比如
stream.forEach(s -> System.out.println(s)); |
会逐个打印出流中的元素。
limit
limit
方法需要传入一个long
类型的数值maxSize
,该方法会截取流中的前maxSize
个元素放到新流中并返回,如
//这是链式编程 |
输出为
迪丽热巴 |
skip
该方法接收一个long
类型的数据n
,它会跳过流中的前n
个元素,将剩下的元素放入到一个新流中并返回,如
Stream.of(strings).skip(2).forEach(s -> System.out.println(s)); |
输出为
哪吒 |
count
该方法不需要传入参数,返回一个long
类型的整数,该整数是流中元素的个数,这个方法是一个终结方法,不返回Stream
流
long num = Stream.of(strings).count(); |
输出为
4 |
方法引用
我们之前在Stream
流使用forEach()
去打印流中的元素,如
stream.forEach(s -> System.out.println(s)); |
但是打印这个方法(System.out.println())
是已经存在了的,我们可不可以直接传入这个方法,在这里或者说是引用这个方法,答案是可以的,如下
stream.forEach(System.out::println); |
在这里我们引用了System.out
对象的println
方法,这行语句的作用是上面的语句作用是完全相同的,这就是方法的引用,::
就是方法引用的运算符,这是新增的运算符。
那方法引用也要遵循一定的原则,比如你引用的对象必须是存在的,你引用的方法需要传入的参数的个数和类型必须是对的上的,否则就会抛出异常,由于方法的性质不同,所以有很多类型的引用,比如
- 对象引用成员方法
- 类引用静态方法
super
引用父类方法this
引用成员方法- 引用构造方法
- 引用数组构造方法
下面会详细的展开讲解。
对象引用成员方法
其实
stream.forEach(System.out::println); |
就是对象引用成员方法,我们引用了System.out
对象的成员方法println
。
类引用静态方法
假设有一个接口Calculate
,里面只有一个抽象方法cal(int i)
public interface Calculate { |
所以这是一个函数式接口,现在在有一个方法需要调用这个接口去得到一个数字的绝对值,如
public static int getAbs(int i, Calculate calculate) { |
我们知道Math
类的静态方法abs()
可以做到这件事情,所以我们可以直接引用这个方法,如
int num = getAbs(-10,Math::abs); |
输出结果为
10 |
super引用父类成员方法
假设有一个Greet
接口,里面只有一个抽象方法greet()
,所以这是一个函数接口
public interface Greet { |
现在有一个父类Person
,里面有一个greet()
方法,这个方法在后面是要被子类引用的
public class Person { |
现在有一个子类Student
继承了Person
类
public class Student extends Person { |
Student
中的sayHello()
方法需要一个Greet
接口,然后我们又在greet()
方法中调用了这个方法,并且传入一个super::greet
的方法引用(当然这样的代码没有什么意义,只是为了演示),我们在main中创建一个对象,并调用此方法
Student student = new Student("小明"); |
输出为
I'm 小明 |
this引用成员方法
还是以上面的Student
类为例,假设Student
类中有一个成员方法为
public void tempt() { |
然后在greet()
方法中再增加一个sayHello()
,这时方法的引用指向的是tempt
方法,如下
public void greet() { |
现在在main
方法中运行一下,输出为
I'm 小明 |
引用构造方法
现在假设有这么一个接口
public interface Personable { |
里面只有一个抽象方法getPerson
,所以这是一个函数式接口,该方法根据name
返回一个Person
对象,现在有一个方法需要传入这个接口得到一个Person
对象
public static Person getPerson(String name, Personable personable) { |
现在我们在main
方法中调用该方法,传入的接口我们使用构造器引用Person::new
Person person = getPerson("迪丽热巴",Person::new); |
运行输出为
I'm 迪丽热巴 |
引用数组构造方法
引用数组构造方法的格式是int[]::new
(这里只以int
为例,当然也可以double[]::new
),具体的使用方法同上面的Person
类的构造方法引用一致,这里就不多加介绍了。