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


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

6.1 接口

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

  • 1)接口 不能含有 实例域;
  • 2)不能在 接口 中实现方法;

  ** 提供 实例域 和 方法实现 的任务,由实现接口的那个类 来完成。

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

  • 1)将类声明为实现给定的接口;
  • 2)对接口中的所有方法进行定义;
public class Employee implements Comparable
{
    @Override
    public int compareTo(Employee other) {
        return Double.compare(salary, other.salary);
    }
    ...
}

3. 接口的特征:

// 1)接口不是类,不能 new 来实例化一个接口;
x = new Comparable(...);   // ERROR

// 2)可声明接口的变量,但必须引用实现了接口的类对象;
Comparable x;                 // OK,声明接口变量
x = new Employee(...);    // OK,引用实现了接口的类对象

// 3)可使用 instanceof 检查一个对象是否实现了特定的接口;
if (anObject instanceof Comparable) {...}
   
// 4) 接口可以再扩展;

// 5)接口不能包含 实例域 或 静态方法,但可包含 常量;

// 6)接口中的方法自动标记为 public,接口中的域自动设为 public static final(一般不用写)

// 7)一个类:仅有一个超类,但可实现 多个接口
class Employee implements Cloneable, Comparable

4. 接口与抽象类

引入 接口 的目的:每个类只能扩展一个类(如抽象类),但是可以 同时引用 多个接口。

----- 4.1 Java 抽象类 -----
public abstract class Employee
{
    ...
}

    在 OOP 的概念中,所有的对象都是通过类来描绘的,但反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
    抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
    由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。正因如此,通常在设计阶段决定要不要设计抽象类。
    父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。
    在 Java 中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口。

----- 4.2 Java 抽象方法 -----
public abstract class Employee
{
   ...
   public abstract double computePay();
   ...
}
    如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。
    Abstract 关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。

    声明抽象方法会造成以下两个结果:
    1)如果一个类包含抽象方法,那么该类必须是抽象类。
    2)任何子类必须重写父类的抽象方法,或者声明自身为抽象类。

    继承抽象方法的子类必须重写该方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。

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

5. 静态方法

   在 Java SE 8 中,允许在接口中增加静态方法;

public interface Path
{
    public static Path get(String first, String... more) {
        return FileSystems.getDefualt().getPath(first, more);
    }
    ...
}

6. 默认方法 default

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

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

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

7. 解决默认方法冲突

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

  规则:

  • 1)超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
  • 2)接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突。
// “类优先”原则
class Student extends Person implements Named {...}

如果 超类 Person 和 接口 Named 都提供了 getName 方法,那么,最终 Student 将从超类中继承 getName 方法

6.2 接口示例

1. 接口与回调

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

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

        // construct a timer that calls the listener
        // once every 10 seconds
        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();
    }
}

2. Comparator 接口

public class ComparatorTest {
    public static void main(String[] args) {
        String[] friends = {"Peter", "Paul", "May", "Albert"};
        String[] friends2 = friends;

        // 按照 字典顺序排序, [Albert, May, Paul, Peter]
        Arrays.sort(friends2);
        System.out.println(Arrays.toString(friends2));
        
        // 按照 字符串长度 排序,[May, Paul, Peter, Albert]
        Arrays.sort(friends,new LengthComparator());
        System.out.println(Arrays.toString(friends));
    }
}

/**
 * sort the arrays by length
 */
class LengthComparator implements Comparator
{
    @Override
    public int compare(String o1, String o2) {
        return o1.length() - o2.length();
    }
}

3. 对象克隆

  对于每一个类,需要确定:

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

  如果确定 1)或 2),则该类必须:

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

