抽象类,接口和泛型

1. 抽象类和接口

父类中定义了相关子类中的共同行为。 接口可以用于定义类的共同行为(包括 非相关的类)。

抽象类

类的设计应该确保父类包含它的子类的共同特征。有时候,一个父类设计得非常抽象,以至于它都没有任何具体的实例。这样的类称为抽 象类(abstract class)。

Circle 类和 Rectangle 类分别包含计算圆和矩形的面积和周长的方法 getArea() getPerimeter()。因为可以计算所有几何对象的面积和周长,所以最好在 GeometricObject 类中定义 getAreaO getPerimeterO 方法。但是, 些方法不能在 GeometricObject类中实现,因为它们的实现取决于几何对象的具体类型。 这样的方法称为抽象方法(abstract method ), 在方法头中使用 abstract 修饰符表示。GeometricObject 类中定义了这些方法后,GeometricObject 就成为一个抽象类。在类头使用 abstract 修饰符表示该类为抽象类。

抽象类和常规类很像,但是不能使用 new 操作符创建它的实例。抽象方法只有定义而没有实现。它的实现由子类提供。一个包含抽象方法的类必须声明为抽象类。 抽象类的构造方法定义为protected, 因为它只被子类使用。创建一个具体子类的实例 ,它的父类的构造方法被调用以初始化父类中定义的数据域。

使用抽象类的好处:

如果在某个函数(方法)中,传入了父类对象,却要使用子类中有的方法,则应该将父类定义为抽象类,同时在父类中定义该方法(无须实现)。

关于抽象类值得注意的几点:

  • 抽象方法不能包含在非抽象类中。如果抽象父类的子类不能实现所有的抽象方法, 那么子类也必须定义为抽象的。还要注意到,抽象方法是非静态的。
  • 抽象类是不能使用 new 操作符来初始化的。但是,仍然可以定义它的构造方法,这个构造方法在它的子类的构造方法中调用。
  • 子类可以覆盖父类的方法并将它定义为 abstract。这是很少见的,但是它在当父类的方法实现在子类中变得无效时是很有用的。在这种情况下,子类必须定义为 abstract。
  • 即使子类的父类是具体的,这个子类也可以是抽象的。
  • 不能使用 new 操作符从一个抽象类创建一个实例,但是抽象类可以用作一种数据类型。

抽象的Number类

Number 类是数值包装类、Biglnteger 以及 BigDecimal 的抽象父类。

由于intValue()、longValue()、floatValue() 以及 doubleValue() 等方法不能在 Number 类中给出实现,它们在 Number 类中被定义为抽象方法。

接口

接口在许多方面都与抽象类很相似,但是它的目的是指明相关或者不相关类的多个对象的共同行为。

为了区分接口和类,Java 采用下面的语法来定义接口:

修饰符 interface 接口名

{//常量声明

//方法签名

}

在Java中,接口被看作是一种特殊的类。就像常规类一样,每个接口都被编译为独立的字节码文件。使用接口或多或少有点像使用抽象类。例如,可以使用接口作为引用变量的数据类型或类型转换的结果等。与抽象类相似,不能使用 new 操作符创建接口的实例。

类和接口之间的关系称为接口继承(interface inheritance)。因为接口继承和类继承本质上是相同的,所以我们将它们都简称为继承。使用inplements关键字让对象的类实现这个接口。

由于接口中所有的數据域都是 public static final 而且所有的方法都是 public abstract, 所以 Java 允许忽略这些修饰符。

Comparable接口

Java提供了Comparable 接口,用来对两个可比较的对象(譬如,可以是是两个学 、两个日期、两个圆、 两个矩形或者两个正方形)提供比较大小。接口的定义如下所示:

package java.lang;

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

compareTo 方法判断这个对象相对于给定对象 o 的顺序,并且当这个对象小于、等于或 大于给定对象o ,分别返回负整数、0或正整数。

Comparable 接口是一个泛型接口。在实现该接口时,泛型类型 E 被替换成一种具体的类型。Java 类库中的许多类实现了 Comparable 接口以定义对象的自然顺序。Byte、Short、 Integer、Long、Float、Double、Character、Biglnteger、BigDecimal、Calendar、String 以及Date类都 实现了 Comparable 接口。

