大纲
函数式接口
在JDK8之后引入了一个特殊的函数式接口,其本质还是接口。
它与其他接口的区别在于函数式接口中只允许一个抽象方法,否则就会报错。
但是呢,JDK8的函数式接口里可以添加默认方法和静态方法
也可以有重写的Object方法。因为 Object是所有类的默认父类,即使是函数式接口的接口的实现类都会直接或间接继承 Object 这个根类,所以在函数式接口中定义与 Object 类中签名一样的方法是不会导致函数式接口失效的。(Object有些方法是final修饰的不能重写)。
关于函数式接口有个相关的注解@FunctionalInterface,这个和@Override注解是一样的。函数式接口加不加都是一样的,只是加了这个会帮我们检验我们的接口是否满足函数式接口的要求而已。
下面给一个函数式接口的例子
@FunctionalInterface
public interface WorkInterface {
/**
* 抽象方法
*/
void work();
/**
* 默认方法
*/
default void study() {
System.out.println("studying...");
}
/**
* 静态方法
*/
static void sleep(){
System.out.println("sleeping...");
}
/**
* 重写Object类的toString方法
*/
String toString();
}
在JDK中也提供了很多函数式接口,足够我们使用了:
总的来说,JDK提供的函数式接口,常用的如下四种类型
-
Consumer消费型。 -
Predicate断言型。 -
Supplier生产型。 -
Function转换型。
大体上就这四种,每种有很多实现,它们的区别是有的函数式接口的抽象方法的参数个数不同,有的是类型不同,基本上使用方式都差不多,这些函数式接口足够我们使用了。文章最后会详细讲一下几个核心的函数式接口。
小结,函数式接口的要求:
-
只能存在一个抽象方法。 -
可以有一个或多个静态方法和默认方法。 -
可以重写Object类的toString、equals等可以重写的方法。
Lamda表达式
引入
我们先看一下传统的代码和lambda表达式的代码比较,很明显lambda表达式的写法比传统的匿名内部类更加简洁。
使用匿名内部类做编写线程任务时,除了要编写方法体,还需要编写run方法的声明等,比较繁琐。
而lambda表达式的写法,让我们只需要关注我们的线程任务代码,更加简洁。
public static void main(String[] args) {
// 匿名内部类开启线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类...");
}
}).start();
// lambda表达式开启线程
new Thread(()-> System.out.println("lambda表达式...")).start();
}
lambda标准格式
在学习语法之前,先看一个语法例子例子。
List<Integer> list = Arrays.asList(1,2,3);
list.forEach((Integer element) -> {
System.out.println("遍历元素:"+element);
});
(数据类型 变量名) -> {
代码语句
})
-
其中括号
()
用来描述参数列表,无参数则留空,多个参数则用逗号分隔。 -
{}
用来描述方法体。 -
->
为 lambda运算符。
lambda省略格式
lambda表达式在特定的情况下可以省略部分符号,写省略格式的lambda表达式的函数式接口的条件如下:
-
括号
()
的参数列表的参数类型可以省略不写。 -
当函数式接口的方法的是一个参数的话,那么lambda表达式的括号
()
和参数类型都可以省略。 -
当lambda表达式的方法体的代码仅仅只有一行的时候,方法体的大括号
{}
可以省略不写,return和分号;
都可以省略,但是它们都必须同时省略。
例子:
省略参数类型:
/**
* 函数式接口:两个参数,没有返回值
*/
@FunctionalInterface
public interface DoubleParamNoReturn {
void function(Integer age,String name);
}
@Test
public void OmitType() {
// 完整格式
DoubleParamNoReturn fun1 = (Integer age, String name) -> {
System.out.println("年龄:" + age + ",名称:" + name);
};
// 省略参数类型
DoubleParamNoReturn fun2 = (age, name) -> {
System.out.println("年龄:" + age + ",名称:" + name);
};
// 调用函数式接口方法
fun1.function(21,"陈树生"); // 年龄:21,名称:陈树生
fun2.function(40,"张译"); // 年龄:40,名称:张译
}
省略参数类型和括号()
:
@Test
public void OmitTypeAndBrackets() {
// 完整格式
OneParamNoReturn fun1 = (Integer age) -> {
System.out.println("年龄:" + age);
};
// 省略参数类型和括号
OneParamNoReturn fun2 = age -> {
System.out.println("年龄:" + age);
};
fun1.function(21); // 年龄:21
fun2.function(40); // 年龄:40
}
省略大括号{}
、return和结束符;
@Test
public void OmitOthers() {
// 完整格式
DoubleParamReturn fun1 = (Integer age, String name) -> {
return age > 18 && name.length() > 3;
};
// 省略格式
DoubleParamReturn fun2 = (age, name) ->
age > 18 && name.length() > 3
;
System.out.println(fun1.function(21, "陈树生")); // false
System.out.println(fun2.function(29, "第五佩佩")); // true
}
常用的内置函数接口
Consumer接口
消费接口里面有一个抽象方法和一个默认方法。
抽象方法只是接收一个泛型参数而没有返回值,所以叫它消费接口
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
例子:
将一个字符串变成大写,下面的printStrToLower方法的第二个参数需要传入一个Consumer接口的参数,我们直接传给他一个Lambda表达式即可。
public static void main(String[] args) {
// 需要将字符串全部变成小写
printStrToUpper("Hello Consumer", str ->
System.out.println(str.toLowerCase())
); // hello consumer
}
public static void printStrToLower(String str,Consumer<String> c){
c.accept(str);
}
测试默认方法andThen:
先将一个字符串变成小写,再将其变成大写
public static void main(String[] args) {
secretLowB("Dylan Kwok",
name -> {
System.out.println(name.toLowerCase());
},
name -> {
System.out.println(name.toUpperCase());
});
}
public static void secretLowB(String name, Consumer<String> c1,Consumer<String> c2){
c1.accept(name);
c2.accept(name);
}
控制台:
dylan kwok
DYLAN KWOK
这个secretLowB方法里调用两次accept方法也是可以的,但是Consumer接口里面提供了一个默认方法andThen可以方便我们做链式编程。
public static void main(String[] args) {
secret("Dylan Kwok",
name -> {
System.out.println(name.toLowerCase());
},
name -> {
System.out.println(name.toUpperCase());
});
}
public static void secret(String name, Consumer<String> c1, Consumer<String> c2) {
c1.andThen(c2).accept(name);
}
控制台:
dylan kwok
DYLAN KWOK
Supplier接口
生产接口里面有一个抽象方法,实现类可以实现这个方法返回一个值,所以叫它生产接口。
@FunctionalInterface
public interface Supplier<T> {
T get();
}
例子:生成一个随机数字字符串
public static void main(String[] args) {
// 生成一个随机数字字符串
Integer numString = randomNumString(() -> {
SecureRandom random = new SecureRandom();
return random.nextInt(10000);
});
System.out.println(numString); // 3477
}
public static Integer randomNumString(Supplier<Integer> s){
return s.get();
}
Function接口
该接口也叫转换接口,最主要的就是apply方法了,用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
apply方法:首先看一个apply方法的例子:获得一个字符串的长度
public static void main(String[] args) {
// 计算雨果名字的长度,apply方法输入字符串类型,输出Integer类型
Integer lenth = getStrLenth("Victor Hugo",
name -> name.length());
System.out.println("雨果名字的长度为:" + lenth);
}
public static Integer getStrLenth(String name, Function<String, Integer> f) {
return f.apply(name);
}
compose方法和andThen方法的用法是一样的,只不过调用顺序不一样而已:
-
compose方法的Function类型参数的apply方法先执行。 -
andThen方法的Function类型参数的apply方法后执行。
以compose举例,andThen用法一样的
public static void main(String[] args) {
Integer timesLength = getTenTimesLength("令狐毕玉",
length -> length *= 10,
name -> name.length());
System.out.println("名字的十倍长度是:" + timesLength); // 40
}
public static Integer getTenTimesLength(String name,
Function<Integer, Integer> f1, Function<String, Integer> f2) {
return f1.compose(f2).apply(name);
}
identity方法返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t
形式的Lambda表达式,它的存在是有意义的:
public static void main(String[] args) {
List<String> names = Arrays.asList("司马懿", "夏侯惇", "张辽", "夏侯子臧");
Map<String, Integer> map =
names.stream().collect(Collectors.toMap(Function.identity(), name -> name.length()));
map.forEach((k, v) -> System.out.println(k + "==" + v)); // 遍历
}
控制台:
张辽==2
夏侯子臧==4
司马懿==3
夏侯惇==3
上面的Collectors.toMap(Function.identity(), name -> name.length()))
和Collectors.toMap(name -> name, name -> name.length()));
是等价的。
这就是静态方法identity的用法。
Predicate接口
Predicate是一个判断接口,有些时候我们需要针对某些数据进行判断得到boolean值,继而进行下一步操作。我们就可以使用这个接口完成判断。
@FunctionalInterface
public interface Predicate<T> {
/**
* 判断方法
*/
boolean test(T t);
/**
* 多个Predicate,与操作
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
/**
* 多个Predicate,取反
*/
default Predicate<T> negate() {
return (t) -> !test(t);
}
/**
* 多个Predicate,或操作
*/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
例子:
public static void main(String[] args) {
boolean adult = isAdult(23, age -> age >= 18);
System.out.println("是否是成年人:" + adult); // true
}
public static boolean isAdult(Integer age, Predicate<Integer> p) {
return p.test(age);
}
测试逻辑运算:
public static void main(String[] args) {
boolean ageRange1 = judgeAnd(18,
age -> age >= 18,
age -> age <= 40);
boolean ageRange2 = judgeOr(18,
age -> age >= 18,
age -> age <= 40);
boolean ageRange3 = judgeNegate(25,
age -> age >= 18);
System.out.println("是否在年龄范围内:" + ageRange1); // true
System.out.println("是否在年龄范围内:" + ageRange2); // true
System.out.println("是否在年龄范围内:" + ageRange3); // false
}
public static boolean judgeAnd(Integer age,
Predicate<Integer> p1,
Predicate<Integer> p2) {
return p1.and(p2).test(age);
}
public static boolean judgeOr(Integer age,
Predicate<Integer> p1,
Predicate<Integer> p2) {
return p1.or(p2).test(age);
}
public static boolean judgeNegate(Integer age,
Predicate<Integer> p) {
return p.negate().test(age);
}
Predicate接口的静态方法isEqual,其实就是判断两个对象是否相等。
public static void main(String[] args) {
boolean equals = isEquals("曹仁", Predicate.isEqual("曹爽"));
System.out.println("是否相等:" + equals); // false
}
public static boolean isEquals(String name, Predicate<String> p) {
return p.test(name);
}
方法引用
引入
在某些情况下,虽然我们使用了lambda表达式,但是我们的代码还是存在冗余的情况,如下:
@Test
public void redundance(){
// 冗余情况
CalcMax calcMaxRedundance = list ->{
int max = list.get(0);
for (Integer ele : list) {
if(ele > max){
max = ele;
}
}
return max;
};
// Collections类有个静态方法计算集合的最大值
CalcMax calcMax2 = list -> Collections.max(list);
// 使用方法引用
CalcMax calcMax3 = Collections::max;
List<Integer> asList = Arrays.asList(1,2,4,7,3,2);
System.out.println("集合的最大值:" + calcMaxRedundance.getMax(asList)); // 7
System.out.println("集合的最大值:" + calcMax2.getMax(asList)); // 7
System.out.println("集合的最大值:" + calcMax3.getMax(asList)); // 7
}
上面的::
是方法引用的符号
方法引用的应用场景:如果Lambda表达式所需要实现的功能,在别的方法中已经有存在的解决方案,那么就可以使用方法引用。
方法引用的分类:
-
对象引用::实例方法名 -
类名::静态方法名 -
类名::实例方法名 -
类名::new -
类型[]::new
对象名::引用成员方法
如果一个类中已经存在了一个成员方法,就可以直接通过对象名引用该方法。
下例中Date对象now中有成员方法getTime,所以可以直接使用now::getTime
调用
@Test
public void objectReferenceMethod() {
Date now = new Date();
Supplier supplier1 = () -> now.getTime();
// 方法引用
Supplier supplier2 = now::getTime;
System.out.println("当前时间戳:" + supplier1.get()); // 1598267089735
System.out.println("当前时间戳:" + supplier2.get()); // 1598267089735
}
类名::引用静态方法
LocalDateTime日期类有个静态方法now可以直接获得当前日期,所以可以用LocalDateTime::now
替代。
@Test
public void classReferenceStaticMethod() {
Supplier supplier1 = () -> LocalDateTime.now();
// 方法引用
Supplier supplier2 = LocalDateTime::now;
System.out.println("当前日期:" + supplier1.get()); // 2020-08-24T19:09:08.632
System.out.println("当前日期:" + supplier2.get()); // 2020-08-24T19:09:08.632
}
类名::引用实例方法
@Test
public void classReferenceMemberMethod() {
Function<String, Integer> function1 = name -> name.length();
// 类名::成员方法
Function<String, Integer> function2 = String::length;
System.out.println("获得名字的长度:"+function1.apply("第五佩佩")); // 4
System.out.println("获得名字的长度:"+function2.apply("肖珏")); // 2
}
类名::构造器
创建一个ArrayList对象,方法引用可以写作ArrayList::new
。
@Test
public void classReferenceConstruction() throws Exception {
// 创建一个长度默认的ArrayList
Supplier supplier1 = () -> new ArrayList<>();
Supplier supplier2 = ArrayList::new;
// 创建一个长度为32的ArrayList
Function<Integer, ArrayList> function1 = size -> new ArrayList<>(size);
Function<Integer, ArrayList> function2 = ArrayList::new;
// 验证方法引用创建的集合的容量是否是32
// 获得集合对象
ArrayList list = function2.apply(32);
// 反射获得集合容量
Field elementDataField = ArrayList.class.getDeclaredField("elementData");
elementDataField.setAccessible(true);
Object[] elementDate = (Object[]) elementDataField.get(list);
System.out.println("集合容量为:" + elementDate.length); // 32
}
类名[]::构造器
@Test
public void classArrayReferenceConstruction() {
// 创建一个指定长度的int数组
Function<Integer, int[]> function1 = length -> new int[length];
// 方法引用创建数组
Function<Integer, int[]> function2 = int[]::new;
System.out.println("数组长度:" + function1.apply(5).length); // 5
System.out.println("数组长度:" + function2.apply(6).length); // 6
}
this方法引用成员方法
this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用this::成员方法的格式来使用方法引用。
public class ThisDemo {
public static void main(String[] args) {
ThisDemo process = new ThisDemo();
process.goHome();
}
/**
* 创建一辆车
*/
public String buyCar() {
return "奥拓";
}
/**
* 回家需要的交通工具
*/
public void newVehicle(Supplier<String> s) {
String carBrand = s.get();
System.out.println("我开" + carBrand + "回家,很有面子");
}
/**
* 回家过年
*/
public void goHome() {
// newVehicle(() -> this.buyCar()); // Lambda表达式方式
newVehicle(this::buyCar); // this方法引用
}
}
super方法引用成员方法
如果存在继承关系,当Lambda表达式中需要出现super调用时,也可以使用方法引用进行替代。
/**
* 钓鱼活动的函数式接口
*/
@FunctionalInterface
public interface FishingFunction {
void fishing();
}
/**
* 父类
*/
public class Father {
public void goFishing(){
System.out.println("爷在钓鱼");
}
}
/**
* 子类
*/
public class Son extends Father {
public void goFishing(){
// function(()-> super.goFishing());
// 方法引用方式
function(super::goFishing);
}
public void function(FishingFunction fish){
fish.fishing();
System.out.println("小弟弟带了太阳伞...");
}
public static void main(String[] args) {
Son son = new Son();
son.goFishing();
}
}
控制台:
爷在钓鱼
小弟弟带了太阳伞...
注意
(1)假如把下面的Lambda表达式中的注释打开就报错,这里我们虽然没有标识 count类型为 final,但是在编译期间虚拟机会帮我们加上 final 修饰关键字。匿名内部类也是如此。
@Test
public void tips01() {
int count = 5;
Function<Integer, Integer> f1 = num -> {
// count = count + 5;
return num + 5;
};
new Thread(new Runnable() {
@Override
public void run() {
// count = count + 5;
}
}).start();
}
(2)对于匿名类,关键字 this 解读为匿名类,而对于 Lambda 表达式,关键字this 解读为写就 Lambda 的外部类。
虽然Lambda表达式使我们的代码更加简洁,但是正是因为简洁,使我们的代码的阅读性降低了。
原文始发于微信公众号(肝帝笔记):JDK8-Lambda表达式
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/31547.html