深 clone:
1)必须重新定义 clone 方法 为 public;(浅 copy 也需要)
2)!! clone 对象中可变的实例域;
数组类型 都有一个 public 的 clone 方法
public class ArrayCloneTest {
    public static void main(String[] args) {
        int[] luckyNumbers = {2, 3, 5, 7, 11, 13};
        int[] cloned = luckyNumbers.clone();
        cloned[5] = 12;
        // [2, 3, 5, 7, 11, 13]
        System.out.println(Arrays.toString(luckyNumbers));
        // [2, 3, 5, 7, 11, 12]
        System.out.println(Arrays.toString(cloned));
    }
}
---------------- CloneTest.java ---------------
public class CloneTest {
    public static void main(String[] args) {
        try
        {
            Employee original = new Employee("Harry Hacker", 50000.0);
            original.setHireDay(2000, 1, 1);

            // 任一变量改变都会影响另一个,因为:两者是 同一对象的引用
            Employee simplyCopy = original;
            simplyCopy.raiseSalary(5);

            // clone 一个新对象,形成各自不同的状态。需要: 将默认的 protected clone 方法 重新定义为 public
            Employee copy = original.clone();
            copy.setHireDay(2002, 12, 31);
            copy.raiseSalary(10);

            System.out.println("original=" + original);
            System.out.println("simplyCopy=" + simplyCopy);
            System.out.println("copy=" + copy);

        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

---------------- Employee.java ---------------
public class Employee implements Cloneable {
    private String name;
    private Double salary;
    private Date hireDay;

    public Employee(String name, Double salary) {
        this.name = name;
        this.salary = salary;
        this.hireDay = new Date();
    }

    @Override
    public Employee clone() throws CloneNotSupportedException {
        // call Object.clone()
        Employee cloned = (Employee) super.clone();

        // clone mutable fields
        cloned.hireDay = (Date) hireDay.clone();

        return cloned;
    }

    /**
     * Set the hire day to a given date.
     * @param year      the year of the hire day
     * @param month     the month of the hire day
     * @param day       the day of the hire day
     */
    public void setHireDay(int year, int month, int day) {
        Date newHireDay = new GregorianCalendar(year, month-1, day).getTime();
        //Example of instance field mutation
        hireDay.setTime(newHireDay.getTime());
    }

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

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                ", hireDay=" + hireDay +
                '}';
    }
}

6.3 lambda 表达式

1. lambda 表达式

    表达式:(参数)->  { }

1)表达式例子:
(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 comp
    = (first, second)       // same as (String first, String second)
        -> first.length() - second.length();

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

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

6)如只在某些分支返回值,另外的步返回,则不合法;
   (int x) -> { if (x >=0 ) return 1; }      // illegal
public class LambdaTest {
    public static void main(String[] args) {
        String[] planets = new String[] {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"};
        System.out.println(Arrays.toString(planets));

        // 按字典排序
        System.out.println("Sort by dictionary:");
        Arrays.sort(planets);
        System.out.println(Arrays.toString(planets));

        // 按长度排序
        System.out.println("Sort by length:");
        // lambda 表达式
        Arrays.sort(planets, (first, second) -> first.length() - second.length());
        System.out.println(Arrays.toString(planets));

        // lambda 表达式
        Timer t = new Timer(1000, event ->
                System.out.println("The timer is " + new Date()));
        t.start();

        // keep program running until user select "Ok"
        JOptionPane.showMessageDialog(null,"Quit program?");
        System.exit(0);
    }
}

2. 函数式接口

函数式接口(Functional Interface):有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。

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
public class Java8Tester {
    public static void main(String[] args) {
        List 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 list, Predicate predicate)
    {
        for (Integer n : list)
        {
            if (predicate.test(n))
            {
                System.out.print(n + " ");
            }
        }
        System.out.println();
    }
}

3. 方法引用+构造器引用( :: 来分割 对象或类 与 方法名 )

public class MethodTest {
    public static void main(String[] args) {
        // 1) 构造器引用:语法是Class::new,或者更一般的Class< T >::new
        // [functionalinteface.Car@1d81eb93]
        final Car car = Car.create(Car::new);
        final List cars = Arrays.asList(car);
        System.out.println(cars.toString());

        // 2) 静态方法引用:语法是Class::static_method
        // Collided functionalinteface.Car@1d81eb93
        cars.forEach(Car::collide);

        // 3) 特定类的任意对象的方法引用:语法是Class::method
        // Repaired functionalinteface.Car@1d81eb93
        cars.forEach(Car::repair);

        // 4) 特定对象的方法引用:语法是instance::method
        // Following the functionalinteface.Car@1d81eb93
        final Car police = Car.create(Car::new);
        cars.forEach(police::follow);
    }
}

class Car {
    public static Car create(final Supplier supplier) { return supplier.get(); }

