【阅读笔记】Java核心技术卷一 #4

6 接口、lambda 表达式与内部类

6.1 接口

6.1.1 接口概念

接口绝不能含有实例域;但在接口中可以定义常量,被自动设为 public static final

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

  • compareTo() 的例子

需被比较的类要实现 Comparable 接口,然后将对象数组传入Arrays.sort();

6.1.2 接口的特性

可以使用 instanceof 检查一个对象是否实现了某个特定的接口

6.1.3 接口与抽象类(略)

6.1.4 静态方法

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

6.1.5 默认方法

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

6.1.6 解决默认方法冲突

如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法。Java 解决这种二义性的规则:

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

6.2 接口示例

6.2.1 接口与回调(略)

6.2.2 Comparator 接口

public interface Comparator<T>
{
    int compare(T first, T second);
}

可以把对象数组和一个实现了 Comparator 接口的比较器类传入 Arrays.sort();

6.2.3 对象克隆

Cloneable 接口只是一个标记,指示类设计者了解克隆过程;
它唯一的作用就是允许在类型查询中使用 instanceof;
它没有指定 clone 方法,这个protected 的 clone 方法是从 Object 类继承的;
必须重新定义 clone 为 public 才能允许所有方法来克隆对象。

浅拷贝会有什么影响吗?如果原对象和浅克隆对象共享的子对象是不可变(immutable)的,那么这种共享就是安全的。如StringLocalDate

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

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

6.3 lambda 表达式

6.3.1 为什么引入lambda表达式

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

6.3.2 lambda表达式的语法

即使 lambda 表达式没有参数,仍然要提供空括号,就像无参数方法一样;
如果可以推导出一个 lambda 表达式的参数类型 ,则可以忽略其类型;
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号;
无需指定 lambda 表达式的返回类型,lambda 表达式的返回类型总是会由上下文推导得出。

6.3.3 函数式接口

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口称为函数式接口。
实际上,在 Java 中,对 lambda 表达式所能做的也只是能转换为函数式接口。

Comparator<String> comp = (first, second) -> first.length() - second.length();

6.3.4 方法引用

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

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

在前 2 种情况中,方法引用等价于提供方法参数的 lambda 表达式;
对于第 3 种情况,第 1 个参数会成为方法的目标:
String::compareToIgnoreCase等同于(x, y) -> x.compareToIgnoreCase(y)

如果有多个同名的重栽方法,编译器就会尝试从上下文中找出你指的那一个方法;
类似于 lambda 表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。

可以在方法引用中使用 this 和 super

6.3.5 构造器引用

构造器引用与方法引用很类似,只不过方法名为 new。例如,Person::new 是 Person 构造器的一个引用;

Java 有一个限制,无法构造泛型类型 T 的数组。表达式new T[n]会产生错误,因为这会被擦除为new Object[n]
数组构造器引用对于克服这个限制很有用:

Object[] people = stream.toArray();
Person[] people = stream.toArray(Person[]::new);

6.3.6 变量作用域

lambda 表达式可以捕获外围作用域中变量的值。

在 Java 中,为了确保所捕获的值是明确定义的,有一个重要的限制:在 lambda 表达式中,不能改变引用变量的值;
另外如果在 lambda 表达式中引用变量,而这个变量可能在外部改变,这也是不合法的;

规则:lambda 表达式中捕获的变量必须是“实际上final”的(effectively final)。
实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。

在一个 lambda 表达式中使用 this 关键字时,是指创建这个 lambda 表达式的方法的 this 参数(方法所属的对象)。

6.3.7 处理lambda表达式

要接受一个 lambda 表达式,需要选择(偶尔可能需要提供)一个函数式接口。
调用这个函数式接口的抽象方法可以执行这个 lambda 表达式的主体。例如:

/// IntConsumer.java
public interface IntConsumer {
    void accept(int value);
}
/// Test.java
public class Test {
    public static void repeat(int n, IntConsumer action) {
        for (int i = 0; i < n; i++) action.accept(i);
    }

    public static void main(String[] args) {
        int n = 10;
        Test5.repeat(n, x -> System.out.println(n - x));
    }
}
  • 常用函数数接口 见P240表6-1
  • 基本类型的函数式接口规范(提供函数式接口时,最好使用这些特殊化规范来减少自动装箱) 见P241表6-2

