Java核心技术笔记 接口、lambda表达式与内部类

《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<T>
{
  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<Employee>
{
  ...
  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<T>
{
  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<T>
{
  int compare(T first, t second);
}

按字符串长度比较,可以定义一个实现 Comparator<String>
的类:

class LengthComparator implements Comparator<String>
{
  public int compare(String first, String second)
  {
    return first.length() - second.length();
  }
}

具体比较时,建立一个实例:

Comparator<String> comp = 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
方法来建立一个深拷贝,同时克隆所有子对象。

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

  1. 默认的 clone
    是否满足要求
  2. 是否可以在可变子对象上调用 clone
    来修补默认 clone
  3. 是否不该使用 clone

实际上第3个选项是默认选项(这句话没有太读懂)。

如果选第1个或者第2个,类必须:

  1. 实现 Cloneable
    接口
  2. 重新定义 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
对象数组,为此要在各个字符串上调用构造器。

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> 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. 一个代码块
  2. 参数
  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");
Comparator<String> comp =
  (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)定义在另一个类的内部,其中的方法可以访问包含它们的外部类的域。

内部类主要用于设计具有相互协作关系的类集合。

使用内部类的主要原因:

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

将从以下几部分介绍内部类:

  1. 简单的内部类,它将访问外围类的实例域
  2. 内部类的特殊语法规则
  3. 内部类的内部,探讨如何转换成常规类
  4. 讨论 局部内部类
    ,它可以访问外围作用域中的局部变量
  5. 介绍 匿名内部类
    ,说明Java在lambda表达式之前怎么实现回调的
  6. 介绍如何将 静态内部类
    嵌套在辅助类中

内部类访问对象状态

内部类的语法比较复杂。

选择一个简单的例子:

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();
}

先说明一下这里的控制流程:

  1. 调用 start(int, boolean)
  2. 调用局部内部类 TimePrinter
    的构造器,初始化 listener
  3. listener
    引用传递给 Timer
    构造器
  4. 定时器 t
    开始计时
  5. start(int, boolean)
    方法结束,此时 beep
    参数变量不复存在
  6. 某个时刻 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表达式写会简洁得多。

双括号初始化

如果想构造一个数组列表,并传递到一个方法:

ArrayList<String> friends = new ArrayList<>();
friends.add("Harry");
friends.add("Tony");
invite(friends);

如果之后都没有再需要这个数组列表,那么最好使用一个匿名列表解决。

invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }};

注意这里的双括号:

ArrayList

静态内部类

有时使用内部类只是 为了把一个类隐藏在另一个类的内部
,并不需要内部类引用外围类对象,为此可以将内部类声明 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表达式变量总用域
  • 内部类
  • 局部内部类
  • 匿名内部类
  • 静态内部类

个人静态博客

  • 气泡的前端日记: https://rheabubbles.github.io

原文 

https://segmentfault.com/a/1190000018068391

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » Java核心技术笔记 接口、lambda表达式与内部类

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址