java学习之路--lambda表达式

1.1 为什么引入lambda表达式
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。我们先观察一下在Java中的哪些地方用过这种代码块。
在接口那一节中,已经了解如何按照指定时间间隔完成工作。将这个工作放在ActionListener的actionPerformed方法中:

 class Worker implements ActionListener
 {
     public void actionPerformed(ActionEvent  event)
     {
         //do  some  work
     }
}

如果想要反复执行这个代码,可以构造一个Worker类的实例。之后把这个实例提交到一个Timer对象。重点是actionPerformed方法包含希望以后执行的代码。
可以用一个定制比较器完成排序。如果想按长度而不是默认的字典顺序对字符串排序,可以向sort方法传入一个Comparator对象:

class LengthComparator implements  Comparator<String>
{
   public int compare(String first,String second)
   {
      return first.length()-second.length();
    }
}
.....
Arrays.sort(Strings,new LengthComparator());

compare方法不是立即调用,在数组完成排序之前,sort方法会一直调用compare方法,只要元素的顺序不正确就会继续排列。将比较元素所需的代码段放在sort方法中,这个代码将与其余的排序逻辑集成。
这两个例子有一些共同点,都是将一个代码块传递到某个对象(一个定时器,一个sort方法)。这个代码块会在将来某个时间调用。
Java是面向对象语言,不能直接传递代码段,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。
在其他语言中能直接处理代码块。
1.2 lambda表达式的语法
以排序的例子为例,我们传入代码来检查一个字符串是否比另一个字符串短。这里要计算:

  first.length()-second.length()

其中first和second都是字符串,Java是一种强类型语言,必须制定类型:

 (String  first,String second)
 ->first.length()-second.length()

这就是你看到的第一个lambda表达式。lambda表达式就是一个代码块,以及必须传入代码的变量规范。
如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{} 中,并包含显式的return语句。例如:

(String first,String second)  ->
{
   if(first.length<second.length())  return  -1;
   else if(first.length>second.length())  return 1;
   else return 0;
}

即使lambda表达式没有参数,仍然要提供空括号,就像无参方法一样:

()->{for (int i=100;i>=0;i==) System.out.println(i);}

如果可以推导出一个lambda表达式的参数类型,就可以忽略其类型。例如:

Comparator<String> comp  =(first,second) -> first.length()-second.length();

这里编译器可以推导出first,second必然是字符串,因为这个lambda表达式将赋给一个字符串比较器。
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么还可以省略小括号:

ActionListener listene=event  ->System.out.println("The time is"+new Date()");

无需指定lambda表达式的返回类型。
该程序显示了如何在一个比较器和一个动作监听器中使用lambda表达式。
在这里插入图片描述

运行结果:
在这里插入图片描述1.3 函数式接口
Java中有很多封装代码块的接口,如ActionListener或Comparator。lambda表达式与这些接口是兼容的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface).
演示如何转换为函数式接口,以Arrays.sort方法为例。它的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:

  Arrays.sort(words,
  (first,second)->first.length()-second.length());

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

 Timer t=new Timer(1000,event ->
 {
    System.out.println("At the tone,the time is"+new Date());
    Toolkit.getDefaultTookit().beep()});

与使用是下了ActionListener接口的类相比,这个代码的可读性要好很多。
在java中,对lambda表达式所能做的也只是能转化为函数式接口。
java.util.function包中有一个尤其有用的接口Predicate:

public interface Predicate<T>
{
  boolean test(T t);
  //添加默认和静态方法
}

ArrayList类有一个removeIf方法,它的参数就是一个Predicate.这个接口专门用来传递lambda表达式。如:从一个数组列表删除所有的null值:

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

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

  Timer t=new Timer(1000,event->System.ou.println(event));

但是,如果直接吧println方法传递到Timer构造器就更好了:

Timer t=new Timer(1000,System.out::println);

表达式System.out::println是一个方法引用(method reference),它等价于lambda表达式 x->System.out.println(x).
加入你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:

Arrays.sort(strings,String::compareToIgnoreCase)

可以看出,要用::操作符分隔方法名或类名。主要有3种情况:

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod
    前2个,方法引用等价于提供方法参数的lambda表达式。前面的System.out::println等价于x->System.out.println(x).类似地,Math::pow等价于(x,y)->Math.pow(x,y).
    第3个,第一个参数会成为方法的目标。例如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y).
    可以在方法引用中使用this参数,例如,this::equals等同于x->this.equals(x).使用super也是合法的。
 super::instanceMethod

