读后笔记 -- Java核心技术(第11版 卷I ) Chapter6 接口、lambda 表达式与内部类

6.1 接口

6.1.1.  接口

  不是类,而是对类的一组需求描述,这些类要遵从接口描述的同一格式定义。

接口与类的区别:
  接口中默认的方法是 public abstract,所以不写的都是这个类型;
不能用于实例化对象; 没有构造方法; 所有的方法必须是抽象方法,Java
8 之后 接口中可以使用 default 关键字修饰的非抽象方法; 不能包含成员变量,除了 staticfinal 变量; 不是被类继承了,而是要被类实现; 支持多继承; ** 提供 实例字段 和 方法实现 的任务,由实现接口的那个类 来完成。

   类实现一个接口,需要两步:

  • 1)将类声明为实现给定的接口;
  • 2)对接口中的所有方法进行定义
public interface Comparable<T> {  // since Java 5,提供泛型类型
    int compareTo(T other);       // 接口中的所有方法都是 public
}

public class Employee implements Comparable<Employee> {    // 接口实现,Comparable 的 CompareTo()接受任意的 Object,上面接口有泛型类型支持的 Comparable 版本,可直接传递 Employee 类型,不用进行类型转换
    @Override
    public int compareTo(Employee other) {                 // 接口方法的实现
        return Double.compare(salary, other.salary);       // 浮动数比较 Double.compare(),整数比较 Integer.compare()
    }
    ...
}        
知识点: Arrays.sort(Object[] a);  // 由于历史原因,API 里面的 sort 接受的是 Object[] 数组,然后再强制转换为 Comparable。理想情况应该是 public static void sort(Comparable[] a)
如果要对数组 a 中的元素排序,要求:数组中的元素必须属于实现了 Comparable 接口的类,并且元素间可比。

6.1.2. 接口的属性

// 1)可声明接口的变量,但必须引用实现了接口的类对象;
Comparable x;                 // OK,声明接口变量
x = new Employee(...);        // OK,Employee类 实现了 Comparable 接口(上面6.1.1的例子中),所以 x 可以引用 Employee 的类对象

// 另一个实际使用的例子,如:
List<String> list = new ArrayList(); // 可声明 List 接口的变量 list,但必须引用实现了该接口的类 ArrayList 的对象(通过new)
list.add("Amy");
// 2)可使用 instanceof 检查一个对象是否实现了特定的接口(类似于类); if (anObject instanceof Comparable) {...} // 3) 接口可以通过 extends 再扩展;
public interface Moveable { ...; }
public interface Powered extends Moveable { ...; }
// 4接口不能包含 实例字段 或 静态方法,但可包含 常量; public interface Powered extends Moveable { double milesPerGallon(); // 方法自动标记为 public (一般不写) double SPEED_LIMIT = 95; // 自动标记为 public static final constant (一般不写) }

// 5)一个类:仅有一个超类,但可实现 多个接口 class Manager extends Employee implements Cloneable, Comparable { ...; }

6.1.3. 接口与抽象类

接口中包含的几种方法

  • 1) abstract  => 在实现类中被覆盖
  • 2-3) default, static (from java 8)
  • 4-5) private, private static (from Java 9)  => 目的:用于抽取这个接口中的公共代码
Q: 为什么不使用抽象类,而采用接口?
A: 每个类只能扩展一个类(如抽象类),但是可以引用 多个接口。如果将 Comparable 定义为抽象类,那么 Manager 就没办法同时扩展 Employee 类和 Comparable 类
----- 4.1 Java 抽象类 ----- public abstract class Employee { ... } // 详细描述参看 5.1.9 https://www.cnblogs.com/bruce-he/p/15738254.html ----- 4.2 Java 抽象方法 ----- public abstract class Employee { ... public abstract double computePay(); ... }
如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。 abstract 关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。 声明抽象方法会造成以下两个结果:
1)如果一个类包含抽象方法,那么该类必须是抽象类; 2)任何子类必须重写父类的抽象方法,或者声明自身为抽象类。最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。

更多详情参考:https://www.runoob.com/java/java-abstraction.html

