Java核心技术卷一 4. java接口lambda内部类和代理

Posted lovezyu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java核心技术卷一 4. java接口lambda内部类和代理相关的知识,希望对你有一定的参考价值。

接口

接口概念

接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。

如果类遵从某个特定接口,那么久履行这项服务。

public interface Comparable<T>{
    int compareTo(T other);
}

任何实现 Comparable 接口的类都需要包含 compareTo 方法,并且这个方法的参数必须是一个 T 对象,返回一个整形数值。

接口的特点

  • 接口中所有方法自动地属于 public,所以接口的方法不需要提供关键字 public 。
  • 接口中可以定义常量。更多请看接口的特性

接口不能提供的功能

  • 不能含有实例域
  • 不能在接口中实现方法(java 8 之前)

接口与抽象类的区别

  • 可以将接口看成是没有实例域的抽象类,但还是有一定区别。

让类实现一个接口的步骤

  1. 将类声明为实现给定的接口。
  2. 对接口中的所有方法进行定义。
class Employee implements Comparable<Employee> {
    public int compareTo(Employee other){
        return Double.compare(salary, other.salary);
    }
    ...
}

实现类中的特点

  • 实现方法的方法声明为 public ,因为接口中的方法都自动地是 public。
  • 为泛型 Comparable 接口提供一个类型参数,就可以不使用 Object 类型,使得程序省略了强制装换的步骤。

当使用 Array.sort() 方法时,必须实现 Comparable 接口方法,并且元素之间必须是可比较的,不然会报异常:

public class ArrayGood {
    public static void main(String[] args) {
        int[] a = Arrays.copyOf(new int[2], 100);
        System.out.println(a.length);//100

        Employee[] employees = new Employee[10];
        employees = Arrays.copyOf(employees, 100);

        int[] aint = {5, 3, 6, 14, 9, 7, 22, 10};
        System.out.println(Arrays.toString(aint));
        Arrays.sort(aint);
        System.out.println(Arrays.toString(aint));

        Employee[] employees1 = {new Employee("n")
                , new Employee("h")
                , new Employee("e")
                , new Employee("n")
                , new Employee("a")
                , new Employee("r")
                , new Employee("n")
                , new Employee("i")
                , new Employee("m")};
        Arrays.sort(employees1);//java.lang.ClassCastException: Employee cannot be cast to java.lang.Comparable
    }
}

public class Employee {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    Employee(String name){
        this.name = name;
    }

}

实现接口并实现方法后,Employee 数组调用了排序 sort() 方法,实现了排序:

public class ArrayGood {
    public static void main(String[] args) {
        int[] a = Arrays.copyOf(new int[2], 100);
        System.out.println(a.length);//100

        Employee[] employees = new Employee[10];
        employees = Arrays.copyOf(employees, 100);

        int[] aint = {5, 3, 6, 14, 9, 7, 22, 10};
        System.out.println(Arrays.toString(aint));
        Arrays.sort(aint);
        System.out.println(Arrays.toString(aint));

        Employee[] employees1 = {new Employee("n")
                , new Employee("h")
                , new Employee("e")
                , new Employee("n")
                , new Employee("a")
                , new Employee("r")
                , new Employee("n")
                , new Employee("i")
                , new Employee("m")};
        Arrays.sort(employees1);
        for (Employee emp:
             employees1 ) {
            System.out.print(emp.getName() + " ");
        }
        //a e h i m n n n r
    }
}

public class Employee implements Comparable<Employee>{
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    Employee(String name){
        this.name = name;
    }

    @Override
    public int compareTo(Employee o) {
        return name.compareTo(o.name);
        //return Double.compare(double x, double y);
        //return Integer.compare(int x, int y);
    }
}

所以,要让一个类使用排序服务必须让它实现 compareTo 方法。这是理所当然的,因为要向 sort 方法提供对象的比较方法。

疑问:为什么不能在 Employee 类直接提供一个 compareTo 方法,而必须实现 Comparable 接口呢?

解答:原因在于 Java 程序设计语言是一种强类型语言。在调用方法的时候,编译器将会检查这个方法是否存在。调用 compareTo 方法时,sort 传入的 Object 对象会被强制转换为 Comparable 类型,因为只有一个 Comparable 对象才确保有 compareTo 方法。又因为存在这个强制转换,所以类必须还实现 Comparable 接口,这样才可以将 Object 引用的参数转化为一个 Comparable。

