第六章——接口、lambda表达式与内部类(二)

目录

 

3、lambda表达式

为什么引入lambda表达式

lambda表达式的语法

函数式接口

方法引用

构造器引用

变量作用域

处理 lambda 表达式

再谈Comparator

4、内部类

使用内部类访问对象状态

内部类的特殊语法规则

局部内部类

匿名内部类(anonymous inner class)

静态内部类


 

3、lambda表达式

为什么引入lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

之前讲的定时输出语句和按长度进行数组排序示例有一个共同点就是:代码块传递到某个对象(定时器或sort方法),并在将来某个时间调用。

到目前为止,Java要传递代码块并不容易,不能直接传递代码段。Java是一种面向对象语言,所以必须构在一个对象,且这个对象的类需要一个方法包含所需要的代码。

lambda表达式的语法

以按长度排序为例子。我们比较字符串长度,需要计算:first.length() - second.length();first和second是字符串,Java是强类型语言,所以我们还要指定他们的类型:

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

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

 

lambda的一般形式:(参数)-> { 表达式 }

1、即使没有参数,也需要空括号,像无参数方法一样。

2、如果参数类型可以推导出来,可以忽略其类型,如:

Comparator cmp = {first, second) -> first.length() - second.length();
//因为范型声明为String,一个lambda表达式赋值给一个字符串比较器对象,所以编译器可以推导出两参数为String对象

3、如果仅有一个参数,且类型可推导,还能省略括号。

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

4、无需声明返回类型,会根据上下文推导。

5、若一个lambda 表达式只在某些分支返回一个值,另外一些分支不返回值,这是不合法的。如

(int x) -> {
    if(x > 0)
        return 1;
};
//不合法

lambda使用示例:

1、按字符串长度排序:源代码

package com.company;

import java.util.Arrays;
import java.util.Comparator;

/**
 * This program demonstrates the use of the Comparator interface
 */
public class Main {
    public static void main(String[] args) {
        String[] friends = {"Amy","Peter","John","Tifany","Jonathan"};
        Arrays.sort(friends, new LengthComparator());

        for(String s : friends){
            System.out.println(s);
        }
    }
}

class LengthComparator implements Comparator{
    @Override
    public int compare(String first,String second){
        return first.length() - second.length();
    }
}

使用lambda表达式:

package com.company;

import java.util.Arrays;
import java.util.Comparator;

/**
 * This program demonstrates the use of the Comparator interface
 */
public class Main {
    public static void main(String[] args) {
        String[] friends = {"Amy","Peter","John","Tifany","Jonathan"};
        Arrays.sort(friends,(String first,String second) -> second.length() - first.length());

        for(String s : friends){
            System.out.println(s);
        }
    }
}

2、定时输出语句:源代码

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
//消除了javax.swing.Timer与javax.util.TImer之间的二义性。

public class TimerTest{
    public static void main(String[] args){
        ActionListener listener = new TimePrinter();

        //construct a timer that calls the listener
        //once every 5 seconds
        Timer t = new Timer(5000, listener); //Timer构造器有两个参数,第一个是时间间隔,第二个是监听器对象
        t.start(); //启动定时器
        
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit();
    }
}

class TimePrinter implements ActionListener{
    @Override
    public void actionPerformed(ActionEvent event){
        System.out.println("The time is: " + new Date());
        Toolkit.getDefaultToolkit().beep();
    }
}

使用lambda 表达式:

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
//消除了javax.swing.Timer与javax.util.TImer之间的二义性。

public class TimerTest{
    public static void main(String[] args){
        ActionListener listener = new TimePrinter();

        //construct a timer that calls the listener
        //once every 5 seconds
        Timer t = new Timer(5000, event -> {
            System.out.println("The time is: " + new Date());
            Toolkit.getDefaultToolkit().beep();
        }); 
        t.start(); //启动定时器
        
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit();
    }
}

函数式接口

Java中有很多封装代码块的接口,如ActionListener或Comparator。lambda表达式与这些接口是兼容的。

对于只有一个抽象方法的接口,需要这种接口对象时,就可以提供一个lambda 表达式。这种接口称为函数式接口(functional interface)。

为什么要求函数式接口必须有一个抽象方法?因为接口中完全有可能重新声明 Object 的方法,如 toString 或 clone,这些声明有可能会让方法不再是抽象的,更重要的是,在 Java SE 8 中,接口可以声明非抽象方法(默认方法)。

