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

《Java核心技术 卷Ⅰ》 第6章 接口、lambda表达式与内部类前端

  • 接口
  • 接口示例
  • lambda表达式
  • 内部类

接口

接口技术,这种技术主要用来描述类具备什么功能,而并不给出每一个功能的具体实现。一个类能够实现(implement)一个或多个接口,并在须要接口的地方,随时使用实现了相应接口的对象。java

接口概念

在Java程序设计语言中,接口不是类,是对类的一组需求的描述,这些类要听从接口描述的统一格式进行定义。

Arrays类中的sort方法承诺能够对对象数组进行排序,但要求知足下列条件,对象所属的类**必须实现了Comparable接口。git

public interface Comparable
{
  int compareTo(Object other)
}

这就是说,任何实现Comparable接口的类都须要包含compareTo方法,而且这个方法的参数必须是一个Object对象,返回一个整型数值;好比调用x.compareTo(y)时,当x小于y时,返回一个负数;当x等于y时,返回0;不然返回一个正数。程序员

在 Java SE 5中,Comparable接口改进为泛型类型。github

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/CollectionsPath/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方法来克隆它本身的对象

必须从新定义clonepublic,才能容许全部方法克隆对象。

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接口,Objectclone方法就会抛出一个CloneNotSupportedExcptionEmployeeDate类实现了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())。

Employeeclone必定能完成克隆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中已经有不少封装代码块的接口,好比ActionListenerComparator,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)

能够在方法引种中使用thissuper也是合法的,好比super::instanceMethod,使用this做为目标,会调用给定方法的超类版本。

构造器引用

构造器引用与方法引用相似,只不过方法名为new,例如Person::newPerson构造器的一个引用,具体选择Person多个构造器中的哪个,这个取决于上下文。

如今有一个字符串列表,你能够把它转换为一个Person对象数组,为此要在各个字符串上调用构造器。

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

streammapcollect方法会在卷Ⅱ的第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();
}

局部类不能用publicprivate,它的做用域被限定在生命这个局部类的块中。

可是有很是好的隐蔽性,除了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的一个匿名子类
  • 内层括号则是一个对象构造块(见第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:声明在接口中的内部类自动成为staticpublic类。

代理

代理(proxy),这是一种实现任意接口的对象。

利用代理能够在运行时建立一个实现了一组给定接口新类

这种功能只有在编译时没法肯定须要实现哪一个接口时才有必要使用。

对于应用程序设计人员来讲,遇到的状况不多,因此先跳过,若是后面有必要再开一个专题进行说明。

Java接口、lambda表达式与内部类总结

  • 接口概念、特性
  • 接口与抽象类
  • 静态方法
  • 默认方法
  • 解决默认方法冲突
  • 接口示例
  • lambda表达式
  • 函数式接口
  • 方法引用
  • 构造器引用
  • lambda表达式变量总用域
  • 内部类
  • 局部内部类
  • 匿名内部类
  • 静态内部类

我的静态博客:

相关文章
相关标签/搜索