反对称规则:如果子类之间的比较含义不一样,那就属于不同类对象的非法比较。每个 compareTo 方法都应该在开始时进行下列检测

if (getClass() != other.getClass()) throw new ClassCastException();

接口的特性

  • 接口不是类,不能使用new实例化

  • 可以声明接口的变量

  • 接口变量必须引用实现了接口的类对象,可以使用instance检查一个对象是否实现了某个特定的接口

java x = new Comparable(...);//ERROR Comparable x;//OK x = new Employee(...)//Employee实现了接口 if(anObject instanceof Comparable) {...}

  • 如果类不实习接口的方法,那么这个类就要定义为抽象类

```java
public interface Named {
String getName();
}

abstract class Student implements Named{

}
```

  • 接口可以被扩展,并且能用 extends 扩展多个接口

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

public interface Powered extends Moveable, Comparable

  • 接口不能包含实例域或静态方法,但可以包含常量,接口中的域自动设为public static final

public interface Powered extends Moveable{ double milesPerGallon(); double SPEED_LIMIT = 95; }

  • 接口只能继承一个超类,却可以实现多个接口

class Employee implements Cloneable, Comparable{ ... }

接口与抽象类

疑问:为什么引用接口概念,为什么不将 Comparable 直接设计为抽象类。

解答:如果使用抽象类表示通用属性存在一个问题,每个类只能扩展一个类。每个类却可以实现多个接口。

没有多重继承:许多设计语言允许一个类有多个超类,如 C++。而 Java 没有多继承是因为它会让语言本身变得非常复杂,降低效率。

静态方法

Java SE 8 中,允许在接口中增加静态方法。这有违接口作为抽象规范的初衷。

目前的方法(2018)都是将静态方法放在伴随类中。标准库中的接口和工具类,可能只包含一些工厂方法。

如 Path 接口定义了 Paths 类中的工厂方法,这样一来,Paths 类就不再是必要的了。

默认方法

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

public interface Comparable<T> {
    default int compareTo(T oter){
        return 0;
    }
}

一般情况没有用处,因为方法会被覆盖。

但有时一个接口定义了大量的方法,我们又不需要实现这么多方法,只关心其中一两个方法。在 Java SE 8 中,可以把所有方法声明为默认方法,这些默认方法声明也不做。

public interface MouseListener{
    default void moseClicked(MouseEvent event){}
    default void mosePressed(MouseEvent event){}
    default void moseReleased(MouseEvent event){}
    default void moseEntered(MouseEvent event){}
    default void moseExited(MouseEvent event){}
}

默认方法可以调用任何其他方法:

public interface Collection{
    int size();
    default boolean isEmpty(){
        retrun size()==0;
    }
    ...
}

public class Test implements Collection{
    public static void main(String[] args) {
        Test test = new Test();
        System.out.println(test.size());//0
        System.out.println(test.isEmpty());//true
    }

    @Override
    public int size() {
        return 0;
    }
}

这样实现 Collection 的程序员就不用操心实现isEmpty方法了。

解决默认方法冲突

如果接口中定义了默认方法,然后又在超类或另一个接口中定义了同样的方法,规则如下:

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

来看看接口冲突的场景:

interface Named{
    default String getName() {
        return getClass().getName() + "_" + hashCode();
    }
    ...
}

interface Person {
    default String getName() {
        return getClass().getName() + "_" + hashCode();
    }
    ...
}

class Student implements Person, Named {
    public String getName() {
        return Person.super.getName();
    }
}

当 Student 继承两个接口时,Java 编译器会报告一个错误,让程序员重写有冲突的方法解决二义性。我们使用接口类型.super.接口方法的方法,在两个接口中选择一个方法,解决二义性。

假设 Named 接口没有为 getName 提供默认实现:

interface Named{
    String getName();
}

如果这两个接口有一个提供了实现,那么编译器就会报告错误,让程序员解决二义性。

如果两个接口没有提供默认实现,那么编译器不会报错,程序员实现这个方法即可。不实现他们的方法,则类定义为抽象类。

类优先规则

如果类继承的类和继承的接口有相同的方法,那么接口的默认方法都会被忽略。

接口示例

接口与回调

回调:常见的程序设计模式,可以指出某个特定事件发生时应该采取的动作。

