跳至主要內容

Java

LincZero大约 15 分钟

Java

目录

接口

概念

API: java.lang.Comparable<T>1.0 API: java.util.Arrays 1.2 API: java.lang.Integer 1.0 API: java.lang.Double 1.0

接口和抽象基类的区别(先看我)

详见对应 “接口和抽象基类的区别” 笔记

基本使用

public interface Comparable
{
    int compareTo(Object other);  // 派生类必须实现该方法,自动地属于public,但在实现接口时,必须把方法声明为public
}

// 实现用关键字implements
class Employee implements Comparable
{
    public int compareTo(Object otherObject)
    {
        Employee other = (Employee) ohterObject;
        return Double.compare(salary, other.salary);
    }
}

// 或者结合泛型。在Java SE 5.0中,Comparable接口已经改进为泛型类型
class Employee implements Comparable<Employee>
{
    public int compareTo(Employee otherObject)
    {
        return Double.compare(salary, other.salary);
    }
}
public interface Comparable
{
    int compareTo(T other);  // paramter has type T
}

// 或者对接口进行扩展
public interface Comparable2 extends Comparable{
    double milesPerGallon();
}

final关键字

如果存在这样一种通用算法,它能够对两个不同的子类对象进行比较,则应该在超类中提供一个compareTo方法,并将这个方法声明为final。

  • Java中
    • 修饰基本数据类型:表示常量,只能被赋值一次,赋值后值无法改变。又分为:静态变量、实例变量和局部变量
    • 修饰引用类的变量类型:这个变量就不能再修改引用对象
    • 修饰类:final类不能被继承
    • 修饰方法:那么该方法不能被子类的方法所覆盖,但可以被继承
  • C++中
    • 其实用差不多

接口特点

接口特点

  • 接口中的所有方法自动地属于public。接口中的域将被自动设为public static final。 在接口中声明方法时,不必提供关键字public。但在实现接口时,必须把方法声明为public。 虽然也能进行标记,但Java语言规范建议不要书写这些多余的关键字

允许的操作

  • 在接口中还可以定义常量

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

  • 接口也可以被扩展。例如:

    public interface Moveable
    {
        void move(double x, double y);
    }
    
    public interface Powered extends Moveable{
        double milesPerGallon();
    }
    

不允许的操作

  • 不能实例化。接口不是类,绝不能含有实例域。不过声明接口的变量是允许的
  • 不能在接口中实现方法(在Java SE 8之前),但可以包含常量 有些接口只定义了常量,而没有定义方法。例如,标准库中的SwingConstants接口

版本改进归纳

归纳

  • JDK7及以前:接口中只能定义抽象方法
  • JDK8:接口中可以定义有方法体的方法(默认、静态)
  • JDK9:接口中可以定义私有方法(普通的私有方法、静态的私有方法)

接口中实现方法(Java SE 8)

静态方法 static

通常用途
  • 理论上讲,没有任何理由认为这是不合法的。只是这有违于将接口作为抽象规范的初衷。 目前为止,通常的做法都是将静态方法放在伴随类中。 在标准库中,你会看到成对出现的接口和实用工具类,如Collection/Collections或Path/Paths。
使用举例
  • Paths类(历史残留问题) 其中只包含两个工厂方法。可以由一个字符串序列构造一个文件或目录的路径 在Java SE 8中,可以为Path接口增加以下方法:

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

    这样一来,Paths类就不再是必要的了 不过整个Java库都以这种方式重构也是不太可能的,但是实现你自己的接口时,不再需要为实用工具方法另外提供一个伴随类。

默认方法 default

使用方法

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

public interface Comparable<T>
{
    default int compareTo(T other){return 0;} 
    	// 默认方法,所有元素是相同的?
}

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

默认方法可以调用任何其他方法。例如,Collection接口可以定义一个便利方法:

public interface Collection
{
    int size(); // 一个抽象方法
    default boolean isEmpty()
    {
        return size() == 0;
    }
    ...
}

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

应用举例
默认空方法

例如,在第11章会看到,如果希望在发生鼠标点击事件时得到通知,就要实现一个包含5个方法的接口

public interface MouseListener
{
	void mouseClicked(MouseEvent event);
    void mousePressed(MouseEvent event);
    void mouseRelease(MouseEvent event);
    void mouseEntered(MouseEvent event);
    void mouseExited(MouseEvent event);
}

大多数情况下,你只需要关心其中的1、2个事件类型。在Java SE 8中,可以把所有方法声明为默认方法,这些默认方法什么也不做。

public interface MouseListener
{
    default void mouseClicked(MouseEvent event){}
    default void mousePressed(MouseEvent event){}
    default void mouseRelease(MouseEvent event){}
    default void mouseEntered(MouseEvent event){}
    default void mouseExited(MouseEvent event){}
}

这样一来,实现这个接口的程序员只需要为他们真正关心的事件覆盖相应的监听器

接口演化 / 源代码兼容(interface evolution / source compatible)

默认方法的一个重要用法是“接口演化”

以Collection接口为例,这个接口作为Java的一部分已经有很多年了。

假设很久以前你提供了这样一个类:

