《Java核心技术 卷Ⅰ》 第6章 接口、lambda表达式与内部类
- 接口
- 接口示例
- lambda表达式
- 内部类
接口
接口技术,这种技术主要用来描述类具有什么功能,而并不给出每个功能的具体实现。一个类可以实现(implement)一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象。
接口概念
在Java程序设计语言中,接口不是类,是对类的一组需求的描述,这些类要遵从接口描述的统一格式进行定义。
Arrays
类中的sort
方法承诺可以对对象数组进行排序,但要求满足下列条件,对象所属的类**必须实现了Comparable
接口。
public interface Comparable{ int compareTo(Object other)}
这就是说,任何实现Comparable
接口的类都需要包含compareTo
方法,并且这个方法的参数必须是一个Object
对象,返回一个整型数值;比如调用x.compareTo(y)
时,当x
小于y
时,返回一个负数;当x
等于y
时,返回0;否则返回一个正数。
在 Java SE 5中,Comparable
接口改进为泛型类型。
public interface Comparable{ int compareTo(T other); // 参数拥有类型T}
例如在实现Comparable<Employee>
接口类中,必须提供int compareTo(Employee other)
方法。
接口中的所有方法自动地属于public
,因此,在接口中声明方法时,不必提供关键字public
。
- 接口可以包含多个方法
- 接口中可以定义常量
- 接口中不能含有实例域
- Java SE 8 之前,不能在接口中实现方法
提供实例域和方法实现的任务应该由实现接口的那个类来完成。
在这里可以将接口看成是没有实例域的抽象类。
现在希望用Arrays
类的sort
方法对Employee
对象数组进行排序,Employee
类必须实现Comparable
接口。
为了让类实现一个接口,通常需要下面两个步骤:
- 将类声明为实现给定的接口
- 对接口中的所有方法进行定义
将类声明为实现某个接口,使用关键字implements
:
class Employee implements Comparable{ ... public int compareTo(Object otherObject) { Employee other = (Employee) otherObject; return Double.compare(salary, other.salary); } ...}
这里使用了静态Double.compare
方法,如果第一个参数小于第二个参数,它会返回一个负值,相等返回0,否则返回一个正值。
虽然在接口声明中,没有将compareTo
方法声明为publuc
,这是因为接口中所有方法都自动地是public
,但是,在实现接口时,必须把方法声明为public
,否则编译器将认为这个方法的访问属性是包可见性,即类的默认访问。
可以为泛型Comparable
接口提供一个类型参数。
class Employee implements Comparable{ ... public int compareTo(Employee other) { return Double.compare(salary, other.salary); } ...}
为什么不能再Employee
类直接提供一个compareTo
方法,而必须实现Comparable
接口呢?
主要原因是Java是一种强类型(strongly type)语言,在调用方法时,编译器将会检查这个方法是否存在。
在sort方法一般会用到compareTo
方法,所以编译器必须确认一定有compareTo
方法,如果数组元素类实现了Comparable
接口,就可以确保拥有compareTo
方法。
接口的特性
接口不是类,尤其不能用new
实例化接口:
x = new Comparable(...); // Error
尽管不能构造接口的对象,却能声明接口的变量:
Comparable x; // OK
接口变量必须引用实现了接口的类对象:
x = new Employee(...); // OK
也可以使用instanceof
检查一个对象是否实现了某个特定的接口:
if(x instanceof Comparable) { ... }
与类的继承关系一样,接口也可以被扩展。
这里允许存在多台从具有较高通用性的接口到较高专用性的接口的链。
假设有一个称为Moveable
的接口:
public interface Moveable{ void move(double x, double y);}
然后,可以以它为基础扩展一个叫做Powered
的接口:
public interface Powered extends Moveable{ double milesPerGallon();}
虽然接口中不能包含实例域或者静态域,但是可以定义常量:
public interface Powered extends Moveable{ double milesPerGallon(); double SPEED_LIMIT = 95; // a public static final constant}
与接口中的方法自动设置为public
一样,接口中的域被自动设为public static final
。
尽管每个类只能拥有一个超类,但却实现多个接口。
class Employee implements Coneable, Comparable { ... }
接口与抽象类
你可能会问:为什么这些功能不能由一个抽象类实现呢?
因为使用抽象类表示通用属性存在这样的问题:每个类只能扩展于一个类,无法实现一个类实现多个接口的需求。
class Employee extends Person implements Comparable { ... }
静态方法
在 Java SE 8 中,允许在接口中增加静态方法。 虽然说这没有什么不合法的,只是这有违接口作为抽象规范的初衷。
通常的做法是将静态方法放在伴随类中。在标准库中,有成对出现的接口和实用工具类,如Collection
/Collections
或Path
/Paths
。
虽然Java库都把静态方法放到接口中也是不太可能,但是实现自己接口时,不需要为实用工具方法另外提供一个伴随类。
默认方法
可以为接口方法提供一个默认实现,必须用default
修饰符标记方法。
public interface Comparable{ default int compareTo(T other) { return 0; }}
默认方法的一个重要用法是接口演化(interface evolution)。
以Collection
接口为例,这个接口作为Java
的一部分已经很久了,假如很久以前提供了一个类:
public class Bag implements Collection { ... }
后来,在Java SE 8中,为这个接口增加了一个stream
方法。如果stream
方法不是默认方法,那么Bag
类将不能编译——因为它没有实现这个新方法。
为接口增加一个非默认方法不能保证“源代码兼容”(source compatible)。
解决默认方法冲突
如果一个接口中把方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,会发生什么情况?
解决这种二义性,Java的规则是:
- 超类优先,如果超类自己提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略
- 接口冲突,如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须覆盖这个方法来解决冲突
着重看一下第二个规则,考虑另一个包含getName
方法的接口:
interface Named{ default String getName() { return getClass().getName() + "_" + hashCode(); }}
现在有一个类同时实现了这两个接口,这个时候需要程序员来解决这个二义性,在这个实现的方法中提供一个接口的默认getName
方法。
class Student implements Person, Named{ public String getName() { return Person.super.getName(); }}
就算Named
接口并没有getName
的默认方法,同样需要程序员去解决这个二义性问题。
上面的是两个接口的命名冲突。
现在考虑另一种情况:一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。
class Student extends Person implements Named { ... }
这种情况下只会考虑超类的方法,接口所有默认方法会被忽略。
接口示例
接口与回调
回调(callback),可以指出某个特定事件时应该采取的动作。
java.swing
包中有一个Timer
类,可以使用它在到达给定的时间间隔发送通告。
在构造定时器时,需要设置一个时间间隔,并告知定时器,达到时间间隔时需要做什么。
其中一个问题就是如何告知定时器做什么?在很多语言中,是提供一个函数名,但是,在Java标准类库中的类采用的是面向对象方法,它将某个类的对象传递给定时器,然后定时器调用这个对象的方法。
定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了java.awt.event
包的ActionListener
接口:
public interface ActionListener{ void actionPerformed(ActionEvent event);}
当到达指定时间间隔,定时器就调用actionPerformed
方法。
使用这个接口的方法:
class TimePrinter implements ActionListener{ public void actionPerformed(ActionEvent event) { System.out.println(...); ... }}
其中接口方法的ActionEvent
参数提供了事件的相关信息。
接下来构造类的一个对象,并传递给Timer
构造器。
ActionListener listener = new TimePrinter()Timer t = new Timer(10000, listener);t.start(); // 启动定时器
Comparator接口
可以对一个字符串数组排序,是因为String
类实现了Comparable<String>
,而且String.compareTo
方法可以按字典顺序比较字符串。
现在需要按长度递增的顺序对字符串进行排序,我们肯定不能对String
进行修改,就算可以修改我们也不能让它用两种不同的方式实现compareTo
方法。
要处理这种情况,Arrays.sort
方法还有第二个版本,一个数组和一个比较器(comparator)作为参数,比较器实现了Comparator
接口的类的实例。
public interface Comparator{ int compare(T first, t second);}
按字符串长度比较,可以定义一个实现Comparator<String>
的类:
class LengthComparator implements Comparator{ public int compare(String first, String second) { return first.length() - second.length(); }}
具体比较时,建立一个实例:
Comparatorcomp = new LengthComparator();// comp.compare(words[i], words[j])Arrays.sort(friends, comp);
对象克隆
Cloneable
接口,指示一个类提供了一个安全的clone
方法。
Employee original = new Employee(...);Employee copy = original.clone();copy.raiseSalary(10); // no changes happen to original
clone
方法是Object
的一个protected
方法,代码不能直接调用这个方法(指的是Object
的这个方法)。
当然,只有Employee
类可以克隆Employee
对象,但是默认的克隆操作是浅拷贝,即并没有克隆对象中引用的其他对象。
浅拷贝可能会产生问题么?这取决于具体情况:
- 原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的
- 在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下也是安全的
一般来说子对象都是可变的,所以需要定义clone
方法来建立一个深拷贝,同时克隆所有子对象。
对于每一个类,需要确定:
- 默认的
clone
是否满足要求 - 是否可以在可变子对象上调用
clone
来修补默认clone
- 是否不该使用
clone
实际上第3个选项是默认选项(这句话没有太读懂)。
如果选第1个或者第2个,类必须:
- 实现
Cloneable
接口 - 重新定义
clone
方法,并指定public
访问修饰符
子类虽然可以访问Object
受保护的clone
方法,但是子类只能调用受保护的clone
方法来克隆它自己的对象。
必须重新定义clone
为public
,才能允许所有方法克隆对象。
Cloneable
接口是一组标记接口,其他接口一般确保一个类实现一个或一组特定的方法,标记接口不包含任何方法,它的唯一作用就是允许在类型查询中使用instanceof
。
即时clone
的默认(浅拷贝)实现能够满足要求,还是需要实现Cloneable
接口,将clone
重新定义为public
,再调用super.clone()
。
class Employee implements Cloneable{ // raise visibility level to public, change return type public Employee clone() throws CloneNotSupportedExcption { return (Employee) super.clone(); }}
与浅拷贝相比,这个clone
并没有增加任何功能,只是让方法变为公有,要建立深拷贝。
class Employee implements Cloneable{ ... public Employee clone() throws CloneNotSupportedExcption { // Obejct.clone() Employee cloned = (Employee) super.clone(); //clone mutable fields cloned.hireDay = (Date) hireDay.clone(); return cloned; }}
如果一个对象调用clone
,但这个对象类没有实现Cloneable
接口,Object
的clone
方法就会抛出一个CloneNotSupportedExcption
,Employee
和Date
类实现了Cloneable
接口,所以不会抛出异常,但是编译器并不知道这点,所以声明异常最好还要加上捕获异常。
class Employee implements Cloneable{ // raise visibility level to public, change return type public Employee clone() throws CloneNotSupportedExcption { try { Employee cloned = (Employee) super.clone(); ... } catch(CloneNotSupportedExcption e) { return null; } // 因为实现了Cloneable,所以这并不会发生 }}
必须当心子类的克隆。
例如,一旦Employee
类定义了clone
,那么就可以用它来克隆Manager
对象(因为在Employee
类中的clone
已经是public
了,可以直接使用Manager.clone()
)。
但Employee
的clone
一定能完成克隆Manager
对象的工作么?
这取决于Manager
类的域:
- 如果是基本类型域,那没有问题
- 如果是需要深拷贝或者不可克隆域,不能保证子类的实现者一定会修正
clone
方法让它正常工作
出于后者的原因,在Object
类中的clone
方法声明protected
。
lambda表达式
一种表示在将来某个时间点执行的代码块的简洁方法。
使用lambda表达式,可以用一种精巧而简洁的方式表示使用回调或变量行为的代码。
为什么引入lambda表达式
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。
之前的监听器和后面的排序比较例子的共同点是:都是把一个代码块传递到某个对象(定时器或者是sort
方法),并且这个代码块会在将来某个时间调用。
lambda表达式的语法
考虑之前的按字符串长度排序例子:
first.length() - second.length()
Java是一种强类型语言,所以还要指定他们的类型:
(String first, String second) -> first.length() - second.length() // 隐式return 默认返回这个表达式的结果
这就是一个lambda表达式,一个代码块以及变量规范。
如果代码要完成的计算不止一条语句,可以像写方法一样,把代码放在{}
中,并包含显式的return
语句。
(String first, String second) -> { if(first.length() < second.length()) return -1; else if(first.length() > second.length()) return 1; else return 0; }
一些省略形式的表达:
- 如果没有参数,仍要提供空括号
- 如果编译器可以推导出参数类型,可以省略类型声明
- 如果只有一个参数,并且参数类型可以推导,则可以省略小括号
需要注意的地方:
- 不需要指定返回类型,返回类型总是由上下文推导出(一般在赋值语句里)
- 如果表达式里只要有一个显式
return
,那就要确保每个分支都有一个return
,否则是不合法的
函数式接口
Java中已经有很多封装代码块的接口,比如ActionListener
或Comparator
,lambda表达式与这些接口兼容。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式,这种接口称为函数式接口(functional interface)。
考虑之前的Arrays.sort
方法,其中第二个参数需要一个Comparator
实例,函数式接口使用:
Arrays.sort(words, (first, second) -> first.length() - second.length());
在底层,Arrays.sort
方法会接收实现了Comparator<Strng>
的某个类的对象,在这个对象上调用compare
方法会执行这个lambda表达式的体。
最好把lambda表达式看作一个函数,而不是一个对象,而且要接收lambda表达式可以传递到函数式接口。
lambda表达式可以转换为接口,这让lambda表达式很有吸引力,具体的语法很简单:
Timer t = new Timer(10000, event -> { System.out.println(...); ... });
与使用实现了ActionListener
接口的类相比,这个代码可读性好很多。
实际上,在Java中,对lambda表达式所能做的也只是能转换为函数式接口,甚至不能把lambda表达式赋给类型为Object
的变量,Object
不是一个函数式接口。
方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。
比如只要出现一个定时器事件就打印这个事件对象:
Timer t = new Timer(10000, event -> System.out.println(event));
但是如果直接把println
方法传递给Timer
构造器就更好了:
Timer t = new Timer(10000, System.out::println);
表达式System.out::println
就是一个方法引用(method reference),它等价于lambda表达式x - > System.out.println(x)
。
考虑一个排序例子:
Arrays.sort(words, String::compareToIgnoreCase);
主要有3种情况:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
前面两种等价于提供方法参数的lambda表达式,比如System.out::println
等价于x -> System.out.println(x)
,以及Math::power
等价于(x, y) -> Math.power(x, y)
。
对于第3种,第1个参数会成为方法的目标,例如String::compareToIgnoreCase
等价于(x, y) -> x.compareToIgnoreCase(y)
。
可以在方法引种中使用this
,super
也是合法的,比如super::instanceMethod
,使用this
作为目标,会调用给定方法的超类版本。
构造器引用
构造器引用与方法引用类似,只不过方法名为new
,例如Person::new
是Person
构造器的一个引用,具体选择Person
多个构造器中的哪一个,这个取决于上下文。
现在有一个字符串列表,你可以把它转换为一个Person
对象数组,为此要在各个字符串上调用构造器。
ArrayListnames = ...;Stream stream = names.stream().map(Person::new);List people = stream.collect(Collectors.toList());
stream
、map
和collect
方法会在卷Ⅱ的第1章讨论。
现在的重点是map
方法会为各个列表元素调用Person(String)
构造器,这里编译器从上下文推导出这是在对一个字符串调用构造器。
可以用数组类型建立构造器引用,int[]::new
是一个构造器引用,有一个参数,就是数组的长度,这等价于x -> new int[x]
。
Java有一个限制:无法构造泛型类型T的数组。
数组构造器引用对于克服这个限制很有用。
假设需要一个Person
对象数组,Stream
接口有一个toArray
方法可以返回Object
数组:
Object[] people = stream.toArray();
但是用户想要一个Person
引用数组,流库利用构造器引用解决了这个问题:
Person[] people = stream.toArray(Person[]::new);
toArray
方法调用构造器获得一个正确类型的数组,然后填充这个数组并返回。
变量作用域
通常可能想在lambda表达式中访问外围方法或类中的变量。
public static void repeatMessage(String text, int delay){ ActionListener listener = event -> { System.out.println(text); ... }; new Timer(delay, listener).start();}
具体调用:
repeatMessage("Hello", 1000);
lambda表达式中的变量text
,并不是在这个lambda表达式中定义的,但是这其实有问题,因为代码可能会调用返回很久以后才运行,而那时这个参数变量已经不存在了,该如何保留这个变量?
重温一下lambda表达式的3个部分:
- 一个代码块
- 参数
- 自由变量的值,指非参数并且不在代码中定义的变量
上面的例子中有1个自由变量text
。
表示lambda表达式的数据结构必须存储自由变量的值,也被叫做自由变量被lambda表达式捕获(captured)。
可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。
关于代码块以及自由变量有一个术语:闭包(closure),Java中lambda表达式就是闭包。
在lambda表达式中,只能引用值不会改变的变量,比如下面这种就是不合法的:
public static void countDown(int start, int delat){ ActionListener listener = event -> { start--; // Error: Can't mutate captured variable System.out.println(text); ... }; new Timer(delay, listener).start();}
如果在lambda表达式中改变变量,并发执行多个操作时就会不安全(具体要见第14章并发)。
另外如果在lambda表达式中引用变量,并且这个变量在外部改变,这也是不合法的:
public static void repeat(String text, int count){ for(int i = 1; i <= count; i++) { ActionListener listener = event -> { System.out.println(i + ":" + text); // Error: Can't refer to changing i ... }; new Timer(1000, listener).start(); }}
所以简单来说规则就是:lambda表达式中捕获的变量必须实际上是最终变量(effectively final),即这个变量初始化之后就不再赋新值。
lambda表达式的体与嵌套块有相同的作用域,所以在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
Path first = Path.get("/usr/bin");Comparatorcomp = (first, second) -> fisrt.length() - second.length(); // Error: Variable first already defined
当然在lambda表达式中也不能有同名的局部变量。
在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。
public class Application(){ public void init() { ActionListener listener = event -> { System.out.println(this.toString()); ... } }}
this.toString()
会调用Application
对象的toString
方法,而不是ActionListener
实例的方法,所以在lambda表达式中this
的使用并没有什么特殊之处。
内部类
内部类(inner class)定义在另一个类的内部,其中的方法可以访问包含它们的外部类的域。
内部类主要用于设计具有相互协作关系的类集合。
使用内部类的主要原因:
- 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据
- 内部类可以对同一个包中的其他类隐藏起来
- 想定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。
将从以下几部分介绍内部类:
- 简单的内部类,它将访问外围类的实例域
- 内部类的特殊语法规则
- 内部类的内部,探讨如何转换成常规类
- 讨论局部内部类,它可以访问外围作用域中的局部变量
- 介绍匿名内部类,说明Java在lambda表达式之前怎么实现回调的
- 介绍如何将静态内部类嵌套在辅助类中
内部类访问对象状态
内部类的语法比较复杂。
选择一个简单的例子:
public class TalkingClock{ private int interval; private boolean beep; public TalkingClock(int interval, boolean beep) { ... } public void start() { ... } // an inner class public class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { System.out.println(...); if(beep) Toolkit.getDefaultToolkit().beep(); } }}
TimePrinter
类位于TalkingClock
类内部,并不意味着每个TalkingClock
对象都有一个TimePrinter
实例域。
TimePrinter
类没有实例域或者beep
变量,而是引用了外部类的域里的beep
。
其实内部类的对象总有一个隐式引用,它指向了创建它的外部类对象,这个引用在内部类的定义中不可见。
这个引用是在对象创建内部类对象的时候传入的this
,编译器通过内部类的构造器传入到内部类对象的域中。
// 由编译器插入的语句ActionListener listener = new TimePrinter(this);
TimePrinter
类可以声明为私有的,这样只有TalkingClock
方法才能构造TimePrinter
对象。只有内部类可以是私有的,常规类只可以是包可见和公有可见。
内部类的特殊语法规则
使用外围类引用的语法为OuterClass.this
。
例如之前的actionPerformed
方法:
public void actionPerformed(ActionEvent event){ ... if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();}
反过来,可以用`outerObject.new InnerClass(construction parameters)更加明确地编写内部类对象的构造器:
// ActionListener listener = new TimePrinter(this);ActionListener listener = this.new TimePrinter();
通常来说this
限定词是多余的,但是可以通过显式命名将外围类引用设置为其他对象,比如当TimePrinter
是一个公有内部类时,对于任意的语音时钟都可以构造一个TimePrinter
:
TalkingClock jabberer = new TalkingClock(1000, true);TalkingClock.ActionListener listener = jabberer.new TimePrinter();
上面的情况是在外围类的作用域之外,所以引用的方法是OuterClass.InnerClass
。
注意:内部类中声明的所有静态域都必须是final
,因为我们希望一个静态域只有一个实例。不过对于每个外部对象,会分别有一个单独的内部类实例,如果这个域不是final
,它可能就不是唯一的。
局部内部类
如果一个类只在一个方法中创建了对象,可以这个方法中定义局部类。
public void start(){ class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { ... } } ActionListener listener = new TimePrinter(); Timer t = new Timer(interval, listener); t.start();}
局部类不能用public
或private
,它的作用域被限定在生命这个局部类的块中。
但是有非常好的隐蔽性,除了start
方法,没有任何方法知道TimePrinter
类的存在。
由外部方法访问变量
局部类还有一个优点:他们还能访问局部变量,但是这些局部变量必须是final
,即一旦赋值就不会改变。
下面的例子相比之前进行了一些修改,beep
不再是外部类的一个实例域,而是方法传入的参数变量:
public void start(int interval, final boolean beep){ class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { ... if(beep) ...; ... } } ActionListener listener = new TimePrinter(); Timer t = new Timer(interval, listener); t.start();}
先说明一下这里的控制流程:
- 调用
start(int, boolean)
- 调用局部内部类
TimePrinter
的构造器,初始化listener
- 将
listener
引用传递给Timer
构造器 - 定时器
t
开始计时 -
start(int, boolean)
方法结束,此时beep
参数变量不复存在 - 某个时刻
actionPerformed
方法执行if(beep) ...
为了让actionPerformed
正常运行,TimePrinter
类在beep
域释放之前将内部类中要用到的beep
域用start
方法的局部变量beep
进行备份(具体实现方式是编译器给内部类添加了一个final
域用来保存beep
)。
编译器检测对局部变量的访问,为每一个量建立相应的数据域,并将局部变量拷贝到构造器中,以便将这些数据域初始化为局部变量的副本。
至于beep
参数前的final
,是因为局部类的方法只能引用定义为final
的局部变量,从而使得局部变量与局部类中建立的拷贝保持一致。
匿名内部类
假设只创建这个局部类的一个对象,就不必命名了,这种类称为匿名内部类(anonymous inner class)。
public void start(int interval, boolean beep){ ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent event) { ... } }; Timer t = new Timer(interval, listener); t.start();}
这种语法的含义是:创建一个实现AcitonListener
接口的类的新对象,需要实现的方法定义在括号内。
通常的语法格式为:
new SuperType(construction parameters) { methods and data }
SuperType
可以是一个接口,也可以是一个类。
由于构造器必须要有一个名字,所以匿名类不能有构造器,取而代之的是:
- 当
SuperType
是一个超类时,将构造器参数传递给超类构造器 - 当
SuperType
是一个接口时,不能有任何构造参数(括号()
还是要保留的)
构造一个类的新对象,和构造一个扩展这个类的匿名内部类的对象的区别:
Person queen = new Person("Mary");Person count = new Person("Dracula") { ... };
多年来,Java程序员习惯用匿名内部类实现事件监听器和其他回调,如今最好还是使用lambda表达式,比如:
public void start(int interval, boolean beep){ Timer t = new Timer(interval, event -> { ... }); t.start();}
可见,用lambda表达式写会简洁得多。
双括号初始化
如果想构造一个数组列表,并传递到一个方法:
ArrayListfriends = new ArrayList<>();friends.add("Harry");friends.add("Tony");invite(friends);
如果之后都没有再需要这个数组列表,那么最好使用一个匿名列表解决。
invite(new ArrayList() { { add("Harry"); add("Tony"); }};
注意这里的双括号:
- 外层括号建立了
ArrayList
的一个匿名子类 - 内层括号则是一个对象构造块(见第4章)
静态内部类
有时使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类引用外围类对象,为此可以将内部类声明static
,取消产生的引用。
编写一个方法同时计算出最大最小值:
double min = Double.POSITIV_INFINITY;double max = Double.NEGATIVE_INFINITY;for(double v : values){ if (min > v) min = v; if (max < v) max = v;}
然而必须返回两个数值,可以顶一个包含两个值的类Pair
:
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; }}
minmax
方法可以返回一个Pair
类型的对象。
class ArrayAlg{ public static Pair minmax(double[] values) { ... return new Pair(min, max); }}
然后调用ArrayAlg.minmax
获得最大最小值:
Pair p = ArrayAlg.minmax(data);
但是Pair
是一个比较大众化的名字,容易出现名字冲突,解决的方法是将Pair
定义为ArrayAlg
的内部公有类,然后用ArrayAlg.Pair
访问它:
ArrayAlg.Pair p = ArrayAlg.minmax(data);
不过与前面的例子不同,Pair
对象不需要引用任何其他的对象,所以可以把这个内部类声明为static
:
class ArrayAlg{ public static class Pair { ... } ...}
只有内部类可以声明为static
,静态内部类的对象除了没有对生成它的外围类对象的引用特权外,其他与所有内部类完全一样。
在上面的例子中,必须使用静态内部类,这是因为返回的内部类对象是在静态方法minmax
中构造的。
如果没有把Pair
类声明为static
,那么编译器将会给出错误报告:没有可用的隐式ArrayAlg
类型对象初始化内部类对象。
- 注释1:在内部类不需要访问外围类对象时,应该使用静态内部类。
- 注释2:与常规内部类不同,静态内部类可以有静态域和方法。
- 注释3:声明在接口中的内部类自动成为
static
和public
类。
代理
代理(proxy),这是一种实现任意接口的对象。
利用代理可以在运行时创建一个实现了一组给定接口的新类。
这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
对于应用程序设计人员来说,遇到的情况很少,所以先跳过,如果后面有必要再开一个专题进行说明。
Java接口、lambda表达式与内部类总结
- 接口概念、特性
- 接口与抽象类
- 静态方法
- 默认方法
- 解决默认方法冲突
- 接口示例
- lambda表达式
- 函数式接口
- 方法引用
- 构造器引用
- lambda表达式变量总用域
- 内部类
- 局部内部类
- 匿名内部类
- 静态内部类
个人静态博客:
- 气泡的前端日记: