六、接口,lambda表达式和内部类

一、接口

  在 JavaSE 5.0 中,Comparable 接口已经改进为泛型类型。

public interface Comparable { int compareTo(T other) ; // parameter has type T } 例如,在实现 Comparable 接口的类中, 必须提供下列方法 int compareTo(Employee other)

  接口中的所有方法自动地属于 public。 因此,在接口中声明方法时,不必提供关键字 public。

 在接口中可以定义常量,绝对不可以含有实例域。

  提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此, 可以将接口看成 是没有实例域的抽象类。

   现在, 假设希望使用 Arrays 类的 sort 方法对 Employee 对象数组进行排序, Employee 类 就必须实现 Comparable 接口。 为了让类实现一个接口, 通常需要下面两个步骤:

  为了让类实现一个接口, 通常需要下面两个步骤:

1 ) 将类声明为实现给定的接口。

2 ) 对接口中的所有方法进行定义。

要将类声明为实现某个接口, 需要使用关键字 implements:

public class A implements Comarable{
}

  在实现接口时,必须把方法声明为public

 例子:

class Employee implements Comparable<Emplyee>
{
    public int compareTo(Employee other){
        return Double.equals(salary,other.salary);
}
}

  语 言 标 准 规 定:对 于 任 意 的 x 和 y, 实 现 必 须 能 够 保 证 sgn(x.compareTo(y)) = -sgn (y.compareTo(x)。) (也 就 是 说, 如 果 y.compareTo(x) 抛 出 一 个 异 常, 那 么 x.compareTo (y) 也 应 该 抛 出 一 个 异 常。)这 里 的“ sgn” 是 一 个 数 值 的 符 号:如 果 n 是 负 值, sgn(n) 等 于 -1 ; 如 果 n 是 0, sgn(n) 等 于 0 ; 如 果 n 是 正 值, sgn(n)等 于 1 .简 单 地 讲, 如 果 调 换 compareTo 的 参 数, 结 果 的 符 号 也 应 该 调 换 (而 不 是 实 际 值).

   符合反对称的规则,两种方式:

  •   如 果 子 类 之 间 的 比 较 含 义 不 一 样, 那 就 属 于 不 同 类 对 象 的 非 法 比 较。
  • 如 果 存 在 这 样 一 种 通 用 算 法, 它 能 够 对 两 个 不 同 的 子 类 对 象 进 行 比 较, 则 应 该 在 超 类 中 提 供 一 个 compareTo 方 法,并 将 这 个 方 法 声 明 为 final 。

接口特性

  •   接口不是类,尤其不能使用 new 运算符实例化一个接口
  •   尽管不能构造接口的对象,却能声明接口的变量。接口变量必须弓I用实现了接口的类对象。

    如同使用 instanceof检查一个对象是否属于某个特定类一样, 也可以使用 instance 检查一个对象是否实现了某个特定的接口

if(anObject instanceof Comparable)

 与可以建立类的继承关系一样,接口也可以被扩展.例:

public interface Moveable
{
void move(double x, double y);
}

public interface Powered extends Moveable
{
double milesPerCallon();
}

  虽然在接口中不能包含实例域或静态方法,但却可以包含常量。与接口中的方法都自动地被设置为 public —样,接口中的域将被自动设为 public static final。

  尽管每个类只能够拥有一个超类, 但却可以实现多个接口。

  例如,Java 程序设计语言有一个非常重要的内置接口,称为 Cloneable 。 如果某个类实现了这个 Cloneable 接口,Object 类中的 clone 方 法就可以创建类对象的一个拷贝。如果希望自己设计的类拥有克隆和比较的能力,只要实现 这两个接口就可以了: 使用逗号将实现的各个接口分隔开。

 class Employee implements Cloneable, Comparable

  静态方法

  在 Java SE 8 中,允许在接口中增加静态方法。

例:

public interface Path
{
public static Path get(String first, String... more) {
  return Fi1eSystems.getDefault().getPath(first, more);}
}

 默认方法

  可以为接口方法提供一个默认实现。 必须用 default 修饰符标记这样一个方法。

