6.12 设计类
1) 通过已定义的基类派生子类, 并且添加方法和数据成员来自定义子类, 创建出类的层次结构; Dog ‘IS-A’ Animal
2) 定义一系列没有层次结构, 由类对象作为数据成员的集合; Zoo ‘HAS-A’ animals
[继承和组合]
继承可以实现多态, 接口, 是面向对象的本质; 将一系列相关对象做相同的处理, 很大程度上简化了程序;
类的示例
PolyLine ‘IS-A’ Line; 一类对象是另一类对象的特殊形式, 可以作为派生类;
PolyLine ‘HAS-A’ Point; 对象中的一个成员是另一个类的对象; House HAS-A Door;
设计PolyLine类 多段线
需求: 能将线段添加到一个已经存在的对象上;
ISSUE: 使用数组存储Point的话会比较低效, 因为增加一条线段就需要新建新的数组, 然后将旧的数组复制到新数组中;
SOLUTION: 创建一个点的链表linked list; 链表中的每个对象都有一个指向下一个对象的引用作为数据成员; 只要有一个变量包含指向第一个Point对象的引用, 就可以访问链表中所有点;
定义了Point类, 有两个成员 double x, double y;
ListPoint类:
1) 定义一个显式存储x, y坐标的ListPoint类;
2) ListPoint对象包含一个指向Point对象的引用和指向链表中后续ListPoint对象的成员;
3) 将ListPoint对象作为特殊的Point;
使用第二种方式, 比较符合直觉; 使用Point类型数据成员, 这个成员定义了一个基本的点, ListPoint拥有额外的数据成员: next, 包含指向链表下一个对象的引用; 以及start; 当next成员指向null, 表示到达链表end;
1 2 3 4 5 6 7 8 | public ListPoint(Point point) { this .point = point; // Store point reference next = null ; // Set next ListPoint as null } // Set the pointer to the next ListPoint public void setNext(ListPoint next) { this .next = next; // Store the next ListPoint }
|
>ListPoint对象使用Point对象来构造, 数据成员point会被拷贝构造;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Construct a polyline from an array of points public PolyLine(Point[] points) { if (points != null ) { // Make sure there is an array for (Point p : points) { addPoint(p); } } } // Construct a polyline from an array of coordinates public PolyLine( double [][] coords) { if (coords != null ) { for ( int i = 0 ; i < coords.length ; ++i) { addPoint(coords[i][ 0 ], coords[i][ 1 ]); } } } // Add a Point object to the list public void addPoint(Point point) { ListPoint newEnd = new ListPoint(point); // Create a new ListPoint if (start == null ) { start = newEnd; // Start is same as end } else { end.setNext(newEnd); // Set next variable for old end as new end } end = newEnd; // Store new point as end }
|
>PolyLine类含有数据成员start和end, 引用了链表中的头尾点, 链表为空时, 值为null; 可以不需要end, 因为可以从start遍历到null就是end; 但是想要添加点到链表中, 有一个end能提高效率;
>addPoint()方法用传入的Point创建一个ListPoint对象, 将旧的end的next成员指向新的点, 然后将新的点存储到end成员中(新的点的next是null);
1 2 3 4 5 6 7 8 9 10 | @Override public String toString() { StringBuffer str = new StringBuffer( "Polyline:" ); ListPoint nextPoint = start; // Set the 1st point as start while (nextPoint != null ) { str.append( " " + nextPoint); // Output the current point nextPoint = nextPoint.getNext(); // Make the next point current } return str.toString(); }
|
>遍历方式是取得next, 检查是否为null;
1) 通用链表
使用Object类变量存储任意类型的对象, 使用ListItem类重新实现ListPoint类;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class ListItem { // Constructor public ListItem(Object item) { this .item = item; // Store the item next = null ; // Set next as end point } // Return class name & object @Override public String toString() { return "ListItem " + item ; } ListItem next; // Refers to next item in the list Object item; // The item for this ListItem }
|
>与ListPoint类似, 省略了对next的set和get;
2) 定义链表类
相同的机制, 只是将ListItem作为一个内部类;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | public class LinkedList { // Default constructor - creates an empty list public LinkedList() {} // Constructor to create a list containing one object public LinkedList(Object item) { if (item != null ) { current = end = start = new ListItem(item); // item is the start and end } } // Construct a linked list from an array of objects public LinkedList(Object[] items) { if (items != null ) { // Add the items to the list for (Object item : items) { addItem(item); } current = start; } } // Add an item object to the list public void addItem(Object item) { ListItem newEnd = new ListItem(item); // Create a new ListItem if (start == null ) { // Is the list empty? start = end = newEnd; // Yes, so new element is start and end } else { // No, so append new element end.next = newEnd; // Set next variable for old end end = newEnd; // Store new item as end } } // Get the first object in the list public Object getFirst() { current = start; return start == null ? null : start.item; } // Get the next object in the list public Object getNext() { if (current != null ) { current = current.next; // Get the reference to the next item } return current == null ? null : current.item; } private ListItem start = null ; // First ListItem in the list private ListItem end = null ; // Last ListItem in the list private ListItem current = null ; // The current item for iterating // ListItem as a nested class private class ListItem { // ListItem class defi nition as before... } }
|
>三个构造函数: 空链表, 单个对象的链表, 从对象数组创建链表; 链表通过addItem()进行扩展, 构造时将current设成第一个数据项, 用来遍历链表;
>ListItem作为private, 增加了安全性;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class PolyLine { //... // Construct a polyline from an array of points public PolyLine(Point[] points) { polyline = new LinkedList(points); // Create the polyline } // Add a Point object to the list public void addPoint(Point point) { polyline.addItem(point); // Add the point to the list } // Add a point from a coordinate pair to the list public void addPoint( double x, double y) { polyline.addItem( new Point(x, y)); // Add the point to the list } //... private LinkedList polyline; // The linked list of points }
|
>创建链表的任务转移到了LinkedList类的构造中, 链表的操作包装到了LinkedList中;
>使用addItem()时, Point到Object的转换是自动进行的(Object是顶层基类, 对象向上转换成Object类型总是自动进行的, 向下转换时才需要显式设定);
>java.util包定义了泛型的LinkedList;
6.13 final 修饰符
final关键字可以固定static数据成员的值, 也可以应用于方法和类的定义;
1) 将方法声明为final, 阻止子类覆盖; abstract方法无法声明为final;
1 | public final void addPoint(Point point) {...}
|
2) 将类声明为final, 阻止派生子类; 抽象类不能被声明为final;
1 | public final class PolyLine {}
|
6.14 接口
接口interface, 所有的子类共享通用的接口, 实现的方法可以通过接口类型的变量进行多态调用;
接口本质上是一系列相关的常量和抽象方法的集合, 一般情况下只包含方法; 接口没有方法的定义, 只有声明: 名称, 参数, 返回类型, 接口的方法都是抽象方法;
使用接口时, 要在类中实现接口: 使用implements关键字声明类, 并且实现接口声明的方法; 接口中定义的常量可以在实现的类中直接使用;
Note 接口包含常量和抽象方法, 接口中的方法默认总是public和abstract的, 绝对不能添加除了默认, public和abstract以外的任何属性; 因此接口中的方法永远都不能是静态的; 接口中的常量总是默认public, static和final的;
定义接口时将class关键字换成interface, java文件名和接口名一致; 接口的名称必须不同于同一包内的其他接口和类;
6.14.1 程序中封装常量
old: 接口中定义常量; new: 静态导入;
1) 接口中的常量
1 2 3 4 | public interface ConversionFactors { double INCH_TO_MM = 25.4 ; //... }
|
Note 接口中的常量会被自动设定为public static final类型; 必须为接口中定义的常量提供初始化值; 约定以大写字母表示final类型的常量;
直接使用时加上接口名称限定; ConversionFactors.POUND_TO_GRAM;
类可以通过声明实现接口来使用常量, 不需要限定名称 (old)
1 2 3 4 5 6 7 8 | public class MyOtherClass implements ConversionFactors { // This class can access any of the constants defined in ConversionFactors // using their unqualified names, and so can any subclasses of this class... public static double poundsToGrams( double pounds) { return pounds*POUND_TO_GRAM; } // Plus the rest of the class definition... }
|
>常量作为成员被继承到了派生类MyOtherClass;
使用类将常量作为静态域包含, 按照需求导入域名的方法更简单有效;
2) 类中定义常量
1 2 3 4 5 6 | package conversions; // Package for conversions public class ConversionFactors { public static final double INCH_TO_MM = 25.4 ; public static final double OUNCE_TO_GRAM = 28.349523125 ; //... }
|
可以通过限定名称从外部访问类成员: ConversionFactors.HP_TO_WATT;
或者将类的静态成员导入到需要使用的类中, 省去限定名称, 这种情况下类必须是有名称的包, import不能使用无名称的包;
1 2 3 4 5 6 7 8 | import static conversions.ConversionFactors.*; // Import static members public class MyOtherClass { // This class can access any of the constants defined in ConversionFactors public static double poundsToGrams( double pounds) { return pounds*POUND_TO_GRAM; } // Plus the rest of the class definition... }
|
>不需要限定名称, 需要import语句, 也可以只导入需要使用的静态成员; import static conversions.ConversionFactors.POUND_TO_GRAM;
也可以使用类的实例方法[static]访问他们;
6.14.2 用接口声明方法
接口的主要作用是定义一系列表示特定功能的方法的形式;
1 2 3 4 5 | public interface Conversions { double inchesToMillimeters ( double inches); double ouncesToGrams( double ounces); //... }
|
接口中声明的每个方法都必须在实现接口的类中定义, 这样才能创建类的对象;
1 2 3 4 | public class MyClass implements Conversions { // Implementations for the methods in the Conversions interface // Definitions for the other class members... }
|
Note 接口中的方法都是默认public, 所以在实现类中的定义必须使用public;
类可以实现多个接口, 使用逗号隔开: public class MyClass implements Conversions, Definitions, Detections {…}
接口定义和类定义类似, 文件名必须和接口名一致;
实现类中接口的定义不能是static类型, 因为接口方法是实例方法, 必须由对象调用;
实现部分接口
可以在实现接口的类中实现部分方法, 这样类会从接口中继承抽象方法, 类本身需要声明为abstract;
1 2 3 4 5 6 7 8 9 10 11 | import static conversions.ConversionFactors; public abstract class MyClass implements Conversions { // Implementation of two of the methods in the interface @Override public double inchesToMillimeters( double inches) { return inches*INCH_TO_MM; } @Override public double ouncesToGrams( double ounces) { return ounces*OUNCE_TO_GRAM; } // Definition of the rest of the class... }
|
>MyClass类不能创建对象; 要获得对象, 必须派生子类实现接口中剩余未定义的方法;
Note 普通实现接口的类不需要使用@Override标记[所有方法必须实现], 但是对于实现接口的abstract类, 建议使用@Override标记, 防止方法签名不一致的错误, 编译器无法检查是否特殊意图[参数写错还是overload];
6.14.3 扩展接口
通过extends标识基本接口名, 可以定义基于基本接口的新接口;
本质上和类的派生一样; 扩展的接口获得基本接口的所有方法常量;
1 2 3 4 5 | public interface Conversions extends ConversionFactors { double inchesToMillimeters( double inches); double ouncesToGrams( double ounces); //... }
|
>和超类相似, ConversionFactor是Conversions的超接口 super-interface;
接口和多重继承
接口可以任意扩展多个接口; 在extends后面设定接口, 使用逗号隔开;
public interface MyInterface extends HisInterface, HerInterface {…}
Note 多重继承 multiple inheritance, 类不支持多重继承, 只有接口支持;
Note 如果两个或更多的超接口声明了相同签名的方法, 那么这个方法必须在声明它的所有类[接口]中都有相同的返回类型;
类中的每一个方法都必须有一个唯一签名, 返回类型不属于签名;
6.14.4 使用接口
接口声明方法, 表示一系列标准操作; 接口为一系列操作定义了契约contract;
接口定义了一种类型, 通过实现同一个接口的一系列类来实现多态;
1) 接口与多态
接口类型无法创建对象, 可以创建变量;
1 | Conversions converter = null ; // Variable of the Conversions interface type
|
接口类型的引用可以指向实现类的对象; 可以使用接口变量多态地调用接口方法;
接口中的方法总是abstract, 默认为public的;
2) 使用多个接口
要调用哪种接口的方法, 就需要要哪种类型的接口变量来存储对象引用, 或者将对象引用转换成实际的类类型;
1 2 3 | public MyClass implements RemoteControl, AbsoluteControl { // Class definition including methods from both interfaces... }
|
类型转换:
1 2 | AbsoluteControl ac = new MyClass(); ((MyClass)ac).powerOnOff();
|
>这样丢失了多态行为; [可以用多重继承的接口解决]
1 2 | if (ac instanceof RemoteControl) ((RemoteControl)ac).mute();
|
Note 接口之间不相关也可以转换引用, 前提是类继承了这两个接口;
6.14.5 将接口作为方法的参数
将方法的参数设定为接口类型, 对于实现接口的任意类型对象, 都能作为参数传入方法;
标准类库中的方法通常有接口类型的参数, e.g. String, StringBuilder, StringBuffer, CharBuffer都实现了CharSequence接口;
6.14.6 在接口定义中嵌套类
可以在接口定义中放入类定义, 作为接口的内部类, 接口的内部类默认是static和public类型;
1 2 3 4 5 6 | interface Port { // Methods & Constants declared in the interface... class Info { // Definition of the class... } }
|
>内部类类型是Port.Info; e.g. Port.Info info = new Port.Info();
标准类库有大量包含内部类的接口, e.g. Port – java.sound.sampled接口有Info内部类;
当内部类类型的对象和接口有很强的关联时, 需要定义接口的内部类; 实现接口的类与接口的内部类没有直接联系;
6.14.7 接口与真实环境
接口类型有时被用于引用封装Java之外内容的对象 [3rdParty, Native API], 比如特定的物理设备;
标准类库中的Port接口, Port类型引用会指向一个对象, 对象是声卡上的物理端口, 比如扬声器, 麦克风; 内部类Port.Info定义了用来封装数据以定义端口的对象;
6.15 匿名类
匿名类anonymous class的定义出现在创建和使用匿名类对象的语句的new表达式中; 一般使用在简单短小的类上;
1 2 3 4 | pickButton.addActionListener( new ActionListener() { // Code to define the class // that implements the ActionListener interface });
|
>类定义出现在创建参数的表达式中: new ActionListener() {…}; 作为一个实现了ActionListerner接口的类的引用;
Note 匿名类包含数据成员和方法, 但不包含构造函数, 匿名类没有名称;
如果匿名类扩展了一个已有的类, 语法基本不变; 匿名类的定义必须出现在花括号中;
6.16 小结
抽象方法: 没有定义方法体的方法, 使用abstract 关键字进行定义;
抽象类: 包含一个或多个抽象方法的, 必须使用abstract 属性进行定义;
派生类: 基于一个类定义另一个类, 这叫做类的派生或继承; 基类被称为超类, 而派生类被称为子类; 超类也可以是另一个超类的子类;
通用超类: 每个类都以Object 类作为基类, 所以每个类都从Object 类中继承成员; toString()方法就是从Object 类中继承而来的;
抽象派生类: 如果抽象类的子类不能为从超类继承而来的所有抽象方法提供实现, 那么就必须被声明为abstract 类型;
类继承: 子类可以继承超类的某些成员; 类的继承成员能够与普通类声明的成员一样被引用和使用;
继承构造函数: 子类不继承超类的构造函数;
私有类成员和包私有类成员: 超类的private 成员不会被继承到子类中; 如果子类与超类不在同一个包中, 那么超类中没有访问属性的成员不会被继承;
实现派生类构造函数: 在子类的构造函数中, 方法体的第一条语句应该是调用超类的一个构造函数; 如果不是, 编译器会插入对超类默认构造函数的调用;
多态: 子类能够通过在定义中重新实现从超类继承的方法来覆盖这些方法; 如果两个或更多个含有一个共同基类的子类覆盖了一个通用方法的集合, 那么这些方法将能够在
运行时被动态地选择执行, 这种行为被称为多态;
多态行为: 通过多态调用选择的子类方法取决于用于调用子类方法的对象类型;
使用超类类型的变量: 超类类型的变量能指向任意子类的对象, 这样的变量能被用于执行从超类多态继承得到的子类方法;
@Override: 应该对能多态调用的派生类方法使用@Override 标记, 这样可以保护由于派生类中不正确的方法签名而引起的错误;
静态导入: 将已命名包中的类的static 成员导入到其他的类中, 以便能够通过导入的static成员的非限定名称来引用它们;
枚举类类型: 枚举类型是一种特殊形式的类, 而且定义的枚举常量都是枚举类类型的实例;
嵌套类: 在另一个类中定义的类称为嵌套类或内部类; 内部类本身也可能包括另一个内部类;
接口: 接口中可以包含常量, 抽象方法以及内部类;
在类中实现接口: 类可以通过在类定义中声明一个或多个接口, 并且通过包括实现每个接口方法的代码来实现它们;
实现部分接口: 如果一个类没有实现自身需要实现的接口所定义的所有方法, 那么这个类就必须被声明为abstract 类型;
接口类型与多态: 如果多个类实现了一个通用接口, 那么声明为接口成员的方法将能够通过使用接口类型的变量存储指向类类型对象的引用而被多态地执行;
—6 End—