6.1.4. 静态方法、私有方法

   Java 8,接口有 静态 static 方法;通常将静态方法放在伴随类中,如 Collection/Collections、Path/Paths;

   Java 9,接口有 私有 private 方法(可以是 静态方法 或 实例方法),只能在接口本身的方法中使用,故作为接口中其他方法的辅助方法;

public interface Path {       // Since Java 11
public static Path of(URI uri) { ... }
public static Path of(String first, String... more) { return FileSystems.getDefualt().getPath(first, more); } ... }

6.1.5. 默认方法 default

默认方法 : 接口可以有实现方法,而且不需要实现类去实现其方法。

Q:为什么要有这个特性?
A:之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8 之前的集合框架没有 foreach 方法,通常能想到的解决办法是在 JDK 里给相关的接口添加新的方法及实现。
然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。

白话解释:新的 SDK 接口中有增加一些方法,因为类要扩展接口,必须实现这些接口的方法,这就导致之前的类代码要重新写。一个办法就是,让接口中的新增方法变成 default 方法,且已经实现,那么以前类的代码就不用重新修改了。

更多参考:https://www.runoob.com/java/java8-default-methods.html

// A default method can call an abstract method:
public interface Collection {
    int size();    // an abstract method
    default bolean isEmpty()  { return size() == 0; }   // call abstract method size()
    ...
}

  【例如 Collection 接口,后来 有人写了一个 Bag 方法实现 Colleciton 接口。 然后,在 JDK 8 中 Collection 接口增加了一个 Stream 方法,如果将其设置为 default 方法,就可以解决 Bag 方法冲突的问题 】

6.1.6. 解决默认方法冲突

  冲突场景:如果先在一个接口中将一个方法定义为默认方法, 然后又在超类或另一个接口中定义了同样的方法

  解决冲突的规则

  • 1)超类优先(超类+接口)。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略;
  • 2)接口冲突(>=2 接口)。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法, 必须 覆盖这个方法 或 指定某接口的方法 来解决冲突;
// 1. 场景:实现两个接口的 默认方法冲突。 两种解决方法:
public interface Vehicle {
   default void print() { System.out.println("我是一辆车!"); }
}

public interface FourWheeler {
   default void print(){ System.out.println("我是一辆四轮车!"); }
}

// 解决方案一:创建自己的默认方法,来覆盖重写接口的默认方法:
public class Car implements Vehicle, FourWheeler {
   default void print(){ System.out.println("我是一辆四轮汽车!"); }
}

// 解决方案二: 使用 super 来调用指定接口的默认方法
public class Car implements Vehicle, FourWheeler {
   public void print(){ Vehicle.super.print(); }
}
// 1. 接口冲突:两个接口都定义相同的方法,类 Student 要同时实现 Person 和 Named 接口
//    场景 1.1: 如果有一个是默认方法,或两个都是默认方法。
//    解决办法:实现类必须做出决定,具体调用哪个接口的方法,“Interface.super.method();”
interface Person {
    default String getName() { return "John Q. Public"; }
}

interface Named {
    default String getName() { return getClass().getName() + "_" + hashCode(); }
}

class Student implements Person, Named {
    public String getName() { return Person.super.getName(); }    // 指定调用的具体接口的 默认方法
    ...
}

//  场景 1.2:如果 两个方法都是抽象方法,
//    另外一个解决办法:实现 方法 或 将类声明为 abstract
// 2. 类优先: Student 类继承 Person 类 和 Named 接口,两者提供一样的 getName 方法
----- 超类 Person.java ------
public class Person {
    private String name;
    public Person(String name) { this.name = name; }
    public String getName() { return name; }
}

----- 接口 Named.java ------
public interface Named {
    default String getName()    { return getClass().getName() + "_" + hashCode(); }
}

----- 子类 Student.java ------
public class Student extends Person implements Named {
    public Student(String name) { super(name); }
}

----- 测试类 ------
Student harry = new Student("Harry Porter");
System.out.println(harry.getName());     // 输出: "Harry Porter", 超类 Person 有 getName(),接口 Named 有 getName(),当方法冲突时,使用类 Person 的方法

6.1.7. 接口与回调

    回调 Callbacks:常见的程序设计模式,可以指定某个特定事件发生时应该采取的动作。如 Timer 类

