本篇文章会介绍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) {
return supplier.get();
}

我们在main方法中调用该方法

String str = getString(() -> {
return "Hello World!";
});

输出为

Hello World!

Consumer<T>

Supplier接口不同的是,Consumer接口是消费或者说处理一个与泛型类型相同数据类型的数据,它有一个accept(T t)方法,该方法用来消费数据,假设有下面这一个方法

public static void handleString(String str, Consumer<String> consumer) {
consumer.accept(str);
}

我们使用Consumer来消费(处理)传入的这个字符串,而怎么消费,就取决与程序员在调用该方法时传入的Lambda,这时对程序员来说,就是怎么做的问题,相当于传入一个方法去处理数据,这就是函数式编程,这里我们就简单的将传入的数据进行打印

handleString("Hello Again!", (String str) -> {
System.out.println(str);
});

输出为

Hello Again!

Consumer接口中有一个默认方法andThen(Consumer consumer),看下面的程序说明它的用处

con1.andThen(con2).accept(str); //con1和con2都是Consumer接口的实现类对象
//相当于下面的代码
con1.accept(str);
con2.accept(str);

假设有一个方法有需要传入两个Consumer接口对数据进行消费

public static void handleInterger(Integer i, Consumer<Integer> con1, Consumer<Integer> con2) {
con1.andThen(con2).accept(i);
}

mian方法中使用,con1对数字进行+10然后打印,con2对数字进行*10然后打印

handleInteger(10, (i) -> {
i = i + 10;
System.out.println(i);
}, (i) -> {
i = i * 10;
System.out.println(i);
});

输出为

20
100

Predicate<T>

Predicate接口中有一个test(T t),它的作用是对某种数据类型进行判断,它返回一个boolean值。假设有一个集合,我们对其中的元素进行判断,符合条件放入一个新的集合,看下面的方法。

public static HashMap<String, Integer> getMap(HashMap<String,Integer> map, 
Predicate<Integer> predicate) {
//创建一个集合用以放符合条件的元素
HashMap<String,Integer> resmap = new HashMap<>();
//遍历集合
Set<String> key = map.keySet();
for (String str : key) {
int val = map.get(str);
//对值进行判断
boolean res = predicate.test(val);
//如果值符合条件,就加入新的集合
if (res) {
resmap.put(str,val);
}
}

return resmap;
}

现在我们在main方法中进行调用

HashMap<String,Integer> map = new HashMap<>();
map.put("迪丽热巴",18);
map.put("古力娜扎",19);
map.put("佟丽娅",20);
map.put("奥特曼",100);
//筛选出年龄小于等于20岁的
HashMap<String,Integer> resmap = getMap(map, (Integer i) -> {
if (i <= 20) {
return true;
}
return false;
});
System.out.println(resmap);

运行结果为

{佟丽娅=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,
Predicate<Integer> predicate1,
Predicate<Integer> predicate2) {
HashMap<String,Integer> resmap = new HashMap<>();
Set<String> key = map.keySet();
for (String str : key) {
int val = map.get(str);
boolean res = predicate1.and(predicate2).test(val);
if (res) {
resmap.put(str,val);
}
}
return resmap;
}

main方法中调用该方法

HashMap<String,Integer> resmap = getMap(map, (Integer i) -> {
if (i <= 20) {
return true;
}
return false;
}, (Integer i) -> {
if (i > 18) {
return true;
}
return false;
});
System.out.println(resmap);

输出结果为

{佟丽娅=20, 古力娜扎=19}

至于or()negate()的使用方法同上。

Function<T,R>

该接口的作用是将T这种数据类型转化为R这种数据类型,它里面有一个R apply(T t)方法。下面这个方法将一个字符串转化为一个整数

public static Integer StrToInt(String str, Function<String, Integer> fun) {
return fun.apply(str);
}

main()方法中调用该方法

Integer integer = StrToInt("123", (String str) -> {
//将字符串转化为数字
return Integer.parseInt(str);
});
//打印该数字
System.out.println(integer);

输出为

123

Function接口中还有一个默认方法andThen(Function<T,R> fun),这个方法与在上面介绍的Consumer接口的andThen()很像,但是有点不同,Consumer接口的andThen是两个对象消费同一个数据,而Function接口的addThen()是将第一个fun处理后的结果拿给第二个fun去处理,相当于apply(apply())。比如现在我有一个需求,将一个字符串转化为数字,然后将这个数字,+10然后再转化为字符串,这个方法可以这么写