下面程序给出了定时器和监听器的操作行为。定时器启动以后,程序弹出一个消息对话框,并等待用户点击 OK 按钮来终止程序的执行。在程序等待用户操作的同时,每隔 10 秒显示一次当前的时间。

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

        Timer t = new Timer(10000, listener);
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program?");
        t.stop();
    }

    static class TimePrinter implements ActionListener{

        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the time is" + new Date());
        }
    }
}

Comparator 接口

Arrays.sort 的用比较器作为参数,比较器实现了 Comparator 接口的类的实例:

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

如果要比较字符串:

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

具体完成比较时,需要建立一个实例:

LengthComparator comp = new LengthComparator();
if (comp.compare(words[i], words[j]) > 0)) ...

words[i].compareTo(words[j])比较,compare 方法要在比较器对象上调用,而不是在字符串本身上调用。

要对一个数组排序,需要为 Arrays.sort 方法传入一个 LengthComparator 对象:

String[] friends = {"Peter", "Paul", "Mary"};
Arrays.sort(friends, new LengthComparator());

对象克隆

讨论 Cloneable 接口,它指示一个类提供一个安全的 clone 方法。克隆不太常见,细节技术性强,不做深入讨论。

首先,回忆一个包含对象引用的变量建立副本时会发生什么。原变量和副本都是同一个对象的引用:

Employee original = new Employee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(10);//original 也会改变

说明,引用相同的任何一个变量改变都会影响另一个变量。

想要让 copy 的初始状态与 original 相同,但之后他们各自又会有不同的状态,这种情况就要使用 clone 方法:

Employee copy = original.clone();
copy.raiseSalary(10);//original 没有改变

这种拷贝基于 Object 类的 clone 方法属于浅拷贝,如果拷贝的对象的域有子对象,他们之间还是会有联系,改变子对象时,被浅拷贝的对象的子对象也会改变。

通常子对象都是可变的,必须重新定义 clone 方法来建立一个深拷贝,同时克隆所有子对象。

需要确定:

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

选择 1 或 2,类必须:

  1. 实现 Cloneable 接口;
  2. 重新定义 clone 方法,并指定 public 访问修饰符。

Cloneable接口只是个标签接口,不含任何需要实现的方法:

public class Employee implements Cloneable {
    public Employee clone() throws CloneNotSupportedException {
        Employee cloned = (Employee) super.clone();//对象
        cloned.hireDay = (Date) hireDay.clone();//对象子对象
        return cloned;
    }
}

调用super.clone()得到的是当前调用类的副本,而不是父类的副本。根本没有必用调用this.clone(),并且也用不了 this.clone() 因为这里已经重写了。

lambda 表达式

lambda 的好处

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

有些地方,要将一个代码块传递到某个对象,这个代码块会在将来某个时间调用。在 Java 中传递一个代码段并不容易,不能直接传递代码段。Java 是一种面向对象语言,所以必须构造一个对象,这个对象的类型需要有一个方法能包含所需的代码。

Java SE 8 有了好方法来处理代码块。

lambda 表达式的语法

简单的 lambda 表达式:

(String first, String second)
    -> first.length() - second.length()

也可以像写方法一样,把这些代码放在{}中,并包含显式的return语句:

(String first, String second) -> {
    if(first.length() < second.length()) return -1;
    else if(first.length() > second.length()) return 1;
    else return 0;
}

即使没有参数,任然要提供空括号:

() -> {
    for (int i = 100; i >= 0; i--){
        System.out.println(i);
    }
}

如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型:

Comparator<String> comp = (first, second) ->
    first.length() - second.length();
//编译器可以推导出 first 和 second 必然是字符串,
//因为 lambda 表达式将赋给一个字符串比较器。

如果方法只有一个参数,并且类型可以推出省略,那么可以省略参数括号:

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

无需指定 lambda 表达式的返回类型。它的返回类型会有上下文推到得出。

如果一个 lambda 表达式在一个分支返回一个值,另外一些分支不返回值,这是不合法的:

(int x) -> {
    if(x >= 0) return 1;//不合法
}

函数式接口

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

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

lambda 表达式可以转换为接口:

Timer t = new Timer(1000, event -> {
    System.out.println("At the tone, the time is " + new Date());
    Toolkit.getDefaultToolkit().beep();
})

java 没有增强函数类型,所以不能声明函数类型。

方法引用