最好把 lambda 表达式看作是一个函数,而不是一个对象,另外要接受 lambda 表达式可以传递到函数式接口。

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

Java API 在 java.util.function 包中定义了很多非常通用的函数式接口。

1、BiFunction描述了参数类型为T和U而且返回类型为R的函数。可以把我们的字符串比较 lambda 表达式保存在这个类型的变量中

BiFunction cmp = (first, second) -> first.length() - second.length();

不过,这对于排序并没有任何帮助。想要用lambda表达式做某些处理,还是要谨记表达式的用途,为他建立一个特定的函数式接口。

2、Predicate接口

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

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

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

方法引用

有时候,已经存在一个方法可以完成你想要传递给其他代码的某个动作

例如,你希望一出现一个定时器事件就打印这个事件对象:

Timer t = new Timer(5000, event -> System.out.println(event));

但是,可以直接把println方法传递到Timer构造器就更好了:

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

上面的表达式 System.out::println 是一个方法引用(method reference),它等价于lambda表达式:x -> System.out.println(x)

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

1、object :: instanceMethod

2、Class :: staticMethid

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)。

可以在方法引用中使用 this 参数。如:this::equals 等价于 x -> this.equals(x)。

使用 super 表达式也是合法的。如:表达式 super::instanceMethod 使用 this 作为目标,会调用给定方法的超类版本。

构造器引用

构造器引用与方法引用非常类似,只不过方法名为new。如:Person::new 是 Person 构造器的一个引用。

可以用数组类型建立构造器引用。如:int[]::new 是一个构造器引用,他有一个参数,即数组长度。等价于lambda表达式:x -> new int[x]。

Java有一个限制:无法构造范型类型 T 的数组。数组构造器引用对于克服这个限制很有用。

变量作用域

有时希望在lambda表达式中访问外围方法中或类中的变量,如:

class Application{
    public static void repeatMessage(String text, int delay){
        ActionListener listener = event -> {
            System.out.println(text);
            Toolkit.getDefaultToolkit().beep();
        };
        new Timer(delay, listener).start();
    }
    
    public static void main(String[] args){
        repeatMessage("Hello", 1000); //print Hello every 1,000 milliseconds
    }
}

lambda 表达式有 3 个部分:

1、一个代码块

2、参数;

3、自由变量的值,这是指非参数而且不在代码中定义的变量

在我们的例子中,text 就是一个自由变量。表示 lambda 表达式的数据结构必须存储自由变量的值。在这里就是字符串“Hello”,我们说他被 lambda 表达式捕获(captured)。

关于代码块以及自由变量值有一个术语:闭包(closure)。在Java中,lambda 表达式就是闭包。

可以看到,lambda 表达式可以捕获外围作用域中变量的值。在 lambda 表达式中,有一个限制:只能引用值不会变的变量。不论是在表达式的代码块中或外围代码中,若能改变变量的值,就是不合法的。即 lambda 表达式中捕获的变量必须实际上是最终变量(effectively final)

最终变量值的是这个变量初始化之后就不会再为他赋新值。在上例中,text为同一字符串对象,所以引用合法。

除此之外,在 lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。而且在方法中,不能有两个同名的局部变量,因此,lambda 表达式中同样也不能有同名的局部变量。

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

处理 lambda 表达式

如何编写方法处理 lambda 表达式?

lambda 表达式的使用的重点是延迟执行(deferred execution)。之所以希望延迟执行,有很多原因,如:

1、在一个单独的线程中运行代码

2、多次运行代码

3、在算法的适当位置运行代码

4、发生某种情况是执行代码(如按钮点击时、数据到达等)

5、只在必要时才运行代码

在Java API中提供的最重要的函数式接口:

常用函数式接口
函数式接口 参数类型 返回类型 抽象方法名 描述 其他方法
Runnable void run 作为无参数或返回值的动作运行  
Supplier T get 提供一个 T 类型的值  
Consumer T void accept 处理一个 T 类型的值 andThen
BiConsumer T, U void accept 处理 T 和 U 类型的值 andThen
Function T R apply 有一个 T 类型参数的函数 compose, andThen, identity
BiFunction T, U R apply 有 T 和 U 类型参数的函数 andThen
UnaryOperator T T apply 类型 T 上的一元操作符 compose, andThen, identity
BinaryOperator T, T T apply 类型 T 上的二元操作符 andThen, maxBy, minBy
Predicate T boolean test 布尔值函数 and, or, negate, isEqual
BiPredicate T, U boolean test 有两个参数的布尔值函数 and, or, negate

