31、使用Class类型【快速浏览】
对象导向设计中,对象并不是凭空产生的,您必须先定义您的对象,您要一个规格书,这个规格书称之为类别(Class)。
在Java中使用"class"关键词来书写类别(规格书),您使用类别来定义一个对象(object)时,您考虑这个对象可能拥有的「属性」(Property,在Java中则是用Field)与「方法」(Method)。属性是参与对象内部运算的数据成员,而方法则是对象与外界互动的动态操作。
您使用类别定义出对象的规格书,之后根据这个规格书来建构对象,然后透过对象所提供的操作接口来与程序互动。
举个例子来说,您可以定义一个对象:「球」。
考虑球有各种不同的颜色(或名称),以及球最基本的球半径信息,您想到这些信息应该可以取得,并可以进一步取得球的体积,当您在Java中要定义这些信息时,您可以如下进行定义:
public class Ball { private double radius;//半径 private String name;//名称 //无参数构造函数 public Ball(){ this(0.0,"no name"); } //有参数构造函数 public Ball(double radius, String name) { this.radius = radius; this.name = name; } //获取属性 public double getRadius(){ return radius; } public String getName(){ return name; } //设置属性 public void setRadius(double radius){ this.radius = radius; } public void setName(String name){ this.name = name; } }
一个定义良好的类别,即使在不看程序代码实作的情况下,也可以从定义中所提供的公开(public)方法看出这个类别的大致功能。
在类别中的运算参与数据(Field)及互动方法(Method),我们统称其为 类别成员(Class member)。
上例中的radius、name成员是field成员,getRadius()与getName()是method成员。注意到"public"这个关键 字,它表示所定义的成员可以使用宣告的对象名称加上 '.' 运算子直接呼叫,也称之为「公用成员」或「公开成员」。而private这个关键词用来定义一个「私用成员」,它不可以透过参考名称直接呼叫,又称之为 「私有成员」。
在定义类别时,有一个基本原则是:信息的最小化公开。也就是说尽量透过方法来操作对象,而不是直接存取其内部运算参与数据(也就是field成员)。
信息的最小化公开原则是基于安全性的考虑,避免程序设计人员随意操作field成员而造成程序的错误,您可以在日后的程序设计中慢慢来体会;在稍后的实作中,您将可以看到,我们将不会radius与name两个私用成员直接进行存取,而是透过公开的方法来进行设定。
一个类别中的field成员,若宣告为"private",则其可视范围(Scope)为整个类别,由于外界无法直接存取私用成员,所以您使用两个公开方法 getRadius()与getName()分别传回其这两个成员的值。
与类别名称同名的方法称之为 建构方法 Cconstructor),也有人称之为「建构子」,它没有传回值。顾名思义,建构方法的作用是让您建构对象可以设定一些必要的建构信息,它可 以被重载(Overload),以满足对象生成时不同的设定条件。
您在实作中重载了建构方法,在不指定参数的情况下,会将radius设定为0.0,而name设定为 "no name",另一个建构方法则可以指定参数,this()方法用于对象内部,表示呼叫对象的建构方法,另一个就是this,它表示对象本身,您可以在 关于 this 进一步了解其作用。
定义好类别之后,您就可根据这个类别(规格)来建构对象,建构对象时使用new关键词,顾名思义,就是根据所指定的类别(规格书)「新建」一个对象:
Ball ball1 = new Ball(); Ball ball2 = new Ball(3.5, "red ball");
在上例中配置了ball1与ball2两个对象,ball1对象在建立时并不指定任何参数,所以根据之前对Ball类别的定义,ball1的radius 将设定为0.0,name设定为"no name";ball2则给定两个参数,所以ball2的radius设定为3.5,而ball2的name设定为"red ball"。
您可以透过公开成员来操作对象或取得对象信息,方法是使用对象名称加上「.」运算子,例如:
ball1.getRadius(0.1);
ball1.setName("GBall");
以下先看个简单的程序:
public class SimpleClass { public static void main(String[] args) { Ball b1 = new Ball(18.4, "red ball"); System.out.println("名称: " + b1.getName()); System.out.println("半径: " + b1.getRadius()); } }
类别与对象这两个名词会经常混于书籍与文件之中,例如「您可以使用Scanner类别」、「您可以使用Scanner对象」,这两句在某些场合其语义是相 同的,不过要细究的话,两句的意思通常都是「您可以使用根据Scanner类别所建构出来的对象」,不过写这么长很烦,难免就省略了一些字眼。
Java会将参与内部运算的数据命名为field,其实是蛮有道理的,field在英文中有事件的参与者的意义,有限定范围的意思。基本上,在定义对象 时,field成员其作用范围要限定于对象之中,对对象内部数据的变更,都要透过公开方法来进行,避免field成员的作用范围离开了对象之外。
32、类成员
在Java中,类别的存取权限修饰词有"public"、"protected"、"private"三个,如果在宣告成员时不使用存取修饰词,则预设以套件 (package)为存取范围,也就是说在package外就无法存取,这些存取修饰,之后在 套件(package) 还会见到说明。
方法的参数列用来告知方法成员执行时所需的数据,如果传入的自变量是基本数据型态(Primitive data type),则会将值复制至参数列上的变量,如果传入的自变量是一个对象,则会将参数列上的变量参考至指定的对象。
Math.PI是由Java所提供的功能变量,它定义了圆周率3.14159......,在Math类别中还包括有许多公用的数学功能函式,您可以自行查询 java.lang.Math 在线说明文件以得知这些功能。
另外可以注意到,autoboxing、 unboxing 在方法的参数列中是可以作用的,也就是说如果您的方法中是这样设计的:
public class SomeClass { .... public void someMethod(Integer integer) { ...... } .... }
您可以使用这样的方式来设定自变量:
SomeClass someObj = new SomeClass(); someObj.someMethod(1);
autoboxing、unboxing会自动作用,但记得要小心使用这个功能。
一般在命名类别时,类别名称首字会大写,而方法名称首字是小写,名称命名时以一目了解名称的作用为原则,上面所采取的都是骆驼式的命名方式,也就是每个单字的首字予以适当的大写,例如someMethodOfSomeClass这样的方式,这是常见的一种命名惯例。
为field成员设定setXXX()或getXXX()这类的方法时,XXX名称最好与field名称相对应,例如name这个field 成员对应的方法,可以命名为setName()与getName(),而radius这个成员,则对应于setRadius()与getRadius() 这样的名称,如此阅读程序时可以一目了解setter与getter方法的存取对象。
33、static成员【main函数就是典型的静态函数】
对于每一个基于相同类别所产生的对象而言,其拥有各自的数据成员,然而在某些时候,您会想要这些对象拥有相同的数据成员,其数据是共享的。
举个例子来说,在Ball类别中,您会使用到圆周率的这个数据,对于任一个球而言,圆周率都是一样的,您并不需要让不同的球对象拥有各自的数据成员来记录圆周率,而这个记录的值却是相同,这只会增加内存的消耗而已。
您可以将数据成员宣告为"static",被宣告为"static"的数据成员,它是属于类别所拥有,而不是个别的对象,您可以将"static"视为个别对象所拥有、共享的数据成员。
由于static成员属于类别所拥有,所以在不使用对象名称的情况下,您也可以使用类别名称加上 . 运算子来存取static数据成员。
虽然您也可以在宣告对象之后,使用 . 运算子来存取static数据成员,但是这并不被鼓励,通常建议使用类别名称加上 . 运算子来存取,一方面也可以避免与非static成员混淆。
与静态数据成员类似的,您也可以宣告方法成员为static方法,又称静态方法,被宣告为静态的方法通常是为了提供工具。
与静态数据成员一样的,您可以透过类别名称使用'.'运算子来存取static方法(当然要注意权限设定,例如设定为public)。
静态数据与静态方法的作用通常是为了提供共享的数据或工具方法,例如将数学常用常数或计算公式,以static宣告并撰写,之后您可以把这个类别当作工具,透过类别名称来管理与取用这些静态数据或方法,例如像J2SE 所提供的Math类别上,就有Math.PI这个静态常数,以及Math.Exp()、Math.Log()、Math.Sin()等静态方法可以直接使用,另外还有像Integer.parseInt()、Integer. MAX_VALUE等也都是静态方法与静态数据成员的实际例子。
由于static成员是属于类别而不是对象,所以当您呼叫static方法时,并不会传入对象的位置参考,所以static方法中不会有 this参考,由于没有this参考,所以在Java的static方法成员中不允许使用非static成员,因为程序没有this来参考至对象地址,也 就无法辨别要存取哪一个对象的成员,事实上,如果您在static方法中使用非static数据成员,在编译时就会出现以下的错误讯息:
non-static variable test cannot be referenced from a static context
或者是在static函式中呼叫非static函式,在编译时就会出现以下的错误讯息:
non-static method showMe() cannot be referenced from a static context
Java在使用到类别时才会加以加载程序中,如果您要使用一个static数据或方法,而在加载一个类别时,您希望先进行一些初始化动作,您可以使用static定义一个区块,并在当中撰写初始化资源的动作,例如:
public class Ball { public static int[] arr = new int[10]; static { // 一些初始化程序代码 } .... }
像上面这样一个类别,在第一次呼叫而被载入时,static区块中的程序代码就会被执行,且只会执行一次,要注意的是,static属性成员必须撰写在 static区块之前,而如果在static区块中发生了例外,则最后会被包装为 java.lang.ExceptionInInitializerError。
34、构造函数
在定义类别时,您可以使用「建构方法」(Constructor)来进行对象的初始化,而在Java中并没有 「析构函数」(Destructor),而是利用finalize()函式来达到解构方法的作用,这则在 垃圾回收 讨论。
建构方法中,可以不指定自变量的话,您也可以由指定的长度来配置数组;您在无自变量的建构方法中直接使用this() 来呼叫另一个有参数的建构方法,这是一种经常的作法,可以避免撰写一些重复的原始码。
对象在建构之前,对象的属性必须先初始完毕才会执行建构式,例如:
public class Some { private Other other = new Other(); public Some() { System.out.println("Some"); } } public class Other { public Other() { System.out.println("Other"); } }
如果建构Some:
Some some = new Some();
由于先初始Some的属性成员,所以会先显示"Other",再显示"Some"。
如果您如下撰写:
public class Test { { System.out.println("initial..."); } public Test() { System.out.println("Test"); } public Test(int i) { System.out.println("Test i"); } public static void main(String[] args) { new Test(); new Test(1); } }
在 { 与 } 之间的程序,会自动加入所有的建构函式开头,所以程序将显示:
initial...
Test
initial...
Test i
35、this
当对象需要得知自己的内存地址时。这种例子很多,例如告知某个函式处理对象为自己、或在Java窗口程序中,注册对象本身为事件处理者,例如下面这个简单的Java程序中就有使用到this参考:
public class GUIExample extends JFrame implements MouseListener { // .... // 实作事件处理的对象为自已 addMouseListener(this);? // .... // ==?鼠标事件实作 == public void mouseEntered(MouseEvent e) { // ..... } // .... }
this除了上面的用法之外,还有一种可以带参数的用法,主要是用于类别中呼叫建构方法,而避免直接以建构方法的名称来呼叫,例如:
public class Ball { private String name; public Ball() { this("No name"); .... } public Ball(String name) { this.name = name; .... } }
当使用无参数的建构方法时,它会呼叫有参数的建构方法,这是this()一个应用的基本范例,如此,您不用在建构方法中撰写重复的程序代码。
36、重载方法
方法重载的功能使得程序设计人员能较少苦恼于方法名称的设计,以统一的名称来呼叫相同功能的方法,方法重载不仅可根据传递自变量的数据型态不同来呼叫对应的 方法,参数列的参数个数也可以用来设计方法重载。
方法重载时可以根据方法参数列的数据型态,也可以根据参数的个数,不过必须注意的是,方法重载不可根据传回值的不同来区别。
在 J2SE 5.0后,当您使用方法重载时,要注意到 autoboxing、 unboxing 的问题,来看看下面的程序片段,您认为结果要是什么?
public class Test { public static void main(String[] args) { someMethod(1); } public static void someMethod(int i) { System.out.println("i"); } public static void someMethod(Integer integer) { System.out.println("integer"); } }
结果必须是显示 "i",您不能期待装箱(boxing)的动作会发生,如果您想要呼叫自变量列为Integer版本的方法,您要明确指定:
someMethod(new Integer(1));
编译器在处理重载与装箱问题时,会依以下的顺序:
- 找寻在还没有装箱动作前可以符合的方法
- 第一步失败的话,尝试装箱动作后可以符合的方法
- 第二步也失败,尝试有装箱及有不定长度自变量的方法
- 第三步也失败,编译器找不到合适的方法,回报编译错误
37、不定长度自变量
public class MathTool { public static int sum(int... nums) { int sum = 0; for(int num : nums) { sum += num; } return sum; } }
要使用不定长度自变量,在宣告自变量时,于关键词后加上...,然后您可以这么使用它:
public class TestVarargs { public static void main(String[] args) { int sum = 0; sum = MathTool.sum(1, 2); System.out.println(sum); sum = MathTool.sum(1, 2, 3); System.out.println(sum); sum = MathTool.sum(1, 2, 3, 4, 5); System.out.println(sum); } }
执行结果会分别显示3、6、15。
显然的,从MathTool类别的sum()中您可以看到,实际上编译器会将int... nums解译为int[] nums,而设定给方法的自变量则会被化为int数组传入至sum()中,您只要将nums当作数组来处理就是了。
在方法上设定不定长度自变量时,记得必须设定在自变量列的最后一个,例如下面的方式是合法的:
public void someMethod(int arg1, int arg2, int... varargs) { // .... }
您也没办法设定两个以上的不定长度自变量。如果要对象的不定长度自变量,其方法相同。
38、递归方法【略】
39、垃圾回收
在C++中,使用new配置的对象,必须使用delete来 清除对象,以释放对象所占据的内存空间,如果没有进行这个动作,则若对象不断的产生,内存就会不断的被耗用,最后使得内存空间用尽,然而使用 delete并不是那么的简单,如果不小心清除了尚被参考的对象,或是对象间共享的资源,则程序就会发生错误,小心的使用new与delete,一直是C ++中一个重要的课题。
在Java中,使用new配置的对象,基本上也是必须清除以回收内存空间的,但是您并不用特别关心这个问题,因为Java提供「垃圾收集」(Garbage collection)机制,在适当的时候,Java执行环境会自动检查对象,看看是否有未被参考的对象,如果有的话就清除对象、回收对象所占据的内存空间。
垃圾收集的时机我们并无法得知,可能是内存资源不足的时候,或是在程序空闲的时候,您可以建议执行环境进行垃圾收集,但也仅止于建议,如果程序当时有优先权更高的执行绪(Thread)正在进行,则垃圾收集并不一定会马上进行。
在C++中有解构方法(Destructor),它会在对象被清除前执行,在Java中并不明确有解构方法,因为我们不知道对象什么时候会被回收,在Java中有finalize()这 个方法,它被宣告为protected,它会在对象被回收时执行,但您不可以将它当作解构方法来使用,因为不知道对象资源何时被回收,所以也就不会立即执 行我们所指定的动作,但您可以使用finalize()来进行一些相关资源的清除动作,而这些动作与立即性的收尾动作并没有关系。
如果我们确定某个对象不再使用,您可以在参考至该对象的名称上指定null,表示这个名称不在参考至任何对象,可以使用System.gc()建议程序进行垃圾收集,如果建议被采纳,则对象资源会被回收,回收前会执行finalize()方法。
下面这个程序是个简单的示范:
public class GcTest { private String name; public GcTest(String name) { this.name = name; System.out.println(name + "建立"); } // 对象回收前执行 protected void finalize() { System.out.println(name + "被回收"); } }
public class UseGC { public static void main(String[] args) { System.out.println("请按Ctrl + C终止........"); GcTest obj1 = new GcTest("object1"); GcTest obj2 = new GcTest("object2"); GcTest obj3 = new GcTest("object3"); // 令名称不参考至对象 obj1 = null; obj2 = null; obj3 = null; // 建议回收对象 System.gc(); while(true); // 不断执行program } }
您故意加上无穷循环,以让垃圾收集在程序结束前有机会执行,藉以了解垃圾收集确实会运作。
39、扩充父类
Java中使用"extends"作为其扩充父类别的关键词,其实就相当于我们一般所常称的继承(Inherit),只不过"extends"除了继承之外,还有将继承下来的类别予以新增定义的意思。
public class Point2D { private int x,y; public Point2D(){} public Point2D(int x,int y){ this.x = x; this.y = y; } public int getX(){ return x; } public int getY(){ return y; } }
public class Point3D extends Point2D{ private int z;//新增成员变量 public Point3D(){ super();//父类构造函数 } Point3D(int x,int y,int z){ super(x,y);//父类构造函数 this.z = z; } public int getZ(){//新增成员函数 return z; } }
public class UseExtend { public static void main(String[] args) { Point3D p1 = new Point3D(1, 3, 4); Point3D p2 = new Point3D(); System.out.printf("p1: (%d, %d, %d) \n", p1.getX(), p1.getY(), p1.getZ()); System.out.printf("p2: (%d, %d, %d) \n", p2.getX(), p2.getY(), p2.getZ()); } }
当您扩充某个类别时,该类别的所有public成员都可以在衍生类别中被呼叫使用,而private成员则不可以直接在衍生类别中被呼叫使用;在这个例子 中,Point2D中已经有x, y两个成员,您新增z成员,而方法上您新增一个public的getZ()方法,而getX()与getY()直接继承父类别中已定义的内容。
在扩充某个类别之后,您可以一并初始父类别的建构方法,以完成相对应的初始动作,这可以使用super()方法来达到,它表示呼叫基底类别的建构方法。
父类别的public成员可以直接在衍生类别中使用,而private成员则不行,private类别只限于定义它的类别来存取,如果想要与父类别的 private成员沟通,就只能透过父类别中继承下来的public函式成员,例如上例中的getX()与getY()方法。
40、protected成员
1)、您希望扩充了基底类别的衍生类别,能够直接存取呼叫基底类别中的成员,但不是透过"public"方法成员,也不是将它宣告为"public",因为您仍不希望这些成员被对象直接呼叫使用。可以宣告这些成员为「被保护的成员」(protected),保护的意思表示存取它有条件限制以保护该成员,当您将类别成员宣告为受保护的成员之后,继承它的类别就可以直接使用这些成员,但这些成员仍然受到对象范围的保护,不可被对象直接呼叫使用。
2)、事实上,对于同一个 套件(package)下的类别,可以直接呼叫彼此的protected成员,而对于不同套件(package)下的成员,不能呼叫彼此的protected成员。
如果在定义成员时没有设定任何的存取修饰,则为预设(default)的存取权限,预设存取权限可以在同一个套件(package)中的其它类别直接存取,但在子类别中不能被直接存取。
41、重新定义(Override)方法
类别是对象的定义书,如果原来的定义并不符合您的需求,您可以在扩充类别的同时重新定义,举个例子来说,看看下面这个类别:
public class SimpleArray { protected int[] array; public SimpleArray(int i) { array = new int[i]; } public void setElement(int i, int data) { array[i] = data; } .... }
这个类别设计一个简单的数组辅助类别,不过您觉得它的setElement()方法不够安全,您想要增加一些数组的边界检查动作,于是扩充该类别, 并重新定义setElement()方法:
public class SafeArray extends SimpleArray { public SafeArray(int i) { super(i); } public void setElement(int i, int data) { if(i < array.length) super.setElement(i, data); } .... }
这么以来,以SafeArray类别的定义所产生的对象,就可以使用新的定义方法。
当同一个成员方法在衍生类别中被重新定义,使用此衍生类别所生成的对象来呼叫该方法时,所执行的会是衍生类别中所定义的方法,而基底类别中的同名方法并不受影响。
在上面您看到super()与super, 如果您在衍生类别中想要呼叫基底类别的建构方法,可以使用super()方法,另外若您要在衍生类别中呼叫基底类别方法,则可以如使用 super.methodName(),就如上面所示范的,但使用super()或super呼叫父类别中方法的条件是父类别中的该方法不能是 "private"。
重新定义方法时要注意的是,您可以增大父类别中的方法权限,但不可以缩小父类别的方法权限,例如在扩充SimpleArray时,尝试将setElement()方法从"public"权限缩小至"private"权限是不行的。
注意!您无法重新定义static方法,一个方法要被重新定义,它必须是非static的,如果您在子类别中定义一个有同样签署(signature)的static成员,那不是重新定义,那是定义一个属于该子类别的static成员。
42、Object类
在Java中,所有的对象都隐含的扩充了Object类别,Object类别是Java程序中所有类别的父类别。
Object类别定义了几个方法,包括"protected"的clone()、finalize()两个方法,以及几个"public"方法,像是equals()、toString()、getClass()、hashCode()、notify()、notifyAll()等等的方法,这些方法您都可以加以重新定义(除了 getClass()、notify()、notifyAll()、wait()等方法之外,它们被宣告为 "final",无法被子类别扩充,所以无法重新定义),以符合您所建立的类别需求。
由于Object类别是Java中所有类别的父类别,所以它可以参考至任何的对象而不会发生任何错误,这是很有用,以后您会看到一些 Java程序中,有些对象可以加入一些衍生类别对象,并可透过方法呼叫会直接传回Object对象,这些对象可以经由型态(接口)转换而指定给衍生类别型 态参考。
下面这个程序中,您制作一个简单的 集合(Collection)类别,并将一些自订类别对象加入其中,这个程序示范了Object的一个应用:
public class Foo1 { private String name; public Foo1(String name) { this.name = name; } public void showName() { System.out.println("foo1 name: " + name); } // 重新定义toString() public String toString() { return "foo1 name: " + name; } }
public class Foo2 { private String name; public Foo2(String name) { this.name = name; } public void showName() { System.out.println("foo2 name: " + name); } // 重新定义toString() public String toString() { return "foo2 name: " + name; } }
public class SimpleCollection { private Object[] objArr; private int index = 0; public SimpleCollection() { objArr = new Object[10]; // 预设10个对象空间 } public SimpleCollection(int capacity) { objArr = new Object[capacity]; } public void add(Object o) { objArr[index] = o; index++; } public int getLength() { return index; } public Object get(int i) { return objArr[i]; } }
public class Test { public static void main(String[] args) { SimpleCollection objs = new SimpleCollection(); objs.add(new Foo1("f1 number 1")); objs.add(new Foo2("f2 number 1")); Foo1 f1 = (Foo1) objs.get(0); f1.showName(); Foo2 f2 = (Foo2) objs.get(1); f2.showName(); System.out.println(); System.out.println("f1.toString(): " + f1.toString()); System.out.println("f2.toString(): " + f2.toString()); } }
执行结果:
foo1 name: f1 number 1 foo2 name: f2 number 1 f1.toString(): foo1 name: f1 number 1 f2.toString(): foo2 name: f2 number 1
在程序中,SimpleCollection对象可以加入任何型态的对象至其中,而传回对象时,您只要透过型态(接口)转换,就可以操作型态(接口)上的方法。
Object的toString()方法预设会传回以下的字符串:
getClass().getName() + '@' +Integer.toHexString(hashCode());
getClass()方法是Object中定义的方法,它会传回对象于执行时期的Class实例,而hashCode()传回该对象的hash code,toString()方法用来传回对象的描述,通常是个文字性的描述,Object的toString()方法预设在某些场合是有用的,例如物 件的自我检视时,但在这边,您将之重新定义为文字模式下使用者看得懂的文字描述。
上面这个程序范例虽然简单,但您以后一定会常常看到类似的应用,例如窗口程序容器、Vector类别等等。
Object预设的equals()本身是比较对象的内存参考,如果您要有必要比较两个对象的内含数据是否相同(例如当对象被储存至Set时)您必须实作equals()与hashCode()。
一个比较常被采用的方法是根据对象中真正包括的的属性值来作比较,来看看下面的一个例子:
public class Cat { ... public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof Cat)) return false; final Cat cat = (Cat) other; if (!getName().equals(cat.getName())) return false; if (!getBirthday().equals(cat.getBirthday())) return false; return true; } public int hashCode() { int result; result = getName().hashCode(); result = 29 * result + getBirthday().hashCode(); return result; } }
这是一个根据商务键值(Business key)实作equals()与hashCode()的例子,当然留下的问题就是您如何在实作时利用相关的商务键值来组合,这要根据您实际的需求来决定,API中对于equals()的合约是必须具备反身性(Reflexive)、对称性(Symmetric)、传递性(Transitive)、一致性(Consistent)。
- 反身性(Reflexive):x.equals(x)的结果要是true。
- 对称性(Symmetric):x.equals(y)与y.equals(x)的结果必须相同。
- 传递性(Transitive):x.equals(y)、y.equals(z)的结果都是true,则x.equals(z)的结果也必须是true。
- 一致性(Consistent):同一个执行期间,对x.equals(y)的多次呼叫,结果必须相同。
可以参考API文件中Object类别的hashCode()之建议:
- 在同一个应用程序执行期间,对同一对象呼叫 hashCode()方法,必须回传相同的整数结果。
- 如果两个对象使用equals(Object)测试结果为相等, 则这两个对象呼叫hashCode()时,必须获得相同的整数结果。
- 如果两个对象使用equals(Object)测试结果为不相等, 则这两个对象呼叫hashCode()时,可以获得不同的整数结果。
两个不同的对象,可以传回相同的hashCode()结果,这是合法甚至适当的,只是对象会被丢到同一个杂凑桶中。
至于clone()方法,它是有关于如何复制对象本身,您可以在当中定义您的复制方法,不过对象的复制要深入的话必须考虑很多细节。
43、抽象类
当您定义类别时,可以仅宣告方法名称而不实作当中的逻辑,这样的方法称之为「抽象方法」(Abstract method),如果一个类中包括了抽象方法,则该类别称之为「抽象类别」(Abstract class),抽象类别是个未定义完全的类别,所以它不能被用来生成对象,它只能被扩充,并于扩充后完成未完成的抽象方法定义。
在Java中要宣告抽象方法与抽象类别,您使用"abstract"关键词,直接来看个应用的例子,下面定义一个简单的比大小游戏抽象类别:
public abstract class AbstractGuessGame { private int number; public void setNumber(int number) { this.number = number; } public void start() { showMessage("Welcome"); int guess; do { guess = getUserInput(); if(guess > number) { showMessage("bigger than the goal number"); } else if(guess < number) { showMessage("smaller than the goal number"); } else showMessage("you win"); } while(guess != number); } protected abstract void showMessage(String message); protected abstract int getUserInput(); }
在宣告类别时使用"abstract"关键词,表示这是一个抽象类别,在这个类别中,您定义了start()方法,当中先实作比大小游戏的基本规则,然而 您不实作与使用者互动及讯息是如何显示的,这您分别定义为抽象方法showMessage()与 getUserInput(),在方法上使用"abstract"关键词,可以仅定义方法而不实作其内容。
使用这个类别的办法是扩充它,并完成当中未定义完全的抽象方法showMessage()与getUserInput(),例如实作一个简单的文字接口游戏类别:
import java.util.Scanner; public class ConcreteGuessGame extends AbstractGuessGame { private Scanner scanner; public ConcreteGuessGame() { scanner = new Scanner(System.in); } protected void showMessage(String message) { System.out.println(message + "!"); } protected int getUserInput() { System.out.print("input a number: "); return scanner.nextInt(); } }
接下来写个简单的测试程序,看看这个文字接口比大小游戏类别是不是可以运作:
public class Test { public static void main(String[] args) { AbstractGuessGame guessGame = new ConcreteGuessGame(); guessGame.setNumber(50); guessGame.start(); } }
这边必须知道,一个基底类别的对象参考名称,可以用来指向其衍生类别的对象而不会发生错误,所以上面的这个指定是可以接受的:
AbstractGuessGame guessGame = new ConcreteGuessGame();
由于guessGame仍是AbstractGuessGame类型的参考名称,它可以操作子类别 ConcreteGuessGame的实例中名称相同的公开操作接口(方法),简单的说,透过guessGame参考名称,您可以操作 ConcreteGuessGame的实例之setNumber()与start()方法,这是多型(Polymorphism)操作的一个实际例子。
执行结果:
Welcome! input a number: 10 smaller than the goal number! input a number: 60 bigger than the goal number! input a number: 50 you win!
今天如果您想要实作一个有窗口接口的比大小游戏,则您可以扩充AbstractGuessGame并实作您的抽象方法showMessage()与 getUserInput(),事实上,上面的例子是 Template Method 模式 的一个实际例子,使用抽象类别与方法来实作Template Method 模式,在很多应用场合都可以见到。
44、final关键字
"final"关键词可以使用在变量宣告时,表示该变量一旦设定之后,就不可以再改变该变量的值。
如果在方法成员宣告时使用"final",表示该方法成员在无法被子类别重新定义(Override)。
如果您在宣告类别时加上"final"关键词,则表示要终止被扩充,这个类别不可以被其它类别继承。
如果在数据成员上加上final关键词,但未给予该数据成员初值,则初值的初始化被延迟,该数据成员必须在建构方法中进行初始化,且初始化之后不得改变其值。
45、接口类
接口的目的在定义一组可操作的方法,实作某接口的类别必须实作该接口所定义的所有方法,只要对象有实作某个接口,就可以透过该接口来操作对象上对应的方法,无论该对象实际上属于哪一个类别,像上一段所述及的问题,就要靠要接口来解决。
口的宣告是使用"interface"关键词,宣告方式如下:
interface 接口名称 { 传回型态 方法(参数列); 传回型态 方法(参数列); // .... }
一 个宣告界面的例子如下:
public interface IRequest { public void execute(); }
接口的权限都是"public",所以即使不指定public仍是预设为public。
当定义类别时,可以使用"implements"关键词来一并指定要实作哪个接口,接口中所有定义的方法都要实作,例如:
public class HelloRequest implements IRequest { private String name; public HelloRequest(String name) { this.name = name; } public void execute() { System.out.printf("Hello! %s!%n", name); } }
public class WelcomeRequest implements IRequest { private String place; public WelcomeRequest(String place) { this.place = place; } public void execute() { System.out.printf("Welcome to %s!%n", place); } }
由于接口中的方法预设都是public,所以实作接口的类别中,方法必须宣告为public,否则无法通过编译。
public class Test { public static void main(String[] args) { for(int i = 0; i < 10; i++) { int n = (int) (Math.random() * 10) % 2; switch (n) { case 0: doRequest( new HelloRequest("caterpillar")); break; case 1: doRequest(new WelcomeRequest("PmWiki")); } } } public static void doRequest(IRequest request) { request.execute(); } }
在这个程序中,即使doRequest()并不知道传入的对象是哪一种类别的实例,但它只要知道这个对象的操作接口就可以正确的执行请求,这是界面实作的一个实际应用,也是很常见到的一种应用。
在C++中可以使用多重继承,但在Java中只能单一继承,也就是一次只能扩充一个类别,Java使用interface来达到某些多重继承的目的,您可 以一次实作多个接口,就像是同时继承多个抽象类别(实际上这是C++中多重继承的一个实际运用方式),实作多个接口的方式如下:
public class 类别名称 implements 接口1, 接口2, 接口3 { // 界面实作 }
当您实作多个接口时,记得您必须实作每一个接口中所定义的方法,由于您实作了多个接口,所以要操作对象时,必要时您必须作「接口转换」,如此程序才能知道如何正确的操作对象,例如假设someObject实作了ISomeInterface1与ISomeInterface2两个接口,则我们可以如下对对象进行接口转换与操作:
ISomeInterface1 obj1 = (ISomeInterface1) someObject; obj1.doSomeMethodOfISomeInterface1(); ISomeInterface2 obj2 = (ISomeInterface2) someObject; obj2.doSomeMethodOfISomeInterface2();
当 抽象类别 中的所有方法都是抽象方法时,它的作用就与接口有些类似(像在C++中,并没有区分抽象类别与接口),但记得在Java中只允许单一继承,所以您不能同时 继承多个抽象方法。
事实上在Java里,抽象类别中并不会全是抽象方法,这么使用并不适当,在Java中区分抽象类别与接口,其在语义上是有所不同的,例如抽象类别中允许您 先实作某些方法,而保留一些抽象方法不实作,其应用场合之一是像 Template Method 模式 中介绍的接口定义中的方法则是完全不实作,它只定义方法名称,接口常用于规范统一的操作界面,其应用场合之一是像 Command 模式 中介绍的,接口定义一组协议,所有实作它的类别都必须遵守的协议,接口可以保证实作它的类别一定会实作所定义的方法。
接口也可以进行继承的动作,同样也是使用"extends"关键词,例如:
public interface 名称 extends 接口1, 接口2 { // ... }
不同于类别的是,接口可以同时继承多个接口,这也可以达到类似C++中多重继承的功能,而实作子接口的类别必须将所有在父接口和子接口中所定义的方法实做出来。
- 多型(Polymorphism)操作的应用太多了,从 设 计模式 开始学习会是个不错的选择。
- 在宣告接口的名称时,一个常见的惯例是在名称前加上 'I' ,这明显表示这是一个接口。
- 我喜欢用接口转换,而不是用转型(Cast),为对象转换一个操作接口,而不是将对象转型。
45、接口与多重继承
如果有SomeClass类别与OtherClass类别,您想要SomeAndOther类别可以同时拥有SomeClass类别与 OtherClass类别中已定义好的操作,并可以进行多型操作,在C++中可以用多重继承来达到,但在Java中显然的无法使用多重继承,怎么办?您可 以在设计上先绕个弯,先使用两个接口分别定义好SomeClass与OtherClass两个类别的公开方法,例如:
public interface ISome { public void doSome(); }
public interface IOther { public void doOther(); }
接着让Some与Other类别分别实作两个接口:
public class Some implements ISome { public void doSome() { .... } }
public class Other implements IOther { public void doOther() { .... } }
SomeAndOther如何同时拥有两个Some与Other类别已定义好的操作?并可以多型操作?SomeAndOther可以 继承其中之一,并拥有其中之一,例如:
public class SomeAndOther extends Some implements IOther { private IOther other = new Other(); public void doOther() { other.doOther(); } }
46、内部类
在类别中您还可以定义类别,称之为内部类别(Inner class)或「巢状类别」(Nested class)。非"static"的内部类别可以分为三种:成员内部类别(Member inner class)、区域内部类别(Local inner class)与匿名内部类别(Anonymous inner class)。
使用内部类别的好处在于可以直接存取外部类别的私用(private)成员,举个例子来说,在窗口程序中,您可以使用内部类别来实作一个事件倾听者类别,这个窗口倾听者类别可以直接存取窗口组件,而不用透过参数传递。
另一个好处是,当某个Slave类别完全只服务于一个Master类别时,我们可以将之设定为内部类别,如此使用Master类别的人就不用知道 Slave的存在。
成员内部类别是直接宣告类别为成员,例如:
public class OuterClass { // .... // 内部类别 private class InnerClass { // .... } }
内部类别同样也可以使用"public"、"protected"或"private"来修饰,通常宣告为"private"的情况较多,下面这个程序简单示范成员内部类别的使用:
public class OutClass {
// 内部类别
private class Point {
private int x, y;
public Point() {
x = 0; y = 0;
}
public void setPoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
private Point[] points;
public OutClass(int length) {
points = new Point[length];
for(int i = 0; i < points.length; i++) {
points[i] = new Point();
points[i].setPoint(i*5, i*5);
}
}
public void showPoints() {
for(int i = 0; i < points.length; i++) {
System.out.printf("Point[%d]: x = %d, y = %d%n",
i, points[i].getX(), points[i].getY());
}
}
}
上面的程序假设Point类别只服务于OutClass类别,所以使用OutClass时,不必知道Point类别的存在,例如:
public class UseInnerClass { public static void main(String[] args) { OutClass out = new OutClass(10); out.showPoints(); } }
区域内部类别的使用与成员内部类别类似,区域内部类别定义于一个方法中,类别的可视范围与生成之对象仅止于该方法之中,区域内部类别的应用一般较为少见。
内部匿名类别可以不宣告类别名称,而使用new直接产生一个对象,该对象可以是继承某个类别或是实作某个接口,内部匿名类别的宣告方式如下:
new [类别或接口()] { // 实作 }
一个使用内部匿名类别的例子如下所示,您直接继承Object类别来生成一个对象,并改写其toString()方法:
public class UseInnerClass { public static void main(String[] args) { Object obj = new Object() { public String toString() { return "匿名类别对象"; } }; System.out.println(obj.toString()); } }
执行结果:
匿名类别对象
注意如果要在内部匿名类别中使用某个方法中的变量,它必须宣告为"final",例如下面是无法通过编译的:
.... public void someMethod() { int x = 10; Object obj = new Object() { public String toString() { return "" + x; } }; System.out.println(obj.toString()); }
编译器会回报以下的错误:
local variable x is accessed from within inner class; needs to be declared final
您要在 x 宣告时加上final才可以通过编译:
.... public void someMethod() { final int x = 10; Object obj = new Object() { public String toString() { return "" + x; } }; System.out.println(obj.toString()); }
究其原因,在于 区域变量 x 并不是真正被拿来于内部匿名类别中使用,而是在内部匿名类别中复制一份,作为field成员来使用,由于是复本,即便您在内部匿名类别中对 x 作了修改,会不会影响真正的区域变量 x,事实上您也通不过编译器的检查,因为编译器要求您加上"final"关键词,这样您就知道您不能在内部匿名类别中改变 x 的值。
内部类别还可以被宣告为"static",不过由于是"static",它不能存取外部类别的方法,而必须透过外部类别所生成的对象来进行呼叫,一般来说较少使 用,一种情况是在main()中要使用某个内部类别时,例如:
public class UseInnerClass { private static class Point { private int x, y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } } public static void main(String[] args) { Point p = new Point(10, 20); System.out.printf("x = %d, y = %d%n", p.getX(), p.getY()); } }
由于main()方法是"static",为了要能使用Point类别,该类别也必须被宣告为"static"。
被宣告为static的内部类别,事实上也可以看作是另一种名称空间的管理方式,例如:
public class Outer { public static class Inner { .... } .... }
您可以如以下的方式来使用Inner类别:
Outer.Inner inner = new Outer.Inner();