使用this作为目标,会调用给定方法的超类版本。

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

TimerGreeter.greet方法开始执行时,会构造一个Timer,会在每次定时器滴答时执行super::greet方法。这个方法会调用超类的greet方法。
1.5 构造器引用
构造器引用与方法引用很类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用。这个构造器取决于上下文。假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:

ArraysList<String> names=....;
Stream<Person> stream=names.stream().map(Person::new);
List<Person> people=stream.collect(Collectors.toList());

重点是map方法会为各个列表元素调用Person(String)构造器。如果有多个Person构造器,编译器会选择有一个String参数的构造器,因为从上下文推导出这是在对一个字符串调用都早起。
可以用数组类型建立构造器引用。例如:int[]::new 是一个构造器引用,它有一个参数::即数组的长度。这等价于lambda表达式x->new int[x].
java无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这里会 改为new Object[n]。对于开发类库的人来说,这是一个问题。例如,假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:

Object[] people=stream.toArray();

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

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

1.6 变量作用域
通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量。

public stativ void repeatMessage(String text,int delay)
{
  ActionLisner listener=even->
    {
       System.out.println(text);
       Toolkit.getDefaultToolkit().beep();
};
new Timer(delay,listener).start();
}

这样一个引用
repeatMessage(“Hello”,1000); //每隔1000毫秒打印一次Hello
现在来看lambda表达式中的变量text。并不是在lambda表达式中定义的,实际上,这是repearMessage方法的一个参数变量。
lambda表达式的代码可能会在repeatMessage调用返回很久以后才允许,而那时这个参数变量已经不存在了。如何保留text变量呢?
lambda表达式有3部分:

  1. 一个代码块;
  2. 参数
  3. 自由变量的值,这是指非参数而且不在代码中定义的变量。
    text就是自由变量,在这里存储的值是字符串“Hello”。我们说它被lambda表达式捕获(captured)(可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。)
    lambda表达式可以捕获外围作用域中变量的值。在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在lambda表达式中,只能引用值不会改变的变量。例如,下面是不合法的:
 public static void countDown(int start,int delay)
{
   ActionListener listener=event->
   {
       start--;  //错误,无法改变自由变量
       System.out.println(start);
       };
       new Timer(delay,listener).start();
}

如果在lambda表达式中改变变量,并发执行多个动作时就会不安全。
如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。

 public static void repeat(String text,int count)
  {
     for(int i=1;i<=count;i++)
     {
        ActionListener listener=event ->
        {
          System.out.println(i+":"+text);
          //错误:无法改变引用i
          };
          new Timer(1000,listener).start();
          }
}

lambda表达式中捕获的变量必须实际上是最终变量,即初始化之后就不会再为它赋新值。text总是指示同一个String对象,所以捕获这个变量是合法的。但是i的值会改变,因此不能捕获i。
在lambda表达式中声明一个局部变量同名的参数或局部变量是不合法的:

 Path first=Paths.get("/usr/bin");
 Comparator<String> comp=
  (first,second)  ->first.length()-second.length();
  //错误:变量first已经被定义了

在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。

   public class  Application()
   {
      public void  int()
      {
         ActionListener listener =event ->
         {
            System.out.println(this.toString());
            ...
            }
}
}

这里的this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。

1.7处理lambda表达式

下面看如何编写方法处理lambda表达式
使用lambda表达式的重点是延迟执行(deferred execution).毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中。使用lambda表达式的情况:

  • 在一个单独的线程中运行代码;
  • 多次运行代码;
  • 在算法的适当位置运行代码(例如,排序中的比较操作);
  • 发生某种情况时执行代码(如,点击一个按钮,数据到达 ,等等);
  • 只在必要时才运行代码
    假设你想要重复一个动作n次。将这个动作和重复次数传递到一个repeat方法:
  repeat(10,()->System.out.println("Hello,World!"));

要接受这个lambda表达式,需要选择一个函数式接口。下表给出了 java API中提供的最重要的函数式接口。在这里我们可以使用Runnable接口:

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

在这里插入图片描述

现在让这个例子更复杂一些,告诉这个动作出现在那一次迭代中。为此需要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int参数而且返回类型为void.处理int值的标准接口如下:

  public interface IntConsumer
  {
      void accept(int value);
}

下面给出repeat方法的改进版本:

public static void repeat(int n,IntConsumer action)
{
   for(int i=0;i<n;i++)  action.accept(i);
}

可以这样调用:

repeat(10,i->System.out.println("Countdown:"+(9-i)));
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值