lambda

lambda表达式就是一个代码块,以及必须传入代码的一个变量规范。

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

这是lambda的一种表达形式:参数,箭头,以及一个表达式。如果代码要完成的计算无法写在一个表达式中,那么就像写方法一样,把这些代码放在{}中,并包含显式的return语句(必须要return吗?)
return 不是必须的,需要看方法是否需要返回值,如果需要返回值并且在{}中,则必须要显式return

ActionListener listener = event -> {
            System.out.println("time is " + new Date());
            Toolkit.getDefaultToolkit().beep();
        };
Comparator<String> comp1 = (String first, String second) -> {
            if (first.length() > second.length()) return -1;
            if (first.length() < second.length()) return 1;
            return 0;
        };

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

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

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

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

如果方法只有一个参数,而且参数类型可以推导出来,那么甚至可以省略小括号:
原始写法:

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

省略写法:

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

无需指定lambda表达式的返回类型,lambda表达式返回类型总是会由上下文推导得出。例如,下面的表达式:

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

函数式接口

对于只有一个方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)。
下面考虑Arrays.sort方法。它的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:

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

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

实际上,在java中,对lambda表达式能做的也只是能转换为函数式接口。

Java API 在java.util.function包中定义了很多非常通用的函数式接口。其中一个接口BiFunction<T,U,R>描述了参数类型为T和U而返回类型为R的函数。可以把字符串比较的lambda表达式保存在这个类型的变量中:

BiFunction<String,String,Integer> compFc = (first, second) -> first.length() - second.length();

那是否有一个入参的呢?还有无返回值的函数,在API中都可以找到对应的。
java.util.function包中有一个非常有用的接口Predicate:

public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

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

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

方法引用

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

Timer t = new Timer(10000, event -> {
            System.out.println(event);
        });

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

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

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

Arrays.sort(planets,String::compareToIgnoreCase);
Arrays.sort(planets,(first, second)-> first.compareToIgnoreCase(second));

从这些例子可以看出,使用**:😗* 操作符分隔方法名和对象名或者类名。主要有3种情况:

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod
    在前两种情况中,方法引用等价于提供方法参数的lambda表达式。前面已经提到,System.out::println等价于 x->System.out.println(x)。
    类似地,Math::pow等价于(double x, double y) -> Math.pow(x, y);
    对于第三种情况,第一个参数会成为方法的目标。例如,String::compareToIgnoreCase等价于(first, second)-> first.compareToIgnoreCase(second)。

构造器引用

构造器引用和方法引用很类似,只不过方法名改成new。例如MeetUp::new是MeetUp构造器的一个引用。假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:

Stream<Person> stream = names.stream().map((String name)->{return new Person(name);});
Stream<Person> stream1 = names.stream().map(name-> new Person(name));
Stream<Person> stream2 = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

map()接收的是一个Function函数,根据java.util.function 的API可知,function是一个接收一个参数并返回一个结果的函数。
所以,要在遍历中把每一个name转化为Person,用最初的lambda表达式为(String name)->{return new Person(name);}
因为参数类型可以根据上下文推导出来,所以类型可以省略,又因参数只有一个,所以()可以省略,具体方法也只有一句,所以大括号和return可以省略,最后是name-> new Person(name)
又因为new Person(String name)是已实现的方法,所以可用Person::new

可以用数组类型建立构造器引用。例如,int[]::new 是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x -> new int[x] 。
Java有一个限制,无法构造泛型类型为T的数组。数组的构造器引用对于克服这个限制很有用。表达式new T[N]会产生错误,因为这会改为new Object[n]。对于开发类库的人来说,这是一个问题。例如,假设我们需要一个Person数组。Stream有一个toArray的方法可以返回Object数组:

Object[] peoples = stream1.toArray();

不过,这并不令人满意,还需要转化为Person[],stream库利用构造器解决了这个问题。可以把Person[]::new传入toArray方法:

