《Java核心技术》第六章 接口、lambda表达式与内部类 学习笔记

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

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

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

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

在本章的最后还将介绍代理(proxy),这是一种实现任意接口的对象。代理是一种非常专业的构造工具,它可以用来构建系统级的工具。

6.1 接口

6.1.1 接口概念

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

下面给出一个具体的示例。Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了Comparable接口。

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

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

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

面这个接口只有一个方法,而有些接口可能包含多个方法。稍后可以看到,在接口中还可以定义常量。然而,更为重要的是要知道接口不能提供哪些功能。接口绝不能含有实例域,在Java SE 8之前,也不能在接口中实现方法。(现在已经可以在接口中提供简单方法了)

提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此,可以将接口看成是没有实例域的抽象类。但是这两个概念还是有一定区别的。

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

扫描二维码关注公众号,回复: 12482004 查看本文章

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

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

// 实现Comparable接口
public class Employee extends Person implements Comparable<Employee>
{
    
    
		// 实现接口中定义的函数
		@Override
    public int compareTo(Employee e) {
    
    
        return Double.compare(salary, e.salary);
    }
}

6.1.2 接口的特性

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

然而,尽管不能构造接口的对象,却能声明接口的变量:

接口变量必须引用实现了接口的类对象:

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

接口也可以被继承, 虽然在接口中不能包含实例域或静态方法,但却可以包含常量。

尽管每个类只能够拥有一个超类,但却可以实现多个接口。这就为定义类的行为提供了极大的灵活性。

public interface interfaceDemo {
    
    
    int add1();
}

public class Main implements interfaceDemo {
    
    

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

		public static void main(String[] args) {
    
    
		    Main main = new Main();
				// 声明接口类型的变量
		    interfaceDemo interfaceDemo;
				// 接口变量必须引用实现了接口的类对象:
		    interfaceDemo = new Main();
				// 检查一个对象是否实现了某个特定的接口
		    if (main instanceof interfaceDemo) {
    
    
		    }
		}
}

// 接口的继承
public interface extendInterfaceDemo extends interfaceDemo{
    
    
    int sub1();
		// 接口中可以包含常量
		int sum = 10;
}

// 可以实现多个接口
public class Employee extends Person implements Comparable<Employee>, Cloneable

6.1.3 接口与抽象类

使用抽象类表示通用属性存在这样一个问题:每个类只能扩展于一个类。

但每个类可以实现多个接口:

有些程序设计语言允许一个类有多个超类,例如C++。我们将此特性称为多重继承(multiple inheritance)。而Java的设计者选择了不支持多继承,其主要原因是多继承会让语言本身变得非常复杂(如同C++),效率也会降低。

实际上,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。

6.1.4 静态方法

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

目前为止,通常的做法都是将静态方法放在伴随类中。

6.1.5 默认方法

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

public interface interfaceDemo {
    
    
    default int add1() {
    
    
        return 0;
    }
}

当然,这并没有太大用处,因为Comparable的每一个实际实现都要覆盖这个方法。不过有些情况下,默认方法可能很有用。

6.1.6 解决默认方法冲突

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

Java的相应规则要简单得多。规则如下:

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

6.2 接口示例

6.2.1 接口与回调

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

6.2.2 Comparator接口

现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。

要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例。

public interface Comparator<T> {
    
    
		int compare(T o1, T o2);
}

// 要按长度比较字符串,可以如下定义一个实现Comparator<String>的类:
public class LengthComparator implements Comparator<String> {
    
    
    @Override
    public int compare(String o1, String o2) {
    
    
        return o1.length() - o2.length();
    }
}

// 要对一个数组排序,需要为Arrays.sort方法传入一个LengthComparator对象:
Comparator<String> comparator = new LengthComparator();
String[] friends = {
    
    "ff", "grgd", "1"};
Arrays.sort(friends, new LengthComparator());

6.2.3 对象克隆

本节我们会讨论Cloneable接口,这个接口指示一个类提供了一个安全的clone方法。

先来回忆为一个包含对象引用的变量建立副本时会发生什么。原变量和副本都是同一个对象的引用(见图6-1)。这说明,任何一个变量改变都会影响另一个变量。

Employee original = new Employee("1",1,1,1,1);
Employee copy = original;
copy.raiseSalary(50); // original和copy的salary都变了

如果希望copy是一个新对象,它的初始状态与original相同,但是之后它们各自会有自己不同的状态,这种情况下就可以使用clone方法。

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

对于每一个类,需要确定:

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

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

3)是否不该使用clone。

实际上第3个选项是默认选项。如果选择第1项或第2项,类必须:

1)实现Cloneable接口;

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

6.3 lambda表达式

6.3.1 为什么引入lambda表达式

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

到目前为止,在Java中传递一个代码段并不容易,不能直接传递代码段。Java是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。

6.3.2 lambda表达式的语法

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

你已经见过Java中的一种lambda表达式形式:参数,箭头(->)以及一个表达式。如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{}中,并包含显式的return语句。

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

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

public static void main(String[] args) {
    
    
    String[] strings = {
    
    "f","2","r","fff","fg"};
    Arrays.sort(strings, (first, second)->first.length() - second.length());
    Timer t = new Timer(1000, event -> System.out.println("time is" + new Date()));
    t.start();
    JOptionPane.showMessageDialog(null, "quit");
    System.exit(0);
}

