第3章 面向对象程序设计 正如第1章所提到的,最初的计算机程序设计语言主要集中在指令和语义的抽象上,并将操作指令和被操作的数据隔离开,分为代码段和数据段。随着程序复杂度越来越高,这种方法的局限性越来越大,后来计算机科学家将程序设计中模块化的思想进一步地强化,将待操作的数据和相应的操作指令组织在一起,形成了著名的面向对象程序设计方法。在面向对象设计中,人们可以借鉴人类研究和认识世界的诸多理论和方法,将世界中的具体对象经过分类和抽象后变成一个个符号化的模板类,类中有描述对象内部结构状态的数据部分,还有描述此对象具有的功能和行为的指令部分。而在程序运行过程中,又将符号化模板类转换为的具体的时空化的实例对象,用一个设计好的类可以创建许多对象。用计算机术语讲,类就是对象的代码抽象,是创建对象的代码模板,对象是进程中的类的具体实例。对象比结构化程序设计中讲到的函数、过程更具有实际意义,人类认识世界的基础就是从一个个可观测、具体的对象开始的,所以,面向对象程序设计具有现实的认知基础。 从世界观的角度来看,面向对象的基本哲学认为世界是由各种各样具有独特的运动规律和内部结构状态的对象所组成的,对象和系统环境既保有联系又保持独立,即对象必须依赖系统才能存在,但同时它又是一个独立的、可观察的个体。正是不同对象之间的相互作用和通信构成了一个完整的系统。因此,程序员应当按照现实世界的本来面貌来理解程序系统,直接通过对象及其相互关系来反映进程系统和客观世界,这样建立起来的系统才能接近和满足客观世界的真实需求。 从方法论的角度来看,面向对象的方法是面向对象的世界观在程序开发方法中的直接运用。它强调系统的结构应该直接与现实世界的结构相对应,应该围绕现实世界中的对象来构造系统,而不是围绕功能来构造系统。 从程序设计的角度考虑,程序对象应该是将组成对象的数据代码和对象具备的功能代码封装成一个整体,符合强内聚和弱耦合的原则。当然,面向对象的程序设计语言必须有描述对象内部结构及其相互之间关系的语法和规则。 视频讲解 3.1面向对象程序设计的基本概念 对象和类是面向对象程序设计的核心概念,程序员使用对象和类进行程序设计。类可以分成两种,一种是程序员可以直接使用的类,是由JDK提供的或其他人写好的; 另一种需要程序员自行设计。设计一个类大致可分为以下两步。 (1) 对现实世界的实体进行抽象,抽取其合适的状态和行为,形成思维中的类。 (2) 用Java语言来描述思维中的类,使思维中的类变成一个Java语言类。 对象是类的实例,同一个类可以建立多个对象实例,通过对象的使用可以达到程序设计的目的。对象和类既有区别又有联系,类是创建实例对象的代码模板,而对象则是按照类创建出来的一个个实例,有点像汽车的设计图纸和汽车的关系。采用面向对象程序设计技术的原因主要有两个,一是我们认识、研究乃至于改造世界都是以“对象”为基本单位进行的,将这一人类活动衍生到计算机编程中顺理成章; 二是为了提高程序设计的效率,尤其是在越来越复杂的问题环境中解决模块的颗粒度问题,即内聚性和耦合性的分界线问题。 Java语言面向对象程序设计(第3版·微课视频版) 第 3 章 面向对象程序设计 3.1.1对象 在现实世界中,对象一般指的是一个独立的客观实体,它一般都有一定的内部结构,对外表现出一定的属性和行为,并且和周围的世界有一定的交互性,至少占有一定空间和时间。 在面向对象程序设计中,对象就是数据结构加上指令代码,或称数据与代码的组合,它是理解面向对象程序设计技术的关键。为了理解这一点,先来研究现实世界中的对象。我们周围的汽车、电视机、狗、猫、书桌、自行车等都是现实世界中的物理实体,也就是通常所说的客观对象。现实世界中的对象具有3个特征: 即状态、行为和事件响应能力,例如,自行车有状态(传动装置、步度、两个车轮和齿轮的数目等)和行为(刹车、加速等)。对事件的响应能力是对象通过行为或功能方法实现的,它代表了一个对象和周围世界或其他对象的一种交互能力。程序对象是现实世界的对象在计算机内部的模拟化产物,它们也有状态和行为,程序对象把状态用数据来表示并存放在变量中,而行为则用方法(指令集合)来实现。 把一个对象的数据加以包装并置于其方法的保护之下称为封装,所谓封装,就是对象对内部数据和结构的一种隐藏和隔离。封装实现了把数据和操作这些数据的代码包装成一个对象,将数据和操作细节(方法)隐藏起来,只暴露必要的交互接口,这和客观世界中对象是类似的,例如小狗、小猫等。和对象的交互必须符合接口标准选择和限制,观察小狗、小猫和小鸟等动物对象就可以理解,这使得与对象的交互可按照统一的方式进行,这样就能比较容易地产生更为统一和健壮的系统程序。 面向对象程序设计还体现了另外一个哲学思想,即意识和物质的不可分性,行为和肉体的不可分性。以人的大脑为例,如果没有了大脑的神经元细胞物质组成部分,也就无所谓意识,这样的人是不会存在的,因为意识没有载体。同样,如果一个人的大脑的物质组成正常,但将其意识行为去掉,这样的人也只是“植物人”,不是一个真正的人。所以,物质结构决定了行为,行为又改变着物质结构,它们是不可分的一体两面。 在计算机程序设计中,只有设计出精巧的数据结构再配以合适的方法代码(即抽象和封装),加上继承和多态,才是真正的面向对象程序设计。 3.1.2类 在物理或生物世界中,类代表一种抽象概念,例如猫作为一类动物的统称,它们都具有一些基本的、相同的属性和行为。在科学研究中使用类属概念将世界分门别类,再进行归纳、演绎和研究。 在计算机程序设计中,类是一个蓝图或模板,定义了某种类型的所有对象具有的数据特征和行为特征。在Java语言中,程序设计的基本单位就是类,也就是说,一个Java程序是由许多设计好的类组成的。而对象实际是程序运行时通过类创建的一个个实例,生成对象的过程叫作“实例化”。一个实例化的对象实际上是由若干个成员变量和成员方法组成的封装体。当创建一个对象时,系统将为该实例中的成员变量分配内存,然后利用成员方法去和系统或其他对象交互。 为了更好地理解类和对象的概念,这里用生命现象中的相关概念进行类比。如果将胚胎细胞中的DNA分子链看成是由4种基本的核糖核酸ATGC组成的编码序列,是一种代码模版,对应的就是此处讲的类代码模板。那么经过了胚胎发育,最后成长为一个生命体。例如,人、鸡、猫、狗、马、蛇等,就是DNA代码模板创建的一个个实例对象。生命对象具有生命周期,计算机程序中的程序对象也有构造、初始化、功能交互、死亡等周期。 3.1.3类设计的Java语法 类是创建对象的代码模板,一般由两部分组成,即描述对象状态和结构的成员变量和描述对象行为的成员方法。在Java语言的语法中,类由两部分组成: 类声明和类体。其基本格式如下: [修饰符] class类名 [extends父类名] [implements 接口名]{类体的内容} 其中,[修饰符]可以用public代表公开,private代表私有,class是定义类的关键字,extends是继承关键字,implements是实现接口的关键字,类体部分代表此类的主体部分,又包括以下两部分。 1. 成员变量(用来描述对象的属性) [修饰符]类型变量名 [=初值] [,变量名 [=初值] …]; 说明: (1) 类型: 可以是Java的基本类型,例如int、float等,也可以是复杂类型,如自己定义的类,或者数组、接口等。 (2) 变量名: 必须是合法的Java标识符。 (3) 修饰符: 说明变量的访问权限和某些使用规则。可以是public、private、protected、static、final等,后面会一一讲到。 (4) 当成员变量含有自己的初始化表达式时,可以对变量初始化,即赋初值。 2. 成员方法(用来描述对象的行为) 方法是对对象功能行为的描述,对象通过执行它的方法对传来的消息做出响应。方法的定义只能在类中定义,它是完成某种功能的程序块,一个类或对象可以有多个方法。 方法的定义指描述方法的处理过程及其所需的参数,并用一个方法名来标识这个处理过程。方法定义中的形式参数并没有实际值,仅仅是为了描述处理过程而引入的占位符。 方法的使用就是通过向实例对象发送消息执行方法所定义的处理功能。在使用方法时给出参数的实际值,这些实际值称为实际参数(简称为实参)。 [修饰符]返回类型方法名([形式参数列表])[throws 异常列表] {方法体} 说明: (1) 返回类型: 说明此方法执行完后会返回一个值,这里指的是返回值的数据类型,可以是基本类型,也可以是复杂类型。如果返回类型为void,表示返回值为null,即不返回任何值。 (2) 方法名: 方法的名称,必须是合法的Java标识符。 (3) 形式参数列表: 说明使用此方法所需要的参数列表,可以有0个或多个,多个参数间用逗号(,)隔开。在方法执行时,调用者会将调用时的实际参数值复制(传递)一份到形参变量中(也称按值传递),传递过程是按照顺序依次对应传递的。 (4) 修饰符: 说明此方法的访问权限和某些使用规则。可以是public、private、protected、static、abstract和final等。 (5) 方法体: 用一对花括号({})括起来,包含局部变量定义和相应的执行语句。 (6) 异常列表: 说明本方法有可能产生的异常,需要调用者处理,在后面会详细讲解。 举例说明,我们需要抽象一个复数类,读者在数学中应该学过,一个复数由两部分组成,即一个实部一个虚部,组成形如a+bi的形式,复数的各种运算读者也应该清楚,则一种可能的抽象如下例所示,图31对该类的结构进行了说明。 图31类设计示意图 【例31】复数类抽象。 import java.util.Scanner; public class Complex extends Object implements Cloneable{ private double realpart; private double imaginarypart; public Complex(){realpart=0;imaginarypart=0;} //默认构造方法 public Complex(double s,double x){realpart=s;imaginarypart=x;} //构造方法 public void inputme() { Scanner keyin=new Scanner(System.in); System.out.print("real:"); realpart=keyin.nextDouble(); System.out.print("imaginary:"); imaginarypart=keyin.nextDouble(); } public void printme() { String str=""+realpart; if(imaginarypart<0.0) str=str+imaginarypart+"i"; else str=str+"+"+imaginarypart+"i"; System.out.println(str); } } 注意: 和现实世界中对象一样,进程中的每个对象也需要一个构造过程(对应于现实世界中对象的生产过程),构造方法用来完成这一过程,对象一经创建,就可以和其他对象或系统进行交互,交互一般通过方法调用进行。在类中直接定义的变量并且没有static修饰符,被称为对象的成员变量,在创建对象后,每个对象都会有一份。 3.1.4消息 由于不存在孤立系统,在进程中,一个孤独的对象是没有用的。对象往往是作为一个组员出现在包含有许多其他对象的大程序或应用软件之中,通过这些对象的相互作用,可以实现高层次的操作和更复杂的功能。在进程中,对象通过向其他对象发送消息与其他对象进行交互和通信。例如,当对象A要执行对象B中的方法时,对象A便发送一个消息给B。有时,接收消息的对象需要更多的信息以便能精确地知道做什么。消息以参数的形式传递给某个方法。一个消息通常由以下3个部分组成。 (1) 接收消息对象的名称。 (2) 要执行方法的名称。 (3) 方法需要的参数。 举例说明,胡亥给李斯打电话,叫他准备明天早上10点起床。在这里,胡亥是消息的发送者,李斯是消息的接收者,将要执行的方法是“起床”,参数是第二天早上10点。 消息的优点在于提供了对象交互的统一手段。不同进程中或不同计算机上的对象也可以通过消息相互作用。在计算机程序中,消息实际上就是一个方法调用过程,是一个类或对象调用其他对象或类方法的过程,可以理解为消息是方法调用的专署名词,适用于面向对象领域。 例如我们要测试前面定义的复数类Complex,可用以下测试类来进行测试。 【例32】测试复数类TestComplex。 public class TesstComplex { public static void main(String[] args) { Complex m1=new Complex(3.4,8.0); m1.inputme(); //调用inputme()方法,即给m1对象发输入请求的消息 m1.printme(); //调用printme()方法,即给m1对象发打印输出的消息 } } 视频讲解 3.1.5引用和引用变量 在第2章中已经介绍过引用,此处进一步阐述。引用就像现实世界中的空间地址,可以通过地址来找一个具体对象。Java中的引用类似于C语言中的指针概念,但在C语言中,指针是一个内存地址,用一个大于0的正整数来表示,可以进行加减运算。Java中的引用本质上也是内存地址,但是不能进行加减运算,用来说明此地址处有一个对象,理论上它也代表一个对象在内存中开始地址,其中null(即0)代表空引用,即此引用目前不指向任何有效对象。 如果用一个类定义一个变量,或通过数组形式定义的变量,或后面讲到的通过接口定义的变量,则该变量就是一个引用类型变量,可以存储一个内存地址了。基本类型变量和引用类型变量的存储结构示意图参考第2章的图29和图210。 在Java中通常会通过new来创建一个对象和引用进行关联,如例32中的: Complex m1 = new Complex(3.4,8.0); 这样不仅创建了一个对象和引用m1进行关联,同时也进行初始化。如果定义了一个引用,但没有指向任何对象,如用例32中的复数类定义一个变量Complex myvar,此时myvar的值为null,即没有指向任何实际对象。如果调用它的成员方法或访问成员变量就会导致异常发生,因为对象还不存在,所以对象的属性和方法都不存在。 引用可以作为方法的参数,通过引用来传递对象,从而可以改变对象的内部状态,类似于户口本中的地址信息,派出所可以通过地址信息定位、找人,并可以修改户口或身份信息,例如从未婚修改为已婚等,但引用本身并不发生变化。 注意: 如果用类来定义数组,则该数组变量也是一个引用变量,因为Java语言中数组为对象,同时每一个数组元素又都是存储对象的引用,用来指向该类的一个实例对象,所以Java中用类或接口定义的数组,类似于C语言中的指针数组概念。 3.1.6this关键字 Java用this引用指向对象自己,也就是说,当一个对象创建好后,Java虚拟机就会给它分配一个引用自身的符号this。在使用对象的成员变量和成员方法时,如果没有指定相应的对象约束,则默认使用的就是this引用。 程序中一般在以下情况使用this关键字。 (1) 在类的构造方法中,通过this语句调用这个类的另一个构造方法。 (2) 在一个非静态成员方法中,如果局部变量或形参变量与非静态成员变量同名,成员变量被屏蔽,要使用this.varname这种形式来指代成员变量。 (3) 在一个方法调用中,可以使用this将当前实例的引用作为参数进行传递。 【例33】测试this关键字。 public class TestThis { int x; //对象成员变量 TestThis(int x) { //形参变量,局部变量 this.x=x; } public void passingValue(){ System.out.println("x 等于 " +x);//成员变量,即this.x } public static void main(String args[]) { TestThis test = new TestThis(10); test.passingValue(); } } 对例31中的复数类进行改进,增加了加减乘除方法,在下例中综合演示了this、引用变量、方法调用(即消息)等方面。 【例34】复数类的进化版及其测试。 //Complex1.java import java.util.Scanner; public class Complex1 extends Object implements Cloneable{ private double realpart; private double imaginarypart; public Complex1() { realpart = 0; imaginarypart = 0; } // 默认构造方法 public Complex1(double s, double x) { realpart = s; imaginarypart = x; } // 构造方法 public void inputme() { Scanner keyin = new Scanner(System.in); System.out.print("real:"); realpart = keyin.nextDouble(); System.out.print("imaginary:"); imaginarypart = keyin.nextDouble(); } public String toString() { String str = "" + realpart; if (imaginarypart < 0.0) str = str + imaginarypart + "i"; else str = str + "+" + imaginarypart + "i"; return str; } public double getRealpart() { return realpart; } public void setRealpart(double realpart) { this.realpart = realpart; } public double getImaginarypart() { return imaginarypart; } public void setImaginarypart(double imaginarypart) { this.imaginarypart = imaginarypart; } public Complex1 add(Complex1 other) { // 复数加法 return new Complex1(this.realpart + other.realpart, this.imaginarypart + other.imaginarypart); } public Complex1 sub(Complex1 other) {// 复数减法 return new Complex1(this.realpart - other.realpart, this.imaginarypart - other.imaginarypart); } public Complex1 mut(Complex1 other) {// 复数乘法 double r = this.realpart * other.realpart - this.imaginarypart * other.imaginarypart; double i = this.imaginarypart * other.realpart + this.realpart * other.imaginarypart; return new Complex1(r, i); } public Complex1 div(Complex1 other) {// 复数除法 double denominator = other.realpart * other.realpart + other.imaginarypart * other.imaginarypart; double r = (this.realpart * other.realpart + this.imaginarypart * other.imaginarypart) / denominator; double i = (this.imaginarypart * other.realpart - this.realpart * other.imaginarypart) / denominator; return new Complex1(r, i); } } 3.1.7匿名对象 所谓匿名对象,就是创建的对象没有特定引用指向它。如下例所示: //TestAnonymous.java public class TestAnonymous { public static void main(String[] args) { new Complex1(3.0,5.0).printme();//创建匿名对象并调用printme方法 } } 匿名对象在使用完后,即变成垃圾对象,等待垃圾回收器回收。 视频讲解 3.1.8方法重载 在同一个类中有多个同名的方法,但方法参数列表不同,执行代码也不同,称为方法重载。Java中的方法重载也是实现多态性的方法之一,但方法重载是静态绑定的,即在编译时已确定好要执行的方法代码。方法重载主要通过实参列表和形参列表的配对来确定要使用哪个方法。 【例35】方法重载演示。 class Calculation { public void add( int a, int b) { int c = a + b; System.out.println("两个整数相加得 "+ c); } public void add( float a, float b) { float c = a + b; System.out.println("两个浮点数相加得"+c); } public void add( String a, String b) { String c = a + b; System.out.println("两个字符串相加得 "+ c); } public void add(Complex1 a, Complex1 b) { Complex1 f1 = new Complex1(a.getRealpart() + b.getRealpart(), a.getImaginarypart()+b.getImaginarypart()); System.out.println("两个复数相加得 " + f1); } System.out.println("两个复数相加得 "+f); } } class CalculationDemo { public static void main(String args[]) { Calculation c = new Calculation(); c.add(10,20); c.add(40.0F, 35.65F); c.add("早上", "好"); Complex1 f1=new Complex1(3.4,2.8); Complex1 f2=new Complex1(1.6,-7.8); f1.display(); f2.display();; c.add(f1,f2); } } 3.1.9构造方法设计和对象的创建 前面已讲过,对象是类实例化后的产物,所谓实例化是按照类的设计创造对象的过程,就是给此对象分配内存空间并初始化,即要进行一系列的构造工作,使其变成一个合适的、可用的对象,这就是构造方法所完成的工作,类似于现实世界中动物对象的分娩或孵化过程。 注意: ① 构造方法是类中一个特殊的方法,特殊之处在于此方法要与类名同名,并且不能有返回类型。 ② 构造方法不能有返回类型并不能代表它不能有返回值,实际上它要返回对象在内存中的开始地址。 ③ 构造方法可以重载,没有任何参数的构造方法称为默认构造方法。 如例31中的Complex()是默认构造方法,而Complex(double s,double x)则是带有两个形参的构造方法。在Java中,如果程序员没有提供构造方法,则Java编译器会自动提供一个默认的构造方法,如果程序员提供了构造方法,则Java编译器不再提供任何构造方法。 除了特殊的设计模式以外,建议读者最好提供一个默认的构造方法。例如抽象一个学生类和班级类: 【例36】学生类Student。 //Student.java public class Student { private String name; private char sex; private int age; private String[] coursenames; private double[] coursescores; public Student(){ //默认构造方法 name="unknown name!"; sex='M'; age=0; coursenames=new String[3]; coursescores=new double[3]; coursenames[0]=new String("语文"); coursenames[1]=new String("数学"); coursenames[2]=new String("英语"); coursescores[0]=coursescores[1]=coursescores[2]=0.0; } public Student(String n,char s,int a){ //带参数的构造方法 name=n; sex=(s=='F')?s:'M';//过滤数据 if(a>=0&&a<=40) age=a;//过滤数据 else age=18; coursenames=new String[3]; coursescores=new double[3]; coursenames[0]=new String("语文"); coursenames[1]=new String("数学"); coursenames[2]=new String("英语"); coursescores[0]=coursescores[1]=coursescores[2]=0.0; } public void introduceMe() { System.out.println("我的名字是:"+name); System.out.println("我的性别和年龄分别是:"+sex+" 和 "+age); System.out.println("我的成绩还没有输入!"); } } 在定义完类后,就可以用类来创建对象并使用对象。关键字 new 通常称为创建运算符,用于分配对象内存,并将该内存初始化为默认值,然后调用构造方法来执行对象具体初始化。例如下例: //TestStudent.java public class TestStudent { public static void main(String[] args) { Student stu1=new Student(); //调用默认构造方法 Student stu2=new Student("张三",'M',23); //调用带参数的构造方法 stu1.introduceMe(); stu2.introduceMe(); } } 在new分配内存后,各种类型变量的默认初始值如表31所示,也就是调用构造方法之前的值。 表31成员变量的默认值 类型 默认值 类型 默认值 byte (byte)0 char '\u0000 ' short (short)0 float 0.0F int 0 double 0.0D long 0L 对象引用 null boolean false 注意: 如果一个构造方法通过关键字this调用另一个构造方法,则该调用语句必须出现在第一句。 3.1.10getter方法和setter方法设计 前一节已初步设计好Student类,对它内部的数据做了封装,使得类外不能直接访问。例如,在StudentTest类中的main方法中,如果想直接给stu1对象的年龄age赋值,即“stu1.age=200;”,这是错误的,这种破坏封装的语句在Java编译时就不允许通过。封装带来两个好处,一是被封装的数据对外是不可见的,二是通过提供一系列的getter方法和setter方法去读写这些数据,通过这些方法中可以过滤传进来的数据,就像人的消化系统一样,所有的食物经过消化系统后部分转化成了对人有用的营养,而非法数据则被过滤,这就是对象对外提供的交换接口。 getter方法和setter方法的编写也很简单,一般以get和set开头,后面单词的第一个字母一般大写。例如,给Student类加上合适的setter和getter方法。 public String getName(){return name;} public void setName(String n){name=n;} public char getSex(){return sex;} public void setSex(char s){sex=(s=='F')?s:'M';} public int getAge(){return age;} public void setAge(int a){age=(a>=0&&a<=40)?a:18;} public String[] getCoursenames(){return coursenames;} public String getCoursename(int i){return coursenames[i];} public void setCoursenames(String[] cn){coursenames=cn;} public void setCoursename(String cn,int i){ coursenames[i]=cn; } public double[] getCoursescores(){return coursescores;} public double getCoursescore(int i){return coursescores[i];} public void setCoursescores(double[] cs){coursescores=cs;} public void setCoursescore(double cs,int i){coursescores[i]=cs;} 注意: 针对具有多值的成员变量,一般是数组或集合,应该至少提供两套getter方法和setter方法,如上例所示。 3.1.11toString()方法和equals()方法设计 在上一章介绍了Object类,说Object类是Java中所有其他类的根,也可以说它是所有类的一个框架设计,在此类中有两个重要的方法。 (1) toString()方法: 用来将一个对象转换成字符串描述形式。 (2) equals()方法: 用来比较两个对象的内容是否一样。 它们的原始实现非常简单,没有实际用处,所以在类中,要给出更有意义的、更具体的实现代码,在面向对象程序设计理论中这叫作方法重写,在继承中还会讲到。 public boolean equals(Object obj) { student anotherstu=(student)obj; boolean flag=true; if(!name.equals(anotherstu.getName())) flag=false; else if(age!=anotherstu.getAge()) flag=false; else if(sex!=anotherstu.getSex()) flag=false; return flag; } public String toString() { String myinfo="name:"+name+"\tsex:"+sex+"\tage:"+age; myinfo=myinfo+"\n==========================================\n"; for(int i=0;i= 1 && r <= 1000) SCREEN_HEIGHT = r; else SCREEN_HEIGHT = 50; if (c >= 1 && c <= 1000) SCREEN_WIDTH = c; else SCREEN_WIDTH = 80; data = new char[SCREEN_HEIGHT][SCREEN_WIDTH]; } public void init() { for (int i = 0; i < SCREEN_HEIGHT; i++) { for (int j = 0; j < SCREEN_WIDTH; j++) { data[i][j] = ' '; } } } public void display() { for (int i = 0; i < SCREEN_HEIGHT; i++) { for (int j = 0; j < SCREEN_WIDTH; j++) { System.out.print(data[i][j]); } System.out.println(); } } public void repeat(char ch, int m) { for (int i = 1; i <= m; i++) print(ch); } public void print(char ch) { if (y < SCREEN_HEIGHT && x < SCREEN_WIDTH) { data[y][x] = ch; x++; if (x == SCREEN_WIDTH) { y++; if (y == SCREEN_HEIGHT) { scroll(); y = SCREEN_HEIGHT - 1; } x = 0; } } else { System.out.println("错误:超出屏幕了!"); } } public void println() { y++; if (y == SCREEN_HEIGHT) { scroll(); y = SCREEN_HEIGHT - 1; } x = 0; } public void scroll() { for (int i = 0; i < data.length - 1; i++) { data[i] = data[i + 1]; } data[data.length - 1] = new char[SCREEN_WIDTH]; } } 修改后,外部就无法修改屏幕(Screen)类对象的内部数据了,并且相应方法的代码也做了过滤处理,使非法数据无法进入。 视频讲解 3.2.3继承原理 继承也是存储的另一种形式,是“数据和指令代码”的动态存储方式,人类的学习、工作都依赖于此。继承不是简单的复制,其基本内涵中有发展和改变的含义,所以有继承才有进化,这也是生命是程序的另一个证据。 在面向对象程序设计中,从已存在的类产生新类的机制也被定义为继承。原来存在的类叫父类(或叫基类),新类叫子类(或叫派生类),子类中会自动拥有父类中的设计代码,还可以改写原来的代码或添加新的代码。继承带来的好处有两个方面,一方面可减少程序设计的错误,另一方面做到了代码复用,可简化和加快程序设计的流程,提高工作效率。 继承不仅仅是简单的拥有父类的设计代码,继承机制本身就具有进化的能力,跟生物世界一样,子代总是比父代更能适应环境。通过对父类的设计作一些局部的修改,可以使得子类对象具有更好的适应能力和强大的生存能力。 如果从一个抽象模型中剔除足够多的细节,则它将变得更通用,能适应多种情况或场合,这样的抽象常常在程序设计中非常有用。经过对大量事物的抽象和归类,可以形成相应的类属层次。例如,前面的示例,如果想在屏幕中不但可以打印矩形,还可以打印菱形、直角三角形、圆形等一系列形状,则应该分层抽象类如图38所示。 图38继承示意图 面向对象程序设计的最强大功能之一就是代码重用。面向过程的结构化设计提供的代码重用非常有限,基本上限定在编写一个功能模块,然后在进程中多次调用它,或者是对特定的编码复制粘贴再修改。但是在面向对象的设计中代码重用已经很完善了,通过定义类之间的关系,通过组织和识别不同类之间的共性,不仅可以实现代码重用,还可以指导人们对复杂问题分层抽象、分层处理,继承就是实现该功能的主要原理。 在面向对象程序设计中,如何实现继承,不同语言有不同的实现机制,在Java 语言中,通过关键字extends来指明一个子类从一个父类扩展而来。举例说明,以图38来演示。 【例314】继承原理演示。 //Shape.java public class Shape { protected int x; protected int y; public int getX() { return x; } public void setX(int x) { if(x>=0&&x<1000) this.x = x; else this.x=0; } public int getY() { return y; } public void setY(int y) { if(y>=0&&y<1000) this.y = y; else this.y=0; } public Shape() {} public Shape(int x,int y) { if(x>=0&&x<1000) this.x = x; else this.x=0; if(y>=0&&y<1000) this.y = y; else this.y=0; } public void printme(Screen sc) { sc.setY(y); sc.setX(x); System.out.println(); } public void move(int x,int y) { if(x>=0&&x<1000) this.x = x; else this.x=0; if(y>=0&&y<1000) this.y = y; else this.y=0; } } //Lingxing.java public class Lingxing extends Shape { private int h; public Lingxing() { this(0,0,7); } public Lingxing(int x,int y,int h) { super(x,y); this.h=h; } public void printme(Screen myscreen) { // 覆盖父类中printme()方法 myscreen.setY(y); for (int i = 1; i <= (h + 1) / 2; i++) { myscreen.setX(x); myscreen.repeat(' ', h / 2 + 1 - i); myscreen.repeat('*', 2 * i - 1); myscreen.println(); } for (int i = h / 2; i >= 1; i--) { myscreen.setX(x); myscreen.repeat(' ', h / 2 + 1 - i); myscreen.repeat('*', 2 * i - 1); myscreen.println(); } } } //Circle.java public class Circle extends Shape{ private int r; public Circle(int x,int y,int r) { super(x,y); this.r=r; } public void printme(Screen sc) { //覆盖父类中的printme()方法 // x*x+y*y=r*r sc.setY(y); for(int y=0;y<=2*r;y+=2) { int lx=(int)Math.round(r-Math.sqrt(2*r*y-y*y)); int len=2*(r-lx); sc.setX(this.x+lx); sc.print('*'); for(int j=0;j<=len;j++) { sc.print('*'); } sc.print('*'); sc.println(); } } } //Triangle.java public class Triangle extends Shape{ private int h; public Triangle() { this(0,0,7); } public Triangle(int x,int y,int h) { super(x,y); this.h=h; } public void printme(Screen myscreen) { //覆盖父类中printme()方法 myscreen.setY(y); for(int i=1;i<=h;i++) { myscreen.setX(x+h-i); myscreen.repeat('*',2*i-1); myscreen.println(); } } } //TestInherit.java 测试类 public class TestInherit { public static void main(String[] args) { Screen myscreen=new Screen(25,80); myscreen.cls(); Lingxing mylx=new Lingxing(0,0,9); mylx.printme(myscreen); Lingxing mylx2=new Lingxing(20,1,12); mylx2.printme(myscreen); Rectangle rc=new Rectangle(14,1,5,7); rc.printme(myscreen); Triangle tr=new Triangle(56,2,7); tr.printme(myscreen); Circle c=new Circle(34,0,10); c.printme(myscreen); myscreen.display(); } } 注意: Java中实现继承的关键字是extends。 测试类运行结果如图39所示。 图39测试类运行截图 从该示例可以看出,Shape类可以看成各种图形的抽象父类,从而可以派生出各种具体的图形类,如Rectangle、Triangle等,子类自动拥有父类中的成员变量x、y,同时继承了父类中的各种公有成员方法,各子类有根据自己形状的特点,给出了printme()方法的覆盖实现,从而为实现多态打好了基础。 继承提供的是isa关系,即父类相对于子类更为抽象,子类更为具体,子类对象同样隶属父类型。生物类是动物类的父类、动物类是人类的父类,张三是一个人类对象,同样的,张三也是一个动物类对象或者生物类对象。 视频讲解 3.2.4多态原理 多态原理是生物多样性在面向对象程序设计中的应用,指的是在一个系统中同一消息可能会引发多种反应。例如,多个动物面对同样的刺激、消息等,不同的动物的反应是不一样的。在面向对象程序设计中,如果有许多不同的对象,每个对象都具有相应的行为模式(即执行代码),对每个对象发送同样的消息,但每个对象的执行代码是不一样的,这就是面向对象程序设计中的多态原理,多态原理如图310所示,给不同的打印机发送相同的打印消息,不同的打印机有不同的打印实现方式。 图310多态示意图 在具体实现上是指程序中定义的引用变量所指向的具体对象和通过该引用变量发出的方法调用在编译时并不确定,只有在程序运行期间才能确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在程序运行期间才能决定。因为在程序运行时才确定具体的对象,这样不用修改源程序代码就可以让引用变量绑定到各种不同的代码实现上,从而导致该引用调用的具体方法代码随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择不同的代码来运行,这就是多态性实现技术,多态性增强了软件的灵活性和扩展性。 多态性(polymorphism)是面向对象编程的基础属性,它允许多个方法使用同一个接口,从而导致在不同的上下文中对象的执行代码可以不一样。Java从多个方面支持多态性,其中两个方面最为突出,一是每个方法都可以被子类重写; 二是设立interface关键字。另外,Java还支持通过方法重载实现的静态多态方式,即通过在编译时根据方法的参数不同选择编译不同的实现代码来实现多态,在运行时不再根据上下文改变。 由于超类(父类)中的方法可以在派生类(子类)中重写,因此,创建类的层次结构非常重要。在类的层次结构中,每个子类都是它的父类的特殊化(specialization)或具体化。从类属关系上来讲,属于底层类的对象肯定属于高层类。例如,小学生类是学生类的子类,学生类是人类的子类等, 如果张三是一个小学生,则张三一定是一个学生,并且张三一定是一个人。在Java中,父类的引用可以指向子孙类对象,从而可以通过父类引用来调用子类对象的方法。在Java中,多态是通过动态绑定实现的,通过父类的引用调用某子类对象的一个方法时,会自动执行由该子类重写后的版本。因此,可以用父类来定义对象的形式并提供对象的默认实现,而子类根据这种默认实现进行修改,以更好地适应具体情况的要求。总之,在父类中定义的一个接口可以作为多个不同实现的基础。继续用前面的示例程序,重新写一个测试类,采用多态原理,测试类的代码如下: 【例315】多态原理演示。 //TestPolymorphism.java public class TestPolymorphism { public static void main(String[] args) { Screen myscreen=new Screen(25,80); myscreen.cls(); Shape shapes[]=new Shape[5]; //通过父类定义了有5个引用变量的数组 shapes[0]=new Lingxing(0,0,9); //指向一个菱形对象 shapes[1]=new Lingxing(20,1,12); shapes[2]=new Rectangle(14,1,5,7); //指向一个矩形对象 shapes[3]=new Triangle(56,2,7); //指向一个三角形对象 shapes[4]=new Circle(34,0,10); //指向一个圆形对象 for(int i=0;i= 0; i--) { if (coefficient[i] > 0) { str.append("+" + coefficient[i]); if (i > 0) { if (i == 1) { str.append("*x"); } else { str.append("*x^" + i); } } } else if (coefficient[i] < 0) { str.append(coefficient[i]); if (i > 0) { if (i == 1) { str.append("*x"); } else { str.append("*x^" + i); } } } } return str.toString(); } public void add(Item item) { if (item.getIndex() > maxindex) { int newmaxindex = item.getIndex(); double[] newcoefficient = new double[newmaxindex + 1]; for (int i = 0; i <= maxindex; i++) { newcoefficient[i] = coefficient[i]; } for (int i = maxindex + 1; i < newmaxindex; i++) { newcoefficient[i] = 0; } newcoefficient[newmaxindex] = item.getCoefficient(); maxindex = newmaxindex; coefficient = newcoefficient; } else { coefficient[item.getIndex()] += item.getCoefficient(); } normalize();// 最高系数为0,规范化处理 } public void normalize() { if (maxindex == 0) return; int i = maxindex; while (i>0&&Math.abs(coefficient[i])<1e-5) { i--; } maxindex = i; } public Polynomial add(Polynomial another) { int newmaxindex = Math.max(maxindex, another.maxindex); double[] newcoefficient = new double[newmaxindex + 1]; int i = newmaxindex, j = 0; while (i > another.maxindex) { newcoefficient[j++] = coefficient[i--]; } while (i > maxindex) { newcoefficient[j++] = another.coefficient[i--]; } while (i >= 0) { newcoefficient[j++] = coefficient[i] + another.coefficient[i]; i--; } Polynomial tmp = new Polynomial(newmaxindex, newcoefficient); tmp.normalize(); return tmp; } public Polynomial sub(Polynomial another) { int m = another.maxindex; for (int i = m; i >= 0; i--) { another.coefficient[i] = -another.coefficient[i]; } return add(another); } public Polynomial mut(Polynomial another) { int m = maxindex; int n = another.maxindex; int newmaxindex = m + n; double[] newcoefficient = new double[newmaxindex + 1]; Polynomial p = new Polynomial(newmaxindex, newcoefficient); for (int i = m; i >= 0; i--) { double thisco = coefficient[i]; // 系数 for (int j = n; j >= 0; j--) { double anotherco = another.coefficient[j]; double newco = thisco * anotherco; int newindex = i + j; Item item = new Item(newindex, newco); p.add(item); } } return p; } // 用递归方法解决除法 // 如果最高幂次小于another的最高次幂,则结束递归,返回商和余数 // 否则,求出该对象最高次幂和another对象的最高次幂的差值,构造一个Polynomial对象obj,只包含一项item,将其添加到商results[0]中,然后调用sub(another.mut(obj)),消除最高次幂项,得余式results[1] // 继续递归调用results[1].div(results,another); private Polynomial[] div(Polynomial[] results,Polynomial another) { if(this.maxindex