可以定义一个新的 Rectangle 类来实现 Comparable。

public class ComparableRectangle extends Rectangle implements 
    Comparable<ComparableRectangle> {
    public ComparableRectangle(double width, double height) {super(width, height)}
    @Override
    public int compareTo(ComparableRectangle o) {
        if (getArea() > o.getArea())
            return 1;
        else if (getArea() < o.getArea())
            return 0;
        else
            return 0;
    }
    @Override
    public String toString() {
        return super.toString() + " Area: " + getArea();
    }
}

强烈建议(尽管不要求) compareTo 应该与 equals 保持一致。也就是说,对于两个对象 o1和o2, 应该确保当且仅当 o1.equals(o2)为 true时,o1.compareTo(o2) == 0 成立。

Cloneable接口

经常会出现需要创建一个对象拷贝的情况。为了实现这个目的,需要使用 clone 方法并理解Cloneable 接口。接口包括常量和抽象方法,但是 Cloneable 接口是一个特殊情况。 java.lang包中的 Cloneable 接口的定义如下所示:

public java.lang;
public interface Cloneable {}

这个接口是空的。一个带空体的接口称为标记接口(marker interface)。一个标记接口既不包括常量也不包括方法。它用来表示一个类拥有某些特定的属性。

实现 Cloneable 接口的类标记为可克隆的,而且它的对象可以使用在 Object 类中定义的 clone() 方法克隆。Java 库中的很多类(例如,Date、Calendar ArrayList) 实现 Cloneable。

为了定义一个自定义类来实现 Cloneable 接口,这个类必须覆盖 Object 类中的 cloneO 方法。

public class House implements Cloneable, Comparable<House> {
    private int id;
    private double area;
    private java.util.Date whenBuilt;
    public House(int id, double area) {
        this.id = id;
        this.area = area;
        whenBuilt = new java.util.Date();
    }
    public int getId() {
        return id;
    }
    public double getArea() {
        return area;
    }
    public java.util.Date getWhenBuilt() {
        return whenBuilt;
    }
    
    @Override /**Override the protected clone method defined in
              the Object class, and strengthen its accessibility**/
    public Object clone() throws CloneNotSupportedException {return super.clone();}
    
    @Override
    public int compareTo(House o) {
        if (area > o.area)
            return 1;
        else if (area < o.area)
            return -1;
        else
            return 0;
    }
}

// 以创建一个 House 类的对象,然后从这个对象创建一个完全一样的拷贝:
House house1 = new House(1, 1750.50);
House house2 = (House)house1.clone();

House 类实现在 Object 类中定义的 clone 方法,方法头是:

protected native Object clone() throws CloneNotSupportedException;

关键字 native 表明这个方法不是用 Java 写的,但它是 JVM 针对自身平台实现的。 关键字 protected限定方法只能在同一个包内或在其子类中访问。由于这个原因,House 必须覆盖该方法并将它的可见性修饰符改为 public, 这样,该方法就可以在任何一个包中使用。

Object 类中的 clone 方法将原始对象的每个数据域复制给目标对象。如果一个数据域是基本类型,复制的就是它的值。如果一个数据域是对象,复制的就是该域的引用。这意味着浅复制。

接口和抽象类

变量 构造方法 方法
抽象类 无限制 子类通过调用构造方法链调用构造方法, 抽象类不能用 new 操作符实例化 无限制
接口 所有的变量必须是 public static final 没有构造方法。接口不能用 new操作符实例化 所有方法必须是公共的抽象实例方法

Java 只允许为类的扩展做单一继承,但是允许使用接口做多重扩展。利用关键字 extends, 接口可以继承其他接口。这样的接口称为子接口(subinterface)。

所有的类共享同一个根类 Object, 但是接口没有共同的根。与类相似,接口也可以定义一种类型。一个接口类型的变量可以引用任何实现该接口的类的实例。