public static String StrPlus(String str, 
Function<String,Integer> fun1,
Function<Integer,String> fun2) {
return fun1.andThen(fun2).apply(str);
}

我们在main方法中调用该方法

String s =  StrPlus("123", (String str) -> {
//将字符串转化为数字并加10
Integer i = Integer.parseInt(str);
i = i + 10;
return i;
}, (Integer i) -> {
//将数字转化为字符串
return i + "";
});
System.out.println(s);

输出为

133

Stream流

Stream流是Java 8引入的新特性,首先它跟I/O流没有任何的关系,它主要是用来处理集合、数组问题的。要看Stream流有什么用处,还是要看集合处理有什么缺点。

问题引出

有下面这么一个数组

String[] strings = {"张无忌", "张三丰", "赵敏", "张翠山", "小昭", "张良"};

现在我们有如下要求

  • 筛选出以”张”字开头的字符串,放入一个Arraylist集合中
  • ArrayList集合中筛选出字符串长度为3的字符串,放入一个新的集合
ArrayList<String> list1 = new ArrayList<>();
for (String string : strings) {
if (string.startsWith("张")) {
list1.add(string);
}
}
System.out.println(list1);
ArrayList<String> list2 = new ArrayList<>();
for (String string : list1) {
if (string.length() == 3) {
list2.add(string);
}
}
System.out.println(list2);

输出为

[张无忌, 张三丰, 张翠山, 张良]
[张无忌, 张三丰, 张翠山]

现在我们使Stream流的方式实现

Stream<String> stream = Stream.of(strings);
stream.filter(str -> str.startsWith("张"))
.filter(str -> str.length() == 3)
.forEach(str -> System.out.print(str + " "));

输出为

张无忌 张三丰 张翠山 

我们发现使用Stream流的代码比遍历集合简单很多,因为使用集合直接遍历真正核心的代码就那么一两句,比如

for (String string : strings) {
if (string.startsWith("张")) {
list1.add(string);
}
}

这些代码中核心的就是string.startsWith("张"),而其他的代码是为了达到这个目的不得不写的代码。这就是集合相较于Stream流的局限性所在,观察Stream流的写法,根本没有什么遍历集合的代码,直接就是你想要干的事情。

获取Stream流的方法

获取Stream流有两种方法

  • Collection中新加的stream()方法,该方法可以得到一个Stream流,对于Map集合,可以通过keySet(),values(),entrySet()等方法得到Set集合,然后通过Set对象调用stream()方法得到Stream
  • Stream流的静态方法of(),该方法接收一个可变参数,所以可以传入一个数组

下面做一个演示

import java.util.*;
import java.util.stream.Stream;

public class getStream {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("迪丽热巴");
list.add("古力娜扎");
list.add("哪吒");
list.add("杨戬");
Stream stream1 = list.stream();
stream1.forEach(s -> System.out.println(s));

HashMap<String,String> map = new HashMap<>();
map.put("迪丽热巴","女");
map.put("古力娜扎","女");
map.put("哪吒","男");
map.put("杨戬","男");
Set<String> key = map.keySet();
Stream stream2 = key.stream();
stream2.forEach(s -> System.out.println(s));

Collection<String> vals = map.values();
Stream stream3 = vals.stream();
stream3.forEach(s -> System.out.println(s));

Set<Map.Entry<String,String>> entries = map.entrySet();
Stream stream4 = entries.stream();
stream4.forEach(s -> System.out.println(s));

String[] strings = {"迪丽热巴", "古力娜扎", "哪吒", "杨戬"};
Stream stream5 = Stream.of(strings);
stream5.forEach(s -> System.out.println(s));

Stream<Integer> stream6 = Stream.of(1,2,3,4,5);
stream6.forEach(s -> System.out.println(s));
}
}

Stream中的常见方法

Stream流中的方法分为两类,一类叫做延迟方法,该方法返回的还是一个Stream流对象,所以可以进行链式编程,如filter();另一类叫做终结方法,该方法不返回Stream流对象,如forEach()count()(终结方法只有这两个,其他的都是延迟方法)。

filter()

该方法需要传入的是一个Predicate\<T>接口,这个接口我们在常用函数式接口讲过,它是对某中数据进行测试,而filter的作用就是如果test(T t)返回的是true,那么就将这个数据加入到新的流中,遍历完流中所有的元素后返回。

