CH6 接口、lambda表达式与内部类

第6章 接口、lambda表达式与内部类

6.1 接口

6.1.1 接口概念

实现Comparable<T>接口,重写compareTo方法,对实例数组进行排序

@AllArgsConstructor
@Getter
public class Employee implements Comparable<Employee>{
	private String name;
    private double salary;

    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }

    @Override
    public int compareTo(@NotNull Employee o) {
        //Double.compare(d1,d2),d1和d2相比较,相同返回0,d1<d2返回负数,d1>d2返回正数。返回int
        return Double.compare(salary, o.salary);
    }
}

public class EmployeeSortTest {
    public static void main(String[] args) {
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("Harry Hacker", 35000);
        staff[1] = new Employee("Carl Cracker", 75000);
        staff[2] = new Employee("Tony Tester", 38000);

        Arrays.sort(staff);
        
        //staff[0].salary < staff[1].salary , 返回负数,假设为 -40000
        //staff[0].salary < staff[2].salary , 返回负数,假设为 -3000
        //staff[1].salary > staff[2].salary , 返回正数,假设为 37000
        //排序 -40000 -3000 37000
        //staff[0] staff[2] staff[1]

        for (Employee employee : staff) {
            System.out.println("name = " + employee.getName() + " , salary = " + employee.getSalary());
        }
    }
}

/*输出
name = Harry Hacker , salary = 35000.0
name = Tony Tester , salary = 38000.0
name = Carl Cracker , salary = 75000.0
*/
6.1.5 默认方法

可以为接口中的方法提供一个默认实现,必须使用default修饰。接口的实现类可以不用重写默认方法。

public interface Test {
    default void test1(){};
}

public class TestImpl implements Test {
	//默认方法不用实现也行
}
6.1.6 解决默认方法冲突

如果一个类实现了多个接口,这些接口里存在同名同参数的方法,即使所有的这些方法都有默认的实现(即都定义了默认方法),也会出现默认方法冲突,这是就必须在这个类里重写这个方法来解决冲突。如果不想重写这个方法,就必须把这个类修饰成抽象类。

除非这个类在实现了多个接口的同时,还继承了一个超类,而这个超类里也有同名同参数的具体方法。那么就会触发超类优先原则,直接忽略所有接口的同名同参数的默认方法,使用超类的方法。

6.2 接口示例

6.2.1 接口与回调

回调:指出某个特定事件发生时应该采取的工作。

public class TimePrinter implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("At the tone, the time is " + new Date());
        Toolkit.getDefaultToolkit().beep();
    }
}

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

        //参数1:发出通告的时间间隔,单位毫秒。
        //参数2:监听器对象。
        //每隔10s调用监听器对象的回调方法actionPerformed()
        Timer timer = new Timer(10000, listener);
        timer.start();

        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }
}
6.2.2 Comparator接口
public interface Comparator<T> {
    int compare(T o1, T o2);
}


public class LengthComparator implements Comparator<String> {
    //按字符串长度进行比较
    @Override
    public int compare(String o1, String o2) {
        return o1.length() - o2.length();
    }

}

//具体完成比较时,需要建立一个实例:
Comparator<String> comp = new LengthComparator();
if(comp.compare(words[i], words[j]) > 0) {...}

//与words[i].compareTo(words[j])作比较,这个compare方法要在比较器对象上调用,而不是在字符串本身上调用。

6.3 lambda表达式

6.3.2 lambda表达式的语法
/*
(参数类型1 变量名1, 参数类型2 变量名2) -> {
	方法体...
}
*/
//有参数
(String first, String second) -> {
    first.length() - second.length();
    return "whatever";
}

//没有参数,要保留括号
() -> {
    for (int i = 0; i < 100; i++) {
        System.out.println("i = " + i);
    }
}

/*
如果参数类型可以推导,则可以省略参数类型。
comp对象里只有1个方法,或者只有1个含有两个参数的方法,这两种情况都使得参数类型是可以推导的。
*/
Comparator<String> comp = (first, second) -> {
    first.length() - second.length();
}

//如果方法体只有一条语句,则可以省略大括号
Comparator<String> comp 
    = (first, second) -> first.length() - second.length();

/*
如果方法只有1个参数,并且这个参数的类型可以推导得出,那么可以省略小括号。
省略前:
(ActionEvent event) -> {
	System.out.println("The time is " + new Date());
}
*/
ActionListener listener
    = event -> System.out.println("The time is " + new Date());


//无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。
(String first, String second) -> first.length() - second.length();
//可以在需要int类型结果的上下文中使用。

/*注意:
如果一个lambda表达式只在某些分支返回一个值,另一些分支不返回值,这是不合法的。如
(int x) -> { if(x>=0) return 1; }
这个语句不合法,因为else分支没有返回值。
*/
6.3.3 函数式接口