抽象类和接口都是用来明确多个对象的共同特征的。那么该如何确定在什么情况下应该使用接口,什么情况下应该使用类呢?

一般来说,清晰描述父子关系的强的 “是一种” 的关系(strong is-a relationship) 应该用类建模。例如,因为公历是一种日历, 所以,java.util .GregorianCalendar 和java.util.Calendar 是用类继承建模的。弱的“是一种” 的关系(weak is-a relationship) 也称为类属关系(is-kind-of relationship), 表明对象拥有某种属性,可以用接口来建模。例如,所有的字符串都是可比较的,因此,String 类实现 Comparable 接口。

通常,推荐使用接口而非抽象类,因为接口可以定义非相关类共有的父类型。

类设计的原则

  • 内聚性 类应该描述一个单一的实体,而所有的类操作应该在逻辑上相互配合,支持一个一致的目的。
  • 一致性 遵循标准 Java 程序设计风格和命名习惯。为类、数据域和方法选取具有信息的名字。 通常的风格是将数据声明置于构造方法之前,并且将构造方法置于方法之前。一般来说,应该具有一致性地提供一个公共无参构造方法,用于构建默认实例。如果一 个类不支持无参的构造方法,要用文档写出原因。如果没有显式定义构造方法,即假定有一 个空方法体的公共默认无参构造方法。如果不想让用户创建类的对象,可以在类中声明一个私有的构造方法,Math 类就是如此。
  • 封装性 类应该使用 private 修饰符隐藏其数据,以免用户直接访问它。这使得类更易于维护。只在希望数据域可读的情况下,才提供 get 方法;也只在希望数据域可更新的情况下, 才提供 set 方法。
  • 清晰性 方法应在不产生混淆的情况下进行直观定义。
  • 完整性
  • 实例和静态 依赖于类的具体实例的变量或方法必须是一个实例变量或方法。如果一个变量 被类的所有实例所共享,那就应该将它声明为静态的。应该总是使用类名(而不是引用变量)引用静态变量和方法,以增强可读性并避免错误。不要从构造方法中传人参数来初始化静态数据域。最好使用 set 方法改变静态数据域
  • 继承与聚合 继承和聚合之间的差异,就是 is-a (是一种) has-a (具有)之间的关系。
  • 接口和抽象类 接口比抽象类更加灵活,因为一个子类只能继承一个父类,但是却可以实现任意个数的 接口。然而,接口不能具有具体的方法。

2. 泛型

泛型(generic)可以参数化类型,这个能力让我们可以定义带泛型类型的类或方法,随后编译器会用具体的类型来替换它。

泛型可以让我们在编译时而不是运行时检测出错误。

java.lang.Comparable接口被定义如下:

package java.lang;

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

表示形式泛型类型,随后可以用一个实际具体类型来替换它。替换泛型类型称为泛型实例化。

例如,下面的语句创建一个字符串线性表:

ArrayList<String> list = new ArrayList<>();

现在就只能向该线性表中添加字符串,试图添加非字符串就会产生编译错误。

泛型类型必须是引用类型,不能用基本类型来替换泛型类型,例如,为给Int值创建一个ArrayList对象,必须使用

ArrayList<Integer> intList = new ArrayList<>();

无须类型转换就可以从一个元素类型已指定的线性表中获取一个值,因为编译器已经知道这个元素类型。

定义泛型类和接口

public class GenericStack<E> {
    private java.util.ArrayList<E> list = new java.util.ArrayList<>();
    
    public int getSize() {return list.size();}
    
    public void push(E o) {list.add(o);}
    
    public E peek() {return list.get(getSize() - 1);}
    
    public E pop() {
        E o = list.get(getSize() - 1);
        list.remove(getSize() - 1);
        return o;
    }
    
    public boolean isEmpty() {return list.isEmpty();}
    
    @Override
    public String toString() {return "stack: " + list.toString();}
}

// 使用方法
GenericStack<String> stack0 = new GenericStack<>();
stack0.push("London");
stack0.push("Hongkong");

可以不使用泛型,而将元素类型设置为Object,也可以容纳任何对象类型,但是,使用泛型能提高软件的可靠性和可读性,因为某些错误能在编译时而不是运行时被检测到。