Person[] peoples1 = stream1.toArray(Person[]::new);

toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。

变量作用域

lambda和实例化一个接口的实现类还有一个不同,就是lambda可以访问外围的方法或类中的变量。考虑下面这个例子

public class Repeat {
    public static void repeatMessage(String text, int delay) {
        ActionListener listener = event -> {
            System.out.println(text);
            Toolkit.getDefaultToolkit().beep();
        };
        Timer t = new Timer(delay, listener);
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }

    public static void main(String[] args) {
        repeatMessage("hello",1000);
    }
}

如果换成实例化一个接口实现类,是下面这样的。

public class RepeatActionListener implements ActionListener {

    private String text;

    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    }

    public RepeatActionListener(String text) {
        this.text = text;
    }

    public static void repeatMessage(String text, int delay) {
        ActionListener listener = new RepeatActionListener(text);
        Timer t = new Timer(delay, listener);
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }

    public static void main(String[] args) {
        repeatMessage("hello",1000);
    }
}

所以上面那句话说错了,通过实现接口的方式也可以调用外部方法和变量,只不过是需要提前把要调用的方法和变量,实例化到对应的接口实现中。
至于lambda底层是否是这样实现的,留待以后探索。
在lambda表达式中,只能引用值不会改变的变量。例如,下面的做法是不合法的:

public class Repeat {
    public static void repeatMessage(String text, int delay) {
        ActionListener listener = event -> {
            text += "test";
            System.out.println(text);
            Toolkit.getDefaultToolkit().beep();
        };
        Timer t = new Timer(delay, listener);
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }

    public static void main(String[] args) {
        repeatMessage("hello",1000);
    }
}

编译报错:Variable used in lambda expression should be final or effectively final
同样,在等同于lambda表达式的自己实现的接口中操作一遍:

public class RepeatActionListener implements ActionListener {

    private String text;

    private int age;

    @Override
    public void actionPerformed(ActionEvent e) {
        text += "test";
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    }

    public RepeatActionListener(String text) {
        this.text = text;
    }

    public static void repeatMessage(String text, int delay) {
        ActionListener listener = new RepeatActionListener(text);
        Timer t = new Timer(delay, listener);
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }

    public static void main(String[] args) {
        repeatMessage("hello",1000);
    }
}

虽然可以加,但结果是这样的:

hellotest
hellotesttest
hellotesttesttest
hellotesttesttesttest
hellotesttesttesttesttest

虽然自己实现的接口可以修改,但是会有几个问题,第一,根据上面的结果,是否符合改变变量的预期结果,第二,如果变量可以修改,则在任何地方都可以修改,而不是只在实现方法中,第三,由于是实例化一个实现接口的对象,这个对象可以用在任意需要该接口的地方,所以如果产生并发问题,多个线程同时去写一个变量,是线程不安全的。
所以,大家应该能理解为什么lambda表达式中捕获的变量必须是最终变量(effectively final)了吧

lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则:
这样是不对的

String test = "test";
        ActionListener listener = event -> {
            String test = "test1";
            System.out.println(text);
            Toolkit.getDefaultToolkit().beep();
        };

报错:Variable ‘test’ is already defined in the scope
同样在接口实现类中看一下:

public class RepeatActionListener implements ActionListener {

    private String text;

    private int age;

    @Override
    public void actionPerformed(ActionEvent e) {
        String test = "test";
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    }

    public RepeatActionListener(String text) {
        this.text = text;
    }

    public static void repeatMessage(String text, int delay) {
        String test = "test";
        ActionListener listener = new RepeatActionListener(text);
        Timer t = new Timer(delay, listener);
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }

    public static void main(String[] args) {
        repeatMessage("hello",1000);
    }
}

目前是可以的,我也不知道哪里不对,可能这是lambda和嵌套代码块类似它们这样的独有的特性。下次需要详细了解嵌套代码块。

lambda中使用this的时候,它到底是谁?