只有一个抽象方法的接口叫做函数式接口。

/*
Arrays.sort方法的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式
*/
Arrays.sort(words, 
	(first, second) -> first.length() - second.length()
           );


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


/*
java.util.function包中有一个很有用的接口Predicate
*/
public interface Predicate<T>{
    boolean test(T t);
}

/*
ArrayList类有一个removeIf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。下面语句将从一个数组列表删除所有null值
*/
list.removeIf(e -> e == null);
6.3.4 方法引用

如果已经有现成的方法可以完成想要传递到其他代码的某个动作。

例如,你希望只要出现一个定时器事件就打印这个事件对象。可以编码如下

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

如果想要直接把println方法传递到Timer构造器怎么做?

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

表达式:System.out::println 就是一个方法引用,等价于lambda表达式

x -> System.out.println(x)

再看一个例子,相对字符串排序,不考虑字母的大小写。

@Test
public void test8() {
    String[] strings = new String[]{"O", "p", "p", "A", "I", "d", "E", "a"};

    //Arrays.sort(T[] a, Comparator<? super T> c)
    //参数2要传Comparator接口的实现类。
    //String.compareToIgnoreCase()方法就是返回一个Comparator的实现类。
    Arrays.sort(strings, String::compareToIgnoreCase);
    for (String string : strings) {
        System.out.println("string = " + string);
    }
    /*输出
    string = A
	string = a
	string = d
	string = E
	string = I
	string = O
	string = p
	string = p
    */
    
}

/*
Arrays.sort(strings, (str1, str2) -> str1.compareToIgnoreCase(str2));
等价于
Arrays.sort(strings, String::compareToIgnoreCase);

其中compareToIgnoreCase如下
public int compareToIgnoreCase(String str) {
	return CASE_INSENSITIVE_ORDER.compare(this, str);
}
*/

从这些例子可看出,要用 :: 操作符分隔方法名与对象或类名。主要有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种情况,第1个参数会成为方法的目标。

String::compareToIgnoreCase
等价于
(x,y) -> x.compareToIgnoreCase(y)
6.3.5 构造器引用

构造器引用的方法名为new,如 Person::new

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
/*
map方法会为每个列表元素调用Person(String str)构造器。如果有多个Person构造器,编译器会选择一个有String参数的构造器。
*/

//可以用数组类型建立构造器引用。
Person[] people = stream.toArray(Person[]::new);
6.3.6 变量作用域

如果你想在lambda表达式中访问外围方法或类中的变量,则这个变量要遵循一条规则:

lambda表达式中捕获的变量必须实际上是最终变量(effectively final)。

实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。

合法变量:这个变量传进lambda后不会再发生变化

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

//调用上面的方法
repeatMessage("Hello", 1000);
//每1000ms打印一次"Hello"

非法变量:lambda表达之中只能引用值不会改变的变量

public static void countDown(int start, int delay) {
    ActionListener listener = event -> {
        start--; //Error: Can't mutate captured variable
        System.out.println(start);
    };
    new Timer(delay, listener).start();
}

public static void repeat(String text, int count) {
    for (int i = 0; i <= count; i++) {
        ActionListener listener = event -> {
            System.out.println(i + ": " + text);
            //Error: Cannot refer to changing i
        };
        new Timer(1000, listener);
    }
}

lambda表达式中的this关键字

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

//this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。
6.3.7 处理lambda表达式

使用lambda表达式的重点是延迟执行。延迟执行的意义所在:

  • 在一个单独的线程中运行代码;
  • 多次运行代码;
  • 在算法的适当位置运行代码(例如排序中的比较操作);
  • 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
  • 只在必要时才运行代码。

假设想要重复一个动作n次,将这个动作和重复次数传递到一个repeat方法。

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

//调用这个方法
repeat(10, () -> System.out.println("Hello World!"));

/*输出
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
*/

现在我们希望告诉这个动作它出现在哪一次迭代中,为此需要选择一个合适的函数式接口。其中要包含一个方法,这个方法有一个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(11, i -> System.out.println("Countdown: " + (9 - i)));
/*等价于
repeat(11, new IntConsumer() {
    @Override
    public void accept(int value) {
        System.out.println("Countdown: " + (9 - value));
    }
});
*/

/*输出
Countdown: 9
Countdown: 8
Countdown: 7
Countdown: 6
Countdown: 5
Countdown: 4
Countdown: 3
Countdown: 2
Countdown: 1
Countdown: 0
Countdown: -1
*/

自己定义的抽象方法建议加上@FunctionalInterface注解。

6.3.8 再谈Comparator
Arrays.sort(people, Comparator.comparing(Person::getName));