public interface Comparable<T>
{
    default int compareTo(T other){
        //some code
    }
}

     在 JavaAPI 中,你会看到很多接口都有相应的伴随类,这个伴随类中实现了相应接 口 的部分或所有方法,如 Collection/AbstractCollection 或 MouseListener/MouseAdapter。在 JavaSE 8 中, 这个技术已经过时。现在可以直接在接口中实现方法。

   默认方法的一个重要用法是“‘ 接口演化” 。例:

public class Bag implements Collection

      后来, 在 JavaSE 8 中, 又为这个Collection接口增加了一个 stream 方法。 假设 stream 方法不是一个默认方法。那么 Bag 类将不能编译, 因为它没有实现这个新方 法。为接口增加一个非默认方法不能保证“ 源代码兼容“。将方法实现为一个默认方法就可以解决这两个问题。Bag 类又能正常编译了。

解决默认方法冲突

  如果先在一个接口中将一个方法定义为默认方法, 然后又在超类或另一个接口中定义了 同样的方法, 会发生什么情况? 诸如 Scala 和 C++ 等语言对于解决这种二义性有一些复杂的 规则。幸运的是,Java 的相应规则要简单得多。规则如下:

  1 ) 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会 被忽略。

  2 ) 接口冲突。 如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且 参数类型(不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突。

  千万不要让一个默认方法重新定义 Object 类中的某个方法。

二、接口示例

接口与回调

  回调( callback) 是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发 生时应该采取的动作。

 例(定时器):

定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了java.awt. event 包的 ActionListener 接口。下面是这个接口:

public interface ActionListener{
    void actionPerformed(ActionEvent event);
}

当到达指定的时间间隔时,定时器就调用 actionPerformed 方法。

package timer;

/**
   @version 1.01 2015-05-12
   @author Cay Horstmann
*/

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer; 
// to resolve conflict with java.util.Timer

public class TimerTest
{  
   public static void main(String[] args)
   {  
      ActionListener listener = new TimePrinter();

      // 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 TimePrinter implements ActionListener
{  
   public void actionPerformed(ActionEvent event)
   {  
      System.out.println("At the tone, the time is " + new Date());
      Toolkit.getDefaultToolkit().beep();
   }
}

Comparator接口

  可以对一个字符串数组排序, 因为 String 类实现了 Comparable<String>。现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。 肯定不能让 String 类用两种不同的方式实现 compareTo 方法. 要处理这种情况,ArrayS.Sort() 方法还有第二个版本, 有一个数组和一个比较器 ( comparator ) 作为参数, 比较器是实现了 Comparator 接口的类的实例。

例:

package com;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;

/**
 * This program uses big numbers to compute the odds of winning the grand prize in a lottery.
 * @version <b>1.20</b> 2004-02-10
 * @author Wang
 */
public class BigIntegerTest
{
	/**闲来无事
	 * @param a
	 * @param b
	 * @return int
	 * @throws IOexception
	 * @deprecated Use others
	 * @see "Hello"
	 * 
	 */
	BigIntegerTest(){
		
	}
   public static void main(String[] args)
   {
	   Comparator<String> comp=new LengthComparator();
	   String[] friends={"AZX0,","SDS"};
	   Arrays.sort(friends,comp);
	   String str=Arrays.toString(friends);
	   System.out.println(str);
	     }
   
}
class LengthComparator implements Comparator<String>
{
	@Override
	public int compare(String first,String second) {
		return first.length()-second.length();
	}

	
}

对象克隆

  论 Cloneable 接口,这个接口指示一个类提供了一个安全的 clone 方法。clone 方法是 Object 的一个 protected 方法。默认的克隆操作 是“ 浅拷贝”,并没有克隆对象中引用的其他对象。浅拷贝会有什么影响吗? 这要看具体情况。如果原对象和浅克隆对象共享的子对象是不 可变的, 那么这种共享就是安全的。不过, 通常子对象都是可变的, 必须重新定义 clone 方法来建立一个深拷贝, 同时克隆 所有子对象。对于每一个类,需要确定:

1 ) 默认的 clone 方法是否满足要求;

2 ) 是否可以在可变的子对象上调用 clone 来修补默认的 clone 方法;

3 ) 是否不该使用 clone ()实际上第 3 个选项是默认选项。如果选择第 1 项或第 2 项,类必须:

