抽象类,接口和泛型
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} }
-
异常类不能是泛型的