基本类型 int、long 和 double 的34个可能规范。最好使用这些特殊化规范减少自动装箱。

基本类型的函数式接口
函数式接口 参数类型 返回类型 抽象方法名
BooleanSupplier none boolean getAsBoolean
PSupplier none p getSaP
PConsuer p void accept
ObjPConsumer T, p void accept
PFunction p T apply
PToQFunction p q applyAsQ
ToPFunction T p applyAsP
ToPBiFunction T, U  p applyAsP
PUnaryOperator p p applyAsP
PBinaryOperator p, p p applyAsP
PPredicate p boolean test

注:p 为 int、long、double;P、Q 为 Int、Long、Double

再谈Comparator

4、内部类

内部类(inner class)是定义在另一个类中的类。

使用内部类的主要原因有以下三点:

1、内部类方法可以访问该类定义所在的作用域(即其外部类)中的数据,包括私有的数据

2、内部类可以对同一个包中的其他类隐藏起来

3、当想要定义一个回调函数而又不想编写大量代码时,使用匿名(anonymous)内部类比较便捷

使用内部类访问对象状态

在本小点中,将给出一个简单的内部类,访问外围类的实例域。

package com.company;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;

/**
 * This program demonstrates the use of the inner classes
 */
public class Main {
    public static void main(String[] args) {
        TalkingClock tc = new TalkingClock(1000,true);
        tc.start();

        //保持程序运行,知道点击"OK"
        JOptionPane.showMessageDialog(null,"Quit program?");
        System.exit(0);
    }
}

class TalkingClock{
    private int interval;
    private boolean beep;

    public TalkingClock(int interval, boolean beep){
        this.interval = interval;
        this.beep = beep;
    }

    public void start(){
        ActionListener listener = new TimePrinter();
        Timer t = new Timer(interval, listener);
        t.start();
    }

    public class TimePrinter implements ActionListener{
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("The time is:" + new Date());
            if(beep) Toolkit.getDefaultToolkit().beep(); //在此引用外围类的实例域。
        }
    }
}

该实例虽并不实用,但可以体现内部类对外围类实例域的访问实现。为了运行该程序,内部类的对象(例子中为listener)总有一个隐式引用,指向创建它的外部类对象。这种引用在内部类的定义中是不可见的,编译器修改了所有内部类的构造器,添加一个外围类引用的参数。当在外围类中创建内部类对象时,编译器就会将 this 引用传递给外围类的构造器。

从传统意义上讲,若一个对象调用了一个方法,则该方法可以访问这个对象的数据域。内部类既可以访问自身的数据域,也可以访问创建它的外部类对象的数据域。

需要注意:

1、虽然TimePrinter位于TalkingClock内部,但并不意味着每个TalkingClock对象都有一个TimePrinter实例域,TimePrinter是由方法(本例子中为start() )构造出来的。

2、内部类对其他任何除了其外部类以外的所有类都不可见。

在以前版本的相同例子中,若想要访问 beep 这个参数,需要额外定义一个公有的访问器方法,而使用内部类就可以直接访问。

TimePrinter类可以声明为私有的。这样一来,就只有 TalkingClock 的方法才能够构造TimePrinter对象。

只有内部类可以是私有类,常规类只可以具有包可见性,或公有可见性。

内部类的特殊语法规则

上一点中讲内部类有一个对外部类的引用。其表达式为:OuterClass.this 表示外围类引用。如

public void actionPerformed(ActionEvent e){
    ...
    if(TalkingClock.this.beep) ...
}

相反,同样可以采用下列语法格式更加明确地编写内部对象的构造器:OuterClass.new InnerClass ( parameters )。如:

ActionListener listener = this.new TimePrinter();

在这里,最新构造的对象的外围类引用被设置为创建内部类对象的方法(例子中为 start() 方法)中的 this 引用。除此之外,还可以通过显式得命名将外围类引用设置为其他对象。如:若 TimePrinter 为公有内部类,对于任意的 TalkingClock 都可以构造一个 TimePrinter:

TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

源代码可更新为:

package com.company;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;

