java 函数式接口与lambda表达式的关系

函数式接口与lambda表达式的关系

在java中,lambda表达式与函数式接口是不可分割的,都是结合起来使用的。

对于函数式接口,我们可以理解为只有一个抽象方法的接口,除此之外它和别的接口相比并没有什么特殊的地方。为了确保函数式接口的正确性,我们可以给这个接口添加@FunctionalInterface注解(当然,也可以不加此注解),这样当其中有超过一个抽象方法时就会报错。

对于其他类型的接口,我们想要使用就需要定义一个类(或者使用匿名类)来实现那个接口和其中的方法,而函数式接口除了使用普通的方法来实现之外,还有一种更加简单的方法—就是使用lambda表达式。lambda表达式我们可以理解对于函数式接口和其中的抽象方法的具体实现,这样当有一个需要函数式接口参数的方法时,我们就可以给其传递一个对应的lambda表达式作为参数。执行的时候就会自动执行函数式接口中的唯一方法,也就是传递过去的lambda表达式了。

下面我们来举一个例子具体说明一下:

有如下代码List<String> list = Arrays.asList("d", "h", "a", "z", "b");

我们要对其进行排序,有一个对应的list.sort(Comparator<? super E> c)方法,需要我们传递一个Comparator接口的实例,而Comparator之中唯一的抽象方法为int compare(T o1, T o2),完全符合我们之前的函数式接口的定义,并且它还使用了@FunctionalInterface注解,所以除了普通的实现方法之外我们可以使用lambda表达式来实现这个方法,具体代码如下:

List<String> list = Arrays.asList("d", "h", "a", "z", "b");
list.sort((String a, String b) -> {
    return a.compareTo(b);
});

其相当于:

List<String> list = Arrays.asList("d", "h", "a", "z", "b");
Comparator<String> comparator = (String a, String b) -> {
    return a.compareTo(b);
};  // 使用lambda表达式实现函数式接口,并赋值
list.sort(comparator);

当然,对于上面的lambda表达式有很多简略写法,这是主要说明它和函数式接口的关系,关于lambda表达式的其他很多的使用方法大家可以去具体查询使用。

在底层,Arrays.sort()方法会接受实现了Comparable< String>的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效的多。最好把lambda表达式看做是一个函数,而不是一个对象,另外要接受lambda表达式可以转化为函数式接口
lambda表达式可以转换为接口,这一点让lambda表达式很有吸引力

ActionListener work=new aaa();
//aaa为实现了ActionListener接口的类;
Timer t=new Timer(1000,work);
t.start();
//
class aaa implements ActionListener
{
   public actionPeformedPane()
   {
           System.out.println("hello");
   }
}
//对比于
Timer t=new Timer(1000,bbb->
{
    System.out.println("hello");
});

与使用了ActionListener接口的类相比,这个代码的可读性要好很多
实际上,在Java中,对lambda表达式所能做的也只是能转化为函数式接口。在其他支持函数字面量的程序设计语言中,可以声明函数类型(如(String,String)->int)、声明这些类型的变量,还可以使用变量保存函数表达式。不过,java设计者还是决定保持我们熟悉的接口概念,没有为java语言增加函数类型。
甚至不能把lambda表达式赋值给类型为Object的变量,Object不是一个函数式接口
(函数式接口:对于只有一个抽象方法的接口,当需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口)
java中已经有很多封装代码块的接口,如ActionListener或者Comparator。lambda表达式与这些接口是兼容的
Java API在java.util.funcion包中定义了很多非常通用的函数式接口。其中一个接口BiFuncion

BiFuncion<String ,String,Integer>cmp=(first, second)->first.length()-second.length();

不过,这对于排序并没有帮助。没有哪个Arrays.sort方法想要接受一个BiFunction。如果你之前用过某种函数是程序设计语言,可能会发现这很奇怪,不过,对于java程序员而言,这非常自然。类似Comparator的接口往往有一个特定的用途

Arrays.sort(aa,(first,last)->first.length()-last.length());
BiFuncion<String ,String,Integer>cmp=(first, second)->first.length()-second.length();

,而不只是提供有指定参数和返回类型的方法。想要用lambda表达式做某些处理,还是要谨记表达式的用途,为他建立一个特定的函数是接口。
java.util.function包中有一个尤其有用的接口Predicate

public interface Predicate<T>
{   
    boolean test(T t);
    //additional default and static methods
}

removeIf
public boolean removeIf(Predicate<? super E> filter)
说明从界面复制: Collection
删除满足给定谓词的此集合的所有元素。 在迭代或谓词中抛出的错误或运行时异常被转发给调用者。
Specified by:
removeIf在接口 Collection<E>
参数
filter - 一个谓词,为要删除的元素返回 true
结果
true如果有任何元素被删除

ArrayList类有一个removeIf方法,他的参数就是一个Predicate。这个接口专门用来传递lambda表达式,例如,下面的语句就爱那个从一个数组列表删除所有null值。

list.removeIf(e->e==null)

方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设你希望只要出现一个定时器时间就打印这个事件对象。

Timer t=new Timer(1000,event->System.out.println(even));
//对比
 Timer t=new Timer(1001,listener2->
        {
            System.out.println("hello");

        });
        t.start();

但是,如果直接把println方法传递到Timer构造器就更好了。具体做法如下:

Timer t=new Timer(1000,System.out :: println);
//表达式System.out  :: println是一个方法引用(method referenct),他等价于lambda表达式x->System.out.println(x).

再来看一个列子,假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:

Arrays.sort(Strings,String::compareToIgnoreCase);

