读后笔记 -- 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 表达式的参数类型,则可忽略 其类型; Comparatorcomp = (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) { Listlist = 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 Listcars = 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"); Comparatorcomp = (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 局部内部类
局部类不能用 public 或 private 访问说明符进行声明。作用域被限定在声明这个局部类的块中。 局部类优势:对外界完全隐藏。 即使 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)声明在接口中的内部类自动成为 static 和 public 类; class ArrayAlg { public static class Pair { ... } }