public class Repeat {
    public void repeatMessage(String text, int delay) {
        String test = "test";
        ActionListener listener = event -> {
            System.out.println(this.getClass());
            System.out.println(text);
            Toolkit.getDefaultToolkit().beep();
        };
        Timer t = new Timer(delay, listener);
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }

    public static void main(String[] args) {
        new Repeat().repeatMessage("hello",1000);
    }
}

首先,改造了一下方法,之前repeatMessage是static方法,而this是对象的引用(后续在学习this…),所以,要测this,不能用static方法。
直接说结果,this.getClass()输出的是class org.example.Repeat,
同样,我们再看接口实现类中的表现。
修改了一下实现类的结构:

public class RepeatActionListener implements ActionListener {

    private String text;

    private int age;

    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println(this.getClass());
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    }

    public RepeatActionListener(String text) {
        this.text = text;
    }
}

public class RepeatActionListenerTest {
    public static void repeatMessage(String text, int delay) {
        String test = "test";
        ActionListener listener = new RepeatActionListener(text);
        Timer t = new Timer(delay, listener);
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }

    public static void main(String[] args) {
        repeatMessage("hello",1000);
    }
}

这个写之前就知道,输出的肯定是RepeatActionListener。

所以总结下,本质上lambda表达式就是代替了接口的实现,还会有一些和嵌套代码块类似的特性。比如this,还有同名和遮蔽规则,下次需要研究。

上面已经说了如何生成lambda表达式,已经如何把lambda表达式传递到需要一个函数式接口的方法中。
上面说到的都是给JDK或者框架等需要函数式接口的方法中传一个自己写的lambda表达式,接下来来看如何编写需要函数式接口的方法。

先说使用场景,使用lambda表达式的重点是延迟执行(deferred execution)。毕竟,如果想要立即执行代码,完全可以直接执行,而无需把他包装在一个lambda表达式中。之所以希望以后在执行代码,是因为有很多原因,如:

  • 在一个单独的线程中运行代码
  • 多次运行代码
  • 在算法的适当位置运行代码(例如,排序中的比较操作)
  • 发生某种情况时运行代码(如,点击了一个按钮,数据到达,等等);
  • 只在必要时才运行代码
    下面看一个简单的例子,假设要重复一个动作n次,把这个动作和重复次数传递到一个repeat方法:
public class RepeatDemo {
    public static void main(String[] args) {
        repeat(10,()->System.out.println("hello,world!"));
    }

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

调用runable.run时,会执行lambda表达式的主体。

函数式接口参数类型返回类型抽象方法名描述其他方法
Runablevoidrun作为无参数或返回值的动作运行
Suppplier<T,>Tget提供一个T类型的值
Consumer<T,>Tvoidaccept处理一个T类型的值
BiConsumer<T, U>T,Uvoidaccept处理T和U类型的值andThen
Function<T, R>TRapply有一个T类型参数的函数compose,andThen,identity
BiFunction<T, U, R>T,URapply有T和U类型参数的函数andThen
UnaryOperator<T,>TTapply类型T上的一元操作符compose,andThen,identity
BinaryOperator<T,>T,TTapply类型T上的二元操作符andThen,minBy,maxBy
Predicate<T,>Tbooleantest布尔值函数and,negete,or,isEqual
BiPredicate<T, U>T,Ubooleantest有两个参数的布尔值函数and,negete,or

现在要让上一个例子更复杂一点,在运行的过程中打印出当前是在哪一次循环中。为此,需要有一个合适的函数式接口,其中要包含一个方法, 这个方法有一个int参数而且返回类型为void.

public class RepeatDemo {
    public static void main(String[] args) {
        repeat(10,System.out::println);
    }

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

最好使用基本类型的函数来减小自动装箱性能问题,所以我们使用IntConsumer而不是Consumer(各位大佬,哪本书中有自动装箱和拆箱影响性能的)
接下来另开一章学习java8的函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值