可能已经有现成的方法而已完成要传递到其他代码的某个动作:

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

System.out::println是一个方法引用,它们是等价的。

主要有3种情况:

  • object::instanceMethod 如,System.out::println等价x -> System.out.println(x)
  • Class::staticMethod 如,Math::pow等价(x, y) -> Math.pow(x, y)
  • Class::instanceMethod 如,String::compareTolgnoreCase等价(x, y) -> x.compareTolgnoreCase(y)

也可以在方法引用中,使用thissuper参数:

super::instanceMethod
this::instanceMethod

构造引用

Person::newPerson构造器的一个引用,用那个构造器取决于上下文

变量作用域

lambda 表达式可以捕获外围作用域中变量的值,lambda 表达式中只能引用变量的值而不能改变变量的值。另外在 lambda 表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。

规则:lambda 表达式中捕获的变量必须实际上是最终变量。

lambda 表达式的体与嵌套块有相同的作用域。

lambda 表达式中的 this 关键字,指创建这个 lambda 表达式的方法的 this 参数。

处理 lambda 表达式

使用 lambda 表达式重点是延迟执行。

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

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

常用的函数式接口:

略 240 页

内部类

内部类是定义在另一个类中的类,使用原因:

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

使用内部类访问对象状态

内部类方法可以访问自身的数据域,也可以访问创建它的外围类对象的数据域,包括私有数据。

public class Outer {
    int num = 10;
    class Inner{
        int count = 20;
        public void print(){
            System.out.println("直接访问外部类属性"+num);
        }
    }
    public void show(){
        System.out.println("外部类。。。");
        System.out.println("在外部类访问内部类属性" + new Inner().count);
        System.out.println("在外部类访问内部类方法:");
        new Inner().print();
    }
    public static void main(String[] args) { 
        new Outer().show();
        System.out.println();
        System.out.println("main。。。");
        System.out.println("在mian中访问内部类属性" + new Outer().new Inner().count);
        System.out.println("在mian中访问内部类方法:");
        new Outer().new Inner().print();
    }
}

内部类的特殊语法规则

内部类访问外部类的复杂形式:Outer.this.属性

外部类访问内部类的形式:Inner inner = new Inner()
其他类访问外部类中的内部类的形式:Outer.Inner inner = new Outer().new Inner()

  • 内部类中声明的静态域必须是 final
  • 内部类不能有 static 方法。也允许有,但只能访问外围类的静态域和方法。

私有内部类

class Out {
    private int age = 12;
     
    private class In {
        public void print() {
            System.out.println(age);
        }
    }
    public void outPrint() {
        new In().print();
    }
}
 
public class Demo {
    public static void main(String[] args) {
        //此方法无效
        /*
        Out.In in = new Out().new In();
        in.print();
        */
        Out out = new Out();
        out.outPrint();
    }
}

如果一个内部类只希望被外部类中的方法操作,那么可以使用private声明内部类。
上面的代码中,我们必须在Out类里面生成In类的对象进行操作,而无法再使用Out.In in = new Out().new In() 生成内部类的对象。
也就是说,此时的内部类只有外部类可控制;如同是,我的心脏只能由我的身体控制,其他人无法直接访问它。

局部内部类

局部内部类可以对外界完美的隐藏起来。除了 Print 方法没人知道这个内部类。

我们将内部类移到了外部类的方法中,然后在外部类的方法中再生成一个内部类对象去调用内部类方法,这就是局部内部类。

class Out {
    private int age = 12;
 
    public void Print(final int x) {
        class In {
            public void inPrint() {
                System.out.println(x);
                System.out.println(age);
            }
        }
        new In().inPrint();
    }
}
 
public class Demo {
    public static void main(String[] args) {
        Out out = new Out();
        out.Print(3);
    }
}

由外部方法访问变量

局部内部类,不仅能够访问包含他们的外部类,还可以访问局部变量,但必须被声明为 final。

匿名内部类

加入只创建这个类的一个对象,就不必命名了,这种了被称为匿名内部类。

类名可以是一个接口,于是内部类就是实现这个接口;也可以是一个类,于是内部类就是对这个类扩展。

abstract class Person {
    public abstract void eat();
}
 
public class Demo {
    public static void main(String[] args) {
        Person p = new Person() {
            public void eat() {
                System.out.println("eat something");
            }
        };
        p.eat();
    }
}
  • 匿名类没有类名,所以类名没有构造器。
  • 构造器参数会传递给超类构造器。内部类实现接口的时候,不能有任何构造器。
  • 构造参数后面加个{}就代表是匿名内部类。