可以看出,要用::操作符分隔方法名与对象或者方法名与类名。主要有三种情况:
1、object::instanceMethod
2、Class::staticMethod
3、Class::instanceMethod
在前两种情况中,方法引用等价于提供方法参数的lambda表达式。前面已经提到,System.out::println等价于x->System.out.println(x)类似的Math::pow等价于(x,y)->Math.pow(x,y);
对于第三种情况,第一个参数会成为方法的目标。例如:String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y)
注释:如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的是哪一个方法。例如:Math.max方法有两个版本,一个用于整数,另一个用于double值。选择哪一种版本取决于Math::max 转换为哪个函数式接口的方法参数。类似于lambda表达式,方法应用不能独立存在,总是会转换为函数式接口的实例。
可以在方法引用中使用this参数。例如:this::equals等同于x->this.equals(x).使用super也是合法的。下面的方法表达式:
super::instanceMethod
使用this作为目标,会调用给定方法的超类版本
为了展示这一点,下面给出一个假象的例子:

class Greeter
{
    public void greet()
    {
        System.out.println("Hello,world");
    }
}
class TimeGreeter extends Greeter
{
    public void greet()
    {
        Timer t=new Timer(1000,super::greet);
        t.start();
    }
}

TimerGreeter.greet方法开始执行时候,会构造一个Timer,他会在每次定时器滴答时执行super::greet方法。这个方法会调用超类greet方法。

package3章;   //run time error !

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.Timer;

/**
 *
 * @author cmx
 */
public class Method_use 
{
    public static void main(String[] args)
    {
        Method2 well=new Method2();
        well.prints();

    }

}
class Method1 
{
    public void prints()
    {
        System.out.println("hello,world");
    }
}
class Method2 extends Method1
{

    public void prints()
    {
        Timer t=new Timer(1000,super::prints);
        t.start();
    }
}

构造器引用
构造器引用与方法引用很类似,只不过方法名为new。例如:Person::new是Person构造器的一个引用。哪个构造器呢?这取决于上下文。假设你有一个字符串列表,可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:
可以用数组类型建立构造器引用。例如:int[]::new是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x->new int[x]
java有一个限制,无法构造反省类型T的数组。数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这会改变为new Object[n]。对于开发类库的人来说,这是一个问题。例如,假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组

Object[] people =stream.toArray();
compact1, compact2, compact3
java.util.stream
Interface Stream<T>

参数类型
T - 流元素的类型


toArray
Object[] toArray()
返回一个包含此流的元素的数组。
这是一个terminal operation 。

结果
一个包含此流的元素的数组

不过,这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了这个问题。可以把Person[]::new传入toArray方法:

Person [] people=stream.toArray(Person[]::new)

来得到一个正确类型的数组,然后甜宠这个数组并返回。

toArray
<A> A[] toArray(IntFunction<A[]> generator)
使用提供的generator函数返回包含此流的元素的数组,以分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组。

使用lambda表达式的重点是延迟执行(deferred execution),毕竟,如果想要立即执行代码,完全可以立即执行,而无需把它包装在一个lambda表达式中。之所以希望以后在执行,有很多原因,比如:
1、在一个单独的线程中运行代码
2、多次运行代码
3、在算法的适当位置运行代码(例如:排序中的比较操作)
发生某些情况时执行代码(如:点击了一个按钮,数据到达,等等)
4、只有在必要时执行代码。
下面来看一个简单的例子:

package3章;

/**
 *
 * @author cmx
 */
public class lambda_3 
{
    public static void main(String[] args)
    {
        helo.repeat(3, ()->{
            System.out.println("well");
        });
    }

}
class helo
{
    public static void repeat(int n,Runnable ac)
    {
        for(int i=0;i<n;++i)
        {
            ac.run();
        }
    }
}

现在让这个例子更复杂一些

//相较于上一个例子,这个增加了数字处理(因为lambda表达式中引用的值在外部不能在外部改变,但是可以在内部改变,所以对于以上的改进
package3章;
import java.util.function.*;
/**
 *
 * @author cmx
 */
public class lambda_31
{
    public static void main(String[] args)
    {
        well_31.repeat1(5, i->System.out.println("Count: "+(9-i)));
    }

}
class well_31
{
    public static void repeat1(int n,IntConsumer ac )
    {
        for(int i=0;i<n;++i)
        {
            ac.accept(i);
        }
    }
}
package3章;
import java.util.function.*;
/**
 *
 * @author cmx
 */
public class lambda_3 
{
    int i=0;
    public static void main(String[] args)
    {
        helo.repeat(3, ()->{
            for(int j=0;j<5;++j)
                System.out.println("well"+j);
        });
    }

}
class helo
{
    public static void repeat(int n,Runnable ac)
    {
        for(int i=0;i<n;++i)
        {
            ac.run();
        }
    }
}

表中列出了基本类型int 、long、和double的34个可能的标准规范
提示:最好使用表中的接口。
注释:大多数标准函数式接口都提供了非抽象方法来生成或者合并函数。例如,Predicate.isEqual(a)等同于a:equals,不过如果a为null也能正常工作。已经提供了默认方法and、or、和negate来合并谓词。例如,Predicate.isEqual(a).or(Predicate.isEqual(b))就等同于x->a.equals(x)||b.equals(x)
如果设计你自己的接口,其中只有一个抽象方法,可以用@FunctionalInterface注释来标记这个接口。这样做有两个优点:
1、如果无意中增加了另一个非抽象方法,编译器会产生一个错误消息。
2、另外javadoc页里会指出你的接口是一个函数式接口。
并不是必须使用注释。
根据定义,任何一个只有一个抽象方法的接口都是函数式接口。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值