1 ) 实现 Cloneable 接口;

2 ) 重新定义 clone 方法,并指定 public 访问修饰符。

  Object 类中 clone 方法声明为 protected , 所以你的代码不能直接调用 anObject. clone() 但是, 不是所有子类都能访问受保护方法吗? 不是所有类都是 Object 的子类 吗? 幸运的是, 受保护访问的规则比较微妙。子类只能调用受保护的 clone 方法来克隆它自己的对象。 必须重新定义 clone 为 public 才能允许所有方法克隆对象。

   Cloneable 接口是 Java 提供的一组标记接口 ( tagging interface ) 之一。(有些程序员 称之为记号接口 ( marker interface))。应该记得,Comparable 等接口的通常用途是确保一个类实现一个或一组特定的方法。标记接口不包含任何方法; 它唯一的作用就是允许 在类型查询中使用 instanceof:

if (obj instanceof Cloneable) .

  即使 clone 的默认(浅拷贝)实现能够满足要求, 还是需要实现 Cloneable 接口, 将 clone 重新定义为 public,再调用 super.clone().

  如果在一个对象上调用 clone, 但这个对象的类并没有实现 Cloneable 接口, Object 类 的 clone 方法就会拋出一个 CloneNotSupportedException。

例:

package clone;

import java.util.Date;
import java.util.GregorianCalendar;

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;
      hireDay = new Date();
   }

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

   public String toString()
   {
      return "Employee[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]";
   }
}


package clone;

/**
 * This program demonstrates cloning.
 * @version 1.10 2002-07-01
 * @author Cay Horstmann
 */
public class CloneTest
{
   public static void main(String[] args)
   {
      try
      {
         Employee original = new Employee("John Q. Public", 50000);
         original.setHireDay(2000, 1, 1);
         Employee copy = original.clone();
         copy.raiseSalary(10);
         copy.setHireDay(2002, 12, 31);
         System.out.println("original=" + original);
         System.out.println("copy=" + copy);
      }
      catch (CloneNotSupportedException e)
      {
         e.printStackTrace();
      }
   }
}

三、lambda表达式

  lambda 表达式是一个可传递的代码块, 可以在以后执行一次或多次。

  是将一个代码块传递到某个对象(一个定时器, 或者一个 sort 方法。) 这个代码块会在将来某个时间调用。 到目前为止,在 Java 中传递一个代码段并不容易, 不能直接传递代码段 。Java 是一种面 向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。

lambda表达式的语法

  lambda 表达式就是一个代码块, 以及必须传人 代码的变量规范。

 Java 中的一种 lambda 表达式形式:参数, 箭头(->) 以及一个表达式。如 果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在 {}中, 并包含显式的 return语句。即使 lambda 表达式没有参数, 仍然要提供空括号,就像无参数方法一样。如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型。例如:

Comparator<String> comp
= (first, second) // Same as (String first, String second)
-> first.lengthO - second.length();

  在这里, 编译器可以推导出 first 和 second 必然是字符串,因为这个 lambda 表达式将赋 给一个字符串比较器。

 如果方法只有一 参数, 而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:

