关键词:Java 继承 抽象类 接口 多态 匿名类
Java 是一种纯面向对象的编程语言,与另一种常用的面向对象语言C ++相比较,它具有简单、易学、与平台无关和安全性高等优点。另外,Java 还是目前使用最为广泛的网络编程语言之一。但由于它的结构体系庞大,类库较多,常使初学者望而生畏。在Java 语言的学习过程中,每个人所遇到的问题都不尽相同。本文结合自己的教学经验,对一些常见问题进行探讨,并对初学者普遍感觉较难理解的内容,如接口、多态及匿名类进行剖析,期望对Java 语言的初学者能有较大的帮助和启发。
一、初学者经常遇到的问题
(一)面向对象的程序设计(OOP)
Java 语言与面向过程编程的C 语言相比是完全不同的,从根本上说,是解决问题的思路不同。因此,不管初学者是否有C 语言的经验,这种思路的转变都需要经历很长的过程。
大部分院校对学生的教学安排是先学C ++再学Java,可是有一半的学生在学完C++语言后还没有理解面向对象的编程特点。
1.基本概念
Alan Kay 总结了面向对象语言同时也是Java 所基于的语言之一的Smalltalk 的基本特性。
(1)万物皆为对象。应到程序中,对象是一些相关的变量和方法的集合,其中变量表明对象的状态,方法表明对象所具有的行为。
(2)程序是对象的集合,对象之间通过发送消息来完成交互。想要请求一个对象完成某项功能,就必须向该对象发送一条相应的消息。
(3)每个对象都拥有其类型。每个对象都是某个类(class)的一个实例(instance)。
每个类区别于其他类的最重要特性是“可以发送什么样的消息给它”。
(4)某一个特定类型的所有对象都可以接收同样的消息。这个特性是和多态(PolymorPhism)紧密相关的。多态是OOP 中最重要的特性之一,但在实际教学实践中它是学生理解和使用的难点,本文的第二节对其进行更深入的剖析。
2.类和对象的关系
对现实生活中的对象进行抽象,只抽取与当前研究有关的本质特征,而忽略非本质特性,再将其映射为程序中的对象。具体在程序中,现实中的对象是通过一种抽象数据类型来描述的,这种抽象数据类型称为类(class)。因此,类是具有相同属性和操作的一组对象的集合,它为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和操作两个主要部分。在面向对象的程序设计中,类是程序的基本单元。对象是类的一个实例(instance),或者说对象是具有特殊属性和行为方式的实体。因此,类是用来定义对象的模板(temPlate)。
在Java 语言中,可以通过new 操作符调用一个类的构造方法实例化一个该类的对象。这时,内存中存放的是这个对象而不是这个类。从这个意义上理解,类是“死”的,而对象是“活”的,因为对象这个实体占有了内存空间。
(二)Java 中的变量
Java 中的变量分成员变量和局部变量两大类,而成员变量又分为实例变量和类变量两种。对于成员变量,如果声明后没有给初始值,系统会根据该变量的数据类型给其自动赋初始值,具体赋值可参见一、(六)节。而对于局部变量,在声明后必须显式赋给其初始值才能使用。
1.局部变量
在类的成员方法体内或参数列表中声明的变量称为局部变量。下述代码中m 和n 都是局部变量。初学者经常忽略的是方法参数列表中声明的变量,即m 是局部变量。
2.类变量
在类体中定义的变量称为成员变量,如上述代码中的变量i 和j。使用static 关键字修饰的成员变量称为类变量,又称静态变量,如上例中的变量i。在访问类变量时,不需要类的实例化,可以直接调用。调用时,前缀可使用类名或对象名,为增加程序的可读性,建议使用类名调用。使用一个类可以实例化的多个对象,这时所有的对象共享一个类变量,即该类变量在内存中是唯一的,与实例化对象的数量没有关系。因此,可以将网站首页用于访问计数的变量设计为类变量。初学者往往错误地认为类变量比实例变量高级,滥用类变量,容易导致不易察觉的运行时错误。
3.实例变量
没有static 关键字修饰的成员变量称为实例变量,如上例中的变量j。如果一个类有多个实例对象,那么每个对象都有自己的实例变量拷贝,它们之间相互不影响。实例变量必须在实例化对象后才可以使用。使用static 关键字修饰的成员方法称为类方法或静态方法。在静态方法中不允许使用实例变量。
(三)构造方法
构造方法是一种特殊的成员方法,它的特殊性在于:(1)构造方法的名字必须与类名一致,包括大小写;(2)构造方法只能在创建类的对象时由系统隐含调用或在某构造方法内调用另一构造方法;(3)构造方法没有返回类型。初学者容易将构造方法的返回类型说明为void,而这个编译错误很不容易查找。
1.构造方法的作用
在定义类的成员变量时,可以同时赋予初值。除此之外,对成员变量的操作只能放在方法中。
2.在继承树中构造方法的调用顺序
对于存在直接继承关系的父类和子类,如果子类中没有定义构造方法,则自动继承父类的不含参数的构造方法,并在创建一个子类的对象时自动执行。如果子类有自己的构造方法时,则创建子类的一个对象时也要先执行父类不含参数的构造方法,再执行自己的构造方法。具体来说,在实现子类的构造方法时,构造方法的调用顺序如下:
(1)先调用父类的构造方法,一直追溯到类的根源(即Object 类);
(2)如果该子类有引用类型的成员变量,则根据这些成员变量声明的顺序,调用各个成员变量的构造方法;
(3)最后才调用子类自己的构造方法。
(四)“is-a”和“has-a”
“is-a(是一种)”指继承;而“has-a(拥有)”指组合。如在上述代码中,Car 和Vehicle 是“is-a”关系,因为“小汽车是一种机动车”;而Car 和Engine 则是“has-a”关系,因为“小汽车有引擎”。由于继承在面向对象设计中的重要性,所以经常强调它的使用。于是,初学者就会有这样的观点:处处都应该使用继承。这会导致类的设计很复杂并难以被使用。实际上,在设计新类时,应首先考虑使用组合,即“has-a”关系,因为它更加简单灵活。
(五)Java中成员方法的参数传递
Java 中成员方法的参数传递究竟是“传值”还是“传地址”有很多争论,下面分基本数据类型和对象引用类型来讨论。
在Java 语言中,不同的方法中的变量各自独立使用自己的内存,不会相互影响。因此,上述代码中尽管两个变量名字都是i,但它们在内存中所占的位置不同。
(六)Java中的一些隐含规则
Java中包含一些隐含规则,不了解这些规则会使初学者感到很困惑。
首先,系统自动导入了java.lang包,相当于在程序的首行隐含加入了“imPortjava.lang.*;”语句;其次,类HelloWorld隐含继承自类Object,相当于类的声明为“classHelloWorld extends Object{?}”。因为,Java中的类是单根继承结构,除Object之外的所有类都是Object 的子类或间接子类。经常遇到的一些隐含规则如下。
(1)当类的成员变量在声明时没有赋初值,系统会自动赋给它们缺省值。如果成员变量的类型是对象引用类型,则其初值被赋为null。如果成员变量的类型是8种基本类型,即byte,short,int,long,flost,double,boolean或char。
二、Java学习中的一些难点
(一)抽象类和接口
抽象类和接口是Java语言中两个重要的对象引用类型,它们是Java语言程序设计中使用多态的基础。
1.基本概念
用关键字abstract修饰的类称为抽象类。抽象类的对象不能使用new运算符来创建,而需要产生该类的非抽象子类,由这个子类来创建对象。一个抽象类只关心它的子类是否具有某种功能,并不关心功能的具体行为,功能的具体行为由子类负责实现。用关键字abstract修饰的方法称为抽象方法。使用关键字interface声明一个接口,接口体中包括常量声明和方法声明,特别指出的是接口中的方法只有声明而没有实现。接口定义了两个对象之间进行通信的协议。使用接口的核心原因是为了实现多重继承。Java中的类是单根继承结构,不允许多重继承。为解决这个问题,引入了接口,而接口允许多重继承。初学者对接口的理解不深,事实上接口是一种特殊的类,在功能上像抽象类,为抽象方法和常量的整体声明。这一点对理解后面的接口回调过程很重要。
2.抽象类和接口的区别
虽然接口在功能上像抽象类,但它与抽象类是有区别的,主要表现在:
(1)接口中不能有非抽象的方法,但抽象类可以有;
(2)一个类能实现多个接口,但只能有一个父类;
(3)接口与类的继承无关,所以逻辑上无关的类也可以实现同一个接口;
(4)接口中不需要构造方法;
(5)接口中只能有类常量,不能有实例变量。
3.抽象类和接口的适用场合
两个毫不相关的类,如ComPuter和Door可以实现同样的功能oPen和close,因此使用接口会使程序设计更加灵活。有的初学者就提出这样的观点:接口有这么多优点,为什么不用接口代替抽象类呢?事实上它们有各自的适用场合。
(1)接口更有用一些,因为任何类都能实现(imPlements)它,即使该类已经继承了某个类。
(2)由于接口纯粹是一种API声明,使用它会使代码的设计更简洁。
(3)如果把一个接口定义为公共API的一部分,然后又要给这个接口增加一个新的方法,就会破坏实现原先接口的所有类,这时选择抽象类比较合适。
(4)如果要设计的类型要用到继承的优点时应选择将其设计为抽象类。
(5)两者可以结合起来使用。首先把设计的类型定义为接口,然后创建一个实现了该接口并且提供了可供子类使用的默认实现的抽象类。
(二)多态
面向对象编程的特点是封装、继承和多态,继承是多态的基础。很多初学者在学完一门面向对象的语言后,只记得封装和继承,而对多态缺乏感性认识,感觉它很难理解。多态是指“当某变量的实际类型和形式类型不一致时,调用此变量的方法,一定会调用到正确的版本,也就是实际类型的版本”。多态方法的版本不同的根源是方法的覆盖(override),而变量实际类型和形式类型不一致是对象的向上转型造成的。因此,首先要弄清楚方法的覆盖(override)和对象的向上转型两个概念。
1.方法的覆盖(override)
方法的覆盖是指子类中定义一个方法,该方法的名字、返回类型、参数个数和类型与从父类继承的方法完全相同。对于用子类创建的一个对象,如果该子类覆盖了父类的方法,则运行时系统调用子类覆盖的方法。
2.对象的向上转型
假设类B是类A的子类或间接子类。当创建一个子类B的对象,并把这个对象的引用放到父类A的对象中时则称对象a是子类对象b的上转型对象。对于上转型对象,要注意以下几点。
(1)上转型对象的实体是由子类负责创建的,但上转型对象会失去子类对象的一些属性和功能;具体讲,上转型对象不能操作子类新增的成员变量,也不能使用子类新增的方法。
(2)上转型对象可以操作子类继承或隐藏的成员变量,也可以使用子类继承的或覆盖的方法。
(3)当用上转型对象操作子类覆盖的方法时,就是通知其对应的子类对象去调用自己的方法,也就是子类覆盖的方法。
(4)可以将对象的上转型对象再强制转换到一个子类对象。这时,该子类对象又具备了该子类的所有属性和功能。
其中,第(3)条是理解多态的关键。
3.多态(PolymorPhism)
如果一个父类有很多子类,并且这些子类都各自覆盖了父类中的某个方法。那么当把子类创建的对象的引用放到一个父类的对象中时,就得到了该对象的一个上转型对象。这时,利用该上转型对象调用这个方法就可能具有多种形态,这种特性称为多态。
当增加一种继承自Instrument的新类如Stringed时,类Music中的tune方法不用做任何改动,就可以在main方法中使用语句“tune(new Stringed);”来调用类Stringed覆盖的方法Play。由于使用了多态机制,可以任意加很多这样新的类型,而不用修改tune方法。这样的程序可扩展性很好。因此,多态是一项让程序设计者“将改变的事物和未变的事物分开”的重要技术。多态机制的使用有以下限制:①多态机制只能用于对象引用类型,而不能用于基本数据类型;②多态机制只能用于成员方法,而不能用于成员变量;③多态机制只能用于实例方法,而不能用于类方法。