//当字符串以迪开头时返回true,加入到新的流中,这个流会被返回
Stream<String> stream1 = stream.filter(s -> s.startsWith("迪"));
//forEach是后面要介绍的方法,这里只需要理解为遍历流并打印
stream1.forEach(s -> System.out.println(s));

输出为

迪丽热巴

map()

该方法传入的是一个Function<T,R>接口,所以它的作用是将一个类型的转转化为另一个类型的流。如下

//得到一个流,这个流是字符串的长度
Stream<Integer> stream1 = stream.map(s -> s.length());
stream1.forEach(s -> System.out.println(s));

这时结果报错了

这是因为这个stream在调用上面的filter()的时候已经使用过了,而流使用了一次就会关闭,不能在使用,这就是为什么会报错的原因,所以我们把代码改为

//得到一个流,这个流是字符串的长度
Stream<Integer> stream2 = Stream.of(strings).map(s -> s.length());
stream2.forEach(s -> System.out.println(s));

这时输出为

4
4
2
2

forEach

该方法传入的是一个Consumer\<T>接口,是一个终结方法,该方法会遍历流中的元素,然后使用Consumer接口中的accept()方法对元素进行处理,比如

stream.forEach(s -> System.out.println(s));

会逐个打印出流中的元素。

limit

limit方法需要传入一个long类型的数值maxSize,该方法会截取流中的前maxSize个元素放到新流中并返回,如

//这是链式编程
Stream.of(strings).limit(2).forEach(s -> System.out.println(s));

输出为

迪丽热巴
古力娜扎

skip

该方法接收一个long类型的数据n,它会跳过流中的前n个元素,将剩下的元素放入到一个新流中并返回,如

Stream.of(strings).skip(2).forEach(s -> System.out.println(s));

输出为

哪吒
杨戬

count

该方法不需要传入参数,返回一个long类型的整数,该整数是流中元素的个数,这个方法是一个终结方法,不返回Stream

long num = Stream.of(strings).count();
System.out.println(num);

输出为

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 {
int cal(int i);
}

所以这是一个函数式接口,现在在有一个方法需要调用这个接口去得到一个数字的绝对值,如

public static int getAbs(int i, Calculate calculate) {
return calculate.cal(i);
}

我们知道Math类的静态方法abs()可以做到这件事情,所以我们可以直接引用这个方法,如

int num = getAbs(-10,Math::abs);
System.out.println(num);

输出结果为

10

super引用父类成员方法

假设有一个Greet接口,里面只有一个抽象方法greet(),所以这是一个函数接口

public interface Greet {
void greet();
}

现在有一个父类Person,里面有一个greet()方法,这个方法在后面是要被子类引用的

public class Person {
String name;

public Person(String name) {
this.name = name;
}

public Person() {
}

public void greet() {
System.out.println("I'm " + name);
}
}

现在有一个子类Student继承了Person

public class Student extends Person {

public Student(String name) {
super(name);
}

public static void sayHello(Greet gre) {
gre.greet();
}

public void greet() {
sayHello(super::greet);
}
}

Student中的sayHello()方法需要一个Greet接口,然后我们又在greet()方法中调用了这个方法,并且传入一个super::greet的方法引用(当然这样的代码没有什么意义,只是为了演示),我们在main中创建一个对象,并调用此方法

Student student = new Student("小明");
student.greet();

输出为

I'm 小明

this引用成员方法

还是以上面的Student类为例,假设Student类中有一个成员方法为

public void tempt() {
System.out.println("我今晚有空哦");
}

然后在greet()方法中再增加一个sayHello(),这时方法的引用指向的是tempt方法,如下

public void greet() {
sayHello(super::greet);
sayHello(this::tempt);
}

现在在main方法中运行一下,输出为

I'm 小明
我今晚有空哦

引用构造方法

现在假设有这么一个接口

public interface Personable {
Person getPerson(String name);
}

里面只有一个抽象方法getPerson,所以这是一个函数式接口,该方法根据name返回一个Person对象,现在有一个方法需要传入这个接口得到一个Person对象

public static Person getPerson(String name, Personable personable) {
return personable.getPerson(name);
}

现在我们在main方法中调用该方法,传入的接口我们使用构造器引用Person::new

Person person = getPerson("迪丽热巴",Person::new);
person.greet();

运行输出为

I'm 迪丽热巴

引用数组构造方法

引用数组构造方法的格式是int[]::new(这里只以int为例,当然也可以double[]::new),具体的使用方法同上面的Person类的构造方法引用一致,这里就不多加介绍了。