    public static void collide(final Car car) { System.out.println("Collided " + car.toString()); }

    public void follow(final Car another) { System.out.println("Following the " + another.toString()); }

    public void repair() { System.out.println("Repaired " + this.toString()); }
}

@java.lang.FunctionalInterface
interface Supplier {
    T get();
}

4. 变量作用域

   lambda 表达式的几个规则

  • 1)变量必须是最终变量(即初始化后,不会再赋予新值);
  • 2)lambda 表达式的体与嵌套块有相同的作用域。所以,方法中不能有同名的局部变量,声明与一个局部变量同名的参数或局部变量是不合法的;
  • 3)lambda 表达式中 使用的 this,是调用 创建这个表达式的 方法的 this 参数;
// 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: Cannot refer to changing i };
        new Timer(1000, listener).start(); 
    } 
}

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

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

5. 处理 lambda 表达式

   使用 lambda 表达式的重点是 延迟执行。

6.4 内部类

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

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

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

内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域
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("At the tone, the time is " + new Date());
            // beep 引用了创建 TimePrinter 的 TalkingClock 对象的域。下面的语句等价于 if (outer.beep) { Toolkit.getDefaultToolKit().beep(); }
            if (beep) { Toolkit.getDefaultToolkit().beep(); }
        }
    }
}
外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器, 添加一个外围类引用的参数。因为 TimePrinter 类没有定义构造器,所以编译器为这个类生成了一个默认的构造器,其代码如下所示:
public TimePrinter(TalkingClock clock) // automatically generated code
{
outer = clock; 
}

6.4.2 内部类的特殊语法规则

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

2. 采用下列语法更明确地编写内部对象的构造器:
outerObject.new InnerClass(construction parameters)
如:
ActionListener listener = this.new TimePrinter();

3. 如果TimePrinter 是一个公有内部类,在外围类的作用域之外,可以通过 OuterClass.innerClass 引用内部类:
如:
TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

6.4.4 局部内部类

局部类不能用 publicprivate 访问说明符进行声明。作用域被限定在声明这个局部类的块中。
局部类优势:对外界完全隐藏。 即使 TalkingClock 类中的其他代码也不能访问它。除 start 方法之外, 没有任何方法知道 TimePrinter 类的存在。
// 上面的案例中,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()
    {
        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.4.5 由外部方法访问变量

 局部类的优点:

  • 1)可以访问包含它们的外部类;
  • 2)可以访问 定义为 final 的 局部变量;

6.4.6 匿名内部类

1)Java 中可以实现一个类中包含另外一个类,且不需要提供任何的类名直接实例化;
2)匿名类是不能有名字的类,它们不能被引用,只能在创建时用 new 语句来声明它们;
3)匿名类不能有构造器,是将构造器参数传递给超类构造器;
4)通常的语法格式:
new SuperType(construction parameters)
{
    inner class methods and data
}    
匿名类的格式:
class outerClass {
    // 定义一个匿名类
    object1 = new Type(parameterList) {
        // 匿名类代码
    };
}
5)在内部类实现接口的时候,不能有任何构造参数,格式如:
new InterfaceType()
 {
     methods and data
  }
6)对比:
  // a Person object
Person queen = new Person("Mary");

  // an object of an inner class extending Person,后面的{} 就是定义的匿名内部类
Person count = new Person("Dracula") {...};
7)一般常用 匿名内部类实现事件监听和其他回调,如今更多使用 lambda 表达式;

6.4.7 静态内部类

1)在内部类不需要访问外围类对象的时候, 应该使用静态内部类;
2)与常规内部类不同,静态内部类可以有静态域和方法;
3)声明在接口中的内部类自动成为 staticpublic 类;

class ArrayAlg
{
    public static class Pair
    {
         ...
    }
}

6.5 代理(Skipped)