核心技术:接口、lambda表达式与内部类

来自Wikioe
跳到导航 跳到搜索


接口

接口(interface),是对类的一组需求描述(一组行为定义):

  1. 接口的所有方法自动属于public(不用提供public关键字);
  2. 接口中可以有常量,但是不能有实例域;
    即,域自动设置为:“public static final”
  3. 接口中可以有实现方法(Java SE 8 之后),但方法中不能引用实例域;
    默认:“public abstract”

提供实例域和方法实现,应该由实现接口的类来完成。

实现接口:

  1. 声明实现给定的接口:
    class Employee implements Comparable
    
  2. 对接口中的所有方法进行定义;
  • 所有方法都必须有实现;
  • 实现方法时,必须把方法声明为“public”;

接口的特性

  1. 接口不是类,不能用“new”进行实例化;
  2. 可以用接口声明变量,变量只能引用接口实现类的对象;
  3. 可以使用“instanceof”检查一个对象是否实现了某个特定接口(类似于检查对象是否属于某个类);
  4. 接口可以被继承,用新的接口扩展旧接口;

接口与抽象类

Java不支持多重继承(multiple inheritance):引入抽象类,避免多重继承的复杂性和低效性。

静态方法

Java 8 之后,允许接口中增加静态方法:

  • 静态方法无法实现多态,形式上重写,实际上是新方法。

默认方法

可以为接口方法提供一个默认实现(也可以不实现),并用“default”修饰该方法:

  1. 可以不用实现接口所有方法,只关注于需要的某个或某几个方法(可以不再使用伴随类):
    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) {}
    }
    
  2. 在为旧接口增加方法时,使用默认方法可以保证“源代码兼容”(source compatible);
    (增加非默认方法,会导致接口现有实现类不能正常工作)


关于伴随类

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