ActionListener listener = event ->
System.out.println("The time is " + new Date()");
// Instead of (event) -> . . . or (ActionEvent event) -> . . .

无需指定 lambda 表达式的返回类型。lambda 表达式的返回类型总是会由上下文推导得 出。

 如果一个 lambda 表达式只在某些分支返回一个值, 而在另外一些分支不返回值, 这是不合法的。例如,(int x)-> { if(x >= 0) return 1; } 就不合法。

函数式接口

  对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达 式。这种接口称为函数式接口.

Arrays.sort (words,
(first, second) -> first.length() - second.length()) ;

在底层, Arrays.sort 方法会接收实现了 Comparator 的某个类的对象。 在这个对象上调用 compare 方法会执行这个 lambda 表达式的体。lambda 表达式可以转换为接口, 这一点让 lambda 表达式很有吸引力。

  甚至不能把lambda 表达式赋给类型为 Object 的变量,Object 不是一个函数式接口。 Java API 在java.util.function 包中定义了很多非常通用的函数式接口。其中一个接口 BiFunction 描述了参数类型为 T 和 U 而且返回类型为 R 的函数。可以把我们的字符 串比较 lambda 表达式保存在这个类型的变量中:

BiFunction<String, String, Integer〉comp
= (first, second) -> first.length() - second.length();

想要用 lambda 表达式做某些处理,还 是要谨记表达式的用途,为它建立一个特定的函数式接口

方法引用

有时, 可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。

Timer t = new Timer(1000, event -> System.out.println(event)):

但是,如果直接把 println 方法传递到 Timer 构造器就更好了。具体做法如下:

Timer t = new Timer(1000, System.out::println);

表达式 System.out::println 是一个方法引用( method reference ), 它等价于 lambda 表达式 x 一> System.out.println(x) .

要用:: 操作符分隔方法名与对象或类名。主要有 3 种情况:

•object::instanceMethod

•Class::staticMethod

•Class::instanceMethod

  在前 2 种情况中,方法引用等价于提供方法参数的 lambda 表达式。前面已经提到, System.out::println 等价于 x -> System.out.println(x) 类似地,Math::pow 等价于(x,y) -> Math.pow(x, y)

  对于第 3 种情况, 第 1 个参数会成为方法的目标。例如,String::compareToIgnoreCase 等 同于 (x, y) -> x.compareToIgnoreCase(y) .

  可以在方法引用中使用 this 参数。例如,this::equals 等同于 x -> this.equals(x)。 使用 super 也是合法的。下面的方法表达式 super::instanceMethod 使用 this 作为目标,会调用给定方法的超类版本.

构造器引用

  构造器引用与方法引用很类似,只不过方法名为 new。例如,Person::new 是 Person 构造 器的一个引用。哪一个构造器呢? 这取决于上下文。

 可以用数组类型建立构造器引用。例如, int[]::new 是一个构造器引用,它有一个参数: 即数组的长度。这等价于 lambda 表达式 x -> new int[x].

  Java 有一个限制,无法构造泛型类型 T 的数组。数组构造器引用对于克服这个限制很有 用。

Person口 people = stream.toArray(Person::new):

toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。

变量作用域

下面来巩固我们对 lambda 表达式的理解 lambda 表达式有 3 个部分:

1 ) 一个代码块;

2 ) 参数;

3 ) 自由变量的值, 这是指非参数而且不在代码中定义的变量。

  关于代码块以及自由变量值有一个术语: 闭包. 如果有人吹嘘他们的语 言有闭包,现在你也可以自信地说 Java 也有闭包。在 Java 中, lambda 表达式就是闭包。可以看到,lambda 表达式可以捕获外围作用域中变量的值。在 lambda 表达式中, 只能引用值不会改变的 变量。

 另外如果在 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: Cannot refer to changing i
};
new Timer(1000, listener).start();
}
}

  lambda 表达式中捕获的变量必须实际上是最终变量 ( effectively final) 实际上的最终变量是指, 这个变量初始化之后就不会再为它赋新值。lambda 表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规 则。在 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表达式

  使用 lambda 表达式的重点是延迟执行.毕竟, 如果想耍立即执行代 码,完全可以直接执行, 而无需把它包装在一lambda 表达式中。之所以希望以后再执行 代码, 这有很多原因, 如:

•在一个单独的线程中运行代码;

•多次运行代码;

•在算法的适当位置运行代码(例如, 排序中的比较操作;)