6.3.3 函数式接口

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

6.3.4 方法引用

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

要用::操作符分隔方法名与对象或类名

String[] strings = {
    
    "f","2","r","fff","fg","F"};
Arrays.sort(strings, String::compareToIgnoreCase);

6.3.5 构造器引用

构造器引用与方法引用很类似,只不过方法名为new。

例如,Person::new是Person构造器的一个引用。哪一个构造器呢?这取决于上下文。假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器。

public static void main(String[] args) {
    
    
    ArrayList<String> names = new ArrayList<>();
		// map方法会为各个列表元素调用Person(String)构造器
    Stream<Person> stream = names.stream().map(Person::new);
    List<Person> people = stream.collect(Collectors.toList());
}

6.3.6 变量作用域

通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量。

lambda表达式有3个部分:

1)一个代码块;

2)参数;

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

lambda表达式可以捕获外围作用域中变量的值。在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在lambda表达式中,只能引用值不会改变的变量。

如果在lambda表达式中改变变量,并发执行多个动作时就会不安全。

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

6.3.7 处理lambda表达式

使用lambda表达式的重点是延迟执行(deferred execution)。

毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中。之所以希望以后再执行代码,这有很多原因,

  • 在一个单独的线程中运行代码;
  • 多次运行代码;
  • 在算法的适当位置运行代码(例如,排序中的比较操作);
  • 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
  • 只在必要时才运行代码。
public static void repeat(int n, Runnable action)
{
    
    
		// run方法执行lambda主函数
    for (int i = 0; i < n; i++)
        action.run();
}

public static void repeat1(int n, IntConsumer action)
{
    
    
    for (int i = 0; i < n; i++)
        action.accept(i);
}

public static void main(String[] args) {
    
    
		// 执行10次lambda函数
    repeat(10, () -> System.out.println("Hello World!"));
    repeat1(10, i -> System.out.println("Hello World!" + (9 - i)));
}

6.4 内部类

内部类(inner class)是定义在另一个类中的类。为什么需要使用内部类呢?其主要原因有以下三点:

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

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

传统意义上讲,一个方法可以引用调用这个方法的对象数据域。内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。

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

这个引用在内部类的定义中是不可见的。然而,为了说明这个概念,我们将外围类对象的引用称为outer。

outer不是Java的关键字。我们只是用它说明内部类中的机制。

public class TalkingClock {
    
    
    private int interval;
    private boolean beep;
    public TalkingClock(int interval, boolean beep)
    {
    
    
        this.beep = beep;
        this.interval = interval;
    }
    public class TimePrinter implements ActionListener
    {
    
    
        @Override
        public void actionPerformed(ActionEvent e) {
    
    
            System.out.println("the time is " + new Date());
            if (beep)
                Toolkit.getDefaultToolkit().beep();
        }
    }
}

6.4.2 内部类的特殊语法规则

表达式OutClass.this表示外围类引用。

  public void actionPerformed(ActionEvent e) {
    
    
            System.out.println("the time is " + new Date());
            if (TalkingClock.this.beep)
                Toolkit.getDefaultToolkit().beep();
   }

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

内部类不能有static方法。

6.4.4 局部内部类

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

局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。

局部类有一个优势,即对外部世界可以完全地隐藏起来。即使TalkingClock类中的其他代码也不能访问它。除start方法之外,没有任何方法知道TimePrinter类的存在。

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

6.4.5 由外部方法访问变量

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

6.4.6 匿名内部类

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

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

public void start()
{
    
    
    ActionListener listener = new ActionListener() {
    
    
        @Override
        public void actionPerformed(ActionEvent e) {
    
    
            System.out.println("time is " + new Date());
            if (beep)
                Toolkit.getDefaultToolkit().beep();
        }
    };
    Timer t = new Timer(interval, listener);
    t.start();
}

6.4.7 静态内部类

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

当然,只有内部类可以声明为static。静态内部类的对象除了没有对生成它的外围类对象的引用特权外,与其他所有内部类完全一样。

6.5 代理

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

6.5.1 何时使用代理

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

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

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

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

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

6.5.2 创建代理对象

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

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

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

// 使用代理对象对二分查找进行跟踪。这里,首先将用1~1000整数的代理填充数组,然后调用Arrays类中的binarySearch方法在数组中查找一个随机整数。最后,打印出与之匹配的元素。
public class TraceHandler implements InvocationHandler {
    
    
    private Object target;
    public TraceHandler(Object target)
    {
    
    
        this.target = target;
    }

    @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[1000];
        for (int i = 0; i < 1000; 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;
        int result = Arrays.binarySearch(elements, key);
        if (result > 0)
            System.out.println(elements[result]);
    }

6.5.3 代理类的特性

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

所有的代理类都扩展于Proxy类。一个代理类只有一个实例域——调用处理器,它定义在Proxy的超类中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。

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

对于特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance方法的话,那么只能够得到同一个类的两个对象。

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

猜你喜欢

转载自blog.csdn.net/qq_17677907/article/details/112381361