public class TimerTest {
    public static void main(String[] args) {
        ActionListener listener = new TimerPrinter();        // 定义一个接口的变量 listener,必须引用实现了该接口的类的对象;

        // construct a timer that calls the listener
        Timer t = new Timer(10000, listener);
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }
}

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

6.1.8. Comparator 接口

// 使用 Comparator 接口的 Compare 方法,可以自定义排序。有别于 Comparable 接口的 Comparable 排序
public
class ComparatorTest { public static void main(String[] args) { String[] friends = {"Peter", "Paul", "May", "Albert"}; Arrays.sort(friends); System.out.println(Arrays.toString(friends)); // 按照 字典顺序 排序,[Albert, May, Paul, Peter] Arrays.sort(friends, new LengthComparator()); System.out.println(Arrays.toString(friends)); // 按照 字符串长度 排序,[May, Paul, Peter, Albert] } } class LengthComparator implements Comparator<String> { @Override public int compare(String o1, String o2) { return o1.length() - o2.length(); } }

6.1.9. 对象克隆

Employee original = new Employee("John Public", 50000);

// 1) make a copy, original also changed
Employee copy = original;
copy.raiseSalary(10);             // original also changed

// 2) clone, then original is not changed
Employee copy = original.clone();
copy.raiseSalary(10);            // original is not changed

  对于每一个类,需要确定的一个思路是:

  • 1)默认的 clone 方法是否满足要求;
  • 2)是否可以在可变的子对象上调用 clone 来修补默认的 clone 方法;
  • 3)是否不该使用 clone ---- (默认,一般不使用);

  如果选择 1)或 2),则该类必须:

  • 1)实现 Cloneable 接口;
  • 2)重新定义 clone 方法,并指定为 public;
!!! Object.clone 仅提供 浅copy,其特点是:
1. 如果 原对象 和 浅clone 共享的子对象是不可变的(如 String 类);或者 子对象 一直包含不变的常量,没有更改器改变它,也没有方法会生成它的引用,那么这种共享是安全的;
2. 如果 子对象 是可变的,那么 任意改变一个,两者都会改变,这就不适用于 浅copy 了;  => 需要定义一个 deep copy

另外,Object.clone 方法是 protected,子类只能调用该方法来 clone 自己的对象,必须将 clone() 重新定义为 public,才能允许所有的方法来克隆对象。

深copy (即使 浅copy 满足要求,下面的第一步还是需要实现)1). 浅 copy: a) 实现 Cloneable 接口;b) 重新定义 clone 方法为 public; c)调用 super.clone();
2). + clone 对象中可变的实例字段;

注意,Cloneable 接口是标记接口,其内部没有任何方法,唯一用途就是允许使用 instanceof 进行类型查询。
详细的描述:https://www.cnblogs.com/bruce-he/p/17271648.html
// 实现 Cloneable 接口
class Employee implements Cloneable {
    ...
    public Employee clone() throws CloneNotSupportedException {   // 1) 重新定义 Clone(); 2)方法声明为 public
        // 3)调用 super.clone()方法,其调用  Object.clone(),并复制所有字段。Object.clone() 检查方法是否可以 clone,并是否实现了 Cloneable 接口,否则 throw 一个异常
        Employee cloned = (Employee) super.clone();   // 将返回的 Object 对象转回成实际类型对象
        // 4) clone 对象中可变的实例字段 (Employee 类提供了一个可设置的 setHireDay 方法),并 加入到 cloned 对象
        cloned.hireDay = (Date) hireDay.clone();
        return cloned;
    }
}
所有的数组类型 都有一个 public 的 clone 方法,所以可以直接调用,不需要重新定义
int[] luckyNumbers = {2, 3, 5, 7, 11, 13}; int[] cloned = luckyNumbers.clone(); cloned[5] = 12; // doesn't change luckyNumbers[5] // [2, 3, 5, 7, 11, 13] System.out.println(Arrays.toString(luckyNumbers)); // [2, 3, 5, 7, 11, 12] System.out.println(Arrays.toString(cloned));

 


6.2 lambda 表达式

6.2.2. lambda 表达式

// 案例:字符串按长度排序的几种实现方式
// 方式1). 一般情况下,频繁调用  first.length() - second.length();