•发生某种情况时执行代码(如, 点击了一个按钮, 数据到达, 等等;)

•只在必要时才运行代码。

repeat(10, () -> System.out.println("Hello, World!"));

要接受这个 lambda 表达式, 需要选择(偶尔可能需要提供)一个函数式接口。 表 6-1 列 出了 Java API 中提供的最重要的函数式接口。在这里, 我们可以使用 Runnable 接口:

public static void repeat(int n, Runnable action)
{
for (int i = 0; i < n; i++) action.run();
}

 表 6-2 列出了基本类型 int、 long 和 double 的 34 个可能的规范。 最好使用这些特殊 化规范来减少自动装箱。出于这个原因, 我在上一节的例子中使用了 IntConsumer 而不是 Consumer<lnteger〉 。

  如果设计你自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface 注 解来标记这个接口。这样做有两个优点。 如果你无意中增加了另一个非抽象方法, 编译 器会产生一个错误消息。

再谈comparator

Comparator 接口包含很多方便的静态方法来创建比较器。 这些方法可以用于 lambda 表 达式或方法引用

静态 comparing 方法取一个“ 键提取器” 函数, 它将类型 T 映射为一个可比较的类型 ( 如 String )。对要比较的对象应用这个函数, 然后对返回的键完成比较。例如,假设有一个 Person 对象数组,可以如下按名字对这些对象排序:

Arrays.sort(people, Comparator.comparing(Person::getName));

可以把比较器与 thenComparing 方法串起来。例如,

Arrays.sort(people ,
Comparator.comparing(Person::getlastName)
.thenCompararng(Pe rson::getFirstName));

如果两个人的姓相同, 就会使用第二个比较器。 这些方法有很多变体形式。可以为 comparing 和 thenComparing 方法提取的键指定一个 比较器。例如,可以如下根据人名长度完成排序:

Arrays.sort(people, Comparator.comparing(Person::getName,
(s, t) -> Integer.compare(s.1ength(), t.length())));

另外, comparing 和 thenComparing 方法都有变体形式,可以避免 int、 long 或 double 值 的装箱。要完成前一个操作, 还有一种更容易的做法:

Arrays.sort(people, Comparator.comparinglnt(p -> p.getName()-
length()));

  如果键函数可以返回 null, 可 能 就 要 用 到 nullsFirst 和 nullsLast 适配器。这些静态方 法会修改现有的比较器,从而在遇到 null 值时不会  抛出异常, 而是将这个值标记为小于或 大于正常值。例如, 假设一个人没有中名时 getMiddleName 会返回一个 null, 就 可 以 使 用 Comparator.comparing(Person::getMiddleName(), Comparator.nullsFirst(… )。) nullsFirst 方法需要一个比较器,在这里就是比较两个字符串的比较器。naturalOrder 方法可以 为任何实现了 Comparable 的类建立一个比较器。在这里,Comparator. naturalOrderO 正是 我们需要的。下面是一个完整的调用, 可以按可能为 null 的中名进行排序。这里使用了一个静态 导人 java.util.Comparator. ' 以便理解这个表达式。注意 naturalOrder 的类型可以推导得出。

Arrays.sort(people, compari
ng(Person::getMiddleName , nullsFirst(naturalOrder())));

静态 reverseOrder 方法会提供自然顺序的逆序。要让比较器逆序比较, 可以使用 reversed 实例方法.例如 naturalOrder().reversed() 等同于 reverseOrder()

四、内部类

  内部类( inner class) 是定义在另一个类中的类。

写内部类的原因:

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

  假如一个链表类定义了一个存储结点的类和一个定义迭代器位置的类。嵌套是一种类之间的关系, 而不是对象之间的关系。一个 LinkedList 对象并不包含 Iterator 类型或 Link 类型的子对象。