使用抽象类(伴随类)对接口进行部分实现,而后用户类并不之间实现接口(需要实现全部方法),而是继承与该抽象类(只重写需要的类即可)。如:

  1. public interface Collection<E> extends Iterable<E> {
    
    public abstract class AbstractCollection<E> implements Collection<E> {
    
  2. public interface MouseListener extends EventListener {
    
    public abstract class MouseAdapter implements MouseListener, MouseWheelListener, MouseMotionListener {
    
  3. 又见,SpringMVC 中的拦截器(见[1]):
    public interface HandlerInterceptor {
    
    public interface AsyncHandlerInterceptor extends HandlerInterceptor {
    
    public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {
    

解决默认方法冲突

冲突:现在接口中将一个方法定义为默认方法,又在超类或另一个接口中定义了同样的方法;
规则:

  1. 超类优先:接口中的方法(不论是否默认方法)会被忽略;
  2. 接口冲突:一个接口提供默认方法,一个接口提供同名同参数方法(不论是否默认方法),则在实现类必须覆盖这个方法;

接口示例

接口与回调

回调的思想是:

  1. 类A的a()方法调用类B的b()方法
  2. 类B的b()方法执行完毕主动调用类A的callback()方法
  • 回调的核心就是回调方将本身即this传递给调用方


回调与接口:

  1. 将回调方法抽象为接口,用回调方去实现需要的回调接口


关于Java调用

Comparator 接口

  • 若需要将对象数组排序,则必须数组的每一个元素都实现了Comparable 接口,并实现其 compareTo 方法:
    package java.lang;
    import java.util.*;
    
    public interface Comparable<T> {
        public int compareTo(T o);
    }
    


然而,如果需要对 String 数组排序(已经实现了 Comparable<String> 接口),但又不想按照字典顺序排序(String.compareTo 的实现是按字典顺序比较):

  1. 首先我们不能使 String 类实现两种不同的 compareTo 方法,
  2. 其次 String 类也不应该由我们修改。

即:如何使用 Comparable.compareTo 方法之外的比较方法,来进行对象数组的排序?【如何自定义排序】


所以,需要用到:Comparator<T> 接口(不同于 Comparable):

  1. package java.util;
    ...
    public interface Comparator<T> {
        int compare(T o1, T o2);
        ...
    }
    
  • Comparator<T> 接口的方法很多,但并不用全部实现,因为一部分方法有实现,一部分为默认方法;


示例一:对 String 数组自定义排序:

  1. 定义一个 Comparator<String> 接口的实现类:
    class LengthComparator implements Comparator<String>
    {
        public int compare(String first, String second) {
            return first.length() - second.length()
        }
    }
    
  2. 使用 Arrays.sort 进入数组比较:
        String[] friends = { "Peter", "Paul", "Mary" };
        Arrays.sort(friends, new LengthComparator()):
    
    • Arrays.sort 是静态方法,可以类名直接调用;
  • // 对 Arrays 默认排序
    public static <T> void sort(T[] a)
    
    // 对 Arrays 使用自定义排序(Comparator)
    public static <T> void sort(T[] a, Comparator<? super T> c)
    

示例二:单独对两个对象或两个数组元素进行比较:

    Comparator<String> comp = new LengthComparator()
    if (conp.compare(words[i], words[j]) > 0)
        ...
  • Comparator.compare 并非静态方法,所以使用时需要 Comparator 实例;


利用lambda 表达式可以更容易地使用 Comparator

对象克隆

拷贝和克隆

拷贝和克隆.png

  1. 为一个包含对象引用的变量建立副本,直接赋值即可,原件副本都是同一个对象的引用;
  2. 若希望为对象也创建一个副本,则应该使用克隆;

浅拷贝(Object.clone()) 

浅拷贝.png

默认的克隆操作(“Object.clone()”)是“浅拷贝”:如果对象包含子对象的引用,拷贝域会得到相同子对象的另一个引用;

  • 即,原对象域浅克隆对象会引用相同的子对象;
  1. Object.clone() 是Object的一个protected方法;
  2. 若,子对象属于一个不可变的类,或在生命周期中状态不变,则是安全的;否则不应该使用浅拷贝;

深拷贝(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();
    }
    ...
}


深克隆的实现:

  1. 实现 Cloneable 接口,将 clone 重新定义为 public;
  2. 重写 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.length() < second.length()) return -1;
	else if (first.length() > second.length()) return 1;
	else return 0;
}


参数:

  1. 如果没有参数,仍然要保留括号。
    比如:
    () -> { for (int i = 100; i >= 0;i ) System.out.println(i); }
    
  2. 如果可以推导出参数类型,可以忽略参数类型。
    比如:
    Comparator<String> comp
    	= (first, second) // Same as (String first, String second)
    		-> first.length() - second.length();
    
  3. 如果方法只有一个参数,且参数类型可以推导,可以只保留参数名。
    比如:
    ActionListener listener = event ->
    	System.out.println("The time is " + new Date()");
    		// Instead of (event) -> . . . or (ActionEvent event) -> . . .
    

表达式:

  1. 无需指定 lambda 表达式的返回类型。【lambda 表达式的返回类型总是会由上下文推导得出】
    比如:如下表达式可以在 int 类型结果的上下文使用:
    (String first, String second) -> first.length() - second.length()
    
  2. lambda 表达式不能只在某些分支返回值,而存在分支不返回值。
    如下,是不合法的:
    (int x)-> { if (x >= 0) return 1; }
    

函数式接口

函数式接口:有且仅有一个抽象方法,但可以有多个非抽象方法 的接口。(即,接口“只有一个需要实现的方法”)

  • 接口中没有具体实现、没有“public static”修饰的也是抽象方法,因为 接口中的方法默认为“public abstract”(见“接口”一节的描述);
  • Java API 在 java.util.function 包中定义很多非常通用的函数式接口;


需要函数式接口的对象时,可以提供一个 lambda 表达式。【即,用 lambda 表达式来代替函数式接口的抽象方法(“那个唯一需要实现的方法”)】

  • 最好把 lambda 看作是一个函数,而不是一个对象;
  • Java中,lambda所能做的也只是能转换为函数式接口


设计函数式接口时,可以使用@FunctionalInterface注解来标记这个接口。这样做:

  1. 当接口不符合函数式接口定义时(如:无意中增加了另一个抽象方法),编译器会报错;
  2. javaDoc 页中会指示该接口是一个函数式接口;


示例1:

Arrays.sort (words ,
    (first , second) -> first.length() - second.length()) ;
  1. Arrays.sort 方法的第二个参数为 Comparator<String>接口的实现类对象;
  2. 该 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();
    })
  1. Timer 构造函数的第二个参数为 ActionListener接口的实现类对象;
  2. 该 lambda表达式 即为 “ActionListener”接口的“actionPerformed(ActionEvent e)”方法的实现;


示例3:

list.removelf(e -> e == null);
  1. (list 对象属于 ArrayList类)
  2. list.removelf 方法(removeIf(Predicate<? super E> filter))的参数为 “Predicate<T>”接口的实现类对象;
  3. 该 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(stringsString::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(Person::new);
    toArray 方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。

变量作用域

  • 关于“闭包”:闭包就是能够读取其他函数内部变量的函数。如:Java中的lambda,javascript中函数内部的子函数;


lambda 表达式的三个部分:

  1. 一个代码块
  2. 参数
  3. 自由变量的值(指非参数,且不在lambda中定义的变量)


  • 自由变量,能使在 lambda 表达式中访问外围方法或类中的变量:
    public static void repeatMessage(String text, int delay)
    {
       ActionListener listener = event ->
       {
          System.out.println(text):
          Toolkit.getDefaultToolkit().beep() :
       }
       new Timer(delay, listener).start();
    }
    
    调用 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(delay, listener).start();
    }
    
    如上,在 lambda 内改变变量 start 的值,是不合法的;
    public static void repeat(String text , int count)
    {
       for (int i = 1; i <= count; i ++)
       {
          ActionListener listener = event ->
             {
                System.out.print1n(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.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表达式

使用lambda 表达式的重点是延迟执行(deferred execution),如以下情况:

  1. 在一个单独的线程中运行代码;
  2. 多次运行代码;
  3. 在算法的适当位置运行代码(例如,排序中的比较操作);
  4. 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
  5. 只在必要时才运行代码;


使用 lambda 表达式,需要选择(偶尔可能需要提供)一个函数式接口:

常用函数式接口
函数式接口 参数类型 返回类型 抽象方法名 描述 其他方法
Runnable void run 作为无参数或返
Supplier<T> T get 提供一个 T 类型的值
Consumer<T> T void accept 处理一个 T 类型的值 andThen
BiConsumer<T, U> T, U void accept 处理 T 和 U 类型的值 andThen
Function<T, R> T R apply 有一个 T 类型参数的函数 compose, andThen, identity
BiFunction<T, U, R> T, U R apply 有 T 和 U 类型参数的函数 andThen
UnaryOperator<T> T T apply 类型 T 上的一元操作符 compose, andThen, identity
BinaryOperator<T> T,T T apply 类型 T 上的二元操作符 andThen, maxBy, minBy
Predicate<T> T boolean test 布尔值函数 and, or, negate, isEqual
BiPredicate<T, U> T, U boolean test 有两个参数的布尔值函数 and, or, negate


基本类型的函数式接口
函数式接口 参数类型 返回类型 抽象方法名
BooleanSupplier none boolean getAsBoolean
*Supplier none + getAs*
*Consumer + void accept
Obj*Consumer<T> T,+ void accept
*Function<T> + T apply
*To#Function + - applyAs#
To*Function<T> T + applyAs*
To*BiFunction<T,U> T, U + applyAs*
*UnaryOperator + + applyAs*
*BinaryOperator +,+ + applyAs*
*Pedicate + boolean test
  • “+-” 为 int, long, double; “*,#” 为 Integer, Long, Double

再谈 Comparator

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


以下的 comparing()、thenComparing()、nullsFirst()、nullsLast()、naturalOrder()、reverseOrder() 等方法,返回的都是一个比较器Comparator<T>)。
  1. 静态 comparing 方法取一个“键提取器” 函数,它将类型 T 映射为一个可比较的类型(如 String)。对要比较的对象应用这个函数,然后对返回的键完成比较。
    例如:假设有一个 Person 对象数组,可以如下按名字对这些对象排序:
    Arrays.sort(peop1e, Comparator.comparing(Person::getName));
    
  2. 可以把比较器与 thenComparing 方法串起来,如果第一次比较相同则使用下一个比较器。
    例如:
    Arrays.sort(peop1e,
       Comparator.comparing(Person::getLastName)
       .thenComparing(Person::ge FirstName));
    
  3. 可以为 comparing 和 thenComparing 方法提取的键指定一个比较器。
    例如:可以如下根据人名长度完成排序:
    Arrays.sort(peop1e, Comparator.comparing(Person::getName,
       (s, t) -> Integer.compare(s.length(), t.length())));
    
  4. comparing 和 thenComparing 方法都有变体形式,可以避免 int、long 或 double 值的装箱。
    如上例:根据人名长度完成排序,还有一种更容易的做法:
    Arrays.sort(people, Camparator.comparingInt (p -> p.getName().1ength()));
    
  5. 如果键函数可以返回 null,可能就要用到 nullsFirstnullsLast 适配器。这些静态方法会修改现有的比较器,从而在遇到 null 值时不会抛出异常,而是将这个值标记为小于或大于正常值。
    • “nullsFirst”:null < non-null,从小到大时 null 在前;
    • “nullsLast”:null > non-null,从小到大时 null 在后;
    例如,假设一个人没有中名时 getMiddleName 会返回一个 null,就可以使用
    Comparator.comparing(Person::getMiddleName(), Comparator.nullsFirst( ... ))
    
    • nullsFirst、nullsLast 方法需要一个比较器,在这里就是比较两个字符串的比较器。
  6. naturalOrder 方法可以为任何实现了 Comparable 的类建立一个比较器。【自然排序,实现 Comparable 接口时方法作为比较器】
    • 注意 naturalOrder 的类型可以推导得出。
    如上例:可以按可能为 null 的中名进行排序,下面是一个完整的调用:
    Arrays.sort(peop1e, comparing(Person::getMiddleNarne, nullsFirst(naturalOrder())))
    
  7. 静态 reverseOrder 方法会提供自然顺序(naturalOrder)的逆序。
    • 要让比较器逆序比较,可以使用 reversed 实例方法。
      例如:naturalOrder().reversed() 等同于 reverseOrder()

内部类

内部类( inner class ) 是定义在另一个类中的类
使用内部类主要原因:

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


  • 只有内部类可以是私有类(“private class xxx{}”),而常规类只可以具有包可见性(“class xxx{}”),或公有可见性(“public class xxx{}”);

内部类访问外围类的实例域

内部类的对象总有一个隐式引用,它指向了创建它的外部类对象。

  • 通过这个对象,内部类既可以访问自身的数据域,也可以访问创建它的外围类的数据域;
  • 但是除了局部内部类,其他内部类不能访问方法的局部变量;

内部类对象的外围类对象引用.png

内部类的特殊语法规则

内部类的使用:

  • 内部类中的外围类引用为:“outerClass.this”(outerClass为外围类类名)。
  • 在外围类中可以这样构造内部类对象:“outerObject.new InnerClass{construction parameters)”(this可以省略)
    对于公有内部类,任意的外部类对象都可以构造一个内部类;
    TalkingClock jabberer = new Ta1kingClock(1000, true) ;
    TalkingClock.TiiePrinter listener = jabberer.new TimePrinter()
    
  • 在外围类的作用域中,可以用“OuterClass.InnerClass”来引用内部类;
public class TalkingClock 
{
	private int interval:
	private boolean beep;
	
	public TalkingClock(int interval , boolean beep) 
	{
		this,interval = interval;
		this.beep = beep;
	}
	
	public void start() 
	{
		ActionListener listener = new TimePrinter();
		// 或 ActionListener listener = this.new TimePrinter();
		Timer t = new Timer(interval , listener);
		t.start();
	}
	
	public class TimePrinter implements ActionListener
	{
		public void actionPeformed(ActionEvent event) 
		{
			System.out.println("At the tone, the time is " + new Date());
			// 访问外部类的域
			if (beep) Toolkit.getDefaultToolki1().beep();
			// 使用外部类引用
			if (TalkingClock.this,beep) Toolkit.getDefaultToolkitO.beep();
		}
	}
}


内部类的规则:【???说的啥都是】

  • 内部类中声明的所有静态域都必须是 final(即常量);
    如果不为final,则由于每个外部实例都有一个内部类实例,那么静态域可能不唯一(静态域的意义就是多个实例共享一个值)
  • 内部类不能有静态方法(除了静态内部类);
  • 非static的内部类,在外部类加载的时候,并不会加载它,所以它里面不能有静态变量或者静态方法
    因为“static类型的属性和方法,在类加载的时候就会存在于内存中”,“要使用某个类的static属性或者方法,那么这个类必须要加载到jvm中”,如果一个非static的内部类如果具有static的属性或者方法,那么就会出现:内部类未加载(非静态内部类在外围类实例构建之后加载?),但是却试图在内存中创建static的属性和方法,这当然是错误的。

内部类的类文件

  • 内部类是一种编译器现象, 与虚拟机无关。编译器将会把内部类翻译成用“$”分隔外部类名与内部类名的常规类文件。
    如:TalkingClock类内部的TimePrinter类的类文件为“TalkingClock$TimePrinter.class”
public class TalkingClock$TimePrinter
{
   public TalkingGock$TimePrinter(TalkingCtock);   // 构造函数:用外围类构造内部类
   public void actionPerformed(java.awt.event.ActionEvent);   // 内部类方法
   final TalkingClock this$O;   // 外围类引用
}

局部内部类

局部内部类:定义在方法中的内部类:

  • 局部内部类不用 public、private 进行声明;
  • 局部内部类的作用域限定在所在代码块中;
    对代码块外完全隐藏;
public void start()
{
   class TimePrinter implements ActionListener
   {
      public void actionPerforaed(ActionEvent event)
      {
         Systei.out.println("At the tone, the tine is " + new DateO)
         if (beep) Toolkit.getDefaul tToolki10beep():
      }
   }
   ActionListener listener = new TimePrinter();
   Timer t = new Timer(interva1, listener);
   t.start();
}
  • 访问外部方法局部变量:与其他内部类相比较,局部类还有一个优点:它们不仅能够访问包含它们的外部类,还可以访问局部变量:
    编译器会为局部内部类构造具有参数的构造函数和域,用于保存外围方法的局部变量:
    class TalkingClock$ITimePrinter
    {
       TalkingClock$ITinePrinter(TalkingClock, boolean);   // 以外围方法局部变量作为参数的构造函数
       public void actionPerformed(java.awt.event.ActionEvent);   //
       final boolean val$beep;   // 变量:用于存储外围方法的局部变量
       final TalkingClock this$O;   // 外围类引用
    }
    

匿名内部类

匿名内部类(anonymous inner class):没有类名,只用于构造一个该类的实例。形式为:

  • 由于构造器的名字必须与类名相同,而匿名内部类没有类名,所以,匿名内部类不能有构造器
    1. 继承超类的匿名内部类:将构造器参数传给超类构造器;
      new SuperType(construction parameters)
      {
         inner class methods and data
      }
      
    2. 实现接口的匿名内部类:不能有构造参数
      new InterfaceType()
      {
         methods and data
      }
      


如下,创建一个了实现ActionListener 接口的类的新对象,需要实现的方法 actionPerformed 定义在括号内。

public void start(int interval, boolean beep)
{
   ActionListener listener = new ActionListener()
      {
         public void actionPerformed(ActionEvent event)
         {
            System.out.println("At the tone, the time is " + new Date())
            if (beep) Toolkit.getDefaultToolkit().beep();
         }
      }
   Timer t = new Timer(interval, listener);
   t.start()
}


使用匿名内部类与实例化对象:

Person queen = new Person("Mary");   // 这是在构造对象

Person count = new Person("Dracula") { ... }   // 这是在使用匿名内部类


双括号初始化

双括号初始化(double brace initialization):

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

可以被替换为:

invite(new ArrayList<String>0 {{ add("Harry") ; add("Tony") ; }}) ;
  1. 外部大括号用于建立一个ArrayList<String>的匿名内部类;
  2. 内层大括号则是一个初始化块(见[2]);

静态内部类

有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static, 以便取消产生的引用。

  • 只有内部类可以被声明为 static
  • 没有外围类的对象的引用
  • 可以有静态域和静态方法(其他内部类不可以)

代理

参考:动态代理、CGLIB 与 切面编程

如果要为一个没有实现接口的类生成动态代理类,那么可以使用 CGLIB 库。

代理(Proxy):利用代理可以在运行时,创建一个实现了一组给定接口的新类。(动态代理)

  • (静态代理:通过聚合、组合的方式,对需要的类或接口进行前后处理,提供新的调用)
  • 代理类可以在运行时创建全新的类;
  • 只有在编译时无法确定需要实现那个接口时才有必要使用。

创建一个代理对象

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

  1. 类加载器(“ClassLoader”):用 null 表示使用默认的类加载器;(有关类加载器,查阅:卷2第9章)
  2. 接口数组(“Class[]”):一个Class对象数组,每个元素都是需要实现的接口。
  3. 调用处理器(“InvocationHandler”):定义对代理类方法的处理。


  • 调用处理器需要实现 InvocationHandler 接口:仅有一个方法 invoke,用于对代理实例的方法进行处理:
        Object invoke(Object proxy, Method method, Object[] args)
    
  • 代理类属于在运行时定义的类(名字如“$Proxy0”),这个类实现了给定的接口(如“Comparable”),它的方法(如“compareTo”)调用了代理对象处理器的“invoke”方法。

示例

ProxyTest:

package Proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Random;

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);
			
			// 调用 Proxy.newProxyInstance
			// 1、构造实现指定接口(“Comparable”)的代理类的一个新实例
			// 2、所有方法会调用给定处理器对象(“handler”)的 invoke 方法
			Object proxy = Proxy.newProxyInstance(null, new Class[] { Comparable.class }, handler);
			// elements的每个元素都是一个代理类实例
			elements[i] = proxy;
		}
		
		// elements的元素类型为:“class com.sun.proxy.$Proxy0”
		//System.out.println(elements[1].getClass());
		
		// construct a random integer
		// 生成随机数key
		Integer key = new Random().nextInt(elements.length) + 1;

		// search for the key
		// 在elements中使用二分查找key,使用的方法是“binarySearch(Object[] a, Object key)”
		// binarySearch方法会用到,代理类实例所(即每个elements元素)实现的Comparable接口下compareTo方法
		// 而代理类实例的每个方法都会调用 “TraceHandler”的“invoke”(打印内容)
		/* 打印的内容如:
		 * 985.compareTo(973) 
		 * 977.compareTo(973) 
		 * 973.compareTo(973)
		 */
		int result = Arrays.binarySearch(elements, key);

		// print match if found
		if (result >= 0)
			// 调用了代理类实例的“toString()”
			// 而该“toString()”依然会调用 “TraceHandler”的“invoke”
			/* 打印的内容如:1、先调用了 “TraceHandler”的“invoke”,2、toString打印了代理实例的
			 * 973.toString() 
			 * 973
			 */
			System.out.println(elements[result]);
	}
}

TraceHandler:

package Proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

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

	// (所有代理类实例的方法都会调用这个方法)
	// 其中定义了代理对象调用方法时希望执行的动作:打印“target.调用的方法(参数列表)”
	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);
	}
}

