—- 来源: Java核心技术 卷1 基础知识(第十版)—-

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

表达式,这是一种表示可以在将来某个时间点执行的代码块的简洁方法。使用 lambda 表达式, 可以用一种精巧而简洁的方式表示使用回调或变量行为的代码。

内部类(inner class) 机制。理论上讲,内部类有些复杂, 内部类定义在另外一个类的内部, 其中的方法可以访问包含它们的外部类的域。内部类技术主要用于设计具有相互协作关系的类集合。

接口

接口概念

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

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

1.将类声明为实现给定的接口(需要使用关键字implements)

2.对接口中的方法进行定义

  1. public class Order implements Comparable<Order> {
  2. /**
  3. * 价格
  4. */
  5. private Double price;
  6. public Double getPrice() {
  7. return price;
  8. }
  9. public void setPrice(Double price) {
  10. this.price = price;
  11. }
  12. @Override
  13. public int compareTo(Order o) {
  14. return Double.compare(price, o.getPrice());
  15. }

接口特性

1.接口不是类,尤其不能使用 new 运算符实例化一个接口。

2.接口变量必须弓I用实现了接口的类对象。

3.与可以建立类的继承关系一样,接口也可以被扩展。这里允许存在多条从具有较高通用性的接口到较高专用性的接口的链。

接口和抽象类

使用抽象类表示通用属性存在这样一个问题: 每个类只能扩展于一个类。假设 Person 类已经扩展于一个类, 它就不能再扩展第二个类了。

public class A extends Order,Student{//提示报错:Class cannot extend multiple classes
}

但是每个类可以实现多个接口。

public class Order implements Comparable<Order>,Appendable,AutoCloseable {
}

静态方法

在 Java SE 8 中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的。 只是这有违于将接口作为抽象规范的初衷。目前为止, 通常的做法都是将静态方法放在伴随类中。

例如:Paths ,其中只包含两个工厂方法

public final class Paths {
  private Paths() { }

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

  public static Path get(URI uri) {
        String scheme =  uri.getScheme();
        if (scheme == null)
            throw new IllegalArgumentException("Missing scheme");

        // check for default provider to avoid loading of installed providers
        if (scheme.equalsIgnoreCase("file"))
            return FileSystems.getDefault().provider().getPath(uri);

        // try to find provider
        for (FileSystemProvider provider: FileSystemProvider.installedProviders()) {
            if (provider.getScheme().equalsIgnoreCase(scheme)) {
                return provider.getPath(uri);
            }
        }

        throw new FileSystemNotFoundException("Provider \"" + scheme + "\" not installed");
    }
}

默认方法

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

例子:

public interface B1 {
    default String getName() {
        return "hehe";
    }
}

解决默认方法冲突

如果先在一个接口中将一个方法定义为默认方法, 然后又在超类或另一个接口中定义了同样的方法, 会发生什么情况?

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

演示2):

public interface B1 {
    default String getName() {
        return getClass().getName() + "hehe";
    }
}
public interface B2 {
    default String getName() {
        return getClass().getName() + "hehe";
    }
}
public class B0 implements B1,B2{
    //  com.xguo.bootredisson.api.B0 inherits unrelated defaults for getName()
    // from types com.xguo.bootredisson.api.B1 and com.xguo.bootredisson.api.B2
    //com.xguo.bootredisson.api.B0 从 com.xguo.bootredisson.api.B1 和 com.xguo.bootredisson.api.B2 类型继承了 getName() 的不相关默认值
}

解决:

public class B0 implements B1,B2{
    @Override
    public String getName() {
        return B1.super.getName();
    }
}

接口示例

接口与回调

回调(callback) 是一种常见的程序设计模式。在这种模式中, 可以指出某个特定事件发生时应该采取的动作。例如,可以指出在按下鼠标或选择某个菜单项时应该采取什么行动。

Comparator 接口

对一个对象数组排序,前提是这些对象是实现了Comparable 接口的类的实例 。

对象克隆

Cloneable 接口,这个接口指示一个类提供了一个安全的 clone 方法。

未实现Cloneable 接口

public class C1 {
    private Integer id;
    private String name;

    public C1() {
    }