// 方式2). 通过实现 Comparator 接口
class LengthComparator implements Comparator<String> {
    public int compare(String first, String second) {
        return first.length() - second.length();
    }
}
Arrays.sort(strings, new LengthComparator());  // 调用

// 方式3). Lambda 表达式:
(String first, String second) -> first.length() - second.length();   

   

表达式,3个部分构成(参数)->  { 表达式 }; 

// 1)如果表达式不止一行,需要 {} + return
(String first, String second) -> {
        if (first.length() < second.length())  return -1;
        else if (first.length() > second.length())  return 1;
        else return 0;
    }

2)无参数时,依然要有 空括号;
() -> { for (int i = 100; i >= 0; i--)  System.out.println(i); }

3)如可以推导出一个 lambda 表达式的参数类型,则可忽略其类型;
Comparator<String> comp = (first, second)      // same as (String first, String second)
                        -> first.length() - second.length();

4)如方法仅一个参数,且该参数的类型可推导出,那么 可以省略 ();
ActionListener listener = event ->             // Instead of (event) -> ... or (ActionEvent event) -> ...
    System.out.println("The time is " + new Date());

5)无需指定 lambda 表达式的返回类型,总会根据上下文推导出来;
(String first, String second) -> first.length() - second.length()

6)如只在某些分支返回值,另外的不返回,则不合法;
   (int x) -> { if (x >=0 ) return 1; }      // illegal,仅在 x>0 时返回值
// 案例: 对数组的排序 Arrays.sort()
var planets
= new String[] {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"};
// 1). 默认的按字典排序 Arrays.sort(planets); // 2). 按长度排序,lambda 表达式 Arrays.sort(planets, (first, second) -> first.length() - second.length());

6.2.3. 函数式接口

函数式接口(Functional Interface)有且仅有一个抽象方法,需要这种接口的对象时,就可以提供一个 lambda 表达式。
Functional interface = Interface with a single abstract method.
Lambda expression can be used whenever a functional interface value is expected.
Conversion to a functional interface is the only thing that you can do with a lambda expression.
函数式接口可以被隐式转换为 lambda 表达式。

Lambda 表达式的初衷:取一个 Lambda 表达式,1)把它保存到一个接口类型的变量中;2)传递到一个需要接口类型值的方法;

1)函数式接口:
   JDK 1.8 之前已有的函数式接口:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.nio.file.PathMatcher
  • java.lang.reflect.InvocationHandler
  • java.beans.PropertyChangeListener
  • java.awt.event.ActionListener
  • javax.swing.event.ChangeListener

  JDK 1.8 新增加的函数接口:

  • java.util.function

// 场景1:函数式接口 Comparator 的应用 -- 排序

String[] planets = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"};
Arrays.sort(planets, (first, second) -> first.length() - second.length());

=> 解析:
此处的 Arrays.sort() 方法调用的是 Arrays 类的 public static <T> void sort(T[] a, Comparator<? super T> c) {...}
其第二个参数是一个 Comparator 的实例,Comparator 是只有一个抽象方法的接口,所以可以提供一个 lambda 表达式。具体解释参照下面的(ActionListener、Comparator 接口为何是 函数式接口 的解释

 

// 场景2:函数式接口 ActionListener 的应用 -- 事件发生器

lambda 表达式可以转换为接口,其解释为:下面例子的 Timer() 类的第二个参数是 ActionListener 接口,故可以将 lambda 表达式作为一个接口传入。

另外, lammbda 表达式代码相比实现了 ActionListener 接口的类比起来更易读:

// 方式一:通过实现 ActionListener 接口来实现
class TimePrinter implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("At the tone, the time is " + Instant.ofEpochMilli(e.getWhen()));
        Toolkit.getDefaultToolkit().beep();
    }
}