public class Bag implements Collection

后来,在Java SE 8中,又为这个接口增加了一个stream方法。

假设stream方法不是一个默认方法。那么Bag类将不能编译,因为它没有实现这个新方法。 为接口增加一个非默认方法不能保证“源代码兼容”(source compatible) 否则如果程序在一个Bag实例上调用stream方法,就会出现一个AbstractMethodError。

解决默认方法冲突
方法冲突

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

诸如Scala和C++等语言对于解决这种二义性有一些复杂的规则。幸运的是,Java的相应规则要简单得多。规则如下:

  1. 超类优先。 如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。

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

    例如

    class Student implements Person, Named  // 其中这两个接口都包含了getName方法
    {
    	...
    }
    
接口冲突的解决方法

类会继承Person和Named接口提供的两个不一致的getName方法。并不是从中选择一个, Java编译器会报告一个错误,让程序员来解决这个二义性。只需要在Student类中提供一个getName方法。

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

如果至少有一个接口提供了一个实现,编译器就会报告错误,而程序员就必须解决这个二义性。 当然,如果两个接口都没有为共享方法提供默认实现,那么就与Java SE 8之前的情况一样,这里不存在冲突。实现类可以有两个选择:实现这个方法,或者干脆不实现。如果是后一种情况,这个类本身就是抽象的。

接口中私有方法(JDK9)

可以写接口的私有方法

JDK9 之前的写法

interface Inter {
    public default void start(){
        system.out.println( "start方法执行...");
        log();
    }
    
    public default void end(){
		system.out.print1n( "end方法执行...");
        log();
    }
    
    // 相同代码
    public default void log(){
        system.out.println("日志记录");
    }
}

JDK9 之后

interface Inter {
    public default void start(){
        system.out.println( "start方法执行...");
        log1();
    }
    
    public static default void end(){
		system.out.print1n( "end方法执行...");
        log2();
    }
    
    // 接口中普通私有方法
    private default void log1(){
        system.out.println("日志记录");
    }
    
	// 接口中静态私有方法
    private static default void log2(){
        system.out.println("日志记录");
    }
}

伴随类(旧方案)

历史残留问题: 在Java API中,你会看到很多接口都有相应的伴随类,这个伴随类中实现了相应接口的部分或所有方法, 如Collection/AbstractCollection或MouseListener/MouseAdapter。

在Java SE 8中,这个技术已经过时。现在可以直接在接口中实现方法

应用

接口与回调

  • 回调(callback)

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

    • 例如,可以指出在按下鼠标或选择某个菜单项时应该采取什么行动

  • 两种实现方法

    • 在很多程序设计语言中,可以提供一个函数名,定时器周期性地调用它。即回调函数,通常传入函数指针实现

    • 但是,在Java标准类库中的类采用的是面向对象方法。

例子:定时器,ActionListener接口

API: javax.swing.JOptionPane 1.2 API: javax.swing.Timer 1.2 API: java.awt.Toolkit 1.0

在java.swing包中有一个Timer类,可以使用它在到达给定的时间间隔时发出通告。 在构造定时器时,需要设置一个时间间隔,并告之定时器,当到达时间间隔时需要做些什么操作。

原理

// 该例中,ActionListener/TimePrinter是观察者或监听器,Timer是被观察者

// 接口。这是java.awt.event包的ActionListener接口
public interface ActionListener  // 监听器接口
{
    // 当到达指定的时间间隔时,定时器就调用actionPerformed方法
    // 需要注意actionPerformed方法的ActionEvent参数。这个参数提供了事件的相关信息,例如,产生这个事件的源对象。
    void actionPerformed(ActionEvent event);
}

// 实现接口
class TimePrinter implement ActionListener
{
    // 每隔10秒钟打印一条信息“At the tone,the time is...”,然后响一声
    public void actionPerformed(ActionEvent event)
    {
        System.out.println("At the tone, the time is "+new Date());
        Toolkit.getDefaultToolkit().beep();
    }
}

// 调用
ActionListener listener = new TimePrinter();
Timer t = new Timer(10000, listener);	// 可以传入实现了ActionListener接口的类
t.start();

完整代码

package timer;
/**
    @version 1.01 2015-05-12
    @author Cay Horstmann
 */

import java.awt.*;
import java.awt.event.*;
import java.uti7.*;
import javax.swing.*;
import javax.swing.Timer;
// to resolve conflict with java.util.Timer

public class TimerTest
{
	public static void main(String[] args)
    {
        ActionListener 1istener = new TimePrinter();
        
        // construct a timer that calls the listener
        //once every 10 seconds
        Timer t = new Timer(10000,listener);
        t.start();
			
        J0ptionPane.showMessageDialog(nu11,"Quit program?");
        System.exit(O);
	}
}

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

与C++不同

观察者模式的实现细节上

  • C++
    • 观察者/监听器,继承一个观察者基类
    • 被观察者/触发者,包含一个观察者基类指针
  • 而Java则是
    • 观察者/监听器,接口实现一个观察者基类
    • 被观察者/触发者,包含一个观察者基类指针

例子:比较器,Comparator接口

