发信人: webpk(蒸的英雄)
整理人: zjxyz(2002-01-21 08:16:45), 站内信件
|
4. 类
类(class)是传统面向对象编程模型的象征。它们支持数据抽象以及实
现对数据的约束,在Java中,每一个新的类创建一个新类型。
要想得到一个新的类,程序员必须首先找到一个已有的类,新类即
在这个已有类的基础上构造,我们称之为派生(derived)。派生出的类亦称
为原来类的子类,而这个类我们称为超类(super class)。
类的派生具有传递性: 如果B是A的子类,C是B的子类,则C
是A的子类。
一个类的直接超类以及指示这个类如何实现的界面(interface),在类
的声明中,由关键字extends及implements标出。如下示(黑体表示关键字)::
[doc_ comment] [modifer] class ClassName
extends Superclassname
implements interface {,interface } ] {
class body
}
举例:
/** 2 dimension point */
public class Points {
float x,y;
......
}
/** printable point */
class PinttablePoint extends Points implements Printable {
......
public void Print ( ) {
}
}
所有的类均从一个根类 Object中派生出来。除Object之外的任何类
都有一个直接超类。如果一个类在声明时未指明其直接超类,那么缺省
即为Object。如下述:
class Point {
float x,y
}
与下面写法等价
class Point extends Object {
float x, y;
}
Java语言仅支持单继承,通过一个被称作“界面”的机制,来支持
某些在其它语言中用多继承实现的机制(详见“界面”一节)。Java之所
以没有采用C++的多继承机制,是为了避免多继承带来的诸多不便,例
如:可能产生的二义性,编译器更加复杂,程序难以优化等问题。
4.1 类类型之间的强制转换
Java语言支持在两个类型之间的强制转换,因为每个类即是一个新
的类型。Java支持类类型之间的强制转换,如果B是A的子类,那么B
的一个实例亦可作为A的实例来使用,虽然不需要显式的转换,但显式
转换亦是合法的,这被称作拓宽(widening)。如果A的一个实例,想当作
B的实例使用,程序员就应写一个类型转换叫作削窄(narrowing)的强
制。从一个类到其子类的强制转
换在运行时要作例行的检查以确保这个对象就是其子类的一个实例
(或其子类之一)。兄弟类之间的强制类型转换是一个编译错误,类的强制转换的语法如下?? (classname) ref
其中,(classname)是要转换的目的类,而ref是被转换的对象。
强制转换仅仅影响到对象的引用,而不会影响对象本身。然而,对实例
变量的访问却受到对象
引用的类型的影响。一个对象从一个类型到另一类型的强制转换后,
可以使同一变量名对不同的实例变量访问。
class ClassA{
String name = "ClassA"
}
class ClassB extends ClassA { //ClassB是ClassA的子类
String name="ClassB";
}
class AccessTest {
void test( ) {
ClassB b=new ClassB( );
println (b.name); //打印: ClassB
ClassA a
a=(ClassA)b;
println (a.name); //打印: ClassA
}
}
4.2 方法
方法(method)是可施于对象或类上的操作,它们既可在类中,也可在
界面中声明。但是他们却只能在类中实现(Java中所有用户定义的操作均
用方法来实现)。
类中的方法声明按以下方式:
[Doc_ comment] [Access Specifiers] ReturnType methodName(parameterList)
{
method body(本地的native及抽象的方法没有体部分)
}
除构造函数可以无返回类型外,其余的方法都有一个返回类型。如
果一个不是构造函数的方法不返回任何值,那么它必须有一个void的返
回类型。
参数表由逗号分隔的成对的类型及参数名组成,如果方法无参数,
则参数表为空。方法内部定义的变量(局部变量)不能隐藏同一方法的其
它变量或参数。例如: 如果一个方法带以名为i的参数实现,且方法内
又定义一个名为i的局部变量,则会产生编译错误,例如:
class Rectangle {
void vertex (int i,int j) {
for (int i=0; i<=100; i++) { //出错
…
}
}
}
方法体内循环语句中声明的i是一个编译错误。
Java语言允许多态方法命名,即用一个名字声明方法,这个名字已
在这个类或其超类中使用过,从而实现方法的覆盖(overriding)及重载
(overloadding)。所谓覆盖是对继承来的方法提供另一种不同的实现。而
重载是指声明一个方法,它与另外一个方法有相同的名字,但参数表不同。
注: 返回类型不能用来区别方法,即在一个类的范围内,具有相同
的名字,相同的参数表(包括个数、位置及类型)的方法,必须返回相同
的类型。若这样的两个方法有不同的返回类型,将会产生一个编译错误。
4.2.1 实例变量
实例变量(instance variables)是指那些在类内声明,但在方法的作用
域之外尚未被static标记的变( 参照 “静态方法,变量及初始化”段)。
而在一个方法的作用域之内声明的变量是局部变量。实例变量可以有修
饰符(见修饰符)。
实例变量可以是任何的类型,并且可以有初始值。如果一个实例变
量无初始值,它将被初始化成0。布尔型变量被初始化成flase,对象被
初始化成null。下面是一个实例变量j具有初始化值的例子:
class A{
int j =23;
……
}
4.2.2 this 和super变量
在一个非静态方法的作用域内,this这个名字代表了当前对象。例
如: 一个对象可能需要把自己作为参数传给另一个对象的方法:
class MyClass {
void Method (OtherClass obj) {
…
obj.Method (this)
…
}
}
不论何时,一个方法引用它自己的实例变量及方法时,在每个引用
的前面都隐含着“this”。
如:
class Foo {
int a,b,c;
......
void myPrint ( ) {
print (a+ "\n"); // a=="this.a"
}
......
}
super变量类似于this变量。this变量实际上是对当前对象的引用,
它的类型就是包含当前正在处理的方法的类。而super变量则是对其超类
类型对象的引用。
4.2.3 设置局部变量
方法都要经过严格的检查,以确保所有的局部变量(在方法内声明的
变量)在被引用之前已设初,被初始化之前就使用局部变量是一个编译错
误。
4.3 覆盖方法
要想覆盖(overiding)一个方法,必须在声明这个方法的类的子类中声
明一个与它具有相同名字,相同返回类型,以及相同参数表的方法。当
子类的实例调用这个方法时,被调用的是新定义的方法而不是最初的那
个。被覆盖了的方法可通过super变量来调用,如下:
setThermostat(…) //指覆盖的方法
super. setThermostat(…) //指被覆盖的方法
4.4 重载的认定
重载(overload)的方法与已有的某个方法同名,但是变元的个数及/
或类型不同,重载的认定是要决定调用的是哪一个方法,在认定重载的
方法时,不考虑返回类型。方法可以在同一类的内部重载,类内部方法
声明的顺序并不重要。
可同时改变变元个数和类型来实现方法的重载。 编译器认定相匹配
的方法时以最低的类型转换开销为准。只有具有相同名字及相同变元个
数的方法才会优先考虑。所有变元都必须转换,是匹配方法时的最大开
销。有两种变元类型是必需考虑的:对象类型和基本类型。
对象类型间转换的开销的大小指类树上实在参数的类与原型参数的
类之间连线的个数。只考虑拓宽型转换(对象类型转换的详细信息,请参
阅“类之间的强制类型转换”)。对于类型完全匹配的变元,无须进行转
换,它们的开销为零。
基本类型的转换开销按下表计算,开销为零是严格匹配。
to
byte short char int long float double
byte 0 1 2 3 4 6 7
short 10 0 10 1 2 4 5
from char 11 10 0 1 2 4 5
int 12 11 11 0 1 5 4
long 12 11 11 10 0 6 5
float 15 14 13 12 11 0 1
double 16 15 14 13 12 10 0
注:开销 >=10 易引起数据丢失。
一经认定某一匹配方法是哪种转换开销,编译器则选用转换开销最
小的匹配。如果有多于一个方法,其最小开销相同,则匹配有二义性,
要出编译时的错误。
例如:
class A{
int method (Object o, Thread t);
int method(Thread t,Object o);
void g(Object o,Thread t){
method(o,t); //调用第一个方法
method(t,o); //调用第二个方法
method(t,t); //有二义性,编译出错
}
}
4.5 构造函数
构造函数(constructor)是提供初始化的专用方法。它和类的名字相同,
但没有任何返回类型。构造函数在对象创建时被自动地调用,它不能被
对象显式调用。如果你想在包(package)之外调用构造函数,就将构造函
数设为“public”(参见“访问指明符“一节)。
构造函数也可以用不同个数和类型的参数重载,就象其它的方法被
重载一样。
class Foo {
int x;
float y;
Foo() {
x=0;
y=0.0;
}
Foo (int a ) {
x=a;
y=0.0;
}
Foo (float a ) {
x=0;
y=a;
}
Foo (int a,float b ) {
x=a;
y=b;
}
static void myFoo( ) {
Foo obj1=new Foo( ); //调用Foo( );
Foo obj2=new Foo(4 ); //调用Foo( int a );
Foo obj3=new Foo(4.0 ); //调用Foo( float a );
Foo obj4=new Foo(4,4.0 ); //调用Foo(int a , float b );
}
}
超类的实例变量由调用超类的或当前类的构造函数初始化。如果在
代码中没有指定由谁初始化,则调用的是超类中的无参数的构造函数。
如果一个构造函数调用了这个类或其直接超类的另一个构造函数,这个
调用必须是构造函数体的第一条语句,在调用构造函数之前实例变量不
引用。
调用直接超类的构造函数如下:
class MyClass extends OtherClass {
MyClass (someParamenters ) {
/* 调用父类构造函数 */
super(otherParameters);
} …
}…
调用当前类的构造函数如下示:
class MyClass extends OtherClass {
MyClass (someParameters) {
…
}
MyClass(otherParameters) {
/*调用当前类的构造函数,该函数有指定的参数表*/
this (someParameters);
…
}
…
}
下面的Foo和FooSub类的方法中使用构造函数的例子:
class Foo extends Bar {
int a;
Foo(int a) {
//隐含调用Bar( )
this.a=a;
}
Foo( ) {
this (42); //调用Foo(42),代替Bar( )
}
}
class FooSub extends Foo {
int b;
FooSub (int b) {
super(13); //调用Foo(13); 去掉此行将调用Foo( )
this.b=b;
}
}
4.6 用new运算符创建对象
类是用来定义对象的状态和行为的模板,对象是类的实例。类的所
有实例都分配在可作无用单元回收的堆中。声明一个对象引用并不会为
该对象分配存储,程序员必须显式地为对象分配存储,但不必显式删除
存储,无用单元回收器会自动回收无用的内存。
分配对象存储用new运算符。除了分配存储之外,new还初始化实
例变量,调用实例的构造函数。构造函数是初始化对象的一种方法(见“构
造函数”),下面的例子是分配并初始化ClassA的一个实例:
a = new ClassA( );
以下构造函数是带有参数的例子:
b = new ClassA(3,2);
分配的第三种形式允许以串表达式形式提供类名字,字符串在运行
时刻求值,new返回一个Object类型的对象,再转换成所希望的类型。
b = new ("class"+"A");
这种情况下,调用构造函数时无需参数。
4.6.1 无用单元收集
无用单元收集器(Garbage Collector)使存储管理的许多方面变得简
单、安全。程序不需要显式释放内存,它是自动处理的。无用单元收集
器不会释放正在被引用的内存,只释放那些不再被引用的空间。这就防
止了悬挂指针和存储漏洞(leak)。它也使设计人员从系统的存储管理中解
脱出来。
4.6.2终止
Java的构造函数完成一个对象初始化工作,Java的终止
(finalization)方法则完成和析构函数类似的功能,但与C++不同之处,是
它可显式调用。虽然Java的无用单元的回收功能可以自动地释放对象所
占有的资源,但是对象占有的某些资源,例如: 文件描述符、套接字
(socket),无用单元回收是无法处理的。所以程序员必须用终止函数来处
理。诸如:关闭打开的文件,终止网络连接等程序善后工作。
下面的例子是取自Java FileOutpntStream类中的终止函数。这个终止
函数是一个实例的方法,没有参数,也无返回值,名字必须取finalize()。
这里我们应注意它和C++析构函数的区别:
/**
* closes the stream when garbage is collected
* checks the file descriptor first to make sure it
* is not already closed.
*/
protected void finalize( ) throws IOException {
if (fd!=null) close();
}
下面是关于终止函数的几点注意事项:
* 如果一个对象含有终止函数,那么这个方法,在系统收集该对象所占
内存之前被调用。
* Java解释器可能在没有完成无用单元收集的情况下退出,那么某些终
止函数不可能被调用。在这种情况下,未释放的资源通常由操作系统来
处理。
* Java的无用单元的收集发生的时刻是不确定的,因此,Java无法保证
?
(转载于xici)
|
|