使用内部类访问对象状态

  内部类既可以访问自身的数据域,也 可以访问创建它的外围类对象的数据域., 内部类的对象总有一个隐式引用, 它指向了创建它的外部类对 象。这个引用在内部类的定义中是不可见的。然而, 为了说明这个概念, 我们将外围类对象 的引用称为 outer。外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器, 添加一个外围类 引用的参数。outer 不是 Java 的关键字。我们只是用它说明内部类中的机制。

  当在 start 方法中创建了 TimePrinter 对象后, 编译器就会将 this 引用传递给当前的语音 时钟的构造器:

ActionListener listener=new TimePrinter(this);
package com;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import javax.swing.*;
import javax.swing.Timer;
import java.awt.*;
import java.util.*;
import java.awt.event.*;

/**
 * This program uses big numbers to compute the odds of winning the grand prize in a lottery.
 * @version <b>1.20</b> 2004-02-10
 * @author Wang
 */
public class BigIntegerTest
{
	/**闲来无事
	 * @param a
	 * @param b
	 * @return int
	 * @throws IOexception
	 * @deprecated Use others
	 * @see "Hello"
	 * 
	 */
	private int interval;
	private boolean beep;
	BigIntegerTest(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();
	}
	class TimePrinter implements ActionListener{
		public void actionPerformed(ActionEvent event) {
			System.out.println(new Date());
			if(beep)Toolkit.getDefaultToolkit().beep();
		}
	}
   public static void main(String[] args) throws Exception
   {
	   BigIntegerTest test=new BigIntegerTest(1000, true);
	   test.start();
	   
	   JOptionPane.showMessageDialog(null, "Quit?");
	   System.exit(0);
   }
   
	  
   
}


内部类的特殊语法规则

  使用外围类引用的 正规语法:OuterClass.this 表示外围类引用。

  反过来,可以采用下列语法格式更加明确地编写内部对象的构造器: outerObject.new InnerClass (construction parameters)

  例,如果 TimePrinter 是一个公有内部类,对于任意的语音时钟都可以构造一个 TimePrinter:

TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

  内部类中声明的所有静态域都必须是 final。原因很简单。我们希望一个静态域只 有一个实例, 不过对于每个外部对象, 会分别有一个单独的内部类实例。如果这个域不是 final , 它可能就不是唯一的。内部类不能有 static 方法。

内部类是否有用、必要和安全

  内部类是一种编译器现象, 与虚拟机无 关。编译器将会把内部类翻译成用 $ (美元符号)分隔外部类名与内部类名的常规类文件, 而 虚拟机则对此一无所知。

  可以清楚地看到, 编译器为了引用外围类, 生成了一个附加的实例域 this$0 (名字 this$0 是由编译器合成的,在自己编写的代码中不能够引用它)。由于内部类拥有访问特权, 所以与常规类比较起来功能更加强大。

  内部类如何管理那些额外的访问特权呢?请注意编译器在外围类添加静态方法 access$0。它将返回作为参数传递给它的对象域 beep。

局部内部类

  TimePrinter 这个类名字只在 start 方法中创建这个类型的对象时使用了一次。当遇到这类情况时, 可以在一个方法中定义局部类:

public void start() {
		class TimePrinter implements ActionListener{
			public void actionPerformed(ActionEvent event) {
				System.out.println(new Date());
				if(beep)Toolkit.getDefaultToolkit().beep();
			}
		}
		ActionListener listener=new TimePrinter();
		
		Timer t =new Timer(interval,listener);
		t.start();
	}

  局部类不能用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部 类的块中。 局部类有一个优势, 即对外部世界可以完全地隐藏起来。 即使 TalkingClock 类中的其他 代码也不能访问它。除 start 方法之外, 没有任何方法知道 TimePrinter 类的存在。

由外部方法访问变量

  与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类, 还可以访问局部变量。不过,那些局部变量必须事实上为 final。因此,就使得 局部变量与在局部类内建立的拷贝保持一致。这说明, 它们一旦赋值就绝不 会改变。