静态内部类

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

如果用 static 将内部内静态化,那么内部类就只能访问外部类的静态成员变量,具有局限性。

其次,因为内部类被静态化,因此Out.In可以当做一个整体看,可以直接new出内部类的对象(通过类名访问static,生不生成外部类对象都没关系)。

class Out {
    private static int age = 12;
    //静态内部类
    static class In {
        public void print() {
            System.out.println(age);
        }
    }
}
 
public class Demo {
    public static void main(String[] args) {
        Out.In in = new Out.In();
        in.print();
    }
}
  • 只有内部类可以声明为 static 。
  • 内部类只能访问外围类的静态成员变量,具有局限性。
  • 静态内部类可以有静态域和方法。
  • 声明在接口中的内部类自动成为public static类。

代理

利用代理可以在运行时创建一个实现了一组给定接口的新类。

只有在编译时无法确定需要实现那个接口时才有必要使用。

何时使用代理

有一个便是接口的 Class 对象,要想构造一个实现这些接口的类,需要使用 newInstance 方法或反射找出这个类的构造器。但是,不能实例化一个接口,需要在程序处于运行状态时定义一个新类。

代理类可以在运行时创建全新的类。这样代理类能够实现指定的接口:

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

但不能运行时定义这些方法的新代码,而要提供一个调用处理器。

调用处理器是实现了 InvocationHandler 接口的类对象,只有一个方法:

Object invoke(Object proxy, Method method, Object[] args)

创建代理对象

使用 Proxy 类的 newProxyInstance 方法创建一个代理对象,它有三个参数:

  • 一个类加载器。可以使用不同的类加载器,null 表示使用默认的类加载器。
  • 一个Class对象数组,每个元素都是需要实现的接口。
  • 一个调用处理器。现了 InvocationHandler 接口的类对象

使用代理的原因:

  • 路由对远程服务器的方法调用。
  • 在程序运行期间,将用户接口事件与动作关联起来。
  • 为调试、跟踪方法调用。

使用代理和调用处理器跟踪方法调用,并且定义了一个 TraceHander 包装器类存储包装的对象。其中的 invoke 方法打印出被调用方法的名字和参数,随后用包装好的对象作为隐式参数调用这个方法:

public class TraceHandler implements InvocationHandler{
    private Object target;
    public TraceHandler(Object t){
        target = t;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.print(target);
        System.out.print("." + method.getName() + "(");
        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(")");

        return method.invoke(target, args);
    }

    public static void main(String[] args) {
        Object[] elements = new Object[100];

        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;
        }
        Integer key = new Random().nextInt(elements.length) + 1;//随机生成一个数 [1, i + 1]
        int result = Arrays.binarySearch(elements, key);//二分搜索法来搜索指定数组,以获得指定对象的位置
        if (result >= 0) System.out.println(elements[result]);

    }
}

只要 proxy 调用了某个方法,这个方法 method 的名字和参数 args 就会打印出来。

代理的特性

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

  • 代理类都扩展于 Proxy 类。一个代理类只有一个实例域(调用处理器)。任何附加数据都必须存储在调用处理器中。

  • 代理类都覆盖了 Object 类中的方法。覆盖的方法,仅仅调用了调用处理器的 invoke。有些没有被重写定义,如 clone 和 getClass。

  • 没有定义代理类的名字,虚拟机将会自动生成一个类名。

  • 特定的类加载器和预设的一组接口,只能有一个代理类。调用两次 newProxyInstance 方法也只能得到同一个类的两个对象,利用 getProxyClass 方法可以获得这个类:

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

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

java System.out.println(Proxy.isProxyClass(elements[3].getClass()));











以上是关于Java核心技术卷一 4. java接口lambda内部类和代理的主要内容,如果未能解决你的问题,请参考以下文章

关于JAVA核心技术(卷一)读后的思考(接口的基本知识的讨论)

java 核心技术卷一笔记 6 .1.接口 lambda 表达式 内部类

java 核心技术卷一笔记 6 .1接口 lambda 表达式 内部类

Java核心技术卷一笔记7

java 核心技术卷一笔记 6 .2接口 lambda 表达式 内部类

java核心技术卷一