/*
sort方法的参数2需要一个Comparator的接口实现类。
Comparator.comparing(Person::getName) 是一个接口实现类。
Person::getName 是一个 键提取器,类型为 Function<? super T, ? extends U>。
函数式接口Function<? super T, ? extends U>的抽象方法为
R apply(T t);
即
Comparator.comparing(person -> person.getName());
*/

/*所涉及的接口
public static <T> void sort(T[] a, Comparator<? super T> c) {...}

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {...}

public interface Function<T, R> {
    R apply(T t);
    ...
}
*/

可以把比较器与thenComparing方法串起来

Arrays.sort(
    people,
    Comparator
    	.comparing(Person::getLastName)
      	.thenComparing(Person::getFirstName)
);

//变体,根据人名长度排序
Arrays.sort(people, Comparator.comparing(Person::getName, (s,t) -> Integer.compare(s.length(),t.length())));
//或
Arrays.sort(people, Comparator.comparing(Person::getName, Comparator.comparingInt(String::length)));
//或
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));

6.4 内部类

内部类是定义在另一个类中的类,使用内部类的三点主要原因

  • 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
  • 内部类可以对同一个包中的其他类隐藏起来。
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。
6.4.1 使用内部类访问对象状态
package innerClass;

import lombok.AllArgsConstructor;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;

@AllArgsConstructor
public class TalkingClock {

    private int interval;
    private boolean 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("At the tone, the time is " + new Date());
            //内部类的方法里调用了外围类的属性beep
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }

}

测试

package innerClass;

import javax.swing.*;

public class InnerClassTest {
    public static void main(String[] args) {
        //构造器构造外围类,传入参数beep为true
        TalkingClock clock = new TalkingClock(1000, true);
        //start()方法里new了内部类对象,内部类对象被new时调用了beep。
        clock.start();

        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }
}

注意:

TimePrinter类位于TalkingClock类内部,但并非每个TalkingClock都有一个TimePrinter实力与。TimePrinter对象是由TalkingClock类的方法构造的。

内部类既可以访问自身的属性,也可以访问创建它的外围类对象的属性。

外围类的引用是在内部类的构造器中设置的。编译器修改了所有的内部类的构造器,添加一个外围类引用的参数。由于内部类TimePrinter没有定义构造器,编译器为这个类生成了默认的构造器

public class TalkingClock {
	...
    //定义内部类
    public class TimePrinter implements ActionListener{
        public TimePrinter(TalkingClock clock){
            outer = clock;
        }// 编译器在后台自动生成的代码
        ...
    }
	...
}

于是actionPerformed方法等价于以下形式

public void actionPerformed(ActionEvent e) {
    System.out.println("At the tone, the time is " + new Date());
    if (outer.beep) {
        Toolkit.getDefaultToolkit().beep();
    }
}

注意这里的outer不是java的关键字,只是在此处用来说明一下内部类中的机制。

当在外围类的start方法中创建了TimePrinter对象后,编译器就会将this引用传递给TimePrinter的构造器。

ActionListener listener = new TimePrinter(this); //自动添加构造器参数。

内部类TimePrinter可以声明为private,这样就只有TalkingClock的方法可以构造TimePrinter对象。

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

6.4.2 内部类的特殊语法规则

表达式:OuterClass.this,表示外围类引用。

public class TalkingClock {
	...
    //定义一个内部类
    public class TimePrinter implements ActionListener{
        @Override
        public void actionPerformed(ActionEvent event){
            ...
                if(TalkingClock.this.beep) {
                    Toolkit.getDefaultToolkit().beep();
                }
            //TalkingClock是外围类
        }
    }    
}

语法格式:outerObject.new InnerClass(construction parameters)
用于明确地编写内部对象的构造器

//如
ActionListener listener = this.new TimePrinter();
//通常this限定词是多余的。

若TimePrinter是一个公有内部类(用public修饰),则对于任意的TalkingClock实例对象,都可以构造一个TimePrinter:

TalkingCLock jabberer1 = new TalkingClock(1000, true);
TalkingCLock jabberer2 = new TalkingClock(1000, true);

TalkingClock.TimePrinter listener1 = jabberer1.new TimePrinter();
TalkingClock.TimePrinter listener2 = jabberer2.new TimePrinter();

注意到,在外围类的作用域之外(也就是不在外围类里面的时候),可以通过这样引用内部类

OuterClass.InnerClass,如:

TalkingClock.TimePrinter listener = ...
6.4.3 内部类是否有用、必要和安全(略)
6.4.4 局部内部类

在6.4.1的示例代码中,TalkingClock类中TimePrinter这个内部类只在start()方法中创建内部类对象时使用了一次,这时可以选择直接使用局部内部类的写法来替代