/**
 * This program demonstrates the use of the inner classes
 */
public class Main {
    public static void main(String[] args) {
        TalkingClock tc = new TalkingClock(1000,true);
        //tc.start();
        TalkingClock.TimePrinter listener = tc.new TimePrinter();
        Timer t = new Timer(tc.getInterval(), listener);
        t.start();

        //保持程序运行,知道点击"OK"
        JOptionPane.showMessageDialog(null,"Quit program?");
        System.exit(0);
    }
}

class TalkingClock{
    private int interval;
    private boolean beep;

    public int getInterval() {
        return interval;
    }

    public TalkingClock(int interval, boolean beep){
        this.interval = interval;
        this.beep = beep;
    }

    public class TimePrinter implements ActionListener{
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("The time is:" + new Date());
            if(beep) Toolkit.getDefaultToolkit().beep(); //在此引用外围类的实例域。
        }
    }
}

总结:

1、在内部类中引用外部类的域:OuterClass.this.field;(例子中为 TalkingClock.this.beep)

2、在外部类中实例化内部类:this.new InnerClass();(例子中为 this.new TimePrinter();)

3、在其他类中实例化外部类的内部类:OuterClassObject.new InnerClass();(例子中为 tc.new TimePrinter();)

 

4、在其他类中引用外部类的内部类:OutClass.InnerClass;(例子中为 TalkingClock.TimePrinter listener = . . .)

内部类中声明的所有 static 域都必须是 final 的。因为 static 希望唯一,而每一个外部对象都有一个内部类实例,若不是 final 的,可能导致不唯一。

内部类不能有 static 方法。

局部内部类

在上述示例中,TimePrinter 这个类名只在 start() 方法中创建该对象时使用了一次,对这种情况,可以在方法中定义一个局部类。

局部类是定义在方法代码块中类。

public class Main {
    public static void main(String[] args) {
        TalkingClock tc = new TalkingClock(1000,true);
        tc.start();

        //保持程序运行,知道点击"OK"
        JOptionPane.showMessageDialog(null,"Quit program?");
        System.exit(0);
    }
}

class TalkingClock{
    private int interval;
    private boolean beep;

    ...

    public void start(){
        class TimePrinter implements ActionListener{
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("The time is:" + new Date());
                if(beep) Toolkit.getDefaultToolkit().beep(); //在此引用外围类的实例域。
            }
        }

        ActionListener listener = new TimePrinter();
        Timer t = new Timer(interval, listener);
        t.start();
    }
}



局部类不能使用 public 或 private 访问说明符进行声明,其作用域被限定在这个声明它的块中。

在局部类中,相比于内部类,它可以访问包含它的外部类,还能访问局部变量。不过访问的这些局部变量必须是 final 的。

匿名内部类(anonymous inner class)

在局部类的基础上再深入一步。加入只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类。

class TalkingClock{
    private int interval;
    private boolean beep;

    ...

    public void start(){
        ActionListener listener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("The time is:" + new Date());
                if(beep) Toolkit.getDefaultToolkit().beep(); //在此引用外围类的实例域。
            }
        };
        Timer t = new Timer(interval, listener);
        t.start();
    }
}

有一种技巧称为“双括号初始化”(double brace initialization)。

ArrayList friends = new ArrayList<>();
friends.add("Harry");
friends.add("Tiny");
invite(friends);

//若想要一个匿名数组列表:
invite(new ArryaList() {{ add("Harry"); add("Tiny"); }});
//外层花括号为 ArrayList 的匿名子类
//内层括号为对象构造的初始化块(参见第四章)

静态内部类

有时候使用内部类只是想把一个类隐藏在另外一个类的内部,并不需要内部类引用外部类对象。为此,可以声明 static 内部类,以便取消产生的引用。

只有内部类才能声明为static,常规类不可以。

在之前的内部类的特殊语法规则中讲过,不能在内部类中定义 static 方法。但在静态内部类中是可以的,

数组排序示例:

package com.company;

/**
 * This program demonstrates the use of the static inner classes
 */
public class Main {
    public static void main(String[] args) {
        double[] values = new double[100];
        //切忌使用foreach循环进行赋值,因为该循环都是取值后放在本地变量中,并不改变数组值
        for(int i=0;i d) min = d;
            if(max < d) max = d;
        }
        return new Pair(min,max);
    }
}

 

你可能感兴趣的:(Java)