6.3.8 再谈 Comparator

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

  • 如:comparingthenComparingnaturalOrderreverseOrder

comparing 方法取一个“键提取器”函数,它将类型 T 映射为一个可比较的类型(如 String )。对要比较的对象应用这个函数,然后对返回的键完成比较。

naturalOrder 方法可以为任何实现了 Comparable 的类建立一个自然顺序比较器。naturalOrder().reversed() 等同于 reverseOrder()。

还有nullsFirstnullsLast适配器,会修改现有的比较器,从而在键函数返回 null 值时不会抛出异常,而是将这个值标记为小于或大于正常值。

示例:

Arrays.sort(people, Comparator.comparing(Person::getName));
Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
Arrays.sort(people, Comparator.comparing(Person::getName, (s, t) -> Integer.compare(s.length(), t.length())));
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));
Arrays.sort(people, comparing(Person::getMiddleName, nullsFirst(naturalOrder())));

6.4 内部类

内部类的作用:

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

C++ 的嵌套类:嵌套是一种类之间的关系,而不是对象之间的关系;嵌套类有两个好处:命名控制和访问控制。
Java 内部类的对象还有一个隐式引用,它引用了实例化该内部对象的外围类对象。通过这个指针,可以访问外围类对象的全部状态。
Java 中,static 内部类没有这种附加指针,这样的内部类与 C++ 中的嵌套类很相似。

6.4.1 使用内部类访问对象状态

外围类的引用在构造器中设置。编译器修改了内部类的所有构造器,添加一个外围类引用的参数。

只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。

6.4.2 内部类的特殊语法规则

表达式 OuterClass.this 显示表示外围类对象的引用;

在外围类的作用域之外,可以这样引用内部类:OuterClass.InnerClass;

在外围类的 static 方法中,或者在其作用域之外,创建内部类需要外围类的对象的引用:

/// 普通成员方法中
ActionListener listener = /*this.*/new TimePrinter();
/// 其他作用域
TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

示例代码中 TimePrinter 是 TalkingClock 的内部类,下同。

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

编译器为了引用外围类,会生成了一个附加的实例域 this$0(名字 this$0 是由编译器合成的,在自己编写的代码中不能够引用它)。

public class TalkingClock$TimePrinter
{
    public TalkingClock$TimePrinter(TalkingClock);
    public void actionPerformed(java.awt.event.ActionEvent);
    final TalkingClock this$0;
}

若内部类需要访问外围类的私有域,则编译器给外围类生成一个静态方法:
这个静态方法接受一个外围类对象的引用,并返回其对应的域。
这样内部类对外部类私有域的访问实际上转化为对该静态方法的调用。

/// 外部类的部分代码示例
class TalkingClock
{
    private int interval;
    private boolean beep; // 待访问的域

    public TalkingClock(int, boolean);
    static boolean access$0(TalkingClock);  // 编译器生成的静态方法
    public void start();
}
/// 以下是内部类对 外围类私有域 beep 的访问
if (beep)   // 也等同于 if(TalkingClock.this.beep)
/// 以下是内部类 实际上的访问方式
/// outer 仅方便表示 外部类对象的引用
if (TalkingClock.access$0(outer))

风险在于:任何人都可以通过调用 access$0 读取到私有域 beep。
当然, access$0 不是 Java 的合法方法名,但熟悉类文件结构的黑客可以使用十六进制编辑器轻松地创建一个用虚拟机指令调用那个方法的类文件。
由于秘密地访问方法需要拥有包可见性,所以攻击代码需要与被攻击类放在同一个包中。
不过,程序员不可能无意之中就获得对类的访问权限,而必须刻意地构建或修改类文件才有可能达到这个目的。

6.4.4 局部内部类

可以在一个方法中定义局部类。局部类不能用访问限定符(如 public 或 private 等)进行声明。它的作用域被限定在声明这个局部类的块中。

局部类有一个优势:对外部世界可以完全地隐藏起来 。

6.4.5 访问外部方法的变量