public void start(){
    //局部内部类,直接定义在外围类的方法里的类。
    class TimePrinter implements ActionListener{
        public void actionPerformed(ActionEvent event){
            System.out.println("At the tone, the time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
    
    ActionListener listener = new TimePrinter();
	Timer t = new Timer(interval, listener);
    t.start();
}

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

局部类的优势在于对外部世界可以完全隐藏。

即使TalkingClock类中的其他代码也不能访问它。除了start()方法之外,没有任何方法知道TimePrinter类的存在。

6.4.5 访问来自外部方法的变量

原书标题是"由外部方法访问变量",我觉得翻译得不太对,找了一下英文版原文是Accessing Variables from Outer Methods,这里我将其译为"访问来自外部方法的变量"。

局部内部类的优点之一,不仅能够访问包含它们的外围类,还可以访问局部变量。不过这些被局部内部类访问的局部变量,必须事实上是final的。

典型示例:

public void start(int interval, boolean beep){
    
    class TimePrinter implements ActionListener{
        public void actionPerformed(ActionEvent event){
            System.out.println("At the tone, the time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
    ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval, listener);
    t.start();
}

流程上,t.start();语句执行完之后,start(int interval, boolean beep)方法就结束了,beep参数变量就不存在了,局部内部类并没有收到一个final的变量。但是程序还是正常运行了,为什么?

原因在,为了让actionPerformed方法正常运行,TimePrinter类必须在beep的值被释放之前,复制一份beep并把其作为start方法的局部变量。编译器在后台就是做了这么一件事情。

6.4.6 匿名内部类

假如只创建这个类的一个对象,就不必命名了,这种类被称为匿名内部类(anonymous inner class)

public void start(int interval, boolean beep) {
    //使用匿名内部类创建对象
    ActionListener listener = new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent event) {
            System.out.println("At the tone, the time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    };
    Timer t = new Timer(interval, listener);
    t.start();
}

通常的语法格式为:

new SuperType(construction parameters){
    inner class methods and data
}

SuperType可以是接口,这样内部类就要实现这个接口。

SuperType可以是类,这样内部类就要扩展这个类。

由于构造器的名字必须与类名相同,而匿名类没有类名,所以匿名类不能由构造器。取而代之的是,将构造器参数传递给超类构造器。

Person count = new Person("Dracula") {...};
//Person("Dracula")是超类构造器,通过匿名内部类的写法被new出来的其实是这个超类的子类。

构造一个类的新对象和构造一个扩展了那个类的匿名内部类对象质检的语法差别。

Person queen = new Person("Mary");
//一个Person类的对象

Person count = new Person("Dracula") {...};
//对象count是由一个继承于Person类的匿名内部类new出来的

多年来,java程序员习惯用匿名内部类实现事件监听器和其他回调。如今最好使用lambda表达式。

下面用lambda表达式替换内部类的写法。

public void start(int interval, boolean beep) {
    //匿名内部类方式
    Timer t = new Timer(interval, new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    });
    t.start();
}


public void start(int interval, boolean beep) {
	//lambda表达式方式
    Timer t = new Timer(interval, event -> {
        System.out.println("At the tone, the time is " + new Date());
        if (beep) {
            Toolkit.getDefaultToolkit().beep();
        }
    });
    t.start();
}
6.4.7 静态内部类

如果使用内部类的目的只为了把一个类隐藏在另一个类的内部,并不需要在内部类里引用外围类对象,那么就可以将内部类声明为static,以取消产生的引用。

package staticInnerClass;

public class ArrayAlg {
    //静态内部类
    public static class Pair {
        private double first;
        private double second;

        public Pair(double f, double s) {
            first = f;
            second = s;
        }

        public double getFirst() {
            return first;
        }

        public double getSecond() {
            return second;
        }
    }

    //类ArrayAlg的一个静态方法
    //在本例中,内部类对象在静态方法中构造,因此由于这一点,内部类同样必须也要被修饰为是静态的。
    public static Pair minmax(double[] values) {
        double min = Double.POSITIVE_INFINITY;
        double max = Double.NEGATIVE_INFINITY;
        for (double v : values) {
            if (min > v) {
                min = v;
            }
            if (max < v) {
                max = v;
            }
        }
        return new Pair(min, min);
    }

}

测试

package staticInnerClass;

public class StaticInnerClassTest {

    public static void main(String[] args) {
        double[] d = new double[20];
        for (int i = 0; i < d.length; i++) {
            d[i] = 100 * Math.random();
        }
        ArrayAlg.Pair p = ArrayAlg.minmax(d);
        System.out.println("min = " + p.getFirst());
        System.out.println("max = " + p.getSecond());
    }

}

注意几个点

  • 只有内部类可以声明为static。

  • 静态内部类的对象除了没有对生成它的外围类对象的引用特权外,与其他内部类完全一样。

  • 与常规内部类不同,静态内部类可以有静态域和方法。

  • 声明在接口中的内部类自动成为static和public类。

6.5 代理

跳过

本文结束

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值