JDK1.8新特性(1)
函数式接口,lambda表达式
1.接口方法
(1).默认方法
在JDK1.8中,接口里面可以定义默认方法。
定义语法:
interface interfaceName{
default returnType methodName(arg-list){}
}
默认方法有两大优势:
1.可以让接口更优雅的升级,减少使用人员的负担,不必随着接口方法的增加,从而修改实现代码,因为默认方法在子类中不用实现
2.可以让实现类中省去很多不必要方法的空实现
接口继承及默认方法冲突:
java中一个类只能继承一个父类,但是可以实现多个接口,由于Java8中接口中的方法可以直接进行实现,那么类中也可以从多个接口继承这些方法。
例如:
interface A{
default void test(){
System.out.println("Default Method test in A");
}
}
interface B extends A{
default void test(){
System.out.println("default method test in B");
}
}
class C implements A,B{
public static void main(String[] args){
C c = new C();
c.test();
}
} //运行结果:
default method test in B
方法调用的判断规则:
1.类中声明的方法优先级最高,类或父类中,声明的方法要高于任何默认方法的优先级
2.如果无法依据第一条进行判断,那么子接口的优先级更高
3.最后,如果还是无法判断,那么继承了多个接口的类,必须同个实现(重写)方法来确定方法的调用
例如:一个类C,继承父类A,实现接口B
class A{
public void test(){
System.out.println("Default Method test in A");
}
}
interface B {
default void test(){
System.out.println("default method test in B");
}
}
class C extends A implements B{
public static void main(String[] args){
C c = new C();
c.test();
}
} //运行结果:default method test in A
因为A是类,B是接口,A里面的test方法优先级更高
例如:一个类C,实现两个接口A,B,但是A和B之间没有子父接口关系
interface A{
default void test(){
System.out.println("Default Method test in A");
}
}
interface B {
default void test(){
System.out.println("default method test in B")
}
}
//如下代码编译会报错。
/*class C implements A,B{
public static void main(String[] args){
C c = new C();
c.test();
}
}*/
//如果C需要同时实现A和B接口,那么必须显示覆盖
class C implements A,B{
public void test(){
//如果在C中需要显示访问A/B的默认方法,可以使用接口名.super.方法名();
A.super.test();
B.super.test();
//或者自己编写test方法的实现
}
}
(2)静态方法
在JDK1.8中接口还可以定义静态方法。和类中定义的静态方法类似,接口中的静态方法可以直接通过接口名.静态方法名 的形式进行访问。
语法:
interface interfaceName{
static returnType methodName(arg-list){
//实现代码
}
}
访问:InterfaceName.methodName();
接口中的静态方法只能使用当前接口的名字来调用
2.Lambda
Lambda表达式是JDK1.8新增的一种语法,以确保Java代码中可以支持函数式编程。
(1)行为参数化
在java代码中,我们如果需要运算,那么大部分我们的过程是确定的,但是实际参与运算的数却不确定。
例如,根据用户输入的两个数,求两个数之间所有数据的和
public static int sum(int a,int b){
int result = a;
for(int i = a+1;i<=b;i++){
result += i;
}
return result;
}
如果后来用户的需求,变成求两个数之间所有数据的乘积
public static int multiply(int a,int b){
int result = a;
for(int i = a+1;i<=b;i++){
result *= i;
}
return result;
}
如果后来用户的需求,成求两个数之间所有能被3整除的数据的和
public static int addAllByCondition(int a,int b){
int result = a;
for(int i = a+1;i<=b;i++){
if(i%3==0){
result += i;
}
}
return result;
}
在实际项目中,用户需求的变动,是很正常的一件事情,所以在上述的案例中,我们不断的增加方法,来解决用户新的需求,但是在整个过程中,我们复制了大量的相同的代码
这几个方法之间相同的地方,例如:
- 方法的修饰符
- 方法的返回类型
- 方法的参数列表
- 方法对数据的遍历
这几个方法之间不同的地方,例如:
-
方法的名字
-
方法中遍历数据后的核心操作
**需要注意的是,方法的名字其实是无关紧要的,它只是在调用时,用到的一个标示符,最关键的是,对这个对数据的核心操作,也就是代码中核心的行为操作。 **
这时候我们可以将这个核心的行为操作,进行抽象,编程一个参数
public static int calculate(int a,int b, 此处传递一个计算行为){
int result = a;
for(int i = a+1;i<=b;i++){
调用计算行为,操作当前数据
}
return result;
}
注意,方法名改为了calculate,代表了所有的计算情况,每种情况由一个具体的 计算行为 来控制, 而这个 计算行为 就是方法中需要接收的参数。(行为参数化)
我们可以将这个计算行为,用接口的形式进行定义:
interface Action{
int action(int result,int i);
}
这时候calculate方法就可以这样进行定义:
public static int calucate(int a,int b,Action action){
int result = a;
for(int i = a +1;i<=b;i++){
result = cal.action(result,i)
}
return result;
}
具体的使用方式:
//定义累加的核心操作行为
class Add implements Action{
public int action(int result,int i){
return result+i;
}
}
//定义累乘的核心操作行为
class Multiply implements Action{
public int action(int result,int i){
return result*i;
}
}
class Test{
public static void main(String[] args){
int result = 0;
result = calculate(3,5,new Add());
//[3,5]之间,累加的结果
System.out.println(result);
result = calculate(3,5,new Multiply());
//[3,5]之间,累乘的结果
System.out.println(result)
}
上述代码中,将我们要执行的核心计算操作,定义成了一个数,传给了 calculate 方法我们可以通过这个参数,给方法传递不同的行为,来实现不同操作,这就是行为参数化
Java中,不允许孤立的代码存在,我们要想将行为(核心操作代码)传递给 calculate 方法,就必须要将这些核心操作代码,包装在一个实现了Action的类中。
为了减少声明和定义类,Java提供了匿名内部类的实现,来简化我们刚才的调用过程:
class Test{
public static void main(String[] args){
int result = 0;
result = calculate(3,5,new Action(){
public int action(int result,int i){
return result+next;
}
});
//[3,5]之间,累加的结果
System.out.println(result);
result = calculate(3,5,new Action(){
public int action(int result,int i){
return result*next;
}
});
//[3,5]之间,累乘的结果
System.out.println(result)
}
}
上述代码中,使用了匿名内部类的方式,虽然简化了之前的代码,但是每次调用还是编写了很多相同代码,例如 :
-
new Action(){}
-
public int action(int result ,int next){}
这个new对象的操作,还有action方法声明的操作,每次都是重复的,其实我们真正的关心的只有三点 :
-
方法的参数列表
-
方法中的核心操作代码
-
方法的返回类型
也就是,传入指定的参数,通过核心计算,给出最后结果
例如:
class Test{
public static void main(String[] args){
int result = 0;
/*
//忽略掉之前匿名内部类形式中,不重要的部分
Action add = (int result,int i) -> {
return result + i;
};
*/
//简化写法
Action add = (result,i) -> result + i;
result = calculate(3,5,add);
//去掉中间变量add
//相当于,第三个参数,我们传了一个核心的计算行为
//这个核心的计算行为,就是对Action接口中action方法的实现
//result = calculate(3,5,(result,next) -> result + next);
//[3,5]之间,累加的结果
System.out.println(result);
}
}
//运行结果:
(2)函数式编程
函数式编程,和面向对象编程,以及面向过程编程一样,他们都是编程的一种方式。
函数式编程是面向数学的抽象,将计算过程描述为一种表达式求值。简单说,函数式程序就是一个表达式。严格意义上的表达式,就是由数据和操作符按照一定的规则,组合在一起形成的序列,并且所有的表达式都是有返回结果的,这是这里所说的表达式和代码语句的最大的区别。但是在java中,是允许函数式编程中没有任何返回值的,因为java中有关键字 void ,但是在其他一些专门的函数式编程语言中,是没有 void 的。
所以,jdk1.8中,可以把Action接口的实现,简写为:
/*
Action add = (int result,int next) -> {
return result + next;
};
*/
//简化写法
Action add = (result,next) -> result + next;
(result,next) -> result + next; 在JDK1.8中,这部分就称之为Lambda表达式。
(3)Lambda概述
Lambda表达式,可以用来表示一个函数,它只关注函数的参数列表,函数主体、返回类型,并且可以将此函数作为一个参数,进行传递。
在Java中,Lambda表达式还有另一个存在的意义,那就是作为一个接口的实现类对象
例如:
public class Test {
public static void main(String[] args) {
Action a = (str) -> str.length();
System.out.println(a.test("hello"));
}
}
interface Action{
public int test(String str);
}
可以看出,Lambda表达式,虽然可以通过(参数列表,函数主体、返回类型)三部分来表示一个具体的函数操作,但是它必须是依托在一个接口才行,所以Lambda表达式就是对接口中抽象方法的实现。
(4) Lambda使用
接口中有且只有一个抽象方法的时候,才可以使用Lambda表达式来对其进行实现。
- 函数式接口
有且只有一个抽象方法的接口,就是函数式接口。该接口中,也允许有其他的默认方法和静态方法。
JDK1.8中,针对函数式接口,新增了一个注解@FunctionalInterface ,用来检查被标注的接口,是不是一个函数式接口,如果不是,那么编译器会报错。但是,该注解不是必须要用的,它只是会让编辑器帮我们检查一下而已,以免出现接口中抽象方法的个数不是1的情况。
- Lambda语法
lamb表达式的格式为:()->{}
- ()表示参数列表
- ->后面跟的是函数主体
- {}函数主体,表达式的返回值,由这个函数主体的代码来决定
函数式接口中,抽象方法常见的有以下几种情况:
1.函数式接口中,抽象方法无参,无返回值
public class Test {
public static void main(String[] args) {
/**
* Action action1 = new Action(){
* *
public void run(){
* *
}
* };
*/
Action action1 = () -> {};
/**
* Action action2 = new Action(){
* *
public void run(){
* System.out.println("hello");
* }
* };
*/
//函数主体中,如果只有一句代码,那么可以省去大括号
Action action2 = () -> System.out.println("hello");
/**
* Action action3 = new Action(){
* *
public void run(){
* int a = 1;
* int b = 2;
* System.out.println(a+b);
* }
* };
*/
//函数主体中,如果有多句代码,那么大括号必须要写
Action action3= () -> {
int a = 1;
int b = 2;
System.out.println(a+b);
};
}
}
interface Action{
public void run();
}
2.函数式接口中,抽象方法有参,无返回值
public class Test {
public static void main(String[] args) {
/**
* Action action = new Action(){
* *
public void run(int a){
* *
}
* };
1 2 3 4 5 6 7 8 9
10*/
Action action = (int a) -> {};
/**
* Action action1 = new Action(){
* *
public void run(int a){
* *
}
* };
*/
//只有一个参数时,可以不加小括号
//并且参数的类型,JVM运行时会做自动推断的,及时不写,它也知道是int
Action action1 = a -> {};
/**
* Action action2 = new Action(){
* *
public void run(int a){
* System.out.println(a);
* }
* };
*/
Action action2 = a -> System.out.println(a);
/**
* Action action3 = new Action(){
* *
public void run(int a){
* a++;
* }
* };
*/
Action action3 = a -> a++;
}
}
//抽象方法是一个参数的情况
interface Action{
public void run(int a);
}
public class Test {
public static void main(String[] args) {
/**
* Action action1 = new Action(){
* *
public void run(int a,int b){
* *
}
* };
*/
Action action1 = (a,b) -> {};
}
}
//抽象方法是多个参数的情况
interface Action{
public void run(int a,int b);
}
3.函数式接口中,抽象方法 无参,有返回值
public class Test {
public static void main(String[] args) {
/**
* Action action1 = new Action(){
* *
public void run(){
* return 1;
* }
* };
*/
//如果就一句代码,大括号可以去掉,return关键字也会省去
Action action1 = () -> 1;
/**
* Action action2 = new Action(){
* *
public void run(){
* int num = 10;
* return (int)(Math.random()*num);
* }
* };
*/
Action action2 = () -> {
int num = 10;
return (int)(Math.random()*num);
};
}
}
interface Action{
public int run();
}
4.函数式接口中,抽象方法有参,有返回值
public class Test {
public static void main(String[] args) {
/**
* Action action1 = new Action(){
* *
public int run(int a,int b){
* return a+b;
* }
* };
*/
Action action1 = (a,b) -> a + b;
/**
* Action action2 = new Action(){
* *
public int run(int a,int b){
* int num = a+b;
* return (int)(Math.random()*num);
* }
* };
*/
Action action2 = (a,b) -> {
int num = a+b;
return (int)(Math.random()*num);
};
}
}
interface Action{
public int run(int a,int b);
}
**注意,Lambda表达式中的参数列表,里面的参数可以不写类型,因为JVM在运行时会自动推断,当然,如果直接手动写上去,也完全没有问题。 **
-
常用接口
JDK1.8中,已经定了一些会常用到的函数式接口,这些函数式接口都定义在 java.lang.function 包中,例如 Predicate 、 Consumer 、 Function 、 Supplier 、 UnaryOperator 和BinaryOperator 等。
注意,如果需要,也可以自己定义类似的函数式接口,并不是必须要使用这些定义好的接口
1.Predicate
java.util.function.Predicate 接口定义了一个名叫test 的抽象方法,它接受泛型T 对象,并返回一个 boolean 类型的结果
@FuntionalInterface
public interface Predicate<T>{
boolean test(T t);
}
此外Predicate接口中,还定义了一些默认方法和静态方法:
-
and()
-
or()
-
negate()
在使用该接口来做判断的时候,经常需要几个条件同时成立,或者其中一个条件成立,或者求反。
在这种情况下,除了可以在代码中选择使用&&,||,!之外,也可以分别使用这三个方法来代替。
2.Consumer
==java.util.function.Consumer==接口:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<?superT>after{
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
例如,定义一个方法,用来对学生对象进行操作
public class Test {
public static void main(String[] args) {
Test t = new Test();
Student stu = new Student("tom");
//操作1,给stu对象的name属性值加前缀
Consumer<Student> consumer1 = (s) -> s.name = "briup_"+s.name;
//操作2,给stu对象的name属性值加后缀
Consumer<Student> consumer2 = (s) -> s.name =s.name+"_"+System.currentTimeMillis();
//操作3,给stu对象的name属性值,先加前缀,再加 后缀
Consumer<Student> consumer3 = consumer1.andThen(consumer2);
//如果传入consumer1,表示只加前缀
//如果传入consumer2,表示只加后缀
//如果传入consumer3,表示先加前缀,再加后缀
t.operStu(stu,consumer3);
System.out.println(stu.name);
}
public void operStu(Student stu, Consumer<Student> consumer){
consumer.accept(stu);
}
}
class Student{
String name;
public Student(String name) {
this.name = name;
}
}
//运行结果:
briup_tom_1598282322884
JDK1.8中,给Collection集合增加了默认方法,forEach用来遍历集合,定义如下:
例如:
public class Test {
public static void main(String[] args) {
Collection<String> col = new ArrayList<String>();
col.add("abc");
col.add("hello");
col.add("world");
//去掉中间变量,直接把Lambda表达式当前参数传入到forEach方法中
col.forEach((t)->System.out.println(t));
}
}
//运行结果:
abc
hello
world
3.Function
java.util.function.Function<T, R> 接口
@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));
}
//Returns a function that always returns its input argument.
static <T> Function<T, T> identity() {
return t -> t;
}
}
例如:
public class Test {
public static void main(String[] args) {
String str = "a-b-c-a-b-c";
//传入字符串,返回数组,操作为把字符串按照 "-" 进行分割为字符串数组
// "a-b-c-a-b-c" 转换为 {"a","b","c","a","b","c"}
Function<String,String[]> f1 = s -> s.split("-");
//传入字符串数组,返回Set<String>集合,目的是去除数组中重复的数据,存放把结 果存放到Set集合中
//{"a","b","c","a","b","c"} 转换为集合 [a, b, c]
Function<String[], Set<String>> f2 = arr -> {
Set<String> set = new HashSet<>();
for(String string : arr){
set.add(string);
}
return set;
};
//刚好,f1函数的结果,作为f2函数的参数,f1和f2组合成f3函数
//f3函数表示传入字符串,最后返回Set<String>集合
//其实内部是先将字符串交给f1函数转换数组,在将数组交给f2函数转换Set集合
//通过上面列出的andThen源码也可以看出是这个效果
Function<String,Set<String>> f3 = f1.andThen(f2);
Set<String> set = f3.apply(str);
System.out.println(set);
}
}
//运行结果:
[a, b, c]
静态方法identity,可以直接返回一个function对象,传入一个参数直接把该参数返回,不做任何操作,这是一个泛型方法
例如
public class Test {
public static void main(String[] args) {
Function<String,String> f = Function.identity();
//传入hello,返回hello
String result = f.apply("hello");
System.out.println(result);
}
}
//运行结果:hello
4.Supplier
java.util.function.Supplier 接口
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
* *
@return a result
*/
T get();
}
可以看出,Supplier接口中的方法,不需要参数,可以根据我们指定的算法,返回一个数据
JDK1.8中,还专门定义了一些针对基本类型的函数式接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8iMtFjKD-1600181042008)(\images\image-20200910000420454.png)]
3.类型推断
使用Lambda表达式,相当于给函数式接口生成一个实例,但是Lambda表达式本身,并不包含这个接口的任何信息
之所以Lambda表达式中没有接口的任何信息,JVM还能将其和接口匹配的上,那是因为:
- 我们在使用Lambda表达式的时候,JVM是会通过上下文自动推断它所属接口类型的
- 并且接口中只有一个抽象方法,自然也能匹配成功该表达式所对应实现的抽象方法
- 类似的,JVM还能自动推断出Lambda表达式中参数的类型
4.重载分析
如果类中的方法进行了重载,那么在使用Lambda表达式的时候,很可能给它的类型推断带来问题。
例如
public class Test {
public static void main(String[] args) {
//编译报错,因为俩个方法都符合
test(1,num -> num>0);
}
public static void test(int a,Predicate<Integer> p){
}
public static void test(int a,Function<Integer,Boolean> f){
}
}
可以看出,这时候编译报错,因为表达式 num -> num>0 对于俩个方法都符合
既符合 Predicate 的实现,也符合 Function<Integer,Boolean> 的实现
这时候可以做类型转换,来解决这个问题 但是,这种情况很少出现,我们应该在方法重载的时候就提前注意到这个问题,或者给方法起不同的名字。
5.局部变量
如果在Lambda表达式中,使用了局部变量,那么这个局部变量就一定要使用 final 修饰符进行修饰,这方面的语法要求,和之前学习的匿名内部类保持一致
注意,JDK1.8中,被匿名内部类、局部内部类、Lambda表示访问的局部变量,会默认加上final 修饰符