// 调用
Timer t = new Timer(1000, listener);
t.start();
// 方式二:通过 lambda 表达式实现(相比方式一,更易读)
var t = new Timer(1000, event -> {            // Timer(int delay, ActionListener listener),其第二个参数也是 ActionListener,最终还是将 lambda 表达式传入
        System.out.println("The timer is " +Instant.ofEpochMilli(e.getWhen()));
        Toolkit.getDefaultToolKit().beep();
});
t.start();
// ActionListener、Comparator 接口为何是 函数式接口 的解释:
1)ActionListener: 查看源码,它是仅一个 public 方法且没有方法实现,在接口中默认是抽象方法 public interface ActionListener extends EventListener { public void actionPerformed(ActionEvent e); } 2)Comparator 接口: public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); default Comparator<T> reversed() { ... } ... public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() { ... } ... } // 查看 Comparator 的源码,其包含的方法如上: a)boolean equals() 源自 Object.euqals(Object),是对其的重写,已经有方法实现; b)其他 defaultstatic 等方法都有具体实现; c)所以,推断出仅有 compare() 方式是没有方法实现的 抽象方法;

 

综合场景1、2:在 Java 中,对 lambda 表达式所能做的也只是转换为 函数式接口。

其他较常用的函数式接口包含:java.util.function.Predicate,  java.util.function.Supplier

public class Java8Tester {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);

        System.out.println("Print out all data:");
        eval(list, n -> true);

        System.out.println("Print out all data > 3:");
        eval(list, n -> n>3);
    }

    public static void eval(List<Integer> list, Predicate<Integer> predicate) {    // Interface Predicate<T> has an abstract method boolen test(T t)
        for (Integer n : list) {
            if (predicate.test(n)) System.out.print(n + " ");
        }
        System.out.println();
    }
}

 

 

6.2.4  方法引用 Method References

// 应用场景:只要出现一个定时器事件,就打印这个事件对象

// 实现方式1: 函数式接口
var timer = new Timer(1000, event -> System.out.println(event)); // 实现方式2(Better solution): 方法引用 var timer = new Timer(1000, System.out::println); // 解析:指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。此例中,会生成一个 ActionListener,它的 actionPerformed(ActionEvent e) 方法要调用 System.out.println(e)。

 

方法引用的 3 种情况
object::instanceMethod

等价于 向方法传递参数的 lambda 表达式

System.out::println,对象是 System.out,等价于:x -> System.out.println(x)

Class::instanceMethod

第一个参数会成为方法的隐式参数

String::compareToIgnoreCase,等价于: (x, y) -> x.compareToIgnoreCase(y)

 Class::staticMethod

所有参数都传递到静态方法

如 Math::pow,等价于: (x,y) -> Math.pow(x, y)

注意:只有当 lambda 表达式的体只调用一个方法而不做其他操作时,才能把 lambda 表达式重写为方法引用。

            如, s -> s.length == 0;   // 这里有一个方法调用,还有一个比较,所以 不能使用方法引用。

// 关于方法引用的其他注释
1. 类似于 lambda 表达式,方法引用不是一个对象。不过,为一个类型为函数式接口的变量赋值时会生成一个对象;

2. 类似于 lambda 表达式,方法引用不能独立存在,总是会转换为 函数式接口 的实例;
3. 包含对象的方法引用 与 等价的 lambda 表达式还有一个细微的差别。如 方法引用 separator::equals,如果 seperator 为 null,那么: 1)构造 separator::equals 时立即抛出 NullPointerException; 2)lambda 表达式 x -> separator.equals(x) 只在调用时才会抛出 NullPointerException;

 

还可以在方法引用中使用 this、super

this::equals     // 等同于 x -> this.equals(x);

super::instanceMethod         // 调用给定方法的超类版本

       具体案例参考:https://www.w3cschool.cn/java/java-instance-method-reference.html

 

6.2.5 构造器引用

1. 构造器引用:Class::new

// 应用场景1: 将 字符串列表 转换为一个 对象列表
//
处理过程: 字符串 -> 流 -> 对象列表
ArrayList<String> names = ...; Stream<Person> stream = names.stream().map(Person::new); // map()会接受一个lambda 表达式,会为每个列表元素调用 Person(String) 构造器。Person::new, same as "s -> new Person(s)" List<Person> people = stream.collect(Collectors.toList());

2. 可以用数组元素建立构造器引用。 如 int[]::new       // 它有一个参数,即数组的长度,等价于 lambda 表达式 "x -> new int[x]"

3. 无法构造泛型类型 T 的数组,因为构造任意类型的数组是不合法的。使用数组构造器就可以解决。

 

