目录
前言
Java8是Java的一个重要版本。其中最主要的变化就是提供了lambda表达式、方法引用、构造器引用、一个强大的Stream API、集合中的一些小变化和提供了许多的时间相关的API。本文只介绍了与lambda相关的新特性,对于时间相关的新特性请关注后续文章。
1. 函数型接口
1.1 定义
所谓函数式接口就是只有一个抽象方法的接口。如Runnable接口。
/*Runnable接口的源码(不含注释)*/
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
1.2 四大核心函数式接口
函数式接口 | 参数类型 | 返回值类型 | 用途 |
Consumer<T> 消费型接口 | T | void | 对类型T的对象语言操作。 包含方法:void accept(T t) 理解:有输入没输出 |
Supplier<T> 供给型接口 | 无 | T | 返回类型为T的对象。 包含方法:T get() 理解:没输入有输出 |
Function<T,R> 函数型接口 | T | R | 对类型为T的对象操作 返回类型为R的结果。 包含方法:R apply(T t) 理解:一元函数 |
Predicate<T> | T | boolean | 确定T的对象是否满足条件。 包含方法:boolean test(T) 理解:判断对错 |
2. lambda表达式
2.1 定义
lambda表达式的本质就是函数式接口的实例。其语法格式为:lambda形参列表->lambda体。
举例(看不懂也没关系,只是给个例子):(o1,o2)->Integer.compare(o1,o2);
2.2 从一个例子看lambda表达式
如果我们要创建一个Runnable接口实现类的对象,然后调用该对象的run()方法,我们可以按如下操作
@Test
public void test1() {
Runnable run1=new Runnable() {
public void run() {
System.out.println("我是Runnable接口的一个匿名对象");
}
};
run1.run();
}
这里我们通过匿名类的方式来创建了一个Runnable接口的实现类的对象,b并调用了其run()方法。但是我们知道,如果我们要使用匿名类的方式来创建一个接口的实现类的一个对象的话,我们在创建对象时给出的具体接口就代表了我这个接口是什么。比如上面的代码中,我们给出Runnable就已经确定了这是Runnable接口的实现类。那么这样的话,我们后面给出的new Runnable{}就是可以推断出来的;还有,因为这个接口只有run()一个抽象方法,所以public void run()我们也是可以推断出来的。对于这些可以推断出来的内容,我们如果将其省去则可以减少代码量,使代码更简洁,所以就有了lambda表达式。通过lambda表达式上面代码就可以写成这样。
@Test
public void test2() {
Runnable run1=()->{System.out.println("我是Runnable接口的一个匿名对象");};
run1.run();
}
2.3 注意点
我们之所以可以省略那些内容是因为这个接口只有一个抽象方法使得我们省略的内容都可以推断出来。而只有一个抽象方法的接口被称为函数式接口。所以很直接的体现了lambda表达式的本质是函数式接口的一个实例对象这句话。
2.4 使用lambda表达式的六种情况
下面的描述都是针对接口中的抽象方法的
2.4.1 无参无返回值
可以省略:new 构造器、方法修饰(但是构造器的小括号要保留)。如下:
@Test
public void test1() {
/*----------一般写法----------*/
Runnable run1=new Runnable() {
public void run() {
System.out.println("我是Runnable接口的一个匿名对象");
}
};
run1.run();
/*----------lambda表达式写法----------*/
Runnable run2=()->{System.out.println("我是Runnable接口的一个匿名对象");};
run2.run();
}
(这里,我们在声明匿名类对象的时候给出了Runnable接口,所以后面的(应该可以说是构造器)可以省略;同时因为Runnable接口只有一个抽象方法,所以方法的修饰也可以去掉只剩下一个小括号和外面的大括号以及方法体。在后面可以看到,如果方法体只有一条,则大括号也可以去掉)
2.4.2 有一个参数无返回值
可以省略:new 构造器、方法修饰(但是要留下形参)。如下:
@Test
public void test1() {//有一个参数没有返回值
/*----------一般写法----------*/
LambdaTestInterface<String> test=new LambdaTestInterface<String>() {
public void test1(String t) {
System.out.println(t);
}
};
test.test1("我是Test接口的一个匿名实现类对象");
/*----------lambda表达式写法----------*/
LambdaTestInterface<String> test1=(String t)->{System.out.println(t);};
test1.test1("我是Test接口的一个匿名实现类对象");
}
interface LambdaTestInterface<T>{public void test1(Tt) ;}
(因为这个方法有一个参数所以形参列表不能省略,因为省略后就变成一个新方法了;同时,虽然不能完全省略形参,但是后面可以看到参数类型是可以省略的。)
2.4.3 数据类型可省略
在上面的基础上可以将数据类型省略
@Test
public void test3() {//参数类型可以省略
/*----------一般写法----------*/
//同情况二
/*----------lambda表达式写法----------*/
LambdaTestInterface<String> test1=(t)->{System.out.println(t);};
test1.test1("我是Test接口的一个匿名实现类对象");
}
//接口同情况二
(这里出现了泛型,因为前面的泛型确定了这个对象的泛型,所以在匿名类中的泛型也就确定了,进一步的就把方法中的参数类型确定了,所以可以省略参数类型;如果方法没有使用泛型则方法的参数类型是确定的自然可以推断出来,所以可以省略。)
2.4.4 只有一个参数时
在上面的基础上可以将小括号去掉
@Test
public void test4() {//只有一个参数
/*----------一般写法----------*/
//同上
/*----------lambda表达式写法----------*/
LambdaTestInterface<String> test=t->{System.out.println(t);};
test.test1("我是Test接口的一个匿名实现类对象");
}
//接口同上
2.4.5 有多个参数、会有多条执行语句且有返回值
同上,不过这个是省略的最少的。如下
@Test
public void test5() {//有多个参数、会有多条执行语句且有返回值
/*----------一般写法----------*/
LambdaTestInterface1<Integer,Integer> test=new LambdaTestInterface1<Integer,Integer>(){
public int test1(Integer a,Integer b) {
System.out.println(a);
System.out.println(b);
return a.compareTo(b);
}
};
int c=test.test1(1, 2);
/*----------lambda表达式写法----------*/
LambdaTestInterface1<Integer,Integer> test1=(a,b)-> {
System.out.println(a);
System.out.println(b);
return a.compareTo(b);
};
int c1=test1.test1(1, 2);
}
interface LambdaTestInterface1<T,E>{public int test1(T t,E e) ;}
(这个因为有多个执行语句所以大括号不能省略,不然会将要执行的语句放到外面的方法中,则出现了歧义;同时因为有多个参数所以不能省略形参的小括号;因为有多行,所以不能省略return,不然会误以为是执行语句而不是return语句。)
2.4.6 lambda体只有一条语句
在上面的基础上可以把return和大括号省略掉。如下
@Test
public void test6() {//lambda体只有一条语句
/*----------一般写法----------*/
LambdaTestInterface1<Integer,Integer> test=new LambdaTestInterface1<Integer,Integer>(){
public int test1(Integer a,Integer b) {
return a.compareTo(b);
}
};
int c=test.test1(1, 2);
/*----------lambda表达式写法----------*/
LambdaTestInterface1<Integer,Integer> test1=(a,b)->a.compareTo(b);
int c2=test1.test1(1, 2);
}
(这里基本上结合上面的情况,都可以将其内容推到出来,所以可以省略到最简。)
3. 方法引用
3.1 定义
方法引用可以看成是lambda表达式的深层次表现。当要传递给lambda的操作已经有实体方法时,可以使用方法引用。格式:类(或者是对象)::方法名
3.2 注意点
实现接口的抽象方法的形参列表和返回值类型必须和方法引用的方法的形参列表和返回值类型一致。(这是针对下列三种情况中的前两种)
3.3 使用方法引用的三种情况
3.3.1 对象::实例方法名
@Test
public void test7() {//对象::实例方法名
/*----------lambda表达式写法----------*/
Consumer<String> test=str->System.out.println(str);
test.accept("我使用lambda表达式写法");
/*----------方法引用写法----------*/
Consumer<String> test1=System.out::println;
test1.accept("我使用方法引用写法");
/*这里System.out是PrintStream的一个对象println(String t)是其一个方法*/
}
(因为accept()和println()都是没有没有参数和返回值的方法,所以用方法引用的话可以推到出来使用的是传入println()的方法体。)
3.3.2 类::静态方法名
@Test
public void test8() {//类名::静态方法名
//使用Comparator中的int compare(T t1,T t2)
// Integer中的int compare(T t1,T t2)
/*----------lambda表达式写法----------*/
Comparator<Integer> test=(t1,t2)->Integer.compare(t1, t2);
test.compare(1, 2);
/*----------方法引用写法----------*/
Comparator<Integer> test1=Integer::compare;
test1.compare(1, 2);
}
(同上。)
3.3.3 类::实例方法名
@Test
public void test9() {//类名::实例方法
//使用Comparator中的int compare(T t1,T t2)
// String中的int t1.compareTo(t2)
/*----------lambda表达式写法----------*/
Comparator<String> test=(t1,t2)->t1.compareTo(t2);
test.compare("我", "你");
/*----------lambda表达式写法----------*/
Comparator<String> test1=String::compareTo;
test1.compare("我", "你");
}
(这里之所以可以通过类来调用实例方法是因为把参数列表中的第一个参数看做方法的调用者,而这个方法刚好又需要一个参数;所以综合方法的调用者和方法所需要的参数,可以提供两个使用参数的地方,再加上返回值一样,也可以推导出来相应的完整形式。)
4. 构造器引用
@Test
public void test10() {
//使用Supplier中的T get()和Test的空参构造器
/*----------lambda表达式写法----------*/
Supplier<Test1> test=()->new Test1();
/*----------构造器引用写法----------*/
Supplier<Test1> test1=Test1::new;
}
class Test1{int i;Test1(){i=1;}}
(因为这个方法只有一个执行语句且执行结果是返回一个Test 对象,这和Test1类的构造方法的功能一样,所以也能推导出来完整的形式。)
5. Stream API
5.1 定义
Stream是Java8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API对集合数据进行操作。就类似于使用SQL执行的数据库查询。
5.2 为什么要用Stream API
在使用MySQL等数据库时,这些关系型数据库可以直接在内部进行对数据的操作,但是像对MongDB、Radis等NoSQL数据库的数据进行操作就需要到Java层面了。使用Stream API就可以提高操作效率。
5.3 Stream和Collection集合的区别
Stream是有关计算的而Collection是一种静态的内存数据结构。前者主要是面向CPU通过CPU实现计算,后者主要面向内存,存储在内存中。
5.4 注意点
- Stream不会自己存储元素。
- Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream。(也就是说我们使用Stream的相关方法改变Stream的数据后,并不会改变原来的Stream,而是返回一个新的Stream对象。)
- Stream是延迟执行的,只有在需要结果的时候才会执行。
5.5 Stream的操作步骤
- 创建Stream:一个数据源获取一个Stream。
- 中间操作:一个中间操作链,对数据源的数据进行处理(只是处理Stream不会改变源数据)
- 终止操作:只有执行了终止操作才会执行中间操作链,并产生结果,之后这个Stream对象不能再使用。
5.6 创建Stream对象的四个方法
5.6.1 方法一:通过集合
直接调用集合的stream()方法即可获取一个顺序流Stream实例;调用paralleStream()可获取一个并行流Stream实例。
/*-----方法一:使用集合-----*/
ArrayList<Integer> list1=new ArrayList<>();
Stream<Integer> stream=list1.stream();//顺序流
Stream<Integer> stream1=list1.stream();//并序流
顺序流:可以看做一个线程从按照集合中的顺序上到下依次取值,最后顺序和集合中的顺序一样。
并序流:可以看做多个线程同时从集合中取数据,最后顺序与集合中顺序可能不一样。
5.6.2 方法二:通过数组
可以使用Arrays类的静态方法stream()来获取一个数组流Stream对象。
Integer []integer=new Integer[5];
Stream<Integer> stream1=Arrays.stream(integer);
5.6.3 方式三:使用Stream的of()
可以调用Stream类的静态方法of(),通过显示值创建一个流,它可以接收任意数量的参数。
Stream<Integer> stream = Stream.of(1,2,3,4);
5.6.4 方法四:创建无限流
有两种形式。调用iterate(finall T seed,finall UnaryOperator<T> f)和调用generate(Supplier<T> s)。其中UnaryOperator是一个函数式接口,抽象方法有一个参数和T类型返回值。Supplier也是函数式接口,抽象方法没有参数,但有一个返回值。(要结合中间操作来进行,不然他会一直执行)
/*使用iterate*/
Stream.iterate(0,t->t+2).limit(10).forEach(System.out::println);
//这里limit(10)是中间操作,表示取前10个数据,forEach是终止操作,表示遍历Stream对象里面的数据
/*使用generate*/
Stream.generate(Math::random).limit(10).forEach(System.out::println);
5.7 中间操作
5.7.1 筛选与切片
方法 | 作用 |
filter(Predicate p) | 接收lambda,从流中排除某些元素 |
limit(n) | 使流中元素不超过给定的数量(取前n个元素) |
skip(n) | 返回扔掉前n个元素的流,如果元素 不够则返回空流 |
distinct() | 通过流所生成元素的hashCode()和equals() 去除重复元素 |
5.7.2 映射
方法 | 作用 |
map(Function f) | 接收一个函数式接口的实现类对象作为参数, 将其中元素转换成其他类型信息。(如果函数 对象是将元素转换成一个Stream对象,则原来 的Stream对象是包含Stream对象的Stream对象) |
flaMap(Function f) | 接收一个函数作为参数,将流中的每个值都转 换成另一个流,然后把所有元素作为一个流。 (如果函数是将元素转换成一个Stream对象, 则原来的Stream对象元素不包括Stream对象) |
5.7.3 排序
方法 | 作用 |
sorted() | 进行自然排序 |
sorted(Comparator com) | 进行定制排序 |
5.8 终止操作
5.8.1 匹配与查找
方法 | 作用 |
allMatch(Predicate p) | 检查是否所有元素都匹配 |
anyMatch(Predicate p) | 检查是否至少有一个元素匹配 |
noneMatch(Predicate p) | 检查是否没有一个元素匹配 |
findFirst | 返回第一个元素 |
findAny | 返回任意一个元素 |
count | 返回元素总个数 |
max(Comparetor c) | 返回最大值 |
min(Comparator c) | 返回最小值 |
forEach(Consumer c) | 内部迭代 |
5.8.2 规约
方法 | 作用 |
reduce(T identity,BinaryOperator) | 可以将流中元素从第一个到最后一个 结合起来得到一个值,返回T类型对象 (BinaryOperator是函数式接口,其中的抽象方法是两个同类型形参,一个同类型返回值) |
reduce(BinaryOperator) | 可以将流中元素从第一个到最后一个 结合起来得到一个值返回Optional类型对象 |
5.8.3 收集
方法 | 作用 |
collect(Collector c) | 将流中数据转换成其他形式,接收一个 Collector接口的实现,用于给Stream中 元素汇总的方法。 Collector接口中的实现方法决定了对流 如何执行收集的操作(如收集到List、Set 或Map中) Collectors实用类提供了很多静态方法,可以 很方便的创建常见的收集器实例。 |
6. 总结
lambda的核心是:其是函数式接口的一个实例。因为函数式接口的特殊性我们能够在只给出一点条件的情况下就知道其他内容,所以为了代码整洁,就使用了lambda表达式。
使用lambda表达式的基本是能够熟练的进行类型推断。
方法引用可以看做是lambda表达式的一个深层次体现,其省略了更多;而构造方法引用又是方法引用的进一步体现;但它们终究还是对类型推断能力的考查。
Steam API实质就是操作集合的一组API,不过其中的方法比较多涉及函数式接口,所以会和lambda表达式结合起来,这就导致它比一般的API难理解。