    public C1(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "C{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

测试:

public static void main(String[] args) throws CloneNotSupportedException {
        C1 c1 = new C1(1, "c");
        C1 c2 = c1;
        c2.setId(2);
        c2.setName("a");
        System.out.println("c1 = " + c1); // 输出 -> c1 = C{id=2, name='a'}
        System.out.println("c2 = " + c2); // 输出 -> c2 = C{id=2, name='a'}
    }

上面这种方式只是单纯的相互引用,修改了一个会影响另外一个。

实现Cloneable 接口 (浅拷贝)

public class C implements Cloneable {
    private Integer id;
    private String name;

    public C() {
    }

    public C(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "C{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试

public static void main(String[] args) throws CloneNotSupportedException {
    C c = new C(1,"c");
    C clone = (C) c.clone();
    clone.setId(2);
    clone.setName("a");
    System.out.println("c = " + c); // 输出 -> c = C{id=1, name='c'}
    System.out.println("clone = " + clone); // 输出 -> clone = C{id=2, name='a'}
}

lambda 表达式

内部类

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

为什么需要使用内部类呢?

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

使用内部类访问对象状态

简单的内部类

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.util.Date;

/**
 * 简单的内部类
 * 分析 TimerTest 示例, 并抽象出一个 TalkingClock 类。 构造一个
 * 语音时钟时需要提供两个参数:发布通告的间隔和开关铃声的标志
 *
 * @author xGuo
 * @date 2022/03/29
 */
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();
        Timer t = new Timer(interval, listener);
        t.start();
    }

    public class TimePrinter implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the ime is" + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
}

内部类的特殊语法规则

public void start() {
    //另一种写法
    ActionListener listener = this.new TimePrinter();
    Timer t = new Timer(interval, listener);
    t.start();
}

 public class TimePrinter implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the ime is" + new Date());
            //另一种写法
            if (TalkingClock.this.beep){
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }

局部内部类

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

public void start1() {
    class TimePrinter implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the ime is" + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
    ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval, listener);
    t.start();
}

由外部方法访问变量

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

匿名内部类

将局部内部类的使用再深人一步。 假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。

/**
 * start3 匿名内部类
 *
 * @param interval 时间间隔
 * @param beep     哔哔声音
 */
public void start3(int interval, boolean beep) {
    ActionListener listener = new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the ime is" + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    };
    Timer t = new Timer(interval, listener);
    t.start();
}

它的含义是:创建一个实现 ActionListener 接口的类的新对象,需要实现的方法 actionPerformed 定义在括号””{}””内。如果构造参数的闭小括号后面跟一个开大括号, 正在定义的就是匿名内部类。

静态内部类

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

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 pair = ArrayAlg.minmax(d);
            System.out.println("min =" + pair.getFirst());
            System.out.println("max =" + pair.getSecond());
        }
    }
}

class ArrayAlg {
    public static class Pair {
        private double first;
        private double second;

        public Pair(double first, double second) {
            this.first = first;
            this.second = second;
        }

        public double getFirst() {
            return first;
        }

        public double getSecond() {
            return second;
        }
    }

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

代理

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

何时使用代理

假设有一个表示接口的 Class 对象(有可能只包含一个接口,) 它的确切类型在编译时无法知道。这确实有些难度。要想构造一个实现这些接口的类, 就需要使用 newlnstance 方法或反射找出这个类的构造器。但是, 不能实例化一个接口,需要在程序处于运行状态时定义一个新类。了解决这个问题, 有些程序将会生成代码;将这些代码放置在一个文件中;调用编译器;然后再加载结果类文件。很自然, 这样做的速度会比较慢,并且需要将编译器与程序放在一起。

而代理机制则是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。尤其是,它具有下列方法:
1.指定接口所需要的全部方法。
2.Object 类中的全部方法, 例如, toString、 equals 等。
然而,不能在运行时定义这些方法的新代码。而是要提供一个调用处理器( invocationhandler)。调用处理器是实现了 InvocationHandler 接口的类对象。在这个接口中只有一个方法:

Object invoke(Object proxy, Method method, Object □ args)

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

创建代理对象

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

1.一个类加栽器( class loader)。作为 Java 安全模型的一部分, 对于系统类和从因特网上下载下来的类,可以使用不同的类加载器。目前, 用 null 表示使用默认的类加载器。
2.一个 Class 对象数组, 每个元素都是需要实现的接口。
3.一个调用处理器。

还有两个需要解决的问题。 如何定义一个处理器? 能够用结果代理对象做些什么? 当然, 这两个问题的答案取决于打算使用代理机制解决什么问题。 使用代理可能出于很多原因, 例如:
1.路由对远程服务器的方法调用。
2.在程序运行期间,将用户接口事件与动作关联起来。
3为调试, 跟踪方法调用。

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

public class TarceHandler implements InvocationHandler {

    private Object target;

    public TarceHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // print implicit argument
        System.out.print(target);
        // print method name
        System.out.print("." + method.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 method.invoke(target, args);
    }
}
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Random;

public class ProxyTest {
    public static void main(String[] args) {
        Object[] objects = new Object[10000];
        // fill elements with proxies for the integers 1 ... 10000 -> 用整数 1 ... 1000 的代理填充元素
        for (int i = 0; i < objects.length; i++) {
            Integer v = i + 1;
            TarceHandler handler = new TarceHandler(v);
            objects[i] = Proxy.newProxyInstance(null, new Class[]{Comparable.class}, handler);
        }
        // construct a random integer -> 构造一个随机整数
        Integer key = new Random().nextInt(objects.length) + 1;

        // search for the key -> 二分搜索查找结果
        int result = Arrays.binarySearch(objects, key);

        // print match if found -> 如果找到,则打印匹配
        if (result >= 0) {
            System.out.println(objects[result]);
        }
    }
}

输出结果

5000.compareTo(9733)
7500.compareTo(9733)
8750.compareTo(9733)
9375.compareTo(9733)
9688.compareTo(9733)
9844.compareTo(9733)
9766.compareTo(9733)
9727.compareTo(9733)
9746.compareTo(9733)
9736.compareTo(9733)
9731.compareTo(9733)
9733.compareTo(9733)
9733.toString()
9733

代理类的特性

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

所有的代理类都扩展于 Proxy 类。一个代理类只有一个实例域—调用处理器,它定义在 Proxy 的超类中。 为了履行代理对象的职责, 所需要的任何附加数据都必须存储在调用处理器中。 例如, 在上面的代码中中, 代理 Comparable 对象时,TraceHandler 包装了实际的对象。所有的代理类都覆盖了 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 对象是否代表一个代理类。