// 应用场景2:将 流 转换为 数组
Person[] people = stream.toArray(Person[]::new);    // toArray() 调用这个构造器来得到一个有正确类型的数组,然后填充并返回这个数组

案例 参考: https://www.runoob.com/java/java8-method-references.html

 

6.2.6. 变量作用域 Variable Scope

  lambda 表达式有 3 个部分:

  • 1. A block of code 一个代码块; 
  • 2. Parameters 参数; 
  • 3. Value for the free variables 自由变量的值(不是在代码中定义的变量); // 只能引用 值不会改变的变量

  lambda 表达式的几个规则:

  • 1)变量必须是最终变量(即初始化后,不会再赋予新值)。这是因为 Java 实现了闭包,可以捕获外围的变量值,使得外围方法在退出时,变量依然存在,但约束是该变量不可变;
  • 2)表达式的体与嵌套块有相同的作用域。所以,方法中不能有同名的局部变量,声明与一个局部变量同名的参数或局部变量是不合法的;
  • 3)lambda 表达式不能有同名的局部变量(因为归属一个方法);
  • 4)表达式中使用的 this,是调用 创建这个表达式的 方法的 this 参数;
  • 5)不能改变 捕获的值;
// 1) lambda 表达式中的 i 根据 for 循环不断赋予新值 => error
public static void repeat(String text, int count) {
    for (int i = 1; i <= count; i++) {
        ActionListener listener = event -> {
            System.out.println(i + ": " + text); }   // Error: 1) Cannot refer to changing i
        new Timer(1000, listener).start(); 
    } 
}

// 2)变量冲突:variable first already defined
Path first = Paths.get("/usr/bin");
Comparator<String> comp =
    (first, second) -> first.length() - second.length();

// 4) this
public class Application() {
    public void init() {
        ActionListener listener = event -> { 
             System.out.println(this.toString());    // this.toString() 会调用 Application 对象的 toString()
                ...
            }
    }
}

// 5) 不能改变捕获的值
public static void countDown(int start, int delay) {
    ActionListener listener = event -> {
        start--;                         // ERROR: 5) can't mutate captured variable
        System.out.println(start);
    };
    new Timer(delay, listener).start();
}

6.2.7. 处理 lambda 表达式

   使用 lambda 表达式的重点是 延迟执行(deferred execution)适用的场景包括:

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

// step1: 解析成如下方法:
repeat(10, () -> System.out.println("Hello, World!"));

// step2: 实现: Pick a functional interface for the second parameter:
public static void repeat(int n, Runnable action) {
    for (int i = 0; i < n: i++) { action.run(); }          // 调用 action.run() 时,会执行 lambda 表达式的主体
}
// Example2: 输出动作出现在哪一次的迭代中

// step1: 解析,需要一个合适的函数式接口,其中的方法有一个 int 参数且返回类型为 void,故选择接口
public interface IntConsumer {
  void accept(int value);
}

// step2: 改进 repeat 方法。 To pass the count to the action, pick a functional interface form java.utilfunction package:
public static void repeat(int n, IntConsumer action) {
    for (int i = 0; i < n; i++) { action.accept(i); }
}

// Called like this:
repeat(10, i -> System.out.println("Countdown: " + (9-i)));

6.2.8 再谈 Comparator 接口

// the static method comparing makes a comparator from a key extractor function:
Arrays.sort(people, Compartor.comparing(Person::getName));     // 按 getName 的名字排序

// if the key is a primitive type, use comparingInt or comparingDouble to avoid boxing:
Arrays.sort(words, Comparator.comparingInt(String::length));  // 按单次长度比较,比单纯使用 comparing 少一次装箱过程

// the default method then Comparing chains comparators:
Arrays.sort(staff,
    Comparator.comparing(Employee::getName)
        .thenComparing(Person::getSalary));

 


6.3 内部类

  内部类:定义在另一个类中的类。主要原因:

  • 1)内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据;
  • 2)内部类可以对同一个包中的其他类隐藏;
  • 3)当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous) 内部类比较便捷(Before lambda expressions, inner classes were commonly used for callbacks);
  • !!! Java 8 中,内部类的很多用例已经被 Lambda 表达式所取代,不过偶尔还会有内部类用于其他一些用途,如限定类的作用域和信息隐藏

6.3.1 使用内部类访问对象状态