代理类的特性

代理类是在程序运行过程中创建的。然而,一旦被创建,就变成了常规类,与虚拟机中的任何其他类没有什么区别。

  • 所有的代理类都扩展于Proxy 类
  • 一个代理类只有一个实例域(调用处理器),它定义在Proxy 的超类中。
  • 代理类所需要的任何附加数据都必须存储在调用处理器中。
  • 所有的代理类都覆盖了Object 类中的方法 toString、equals 和hashCode。如同所有的代理方法一样, 这些方法仅仅调用了调用处理器的invoke。
    Object 类中的其他方法(如 clone 和 getClass ) 没有被重新定义。
  • 没有定义代理类的名字,Sun 虚拟机中的Proxy 类将生成一个以字符串SProxy 开头的类名。
  • 对于特定的类加载器和预设的一组接口来说,只能有一个代理类。
    也就是说,如果使用同一个类加载器和接口数组调用两次newProxylustance 方法的话, 那么只能够得到同一个类的两个对象,也可以利用getProxyClass 方法获得这个类:
    “Class proxyClass = Proxy.getProxyClass(null, interfaces);”
  • 代理类一定是public 和final。
  • 如果代理类实现的所有接口都是public,代理类就不属于某个特定的包;否则, 所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。
  • 可以通过调用Proxy 类中的isProxyClass 方法检测一个特定的Class 对象是否代表一个代理类。



java.Iang.reflect.InvocationHandler

  1. Object invoke(Object proxy,Method method,0bject[]args)
    定义了代理对象调用方法时希望执行的动作。【对代理类对象的方法进行处理】

java.Iang.reflect.Proxy

  1. static Class<?> getProxyClass(Cl assLoader loader, Class<?>...interfaces)
    返回实现指定接口的代理类。【返回代理类】
  2. static Object newProxyInstance(ClassLoader loader, Class<?>[]interfaces, InvocationHandler handler)
    构造实现指定接口的代理类的一个新实例。【返回代理类的实例】
    所有方法会调用给定处理器对象的invoke 方法。
  3. static boolean isProxyClass(Class<?> cl)
    如果cl 是一个代理类则返回true。