父类中定义了相关子类中的共同行为。接口可以用于定义类
+6的共同行为(包括非相关类)。
抽象类不可以用于创建对象。抽象类可以包含抽象方法,这些方法将在具体的子类中实现。
在继承的层次结构中,每个新子类都使类变得越来越明确和具体。如果从一个子类追溯到父类,类就会变得更通用、更加不明确。类的设计一个确保父类包含它的子类的共同特征。有时候,一个父类设计得非常抽象,以至于它都没有任何具体的实例。这样的类称为抽象类。
方法实现取决于对象的具体类型,而不能在父类中实现,这样的方法称为抽象方法,在方法头中使用abstract修饰符表示。在父类定义抽象方法后,,父类就成为一个抽象类,在类头使用abstract修饰符表示该类为抽象类。在UML图形记号中,抽象类和抽象方法的名字用斜体表示。
抽象类和常规类很像,但是不能使用new操作符创建它的实例。抽象方法只有定义而没有实现。它的实现由子类提供。一个包含抽象方法的类必须声明为抽象类。
抽象类的构造方法定义为protected,因为它只被子类使用。创建一个具体子类的实例时,它的父类的构造方法被调用以初始化父类中定义的数据域。
下面是关于抽象类值得注意的几点:
抽象方法不能包含在非抽象类中。如果抽象父类的子类不能实现所有的抽象方法,那么子类也必须定义为抽象的。换句话说,在抽象类扩展的非抽象子类中,必须实现所有的抽象方法。还要注意到,抽象方法是非静态的。
抽象类是不能使用new操作符来初始化的。但是,仍然可以定义它的构造方法,这个构造方法在它的子类的构造方法中调用。例如,GeometricObject类的构造方法可以在Circle类和Rectangle类中调用。
包含抽象方法的类必须是抽象的。但是,可以定义一个不包含抽象方法的抽象类。在这种情况下,不能使用new操作符创建该类的实例。这种类是用来定义新子类的基类的。
即使子类的父类是具体的,这个子类也可以是抽象的。例如,Object类是具体的,但是它的子类如GeomtricObject可以是抽象的。
不能使用new操作符从一个抽象类创建一个实例,但是抽象类可以用作一种数据类型。因此,下面的语句创建一个元素是GeometricObject类型的数组是正确的:
GeometricObject[] objects = new GeometricObject[10];
然后可以创建一个GeometricObjects的实例,并将它的引用赋值给数组,如下所示:
objects[0] = new Circle();
Number类是数值包装类、BigInteger以及BigDecimal的抽象父类。
10.7节介绍了数值包装类,10.9节介绍了BigInteger以及BigDecimal类。这写类有共同的方法byteValue()、shortValue()、intValue()、longValue()、floatValue()和doubleValue(),分别从这些类的对象返回byte、short、int、long、float、以及double值。这些共同的方法实际上在Number类中定义,该类是数值包装类、BigIntegr和BigDecimal类的父类,如图所示。
Number类是Double、Float,...等类的抽象父类由于intValue()、longValue()、floatValue()以及doubleValue()等方法不能在Number类中给出实现,它们在Number类被定义为抽象方法。因此Number类是一个抽象类。byteValue()和shortValue()方法的实现从intValue()方法而来,如下所示:
public byte byteValue() {
return (byte)intValue();
}
public short shortValue() {
return (short)intValue();
}
Number定义为数值类的父类,这样可以定义方法来执行数值的共同操作。
GregorianCalendar是抽象类Calendar的一个具体子类。
一个java.util.Date的实例表示以毫秒为精度的特定时刻。java.util.Calendar是一个抽象的基类,可以提取出详细的日历信息,例如,年、月、日、小时、分钟和秒。Calendar类的子类可以实现特定的日历系统,例如,公历(Gregorian历)、农历和犹太历。目前,java支持公历类java.util.GregorianCalendar,如图所示。Calendar类类中的add方法是抽象的,因为它的实现依赖于某个具体的日历系统。
抽象的Calendar类定义了各种日历的共同特点
可以使用new GregorianCalendar()利用当前时间构造一个默认的GregorianCalendar对象,可以使用new GregorianCalendar(year, month, date)利用指定的year(年)、month(月)和date(日)构造一个GregorianCalendar对象。参数month是基于0的,即0代表1月(January)。
在Calendar类中定义的get(int field)方法在Calendar类中提取日期和时间信息方面是很有用的,日期和时间域都被定义为常量,如表所示。
常量 | 说明 |
---|---|
YEAR | 日历的年份 |
MONTH | 日历的月份,0表示一月 |
DATE | 日历的天 |
HOUR | 日历的小时(12小时制) |
HOUR_OF_DAY | 日历的小时(24小时制) |
MINUTE | 日历的分钟 |
SECOND | 日历的秒 |
DAY_OF_WEEK | 一周的天数,1是星期日 |
DAY_OF_MONTH | 和DATE一样 |
DAY_OF_YEAR | 当前年的天数,1是一年的第一天 |
WEEK_OF_MONTH | 当前月内的星期数,1是该月的第一个星期 |
WEEk_OF_YEAR | 当前 年内的星期数,1是该年的第一个星期 |
AM_PM | 表明是上午还是下午(0表示上午,1表示下午) |
接口是一种与类相似的结构,只包含常量和抽象方法。
接口在许多方面都与抽象类很相似,但是它的目的是指明相关或者不相关类的多个对象的共同行为。例如,使用正确的接口,可以指明这些对象是可比较的、可食用的,以及可克隆的。
为了区分接口和类,Java采用下面的语法来定义接口:
修饰符 interface 接口名 {
/** 常量声明 */
/** 方法签名 */
}
下面是一个接口的例子:
modifier interface InetrfaceName {
/** Constant declarations */
/** Abstract method signatures */
}
在Java中,接口被看作是一种特殊的类。就像常规类一样,每个接口都被编译为独立的字码节文件。使用接口或多或少有点像使用抽象类。例如,可以使用接口作为引用变量的数据类型或类型转换的结果等。与抽象类相似,不能使用new操作符创建接口的实例。
可以使用Edible接口来明确一个对象是否是可食用的。这需要使用implements关键字让对象的类实现这个接口来完成。类和接口之间的关系称为接口继承(interface inheritance)。因为接口继承和类继承本质上是相同的,所以我们将它们都简称为继承。
由于接口中所有的数据域都是public static final而且所有的方法都是public abstract,所以Java允许忽略这些修饰符。因此,下面的接口定义是等价的:
public interface T {
public static final int k = 1;
public abstract void p();
}
等价于
public interface T {
int k;
void p();
}
Comparable接口定义了compareTo方法,用于比较对象。
假设要求一个求两个相同类型对象中较大者的通用方法。这里的对象可以是两个学生、两个日期、两个圆、两个矩形或者两个正方形。为了实现这个方法,这两个对象必须是可比较的。因此,这两个对象都该有的共同方法就是comparable(可比较的)。为此,Java提供了Comparable接口。接口的定义如下所示:
// Interface for comparing objects, defined in java.lang
package java.lang
public interface Comparable {
public int compareTo(E o);
}
compareTo方法判断这个对象相对于给定对象o的顺序,并且当这个对象小于、等于或者大于给定对象o时,分别返回负整数、0或正整数。
Comparable接口是一个泛型接口。在实现该接口时,泛型类型E被替换成一种具体的类型。Java类库中的许多类实现了Comparable接口以定义对象的自然顺序。Byte、Short、Integer、Long、Float、Double、Character、BigInteger、BigDecimal、Calendar、String已经Date类都实现了Comparable接口。例如,在Java API中,Integer、BigInteger、String以及Date类都如下定义:
public class Integer extends Numbers
implements Comparable {
// class body omitted
@Override
public int compareTo(Integer o) {
// Implementation omitted
}
}
public class BigInteger extends Number
implements Comparable {
// class body omitted
@Override
public int compareTo(BigInteger o) {
// Implementation omitted
}
}
public class String extends Object
implements Comparable {
// class body omitted
@Override
public int compareTo(String o) {
// Implementation omitted
}
}
public class Date extends Object
implements Comparable {
// class body omitted
@Override
public int compareTo(Date o) {
// Implementation omitted
}
}
因此,数字是可比较的,字符串是可比较的,日期也是如此。可以使用compareTo方法来比较两个数字、两个日期。例如,下面代码
1. System.out.println(new Integer(3).compareTo(new Integer(5)));
2. System.out.println("ABC".compareTo("ABE"));
3. java.util.Date date1 = new java.util.Date(2013, 1, 1);
4. java.util.Date date2 = new java.util.Date(2012, 1, 1);
5. System.out.println(date1.compareTo(date2));
显示
-1
-2
1
第一行显示一个负数,因为3小于5.第二行显示一个负数,因为ABC小于ABE。第五行显示一个正数,因为date1大于date2。
将n赋值为一个Integer对象,s为一个string对象,d为一个Date对象。下面的所有表达式都为true。
n instanceof Integer
n instanceof Object
n instanceof Comparable
s instanceof String
s instanceof Object
s instanceof Comparable
d instanceof java.util.Date
d instanceof Object
d instanceof Comparable
由于所有Comparable对象都有compareTo方法,如果对象是Comparable接口类型的实例的话,Java API中的java.util.sort(Object[])方法就可以使用compareTo方法来对数组中的对象进行比较和排序。
接口提供通用程序设计的另一种形式。
Object类包含equals方法,它的目的就是为了让Object类的子类来覆盖它,以比较对象的内容是否相同。假设Object类包含一个类似于Comparable接口中所定义的compareTo方法,那么sort方法可以用来比较一组任意的对象。Object类中是否应该包含ygcompareTo方法尚有争论。由于在Object类中没有定义compareTo方法,所以Java中定义了Comparable接口,以便能够对两个Comparable接口的实例进行比较。强烈建议(尽管不要求)conpareTo应该与equals保持一致。也就是说,对于两个对象o1和o2,应该确保当且仅当o1.equals(o2)为true是o1.compareTo(o2) == 0成立。
Cloneable接口给出了一个可克隆的对象。
经常会出现需要创建一个对象拷贝的情况。为了实现这个目的,需要使用clone方法并理解Cloneable接口。
接口包括常量和抽象方法,但是Cloneable接口是一个特殊情况。在java.util.lang包中的Cloneable接口的定义如下所示:
package java.lang
public interface Cloneable {
}
这个接口是空的。一个带空体的接口标为标记接口(marker interface)。一个标记接口既不包括常量也不包括方法。它用来表示一个类拥有某些特定的属性。实现Cloneable接口的类被标记为可克隆的,而且它的对象可以使用在Object类中定义的clone()方法克隆。
Java库中的很多类(例如,Date、Calendar和ArrayList)实现Cloneable。这样,这些类的实例可以被克隆。例如,下面的代码
1. Calendar calendar = new GregorianCalendar(2013, 2, 1);
2. Calendar calendar1 = calendar;
3. Calendar calendar2 = (Calendar)calendar.clone();
4. System.out.println("calendar == calendar1 is " +
5. (calendar == calendar1));
6. System.out.println("calendar == calendar2 is " +
7. (calendar == calendar2));
8. System.out.println("calendar.equals(calendar2) is " +
9. calendar.equals(calendar2));
显示
calendar == calendar1 is true
calendar == calendar2 is false
calendar.equals(calendar2) is true
在前面的代码中,第2行将calendar的引用复制给calendar1,所以calendar和calendar1都指向相同的Calendar对象。第3行创建一个新对象,它是calendar的克隆,然后将这个新对象的引用赋值给calendar2。calendar2和calendar是内容相同的不同对象。
下面的代码。
1. ArrayList list1 = new ArrayList<>();
2. list1.add(1.5);
3. list1.add(2.5);
4. list1.add(3.5);
5. ArrayList list2 = (ArrayList)list1.clone();
6. Arraylist list3 = list1;
7. list2.add(4.5);
8. list3.remove(1.5);
9. System.out.println("list1 is " + list1);
10. System.out.println("list2 is " + list2);
11. System.out.println("list2 is " + list3);
显示
list1 is [2.5, 3.5]
list2 is [1.5, 2.5, 3.5, 4.5]
list3 is [2.5, 3.5]
前面的代码中,第5行创建了一个新对象作为list1的克隆,并且将新对象的引用赋值给list2。list2和list1是具有同样内容的不同对象。第6行复制list1的引用给list3,因此list1和list3指向同一个ArrayList对象。第7行将4.5添加到list2中。第8行从list3中移除1.5。由于list1和list3指向同一个ArrayList,第9行和第11行显示同样的内容。
可以调用clone方法克隆一个数组。例如,下面的代码
1 int[] list1 = {1, 2};
2 int[] list2 = list1.clone();
3 list[0] = 7;
4 list[1] = 8;
5 System.out.println("list1 is " + list1[0] + ", " + list1[1]);
6 System.out.println("list1 is " + list2[0] + ", " + list2[1]);
显示
list1 is 7,2
list2 is 1,8
为了定义一个实现Cloneable接口,这个类必须覆盖Object类中的clone()方法。
浅复制意味着如果数据域是对象类型,那么复制的是对象的引用,而不是它的内容。
接口的使用和抽象类的使用基本类似,但是,定义一个接口与定义一个抽象类有所不同。表总结了这些不同点。
变量 | 构造方法 | 方法 | |
---|---|---|---|
抽象类 | 无限制 | 子类通过构造方法链调用构造方法,抽象类不能用new操作符实例化 | 无限制 |
接口 | 所有的变量必须是public static final | 没有构造方法。接口不能用new操作符实例化 | 所有方法必须是公共的抽象实例方法 |
Java只允许为类的扩展做单一继承,但是允许使用接口做多重扩展。例如,
public class NewClass extends BaseClass
implements Interface1, ..., InterfaceN {
...
}
利用关键字extends,接口可以继承其他接口。这样的接口称为子接口(subinterface)。例如,在下面的代码中,NewInterface是Interface1, ..., InterfaceN的子接口。
public interface NewInterface extends Interface1, ..., Interface {
// constans and abstract methods
}
一个实现NewInterface的类必须实现在NewInterface,Interface1,…,InterfaceN中定义的抽象方法。接口可以扩展其他接口而不是类。一个类可以扩展它的父类同时实现多个接口。
所有的类共享同一个根类Object,但是接口没有共同的根。与类相似,接口也可以定义一种类型。一个接口类型的变量可以引用任何实现该接口的实例。如果一个类实现了一个接口,那么这个接口就类似于该类的一个父类。可以将接口当作一种数据类型使用,将接口类型的变量转换为它的子类,反过来也可以。例如,假设c是图中Class2的一个实例,那么c也是Object、Class1、Interface1、Interface1_1、Interface1_2、Interface2_1和Inetrface2_2的实例。
类名是一个名词。接口名可以是形容词或名词。
抽象类和接口都是用来明确多个对象的共同特征的。那么该如何确定在什么情况下应该使用接口,什么情况一个使用类呢?一般来说,清晰描述父子关系的强的“是一种”的关系(strong is-a relationship)应该使用类建模。例如,因为公历是一种日历,所以类java.util.GregorianCalendar和java.util.Calendar使用类继承建模的。弱的“是一种”的关系(weak is-a relationship)也称为类属关系(is-kind-of relationship),它表明对象拥有属性,可以用接口来建模。例如,所有的字符串都是可比较的,因此,String类实现Comparable接口。
通常,推荐使用接口而非抽象类,因为接口定义非相关类共有的父类型。接口比类工具灵活。考虑Animal类。假设Animal类类中定义了howToEat方法,如下所示:
abstract class Animal
public abstract String howToEat();
}
Animal的两个子类定义如下:
class Chicken extends Animal {
@Override
public String howToEat() {
return "Fry it";
}
}
class Duck extends Animal
@Override
public String howToEat() {
return "Roast it";
}
}
假设给定这个继承体系结构多态会让你在一个类型为Animal的变量中保存Chicken对象或Duck对象的引用,如下面代码所示:
public static void main(String[] args) {
Animal animal = new Chicken();
eat(animal);
animal = new Duck();
eat(animal);
}
public static void eat(Animal animal) {
animal.howToEat();
}
JVM会基于调用方法时所用的确切对象来动态地决定调用哪个howToEat方法。
可以定义Animal的一个子类。但是,这里有个限制条件。该子类必须是另一种动物(例如,Turkey)。
接口就无此限制。接口比类拥有更多的灵活性,因为不用使所有东西都属于同一个类型的类。可以定义接口中的howToEat()方法,然后把它当作其他类的公用父类型。
public static void main(String args[]) {
Edible stuff = new Chicken();
eat(stuff);
stuff = new Duck();
eat(stuff);
stuff = new Broccoli();
eat(stuff);
}
public static void eat(Edible stuff) {
stuff.howToEat();
}
interface Edible {
public String howToEat();
}
class Chicken implements Edible {
@Override
public String howToEat() {
return "Fly it";
}
}
class Duck implements Edible {
@Override
public String howToEat() {
return "Roast it";
}
}
class Broccoli implements Edible {
@Override
public String howToEat() {
return "Stir-fly it";
}
}
为了定义表示可食用对象的一个类,只须让该类实现Edible接口即可。选择,这个类就成为Edible类型的子类型。任何Edible对象都可以传递以调用howToEat方法。
类的设计原则有助于设计出合理的类
类应该描述一个单一的实体,而所有的类操作应该在逻辑上相互配合,支持一个一致的目的。例如:可以设计一个类用于学生,但不应该将学生与教职工组合在同一个类中,因为学生和教职工是不同的实体。
如果一个实体担负太多的职责,就应该按各自的职责分成几个类。例如,String类、StringBuffer类和StringBuilder类都用于处理字符串,但是它们的职责不同。String类处理不可变字符串,StringBuilder类处理可变字符串,StringBuffer类与StringBulider类相似,只是StringBuffer类还包含更新字符串的同步方法。
遵循标准Java程序设计风格和命名习惯。为类、数据域和方法选取具有信息的名字。通常的风格是将数据声明置于构造方法之前,并且将构造方法置于方法之前。
选择名字要保持一致。给类似的操作选择不同的名字并非良好的实践。例如,length()方法返回String、StringBuilder和StringBUffer的大小。如果在这些类中给这个方法用不同的名字就不一致了。
一般来说,一个具有一致性地提供一个公共无参构造方法,用于构建默认实例。如果一个类不支持无参的构造方法,要用文档写出原因。如果没有显示定义构造方法,即假定有一个空方法体的公共默认无参构造方法。
如果不想让用户创建类的对象,可以在类中声明一个私有的构造方法,Math类就是如此。
一个类应该使用private修饰符隐藏其数据,以免用户直接访问它。这使得类更易于维护。
只在希望数据域可读的情况下,才提供get方法;也只在希望数据域可更新的情况下,才提供set方法。例如:Rational类为numerator和denominator提供了get方法,但是没有提供set方法,因为Rational对象是不可改变的。
为使设计清晰,内聚性、一致性和封装性都是很好的设计原则。除此之外,类应该有一个很清晰的合约,从而易于解释和理解。
用户可以以各种不同组合、顺序,以及在各种环境中结合使用多个类。因此,在设计一个类时,这个类不应该限制用户如何以及何时使用该类;以一种方式设计属性,以容许用户按值的任何顺序和任何组合来设置;设计方法一个使得实现的功能与它们出现的顺序无关。例如:Loan类包含属性loanAmount、numberOfYears和annualInterestRate,这些属性的值可以按任何顺序来设置。
方法应在不产生混淆的情况下进行直观定义。例如:String类中的substring(int beginIndex, int endIndex)方法就有一点混乱。这个方法返回从beginIndex到endIndex -1 而不是endIndex的子串。该方法应该返回从beginIndex到endIndex的子字符串,从而更加直观。
不应该声明一个来自其他数据域的数据域。例如,下面的Person类有两个数据域:birthDate和age。由于age可以从birthDate导出,所以age不应该声明为数据域。
public class Person {
private java.util.Date birthDate;
private int age;
...
}
类是为许多不同的用户使用而设计的。为了能在一个广泛的应用中使用,一个类一个通过属性和方法提供多种方案以适应用户的不同需求。例如,为满足不同的应用需求,String类包含了40多种很实用的方法。
依赖于类的具体实例的变量或方法必须是一个实例变量或方法。如果一个变量被类的所有实例所共享,那就应该将它声明为静态的。例如:在程序清单9-8中,CircleWithPrivateDataFields中的变量numberOfObjects被CircleWithPrivateDateFields类的所有对象共享。因此,它被声明为静态的。如果方法不依赖于某个具体的实例,那就应该将它声明为静态方法。例如:CricleWithPrivateDataFields中的getNumberOfObjects()方法没有绑定到任何具体实例,因此,他被声明为静态方法。
应该总是使用类名(而不是引用变量)引用静态变量和方法,以增强可读性并避免错误。
不要从构造方法中参入参数来初始化静态数据域。最好使用set方法改变静态数据域。图a中的类最好用图b中的代替。
public class SomeThing {
private int t1;
private static int t2;
public SomeThing(int n1, int n2) {
...
}
}
public class SomeThing {
private int t1;
private static int t2;
public SomeThing(int t1) {
...
}
public static void setT2(int t2) {
SomeThing.t2 = t2;
}
}
实例和静态是面向程序设计不可或缺的部分。数据域或方法要么是实例的,要么是静态的。不要错误的忽视了静态数据域或方法。常见的设计错误是将本应该声明为静态方法的方法声明为实例方法。例如:用于计算n的阶乘factional(int n)方法一个定义为静态的,因为它不依赖于任何具体实例。
构造方法永远都是实例方法,因为它是用来创建具体实例的。一个静态变量或方法可以从实例方法中调用,但是不能从静态方法中调用实例变量或方法。
继承和聚合之间的差异,就是is-a(是一种)和has-a(具有)之间的关系。例如,苹果是一种水果;因此,可以使用继承来对Apple类和Fruit类之间的关系进行建模。人具有名字;因此,可以使用聚合来对Person类和name类之间的关系建模。
接口和抽象类都可以用于为对象指定共同的行为。任何决定是采用接口还是类呢?通常,比较强的is-a(是一种)关系清晰地描述了父子关系,应该采用类来建模。例如,因为桔子是一种水果,它们的关系就应该采用类的继承关系来建模。弱的is-a关系,也成为是is-kind-of(是一类)关系,表明一个对象拥有某种属性。弱的is-a关系可以使用接口建模。例如,所有的字符串都是可以比较的,因此String类实现了Comparable接口。圆或者矩形是一个几何对象,因此Circle可以设计为GeometricObject的子类。圆有不同的半径,并且可以基于半径进行比较,因此Circle可以实现Comparable接口。
接口比抽象类更加灵活,因为一个子类只能继承一个父类,但是却可以实现任意个数的接口。然而,接口不能具有具体的方法。可以结合接口和抽象类的优点,创建一个接口,使用一个抽象类来实现它。可以使其方便使用接口或者抽象类。