注意,GenericStack的构造方法被定义为 public GenericStack()

有时候,泛型类可能会有多个参数,此时,应该将所有参数放在尖括号中,并以逗号隔开,比如:<E1, E2, E3>

可以定义一个类或接口作为泛型类或泛型接口的子类型,例如,在Java API中,java.lang.String类被定义为实现Comarable接口:

public class String implements Comarable<String>

泛型方法

可以使用泛型类型来定义泛型方法:

public class GenericMethodDemo {
    public static void main(String[] args) {
        Integer[] intergers = {1,2,3,4};
        String[] strings = {"London", "Paris", "New York", "Austin"};
        
        GenericMethodDemo.<Integer>print(integers);
        GenericMethodDemo.<String>print(strings);
    }
    public static <E> void print(E[] list) {
        for (int i = 0; i < list.length; i++)
            System.out.print(list[i] + " ");
        System.out.println();
    }
}

泛型方法声明如下:

public static <E> void print(E[] list)

调用泛型方法:

GenericMethodDemo.<Integer>print(integers); 或简单地调用:

print(integers);这种情形,实际类型没有明确指定,编译器自动发现实际类型。

可以将泛型指定为另外一种类型的子类型,这样的泛型称为受限的,受限的泛型类型 <E extends GeometricObject> 将 E 指定为GeometricObject的子类型。

非受限泛型类型 <E>等同于 <E extends Object>

通配泛型

public static double max(GenericStack<Number> stack) {
    double max = stack.pop().doubleValue();
    while (!stack.isEmpty()) {
        double value = stack.pop().doubleValue();
        if (value > max)
            max = value;
    }
    return max;
}

试想如果对一个元素为Integer型的intStack调用上面的max方法,能运行成功吗?

不能。因为intStack不是GenericStack<Number>的实例,不能调用max方法,尽管Integer是Number的子类型,但是,GenericStack<Integer>并不是 GenericStack<Number>的子类型。

为避免这个问题,可以使用通配泛型类型。

  • ? 称为非受限通配,和? extends Object一样
  • ? extends T称为受限通配,表示T或T的一个子类型
  • ? super T称为下限通配,表示T或T的一个父类型

可以使用下面的定义方式修复上面的调用错误:

public static double max(Generic<? extends Number> stack)

消除泛型和对泛型的限制

泛型是使用一种称为类型消除的方法来实现的,编译器使用泛型类型信息来编译代码,但是随后会消除它。泛型存在于编译时,一旦编译器确认泛型类型是安全使用的,就会将它转换为原始类型。

需要注意的是,不管实际类型是什么,泛型类是被它的所有实例共享的,假如创建了两个列表对象:

ArrayList<String> list1 = new ArrayList<>();

ArrayList<Integer> list2 = new ArrayList<>();

尽管在编译时,ArrayList<String>ArrayList<Integer>是两种类型,但是,在运行时只有一个ArrayList类会被加载到JVM中,list1和list2都是ArrayList的实例,因此,一下两条语句都为true:

list1 instanceof ArrayList

list2 instanceof ArrayList

然而,表达式 list1 instaceof ArrayList<String>是错误的,由于ArrayList<String>并没有在JVM中存储为单独一个类,所以在运行时使用它毫无意义。

由于泛型类型在运行时被消除,因此,对于如何使用泛型类型是有一些限制的。

  • 不能使用new E()

  • 不能使用new E[]

  • 在静态上下文中,不允许类的参数是泛型类型;由于泛型类的所有实例都有相同的运行时类,所以泛型类型的静态变量和方法是被它的所有实例共享的。因此,在静态方法,数据域中或初始化语句中,为类引用泛型类型的参数是非法的;

    public class Test<E> {
        public static void m(E o1) { // illegal
        }
        public static E o1; // illegal
        
        static { E o2;  // illegal}
    }
    
  • 异常类不能是泛型的

posted @ 2021-05-15 12:21  geeks_reign  阅读(2223)  评论(0编辑  收藏  举报