“核心技术:接口、lambda表达式与内部类”的版本间差异
(→变量作用域) |
(→函数式接口) |
||
第281行: | 第281行: | ||
* 接口中没有具体实现、没有“public static”修饰的也是抽象方法,因为 '''接口中的方法默认为“public abstract”'''(见“接口”一节的描述); | * 接口中没有具体实现、没有“public static”修饰的也是抽象方法,因为 '''接口中的方法默认为“public abstract”'''(见“接口”一节的描述); | ||
* Java API 在 java.util.function 包中定义很多非常通用的函数式接口; | * Java API 在 java.util.function 包中定义很多非常通用的函数式接口; | ||
需要函数式接口的对象时,可以提供一个 lambda 表达式。【即,用 lambda 表达式来代替函数式接口的抽象方法(“那个唯一需要实现的方法”)】 | 需要函数式接口的对象时,可以提供一个 lambda 表达式。【即,用 lambda 表达式来代替函数式接口的抽象方法(“那个唯一需要实现的方法”)】 | ||
* 最好把 lambda 看作是一个函数,而不是一个对象; | * 最好把 lambda 看作是一个函数,而不是一个对象; | ||
* Java中,'''lambda所能做的也只是能转换为函数式接口'''; | * Java中,'''lambda所能做的也只是能转换为函数式接口'''; | ||
设计函数式接口时,可以使用“'''@FunctionalInterface'''”注解来标记这个接口。这样做: | |||
# 当接口不符合函数式接口定义时(如:无意中增加了另一个抽象方法),编译器会报错; | |||
# javaDoc 页中会指示该接口是一个函数式接口; | |||
2020年10月17日 (六) 21:11的版本
接口
接口(interface),是对类的一组需求描述(一组行为定义):
- 接口的所有方法自动属于public(不用提供public关键字);
- 接口中可以有常量,但是不能有实例域;
- 即,域自动设置为:“public static final”
- 接口中可以有实现方法(Java SE 8 之后),但方法中不能引用实例域;
- 默认:“public abstract”
提供实例域和方法实现,应该由实现接口的类来完成。
实现接口:
- 声明实现给定的接口:
class Employee implements Comparable
- 对接口中的所有方法进行定义;
- 所有方法都必须有实现;
- 实现方法时,必须把方法声明为“public”;
接口的特性
- 接口不是类,不能用“new”进行实例化;
- 可以用接口声明变量,变量只能引用接口实现类的对象;
- 可以使用“instanceof”检查一个对象是否实现了某个特定接口(类似于检查对象是否属于某个类);
- 接口可以被继承,用新的接口扩展旧接口;
接口与抽象类
Java不支持多重继承(multiple inheritance):引入抽象类,避免多重继承的复杂性和低效性。
静态方法
Java 8 之后,允许接口中增加静态方法:
- 静态方法无法实现多态,形式上重写,实际上是新方法。
默认方法
可以为接口方法提供一个默认实现(也可以不实现),并用“default”修饰该方法:
- 可以不用实现接口所有方法,只关注于需要的某个或某几个方法(可以不再使用伴随类):
public interface MouseListener { default void mousedieked(MouseEvent event) {} default void mousePressed(MouseEvent event) {} default void mouseReleased(MouseEvent event) {} default void mouseEntered(MouseEvent event) {} default void mouseExited(MouseEvent event) {} }
- 在为旧接口增加方法时,使用默认方法可以保证“源代码兼容”(source compatible);
- (增加非默认方法,会导致接口现有实现类不能正常工作)
关于伴随类
在JavaAPI 中,你会看到很多接口都有相应的伴随类,这个伴随类中实现了相应接口的部分或所有方法, 如 Collection/AbstractCollection 或 MouseListener/MouseAdapter。在JavaSE 8 中,这个技术已经过时。现在可以直接在接口中实现方法。
使用抽象类(伴随类)对接口进行部分实现,而后用户类并不之间实现接口(需要实现全部方法),而是继承与该抽象类(只重写需要的类即可)。如:
public interface Collection<E> extends Iterable<E> { public abstract class AbstractCollection<E> implements Collection<E> {
public interface MouseListener extends EventListener { public abstract class MouseAdapter implements MouseListener, MouseWheelListener, MouseMotionListener {
- 又见,SpringMVC 中的拦截器(见[1]):
public interface HandlerInterceptor { public interface AsyncHandlerInterceptor extends HandlerInterceptor { public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {
解决默认方法冲突
冲突:现在接口中将一个方法定义为默认方法,又在超类或另一个接口中定义了同样的方法;
规则:
- 超类优先:接口中的方法(不论是否默认方法)会被忽略;
- 接口冲突:一个接口提供默认方法,一个接口提供同名同参数方法(不论是否默认方法),则在实现类必须覆盖这个方法;
接口示例
接口与回调
回调的思想是:
- 类A的a()方法调用类B的b()方法
- 类B的b()方法执行完毕主动调用类A的callback()方法
- 回调的核心就是回调方将本身即this传递给调用方
回调与接口:
- 将回调方法抽象为接口,用回调方去实现需要的回调接口
Comparator 接口
- 若需要将对象数组排序,则必须数组的每一个元素都实现了Comparable接口:
package java.lang; import java.util.*; public interface Comparable<T> { public int compareTo(T o); }
然而,如果需要对String数组排序(已经实现了Comparable<String>接口),但又不想按照字典顺序排序(String.compareTo的实现是按字典顺序比较):
- 首先我们不能使String类实现两种不同的compareTo方法,
- 其次String类也不应该由我们修改。
即:如何使用Comparable.compareTo方法之外的比较方法,来进行对象数组的排序?
所以,我们需要用到:
- Arrays.sort 方法
public static <T> void sort(T[] a, Comparator<? super T> c) { if (c == null) { sort(a); } else { if (LegacyMergeSort.userRequested) legacyMergeSort(a, c); else TimSort.sort(a, 0, a.length, c, null, 0, 0); } }
- 以及,Comparator<T>接口
package java.util; ... public interface Comparator<T> { int compare(T o1, T o2); ... }
如:
- 定义一个 Comparator<String> 接口的实现类:
- Comparator<T> 接口的方法很多,但并不用全部实现,因为一部分方法有实现,一部分为默认方法;
class LengthComparator implements Comparator<String> { public int compare(String first, String second) { return first.lengthO - second.lengthO; } }
- 使用Arrays.sort进入数组比较:
String[] friends = { "Peter", "Paul", "Mary" }; Arrays,sort(friends, new LengthComparatorO):
- 也可以,单独对两个对象或两个数组元素进行比较:
Comparator<String> comp = new LengthComparator。; if (conp.compare(words[i], words[j]) > 0)
- Comparator.compare 并非静态方法,所以使用时需要 Comparator实例;
对象克隆
拷贝和克隆
- 为一个包含对象引用的变量建立副本,直接赋值即可,原件副本都是同一个对象的引用;
- 若希望为对象也创建一个副本,则应该使用克隆;
浅拷贝(Object.clone())
默认的克隆操作(“Object.clone()”)是“浅拷贝”:如果对象包含子对象的引用,拷贝域会得到相同子对象的另一个引用;
- 即,原对象域浅克隆对象会引用相同的子对象;
- Object.clone() 是Object的一个protected方法;
- 若,子对象属于一个不可变的类,或在生命周期中状态不变,则是安全的;否则不应该使用浅拷贝;
深拷贝(Cloneable 接口)
Cloneable 接口时Java提供的一组标记接口(tagging interface,或称记号接口 marker interface):
- 标记接口不包含任何方法,它的唯一作用就是允许在类型查询中使用 instanceof;
- 即使 clone 的默认实现(浅拷贝)满足要求,还是需要实现 Cloneable 接口,将 clone 重新定义为 public,再调用 super.clone():
class Employee implements Cloneable
{
// raise visibility level to public, change return type
public Employee clone() throws CloneNotSupportedException
{
return (Employee) super.clone();
}
...
}
深克隆的实现:
- 实现 Cloneable 接口,将 clone 重新定义为 public;
- 重写 clone 方法,克隆需要的子对象(分别调用子对象的克隆方法);
class Employee implements Cloneable
{
...
public Employee cloneO throws Cl oneNotSupportedExcepti on
{
try {
// call Object, clone0
Employee cloned = (Employee) super.cloneO ;
// clone mutable fields
cloned.hireDay = (Date) hireDay.clone() ;
return cloned;
} catch (CloneNotSupportedException e) {
return null;
}
// this won't happen, since we are Cloneable
}
}
lambda
为什么引入 lambda
lambda :带参数变量的表达式,用于传递一个代码块到某个对象;
- Java 是面向对象的语言,所以必须构造一个对象,用对象的类的方法来包含所需的代码块;
lambda表达式语法
lambda 表达式形式:
参数 -> 表达式
如:
(String first, String second) ->
{
if (first.lengthO < second.lengthO) return -1;
else if (first.lengthO > second.lengthO) return 1;
else return 0;
}
参数:
- 如果没有参数,仍然要保留括号:
() -> { for (int i = 100; i >= 0;i ) System.out.println(i); }
- 如果可以推导出参数类型,可以忽略参数类型:
Comparator<String> comp = (first, second) // Same as (String first, String second) -> first.length() - second.length();
- 如果方法只有一个参数,且参数类型可以推导,可以只保留参数名:
ActionListener listener = event -> System.out.println("The time is " + new Date()"); // Instead of (event) -> . . . or (ActionEvent event) -> . . .
表达式:
- 如果返回值可以由上下文推导得出,可以省略返回类型:
(String first, String second) -> first.length() - second.length()
- 可以在 int 类型结果的上下文使用;
- lambda 表达式不能只在某些分支返回值,而存在分支不返回值。如下,是不合法的:
(int x)-> { if (x >= 0) return 1; }
函数式接口
函数式接口:有且仅有一个抽象方法,但可以有多个非抽象方法 的接口。(即,接口“只有一个需要实现的方法”)
- 接口中没有具体实现、没有“public static”修饰的也是抽象方法,因为 接口中的方法默认为“public abstract”(见“接口”一节的描述);
- Java API 在 java.util.function 包中定义很多非常通用的函数式接口;
需要函数式接口的对象时,可以提供一个 lambda 表达式。【即,用 lambda 表达式来代替函数式接口的抽象方法(“那个唯一需要实现的方法”)】
- 最好把 lambda 看作是一个函数,而不是一个对象;
- Java中,lambda所能做的也只是能转换为函数式接口;
设计函数式接口时,可以使用“@FunctionalInterface”注解来标记这个接口。这样做:
- 当接口不符合函数式接口定义时(如:无意中增加了另一个抽象方法),编译器会报错;
- javaDoc 页中会指示该接口是一个函数式接口;
示例1:
Arrays.sort (words ,
(first , second) -> first.length() - second.length()) ;
- Arrays.sort 方法的第二个参数为 Comparator<String>接口的实现类对象;
- 该 lambda表达式 即为 “Comparator<String>”接口的“compare(T, T)”方法的实现;
示例2:
Timer t = new Timer(1000, event ->
{
System.out.println("At the tone, the time is " + new DateO);
Toolkit.getDefaultToolkit().beep();
});
- Timer 构造函数的第二个参数为 ActionListener接口的实现类对象;
- 该 lambda表达式 即为 “ActionListener”接口的“actionPerformed(ActionEvent e)”方法的实现;
示例3:
list.removelf(e -> e == null);
- (list 对象属于 ArrayList类)
- list.removelf 方法(removeIf(Predicate<? super E> filter))的参数为 “Predicate<T>”接口的实现类对象;
- 该 lambda表达式 即为 “Predicate<T>”接口的“boolean test(T t)”方法的实现:
方法引用
利用已有类的方法,来实现函数式接口的抽象方法。形式:
类或对象::方法
- 函数接口抽象方法的参数列表,必须与 方法引用的参数列表 一致
主要有三种情况:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
- 对于前两种,方法引用等价于提供方法参数的 lambda 表达式:
System.out::println
等价于x -> System.out.println(x);
Math::pow
等价于(x, y) -> Math.pow(x, y)
- 对于第三种:第一个参数会成为方法的目标【?即:“para1.instanceMethod(para2)”】:
String::compareToIgnoreCase
等价于(x, y) -> x.compareToIgnoreCase(y)
示例:
Timer t = new Timer(1000, Systei.out::println); // 等同于 Timer t = new Timer(1000, event -> System.out.println(event));
Arrays.sort(strings,String::conpareToIgnoreCase); // 等同于 Arrays.sort(strings,(x, y) - > x.compareToIgnore(y));
构造器引用
构造器引用 与方法引用类似,只不过方法名为“new”,即:
类或对象::new
- 可以用数组类型建立构造器引用。
- 例如,“int[]::new”是一个构造器引用,它有一个参数(即数组的长度)。等价于lambda 表达式“x-> new int[x]”;
示例:(代码中的“stream”得看核心技术卷2)
ArrayList<String> names = . . .;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.col1ect(Col1ectors.toList());
map 方法会为各个列表元素调用Person(String) 构造器。如果有多个Person 构造器,编译器会选择有一个String 参数的构造器, 因为它从上下文推导出这是在对一个字符串调用构造器。
关于Java中创建泛型数组:(泛型是Java的语法糖,在编译时会被替换为实际类型)
Java 有一个限制,无法构造泛型类型 T 的数组。数组构造器引用对于克服这个限制很有用。表达式“new T[n]”会产生错误,因为这会改为“new Object[n]”。 对于开发类库的人来说,这 是一个问题。例如, 假设我们需要一个Person 对象数组。Stream 接口有一个toArray 方法可以返回Object 数组: Object[] people = stream.toArray(); 不过,这并不让人满意。用户希望得到一个Person 引用数组,而不是Object 引用数组。流库利用构造器引用解决了这个问题。可以把Person[]::new 传人toArray 方法: Person[] people = stream.toArray(PersonD::new); toArray 方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。
变量作用域
- 关于“闭包”:闭包就是能够读取其他函数内部变量的函数。如:Java中的lambda,javascript中函数内部的子函数;
lambda 表达式的三个部分:
- 一个代码块
- 参数
- 自由变量的值(指非参数,且不在lambda中定义的变量)
- 自由变量,能使在 lambda 表达式中访问外围方法或类中的变量:
public static void repeatMessage(String text, int delay) { ActionListener listener = event -> { System.out.println(text): Toolkit.getDefaultToolkitO.beep() : }; new Timer(delay, listener) .start0; }
- 调用 repeatMessage("Hello", 1000); 则会每秒打印一个Hello;
- lambda 表达式中捕获的变量必须实际上是最终变量(effectivelyfinal)。,即这个变量初始化之后就不会再为它赋新值:
public static void countDown(int start, int delay) { ActionListener listener = event -> { start--; // Error: Can't mutate captured variable System.out.println(start); }; new Timer(del ay, listener) ,start(); }
- 如上,在lambda内改变变量start的值,是不合法的;
public static void repeat(String text , int count) { for (int i = 1; i <= count; i ++) { ActionListener listener = event -> { System.out.pn'nt1n(i + ": " + text) ; // Error: Cannot refer to changing i }; new Timer(1000, listener) .start(); } }
- 以上同样不合法:lambda捕获的变量 i,也不能在外部被改变;
- lambda 表达式的体与嵌套块有相同的作用域:
Path first = Paths.get("usr/Mn"); Couparator<String> comp = (first , second) -> first.length() - second.lengthO ; // Error: Variable first already defined
- 如上,在lambda中声明与一个局部变量同名的参数或局部变量是不合法的;
- lambda 中的 this 关键字是指创建这个lambda表达式的方法的this参数;
public class ApplicationO { public void init () { ActionListener listener * event -> { System.out.print n(thi s.toString()); ... } ... } }
- 如上,表达式this.toString()会调用Application对象的toString方法, 而不是ActionListener实例的方法;