public void start(int interval,boolean beep) {
		class TimePrinter implements ActionListener{
			public void actionPerformed(ActionEvent event) {
				System.out.println(new Date());
				if(beep)Toolkit.getDefaultToolkit().beep();
			}
		}
		ActionListener listener=new TimePrinter();
		
		Timer t =new Timer(interval,listener);
		t.start();
	}

  为了能够让 actionPerformed方法工作,TimePrinter 类在 beep 域释放之前将 beep 域用 start 方法的局部变量进行备份.

 当创建一个对象的时候, beep 就会 被传递给构造器,并存储在 val$beep 域中。编译器必须检测对局部变量的访问, 为每一个变量建立相应的数据域, 并将局部变量拷贝到构造器中, 以便将这些数据域初始化为局部变量 的副本.

 匿名内部类

  将局部内部类的使用再深人一步。 假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类。通常的语法格式为:

new SuperType(construction parameters)
{
inner class methods and data
}

  SuperType 可以是 ActionListener 这样的接口, 于是内部类就要实现这个接口。 SuperType 也可以是一个类,于是内部类就要扩展它。 由于构造器的名字必须与类名相同, 而匿名类没有类名,所以,匿名类不能有构造器。 取而代之的是,将构造器参数传递给超类( superclass) 构造器。尤其是在内部类实现接口的 时候, 不能有任何构造参数。不仅如此,还要像下面这样提供一组括号:

new InterfaceType()
{
methods and data
}
public void start(int interval,boolean beep) {
		ActionListener listener=new ActionListener() {
			public void actionPerformed(ActionEvent event) {
				System.out.println(new Date());
				if(beep)Toolkit.getDefaultToolkit().beep();
			}
		};
		
		Timer t =new Timer(interval,listener);
		t.start();
	}

请仔细研究一下,看看构造一个类的新对象与构造一个扩展了那个类的匿名内部类的对 象之间有什么差别:

Person queen = new Person("Mary");
// a Person object
Person count = new Person(
"Dracula") { . . .}
// an object of an inner class extending Person

匿名数组列表:

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

注意这里的双括号。外层括号建立了 ArrayList 的一个匿名子类。内层括号则是一个 对象构造块

静态内部类

  有时候, 使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用 外围类对象。为此,可以将内部类声明为 static, 以便取消产生的引用。与常规内部类不同,静态内部类可以有静态域和方法。声明在接口中的内部类自动成为 static 和 public 类。

package staticInnerClass;

/**
 * This program demonstrates the use of static inner classes.
 * @version 1.02 2015-05-12
 * @author Cay Horstmann
 */
public class StaticInnerClassTest
{
   public static void main(String[] args)
   {
      double[] d = new double[20];
      for (int i = 0; i < d.length; i++)
         d[i] = 100 * Math.random();
      ArrayAlg.Pair p = ArrayAlg.minmax(d);
      System.out.println("min = " + p.getFirst());
      System.out.println("max = " + p.getSecond());
   }
}

class ArrayAlg
{
   /**
    * A pair of floating-point numbers
    */
   public static class Pair
   {
      private double first;
      private double second;

      /**
       * Constructs a pair from two floating-point numbers
       * @param f the first number
       * @param s the second number
       */
      public Pair(double f, double s)
      {
         first = f;
         second = s;
      }

      /**
       * Returns the first number of the pair
       * @return the first number
       */
      public double getFirst()
      {
         return first;
      }

      /**
       * Returns the second number of the pair
       * @return the second number
       */
      public double getSecond()
      {
         return second;
      }
   }

   /**
    * Computes both the minimum and the maximum of an array
    * @param values an array of floating-point numbers
    * @return a pair whose first element is the minimum and whose second element
    * is the maximum
    */
   public static Pair minmax(double[] values)
   {
      double min = Double.POSITIVE_INFINITY;
      double max = Double.NEGATIVE_INFINITY;
      for (double v : values)
      {
         if (min > v) min = v;
         if (max < v) max = v;
      }
      return new Pair(min, max);
   }
}