比较器

// 比较器接口
public interface Comparator<T>
{
    int compare(T first, T second);
}

// 接口实现。比较器是实现了Comparator接口的类的实例
class LengthComparator implements Comparator<String>
{
    public int compare(String first, String second){
        return first.length() - second.length();
    }
}

// 调用1,利用比较器判断
Comparator<String> comp = new LengthComparator();	// 具体完成比较时,需要建立一个实例
if (comp.compare(words[i], word[j]) > 0) ...
    
// 调用2,利用比较器进行排序,这里根据字符串长度排序
String[] friends = {"Peter", "Paul", "Mary"};
Arrays.sort(friends, new LengthComparator());	// Arrays.sort方法还有第二个版本,用数组和比较器作为参数

例子:对象克隆,Cloneable接口

本节我们会讨论Cloneable接口,这个接口指示一个类提供了一个安全的clone方法。由于克隆并不太常见,而且有关的细节技术性很强,你可能只是想稍做了解,等真正需要时再深入学习。

区分三种 “复制”

  • 赋值:原变量和副本都是同一个对象的引用
  • 浅拷贝:默认的克隆操作是“浅拷贝”,拷贝域就会得到相同子对象的另一个引用。 不过出如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的
  • 深拷贝:拷贝域的子对象的不和原对象使用同一份拷贝,通常需要循环递归来实现

赋值与拷贝

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

// 原变量和副本都是同一个对象的引用
Employee original = new Employee("Johm Public", 50000);
Employee copy = original;
copy.raiseSalary(10); // oops--also changed original

// 原变量和副本不是同一个对象的引用,不过需要注意这里是浅拷贝
Employee copy = original.clone();
copy.raiseSalary(10); // OK--original unchanged

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

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

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

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

完整代码

默认拷贝:

class Employee implements Cloneable
{
    // 提高可见性级别为public
    public Empoyee clone() throws CloneNotSupportedException
    {
        // 更改返回类型(协变返回类型)
        return (Employee) super.clone;
    }
    ...
}

深度拷贝:

class Employee implements Cloneable
{
    public Empoyee clone() throws CloneNotSupportedException
    {
        // call Object.clone()
        Employee cloned = (Employee) super.clone();
        
        // clone mutable fields
        cloned.hireDay = (Date) hireDay.clone();
        
        return cloned;
    }
    ...
}

上面的是类代码,完整的整段文件代码详见书 “接口示例 > 对象克隆” 的最后

clone方法__特殊的保护方法

保护方法

clone方法是Object的一个protected方法,这说明你的代码不能直接调用这个方法。

原因

这个限制是有原因的 想想看Object类如何实现clone。它对于这个对象一无所知,所以只能逐个域地进行拷贝。如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。 即无法实现深拷贝

特殊的保护方法

并且,可能有人会问:所有子类都能访问受保护方法吗?不是所有类都是Object的子类吗? 那是因为受保护访问的规则比较微妙(见第5章)。子类只能调用受保护的clone方法来克隆它自己的对象。 必须重新定义clone为public才能允许所有方法克隆对象。

clone方法__协变返回类型

备注:在Java SE 1.4之前,clone方法的返回类型总是Object,而现在可以为你的clone方法指定正确的返回类型。 这是协变返回类型的一个例子(见第5章)

Cloneable接口__标记接口

Cloneable接口只是作为一个标记,如果一个对象请求克隆,但没有实现这个接口,就会生成一个受查异常。

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

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

_

Cloneable接口是Java提供的一组标记接口(tagging interface)之一。(有些程序员称之为记号接口(marker interface))

标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用instanceof:

if (obj instanceof Cloneable) ...

建议你自己的程序中不要使用标记接口

Cloneable接口__异常问题

public Empoyee clone() throws CloneNotSupportedException

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

public Employee clone()
    try
    {
        Employee cloned = (Employee) super.clone();
        ...
    }
    catch (CloneNotSupportedException e) {return null;}
    // this won't happen, since we are Cloneable
}

这样就允许子类在不支持克隆时选择抛出一个CloneNotSupportedException

其他

必须当心子类的克隆。例如,一旦为Employee类定义了clone方法,任何人都可以用它来克隆Manager对象。Employee克隆方法能完成工作吗?这取决于Manager类的域。在这里是没有问题的,因为bonus域是基本类型。但是Manager可能会有需要深拷贝或不可克隆的域。不能保证子类的实现者一定会修正clone方法让它正常工作。出于这个原因,在Object类中clone方法声明为protected。

克隆没有你想象中那么常用。标准库中只有不到5%的类实现了clone。

所有数组类型都有一个public的clone方法,而不是protected。可以用这个方法建立一个新数组,包含原数组所有元素的副本。

int[] luckyNumbers = {2, 3, 5, 7, 11, 13};
int[] cloned = luckyNumbers.clone();
cloned[5] = 12;  // 不会改变 luckyNumbers[5]

卷Ⅱ的第2章将展示另一种克隆对象的机制,其中使用了Java的对象串行化特性。这个机制很容易实现,而且很安全,但效率不高。

【扩展】结合泛型