内部类既可以访问自身的数据字段,也可以访问 创建它的外围类对象的数据字段

类与各sample的对应内部类(TalkingClock) -> 局部内部类(TalkingClock2)-> 外部方法访问变量(TalkingClock3)-> 匿名内部类(TalkingClock4)-> Lambda 表达式(InnerClassLambda ))
// 内部类的用例
public
class InnerClass { public static void main(String[] args) { TalkingClock clock = new TalkingClock(2000, true); clock.start(); JOptionPane.showMessageDialog(null, "Quit program?"); // keep program running until user selects "OK" 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() { var listener = new TimePrinter(); var t = new Timer(interval, listener); t.start(); } public class TimePrinter implements ActionListener { // 内部类,定义在 TalkingClock 内部的类 @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + new Date()); // beep 引用了创建 TimePrinter 类 的外部类 TalkingClock 对象的字段,简单理解即为 outer.beep,此 outer 为虚构语法,正确的是 TalkingClock.this,详见 6.3.2 节第1点说明 if (beep) Toolkit.getDefaultToolkit().beep(); } } }
// 外围类的引用在构造器中设置。编译器会修改所有的内部类的构造器, 添加一个对应外围类引用的参数。因为 TimePrinter 类没有定义构造器,所以编译器为这个类生成了一个无参数构造器,其代码如下所示:
public TimePrinter(TalkingClock clock) {  // automatically generated code
    outer = clock; 
}

6.3.2 内部类的特殊语法规则

1. 使用外围类引用的正规语法是: OuterClass.thispublic void actionPerformed(ActionEvent e) {
    ...
    if (TalkingClock.this.beep) { Toolkit.getDefaultToolkit().beep(); }
}

2. 采用下列语法更明确地编写内部对象的构造器: outerObject.new InnerClass(construction parameters)
ActionListener listener = this.new TimePrinter();      // 在这里,最新构造的 TimePrinter 对象的外围类引用被设置为创建内部类对象的方法的 this 引用

3. 也可以通过显式地命名将外围类引用设置为其他的对象。由于 TimePrinter 是一个公共内部类,在外围类的作用域之外,可以通过 OuterClass.InnerClass 引用内部类:
TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();    // 语法规则: OuterClass.InnerClass object = outerClassObject.new InnerClass();

4. 其他规范:
  1)内部类中声明的所有静态字段必须是 final,并初始化为一个编译时常量;
  2)内部类的 static 方法只能访问外围类的静态字段和方法;

6.3.4 局部内部类(Local Inner Classes)

局部类不能用 publicprivate 访问说明符进行声明。作用域被限定在声明这个局部类的块中局部类优势:对外界完全隐藏。 比内部类更隐蔽。即使 TalkingClock 类中的其他代码也不能访问它。除 start 方法之外, 没有任何方法知道 TimePrinter 类的存在。
// 6.3.1 内部类的用例中,TimePrinter 类仅在 start 方法中创建这个类型的对象时使用了一次,那么可以 在一个方法中定义局部类。
class TalkingClock2 {
    private int interval;
    private boolean beep;

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

    public void start() {
// 局部内部类,无 public or private,对比于 6.3.1 的内部类,将 类的定义放置在类 TalkingClock 的一个方法中
class TimePrinter implements ActionListener { @Override public void actionPerformed(ActionEvent e) { 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(); } }

6.3.5 由外部方法访问变量

 局部类的优点:

  • 1)可以访问包含它们的外部类的字段;
  • 2)可以访问 定义为 final 的 局部变量;