五、代理

   利用代理可以在运行时创建一个实现了一组给 定接接口 的新类 : 这种功能只有在编译时无法确定需要实现哪个接 口 时才冇必要使用。

  代理类可以在运行时创建全新的类。这样的 代理类能够实现指定的接口。尤其是,它具有下列方法:

  • 指定接口所需要的全部方法。
  • Object 类中的全部方法, 例如, toString、 equals 等。

    然而,不能在运行时定义这些方法的新代码。而是要提供一个调用处理器( invocation handler)。调用处理器是实现了 InvocationHandler 接口的类对象。在这个接口中只有一个方法:Object invoke(Object proxy, Method method, Object ... args)

     无论何时调用代理对象的方法,调用处理器的 invoke 方法都会被调用, 并向其传递 Method 对象和原始的调用参数。 调用处理器必须给出处理调用的方式。

创建代理对象

  要想创建一个代理对象, 需要使用 Proxy 类的 newProxylnstance 方法。 这个方法有三个 参数:

• 一个类加栽器(class loader。) 作为 Java 安全模型的一部分, 对于系统类和从因特网 上下载下来的类,可以使用不同的类加载器。有关类加载器的详细内容将在卷 n 第 9 章中讨论。目前, 用 null 表示使用默认的类加载器。

• 一个 Class 对象数组, 每个元素都是需要实现的接口。

• 一个调用处理器。

  还有两个需要解决的问题。如何定义一个处理器? 能够用结果代理对象做些什么? 当 然, 这两个问题的答案取决于打算使用代理机制解决什么问题。使用代理可能出于很多原 因,例如:

•路由对远程服务器的方法调用。

•在程序运行期间,将用户接口事件与动作关联起来。

•为调试, 跟踪方法调用。

package proxy;

import java.lang.reflect.*;
import java.util.*;

/**
 * This program demonstrates the use of proxies.
 * @version 1.00 2000-04-13
 * @author Cay Horstmann
 */
public class ProxyTest
{
   public static void main(String[] args)
   {
      Object[] elements = new Object[1000];

      // fill elements with proxies for the integers 1 ... 1000
      for (int i = 0; i < elements.length; i++)
      {
         Integer value = i + 1;
         InvocationHandler handler = new TraceHandler(value);
         Object proxy = Proxy.newProxyInstance(null, new Class[] { Comparable.class } , handler);
         elements[i] = proxy;
      }

      // construct a random integer
      Integer key = new Random().nextInt(elements.length) + 1;

      // search for the key
      int result = Arrays.binarySearch(elements, key);

      // print match if found
      if (result >= 0) System.out.println(elements[result]);
   }
}

/**
 * An invocation handler that prints out the method name and parameters, then
 * invokes the original method
 */
class TraceHandler implements InvocationHandler
{
   private Object target;

   /**
    * Constructs a TraceHandler
    * @param t the implicit parameter of the method call
    */
   public TraceHandler(Object t)
   {
      target = t;
   }

   public Object invoke(Object proxy, Method m, Object[] args) throws Throwable
   {
      // print implicit argument
      System.out.print(target);
      // print method name
      System.out.print("." + m.getName() + "(");
      // print explicit arguments
      if (args != null)
      {
         for (int i = 0; i < args.length; i++)
         {
            System.out.print(args[i]);
            if (i < args.length - 1) System.out.print(", ");
         }
      }
      System.out.println(")");

      // invoke actual method
      return m.invoke(target, args);
   }
}

代理类的特性

  代理类是在程序运行过程中创建的。然而, 一旦被创建, 就变成了常规类,与虚拟机中的任何其他 类没有什么区别。没有定义代理类的名字,Sun 虚拟机中的 Proxy类将生成一个以字符串 $Proxy 开头的类名。也可以利用 getProxyClass方法获得这个类:

Class proxyClass = Proxy.getProxyClass(null, interfaces);

  代理类一定是 public 和 final。如果代理类实现的所有接口都是 public, 代理类就不属于 某个特定的包;否则, 所有非公有的接口都必须属于同一个包.

猜你喜欢

转载自blog.csdn.net/qq_39326472/article/details/87637609