本篇文章会介绍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类的构造方法引用一致,这里就不多加介绍了。