中文版此节标题为“由外部方法访问变量”,英文版标题为"Accessing Variables from Outer Methods"。感觉中文翻译得不太合适

与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类,还可以访问局部变量。 不过,那些局部变量必须effectively final

effectively final意味着它们一旦赋值就绝不会改变。在 JavaSE 8 之前,必须把从局部类访问的局部变量声明为 final。

编译器必须检测对局部变量的访问,为每一个变量建立相应的数据域,并将局部变量拷贝到构造器中,以便将这些数据域初始化为局部变量的副本。

6.4.6 匿名内部类

若只需要创建局部内部类的一个对象,可以不命名,称为 匿名(anonymous)内部类。

// SuperType 可以是接口,也可以是类
new SuperType(construction parameters)
{
    inner class methods and data
}

由于构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类不能有构造器。
取而代之的是,将构造器参数传递给超类构造器。内部类实现接口的时候,不能有任何构造参数。

  • equals 的问题

生成日志或调试消息时 , 通常希望包含当前类的类名 , 如 :
System.err.println("Something awful happened in " + getClass());
不过,这对于静态方法不奏效。毕竟,调用 getClass 时调用的是 this.getClass(),而静态方法没有 this。
所以应该使用以下表达式 :
new Object(){}.getClass().getEnclosingClass() // gets class of static method
在这里,new Object(){} 会建立 Object 的一个匿名子类的一个匿名对象, getEnclosingClass则得到其外围类,也就是包含这个静态方法的类。

6.4.7 静态内部类

有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。
为此,可以将内部类声明为 static,以便取消产生的引用。
静态内部类的对象没有对生成它的外围类对象的引用,除此之外静态内部类与其他内部类完全一样。

只有内部类可以声明为 static。

  • 在内部类不需要访问外围类对象的时候,应该使用静态内部类。有些程序员用嵌套类(nested class)描述静态内部类。
  • 与常规内部类不同,静态内部类可以有静态域和方法
  • 声明在接口中的内部类自动成为 static 和 public 的。

6.5 代理

6.5.1 何时使用代理

假如你要构造一个实现一个或多个接口的类的对象,这个类的确切性质在编译时可能并不知道。

代理类可以在运行时创建全新的类。这样的代理类实现了指定的接口。代理类具有下列方法:

  • 指定接口所需要的全部方法
  • Object类中的全部方法

调用处理器是实现了InvocationHandler接口的对象,这个接口中只有一个方法:
Object invoke(Object proxy, Method method, Object[] args)

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

class TraceHandler implements InvocationHandler {
    private Object target;

    public TraceHandler(Object t) {
        target = t;
    }

    public Object invoke(Object proxy, Method m, Object[] args)
            throws Throwable {
        // print method name and parameters
        ...
        // invoke actual method
        return m.invoke(target, args);
    }
}

6.5.2 创建代理对象

Proxy 类的 newProxylnstance 方法

  • 一个类加栽器(classloader)
  • 一个 Class 对象数组,每个元素都是需要实现的接口
  • 一个调用处理器

6.5.3 代理类的特性

  • 所有的代理类都扩展于 Proxy 类。一个代理类只有一个实例域——调用处理器,它定义在 Proxy 的超类中。

  • 所有的代理类都覆盖了 Object 类中的方法 toString、 equals 和 hashCode。
    如同所有的代理方法一样,这些方法仅仅调用了调用处理器的 invoke。
    Object 类中的其他方法(如 clone 和 getClass)没有被重新定义。

  • 对于特定的类加载器和预设的一组接口来说,只能有一个代理类。
    也就是说,如果使用同一个类加载器和接口数组调用两次 newProxylustance 方法的话,那么只能够得到同一个类的两个对象,
    也可以利用 getProxyClass 方法获得这个类。
    Class proxyClass = Proxy.getProxyClass(null, interfaces);

  • 代理类一定是 public 和 final。
    如果代理类实现的所有接口都是 public,代理类就不属于某个特定的包;
    否则,所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包 。
    可以通过调用 Proxy 类中的 isProxyClass 方法检测一个特定的 Class 类的对象是否代表一个代理类。

猜你喜欢

转载自www.cnblogs.com/caophoenix/p/12523335.html