class TalkingClock3 {                                     // 相对于 TalkingClock2 (6.3.4),更是将变量 interval, beep 作为方法 start() 的参数,少了定义及初始化的过程。外部方法就可以直接访问这两个变量
    public void start(int interval, final boolean beep) {    
        class TimePrinter implements ActionListener {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("At the tone, the time is " + new Date());
                if (beep) Toolkit.getDefaultToolkit().beep();   // 内部类 TimePrinter 可以访问上面定义为 final 的变量 beep          
            }
        }

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

// 调用:
TalkingClock3 clock3 = new TalkingClock3();
clock3.start(1000, true);

6.3.6 匿名内部类(Anonymous Inner Classes)

class TalkingClock4 {
    public void start(int interval, boolean beep) {
        // new ActionListener() {...}; => {} 里面定义的就是 匿名内部类。
// 相比较 TalkingClock3(局部内部类),TimePrinter 只是实现 ActionListener 接口,且对外部隐藏。那么,无须定义该类,转而直接实现 ActionListener 接口
var listener = new ActionListener() { // 定义 listener 接口变量,new ActionListener() {...} 匿名内部类,实现了 ActionListener 接口,故可以赋值 @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + new Date()); if (beep) Toolkit.getDefaultToolkit().beep(); } }; var t = new Timer(interval, listener); t.start(); } }
使用内部类时,如果只想创建这个类的一个对象,甚至不需要为类指定名字,该类被称为 匿名内部类(annoymous inner class)

1. Java 中可以实现一个类中包含另外一个类,且不需要提供任何的类名直接实例化; 2. 匿名类是不能有名字的类,它们不能被引用,只能在创建时用 new 语句来声明它们; 3. 匿名类不能有构造器,实际是将构造器参数传递给超类构造器。具体地说,只要内部类实现一个接口,就不能有任何构造参数; 4. 匿名类通常的语法格式 // SuperType:1)可以是接口(如 ActionListener),内部类就需要实现这个接口(例子参照上面的 TalkingClock4);
2)可以是类,内部类就需要扩展这个类;
new SuperType(construction parameters) { inner class methods and data } 匿名类的格式: class outerClass { object1 = new Type(parameterList) { // 定义一个匿名类 // 匿名类代码 }; }
5. 在内部类实现接口的时候,不能有任何构造参数,格式如: new InterfaceType() { methods and data }
6. 对比 构造类对象 与 构造匿名内部类的差异: Person queen = new Person("Mary"); // 构造一个类的对象 Person count = new Person("Dracula") {...}; // 构造了一个扩展了类的匿名内部类的对象,如果构造参数列表() 后跟一个 {},那么就是定义 匿名内部类
7. 一般常用 匿名内部类实现事件监听和其他回调,如今更多使用 lambda 表达式;
// 匿名内部类 => 现在更多实用 lambda 表达式
class TalkingClock5 {
    public void start(int interval, boolean beep) {
        Timer t = new Timer(interval, event -> {        // Lambda 表达式
            System.out.println("At the tone, the time is " + new Date());
            if (beep) { Toolkit.getDefaultToolkit().beep(); }
        });
        t.start();
    }

6.3.7 静态内部类

1) static inner class = inner class without reference to creating object 没有外部对象引用的内部类
2) useful for a private class that doesn't need to know the creating object

静态内部类的使用规则:只要内部类不需要访问外围类对象

静态内部类的其他特性:
1)与常规内部类不同,静态内部类可以有 静态字段 和 方法; 2)声明在接口中的内部类自动是 staticpublic // Example:返回一个数组的最大、最小值,1)传统方式:使用2个方法分别计数最大、最小,数组遍历 2次;
2)better:只遍历一次数组,同时计数出最大、最小。那么返回值必须是两个数,正好使用类 Pair
class ArrayAlg { public static class Pair { public double first; // 特性1:静态内部类的字段(静态字段,Java 8 以后不用特别指定为 final) public double second;

public Pair(double first, double second) {
this.first = first;
this.second = second;
}

public double getFirst() { return first; } // 特性2: 静态内部类的方法(静态方法)
... } ... public static Pair minmax(double[] values) { // 定义方法提供给 ArrayAlg 调用 ... return new Pair(min, max); // no creating object
} } // Called as: ArrayAlg.Pair p = ArrayAlg.minmax(data); // 由于 Pair 这个词很大众化,所以需要将它定义为 ArrayAlg 的公共内部类,那么就可以通过 ArrayArg.Pair 来调用了
// 因为 ArrayAlg 直接调用 minmax() 而没有创建对象,就必须将 minmax() 声明为 static
// minmax() 方法中,使用 return new Pair() 而未创建对象,那么 Pair 类也必须声明为 static

6.4 服务加载器(Skipped)

6.5 代理(Skipped)

 

posted on 2022-05-11 21:02  bruce_he  阅读(61)  评论(0编辑  收藏  举报