Java编程那些事儿57—面向对象基础
第八章 面向对象
在程序中,最核心的是数据结构和算法,不同的程序需要根据需要设计不同的数据结构,然后依赖程序的功能以及数据结构设计对应的算法,这种设计方式是程序的底层设计,也就是解决具体的功能。
当程序项目复杂到一定程度时,就不仅要专注于底层的设计,更要对程序的结构进行设计,面向对象(Object-Oriented,简称OO)就是一种常见的程序结构设计方法。
面向对象思想的基础是将相关的数据和方法放在一起,组合成一种新的复合数据类型,然后使用新创建的复合数据类型作为项目的基础。
8.1 面向对象简介
前面介绍的有关Java语言的基础知识,只是程序的基础知识,而本章介绍的面向对象的相关知识,则是和设计有关的知识。
面向对象的设计方式采用的是从外到内的设计方式,先设计整个项目的结构,然后再根据关联关注内部的每个细节。再分解整个项目时,也是按照模块化进行分解的。就像要制造一辆汽车,面向对象的设计思路是这样的:首先汽车要生产发动机、变速箱等模块,然后再去考虑每个模块的具体实现。使用这种设计思路,把每个部分都模块化,便于将功能进行分解,可以开发更复杂的项目。
再将模块划分出来以后,然后就来设计每个具体的模块,再设计模块时,如果模块还很复杂,则可以继续进行分解。如果模块已经划分的足够细致了,那么就可以进行具体的设计了。
设计具体模块的方式是确定模块需要的核心数据的结构,以及该模块需要具备的功能,也就是本章一开始提到的数据结构和算法,使每个模块都成为一个独立的完整结构,可以向其它的模块提供对应的服务(功能)。
整个系统(项目)则通过模块之间的互相关联运转起来,而每个模块只需要开放一个接口给其它的模块即可。
上面提到的就是面向对象的设计方式,总结起来是两大部分:
l 模块划分
l 模块实现
在具体的面向对象编程(Object-Oriented Programm,简称OOP)中,划分出来的每个模块一般称为类(class),而模块内部的数据称为field,一般称为属性,模块内部的功能一般称为方法(method)。
按照面向对象的设计方式,在实际的项目开发过程中,面向对象技术一般分为3个部分:
l 面向对象分析(Object-Oriented Analysis,简称OOA)
该步骤按照面向对象的思考方式提取项目的需求信息,一般由系统分析员负责,本部分形成文档为《项目需求分析说明书》。
l 面向对象设计(Object-Oriented Design,简称OOD)
该步骤按照《项目需求分析说明书》进行模块划分,以及进行模块的概要设计,一般由高级程序员负责,本部分形成文档为《项目概要设计说明书》。
l 面向对象编程(Object-Oriented Programm,简称OOP)
该步骤按照《项目概要设计说明书》细化每个模块的结构,一般由程序员负责,本部分形成文档为《项目详细设计说明书》。
最后由编码员(Coder)按照《项目详细设计说明书》进行具体的编码。这个就是面向对象开发的标准过程的简单描述。
而实际的程序开发过程中,则更关注于OOP部分,也就是实际实现时的具体设计以及编码的问题。
面向对象技术除了这些设计方式以外,还有很多的概念和语法知识需要在编程时进行学习,下面以Java语言的语法为基础来介绍面向对象编程的内容。
类(class)是Java语言的最小编程单位,也是设计和实现Java程序的基础,本部分将深入介绍类的相关知识。
8.2.1 类的概念
类是一组事物共有特征和功能的描述。类是对于一组事物的总体描述,是按照面向对象技术进行设计时最小的单位,也是组成项目的最基本的模块。
类的概念是抽象的,类似于建筑设计中的图纸,是对于现实需要代表的具体内容的抽象。类只包含框架结构,而不包含具体的数据。所以类代表的是总体,而不代表某个特定的个体。
例如设计电脑(computer)这个类,电脑是一组事物,则该类中包含的常见特征如下:
l 类型:笔记本或台式机
l 内存容量
l 硬盘容量
l CPU类型
l 屏幕尺寸
l 主板类型
对于每一台具体的电脑来说,每个特征都有自己具体的数值,或者说是将特征数据具体化,而类需要代表的是总体特征,只需要具备特征的类型和结构,不需要具有具体的数值,因为一般一组事物的某个特征的数值都是不尽相同的,但是都统一的具备该特征。
同理,如果设计登录模块中的用户(user)类,则该类中包含的常见特征如下:
l 用户名
l 密码
对于每个具体的用户来说,都有自己特定的用户名和密码,但是对于用户这个类来说,只需要具备用户名和密码这两个特征的类型和结构即可。从这里也可以很直观的体会到,类是抽象的,是一组事物共有特征的描述。
上面是对于类结构具体特征的描述,其实类中除了包含特征的描述以外,还可以包含该类事物共有的功能,这些功能也是类的核心内容。
例如电脑这个类,包含的基本功能有:
l 打开
l 关闭
用户这个类,包含的基本功能有:
l 登录
通过在类的内部包含共有的功能,使得每个类都可以在内部实现一些规定的功能,从而减少和外部的交互,降低整个项目的复杂度。
这就是面向对象技术中类的概念的基本描述,每个类就代表一组事物,通过基本的特征和功能实现该类事物在项目内部的表达。
以上是从设计角度理解类的概念,其实从语法角度理解类的概念也很简单,类就是一种复合数据类型,或者说是一种程序员设计的新类型。因为在实际开发中,程序员可以根据需要声明新的类,所以在面向对象的开发中,程序员可以根据需要设计新的数据类型——类,从而实现项目要求的功能。
把设计角度中类的概念,转换为语法角度类的概念,是每个面向对象技术初学者都必须经历的阶段,通过进行该转换,可以把虚拟的类的概念转换成具体的类的概念,也是面向对象技术入门的标志。
对于一组事物来说,共有的特征和功能有很多,在实际抽象成类时,只需要抽象出实际项目中的需要的属性和功能即可。
8.2.2 类的声明
类是一种复合数据类型,则声明一个类就相当于创建了一种新的数据类型,在面向对象技术中就通过不断的创建新的数据类型来增强程序可以代表的数据的能力。
类声明总体的语法格式如下:
访问控制符 [修饰符] class 类名{ |
访问控制符 [修饰符] class 类名{ [属性声明] [方法声明] [构造方法声明] }
说明:该语法格式中中括号内部的部分为可选。
其中访问控制符用于限定声明的类在多大范围内可以被其它的类访问,主要有默认访问控制符(无关键字)和public;修饰符用于增强类的功能,使声明的类具备某种特定的能力;class是声明类时使用的关键字;类名是一个标识符,用于作为新声明的类的名称,要求必须符合标识符的命名规范。注:在Java语言的编码规范中,类名第一个字母要求大写。
例如如下示例:
public class Computer{} |
接着的大括号内部用于声明类的内部结构,类内部一般包括三类声明,且这三类声明都是可选的。说明如下:
l 属性声明
用于代表共有特征
l 方法声明
用于代码共有功能
l 构造方法声明(功能)
用于初始化类的变量
下面是这些声明的详细说明。
8.2.2.1 属性声明
属性,有些翻译为域、字段等,属性是类内部代表共有特征的结构,或者可以把属性理解为类的某个具体特征,类通过一系列的属性来代表一种新的数据类型。对于类比较基础的理解就是通过多个属性组合成的新的数据类型,这也是复合数据类型的由来。
属性声明的语法格式如下:
访问控制符 [修饰符] 数据类型 属性名[=值];
属性的访问控制符限定该属性被访问的范围,包含如下四种:public、protected、默认的(无关键字)和private,分别代表不同的访问限制,具体的限制范围后续将有详细说明。
修饰符用于使属性具备某种特定的功能。
数据类型为该属性的类型,可以是Java语言中的任意数据类型,也就是说,既可以是基本数据类型也可以是复合数据类型。
属性名是一个标识符,用于代表该属性的名称,在声明属性时的同时可以为该属性进行赋值。
示例格式为:
public int cpuType; |
在实际声明属性时,也可以一次声明多个属性,例如:
public int x = 10, y = 20; |
不过为了程序结构的清晰,一般书写为如下格式:
public int x = 10; |
另外,属性的作用范围是类的内部,可以在类内部的任何位置引用属性,包括在方法和构造方法的内部,而不论属性是否声明在方法的上面。
属性的引用:1.通过对象.属性来引用。
2.通过对象.方法来引用。不过需要写一个方法如getx()或gety();
引用的时候会遇到在静态的方法体类用非静态的属性或方法,这时候就需要new一个对象。
总得来说,类就是通过一系列属性的组合成为一种新的数据类型,从而可以代表一种更复杂的结构,也相当于为程序员提供了一种组合已有数据类型形成新数据类型的方法,从而更直观的去代表代表需要表达的数据。
8.2.2.3 构造方法声明
为什么会有构造方法
构造方法的功能:实现类这种数据类型的变量的初始化。由于类是一种复合数据类型,而复合数据类型的变量也比较复杂,所以专门需要对该类变量进行初始化,则语法上提供了专门的结构——构造方法。这就是构造方法出现的原因。而方法实现的是逻辑的功能,对应于逻辑上的算法,更多的是实现程序逻辑。所以构造方法是语法结构,而方法是逻辑功能,两者之间根本无关
构造方法声明的语法格式:
访问控制符 构造方法名称(参数列表){ |
在该语法中,访问控制符指声明的构造方法被访问的权限,构造方法名称是标识符,语法上要求构造方法的名称必须类名相同,后续小括号内部是参数列表,其语法格式和方法参数列表的语法格式相同。
下面是构造方法的示例:
public class Box{ |
在该Box类中,声明了两个构造方法,一个没有参数,一个包含三个int类型的参数。在没有参数的构造方法中,将三个属性的值都初始化为10.带参数的构造方法中,可以传递进来三个参数,然后在构造方法内部依次把参数的值赋值给属性。
。通常情况下,构造方法的声明放在属性声明和方法声明的中间。
8.2.2.4 面向对象基础使用
下面以一个简单的示例来描述面向对象的基本使用,主要是类声明的相关语法,以及基础的类设计的知识。
使用面向对象的方式来描述房屋的结构,要求如下:
(1) 门:(颜色为红色、可以被推开和关闭)
(2) 窗户:(颜色为白色、有一块玻璃(透明色、可以卸下)、可以被推开和关闭)
(3) 地:(由100块地板砖组成)
(4) 地板砖:(黄色、长1米、宽1米)
说明:其中红色用1代替,白色用2代替,黄色用3代替 透明色用0代替。
在使用面向对象描述时,将其中的名词转换为类,将该类内部的特征转换为属性,将该类内部的功能转换为方法,在构造方法内部实现对于属性的初始化。
则在该要求中,抽象的类一共有5个:门、窗户、玻璃、地和地板砖。其中的颜色作为对应类的属性,推开和关闭作为对应类的方法。前面介绍过类是一种数据类型,则可以声明类类型的变量,并可以将该变量作为类的属性,这种类和类的关系在面向对象中称作使用关系。则按照该思路实现的代码如下:
/** |
说明:在Floor类的代码中涉及到对于类类型的变量初始化的问题,相关语法将在后续详细介绍。
很重要,要改变自己以后想问题的方式 在该示例中,使用面向对象的思想描述了要求的房屋结构,并以Java语言语法的格式将面向对象的思想转化为具体的代码,从而实现对于面向对象技术的基本使用。
8.3.1 什么是对象?
其实面向对象技术只是提供了一种思考的方式,其思考方式就是把一个复杂的结构看成是一个整体,这样可以降低认知的复杂性。比如认识一个电脑,按照面向对象的认知方式,就是先把电脑分成一个个的对象:显示器对象、硬盘对象、CPU对象等等,然后再一个一个的进行认知。
同时面向对象技术也是一种设计方式,其设计方式是把一个负责的模块划分为一个个小的模块分开进行设计,这样可以降低设计的复杂性。比如设计一个电脑,按照面向对象的设计方式,就是把电脑分成一个个的对象:显示器对象、硬盘对象、CPU对象等等,然后再一个一个的进行设计。
正因为面向对象无论在认知和设计方面都降低了复杂性,所以在程序设计语言中得到了广泛的应用。其实也就是是对现实已存在的内容的升华,所以面向对象存在于生活的很多方面,并不是计算机程序设计领域里的“阳春白雪”。
在语法角度来看,对象就是一个变量,只是该变量比较复杂,其内部既包含属性(数据),也包含方法(功能)。在Java语言中,把复合数据类型(包括数组、类和接口)的变量都称作对象。所以对象的概念相对来说,就显得跟具体了。
每个对象中存储类中对应属性的数值,或者从数据角度来理解对象的话,也可以把对象看作是类似C语言中结构体变量类似的结构。
8.3.2 对象的语法
对象相关的语法主要包含四个部分:对象的声明、对象的初始化、引用对象中的属性和引用对象中的方法。
例如,有如下一个类的代码:
public class Box{ |
8.3.2.1 对象的声明
对象的声明,就是声明一个变量,其语法格式和变量声明的语法完全相同,格式如下:
数据类型 对象名;在基本数据类型里面相当于定义一个int或者其它类型复合数据类型要和基本数据类型进行比较
这里要求数据类型必须为复合数据类型,基本数据类型声明的结构只能称为变量,而不能称为对象。
示例代码:
Box b; |
这里声明了一个Box类型的对象b,该对象在内存中不占用存储空间,其值为null.当然声明对象时也可以采用如下格式:
Box b,b1; |
8.3.2.2 对象的初始化
由于只声明的对象在内存中没有存储空间,所以需要为对象在内存中申请空间,并初始化各个属性的值,这个过程称作对象的初始化。
对象的初始化,都是通过直接或间接调用构造方法实现。对象的初始化可以和对象的声明写在一起,也可以分开进行书写,其语法格式如下:
对象名 = new 构造方法(参数);
例如:
Box b = new Box(); |
其中对象b使用Box类中不带参数的构造方法进行初始化,按照Box类的结构,对象b中每个属性的值被都被初始化为10.而对象b1使用Box类中带参数的构造方法进行初始化,依据构造方法的结构,依次指定对象b1中的长、宽、高依次是2、3、4.
在初始化对象时,调用的构造方法必须在类中声明过,否则不能调用。因为类名和构造方法的名称相同,所以名称一般不容易发生错误,在实际使用时注意参数列表的结构也需要匹配。
有些时候,因为某些原因,把构造方法隐藏起来,这个时候可以使用其它的途径来创建对象,例如使用某些方法的返回值进行初始化等。
对象在初始化以后就可以进行使用了。
8.3.2.3 引用对象中的属性
对象是一个复合变量,很多时候需要引用对象内部的某一个属性,其语法格式为:
对象名。属性名
该语法中“。”代表引用,使用该表达式可以引用对象内部的属性,则该表达式的类型和该属性在类中声明的类型一致。
例如:
b.width = 5; |
该语法中,b是对象名,width是对象b中的属性,因为在类Box中width属性的类型是int型,则该表达式的类型也是int类型,在程序中可以把该表达式看成是int类型的变量进行实际使用。
而在实际的面向对象程序中,一般都避免使用对象直接引用属性(使用访问控制符实现访问限制),而替代的以getter和setter方法进行访问。
8.3.2.4 引用对象中的方法
如果需要执行对象内部的功能,则需要引用对象中的方法,也就是面向对象术语中的“消息传递”,其语法格式如下:
对象名。方法名(参数)
这里“。”也代表引用,使用该代码可以引用对象内部的方法,该语法中的参数必须和引用方法的声明结构匹配。
例如:
int v = b.volume(); |
这里引用对象b中的volume方法,实现的功能是求对象b的体积,并且把求得的体积赋值给变量v.
在实际的项目中,通过引用对象中的方法实现项目中信息的传递以及功能的实现,通过对象和对象之间的关联,构造成一个有机的系统,从而代表实际项目中标的各种需求。
对象相关的语法就介绍这么多,在后续的学习中将经常用到。
8.3.3 对象的存储形式
对象是一个复合数据类型的变量,其存储方式和一般变量的存储方式也不相同。在Java的执行环境中,存储区域一般分为两类:
l 栈内存
该区域存储基本数据类型
l 堆内存堆就是二叉树这种类型的
存储实际的对象内容。
而实际的对象存储比一般变量复杂,对象的存储分为两部分:对象的内容、对象内容的起始地址。
总结:现在知道了各种东西存在的原因,比如构造函数。设计方式,设计与实现的相分离
面向对象(设计方式) |
类 |
对象 |
类其实就是一种复合数据类型 |
修饰符用于增强类的功能,使声明的类具备某种特定的能力 |
构造函数存在的原因() |
对象就是一种复合对象的声明数
|
对象的初始化 |
引用对象中的属性 |
引用对象中的方法 |
Java编程那些事儿61—面向对象设计方法
前面介绍了面向对象技术的两个最基本、最重要的概念——类和对象,下面介绍一下面向对象技术的设计思路。
对于初学者来说,面向对象是学习Java语言时的第一个难点,其实面向对象只是一种思考问题的方式,或者理解为组织数据和功能的方式而已,当系统中的数据和功能都实现以后,按照数据和功能的相关性进行组织。
在使用面向对象技术设计项目时,一般的步骤如下:
1、 抽象类
2、 抽象类的属性和方法
3、 通过对象的关联构造系统
其中步骤1和2是设计需要实现的功能,步骤3更多的和业务逻辑相关,体现设计的结构不是很多。
l 抽象类
抽象类最基本的方式是——将名词转换为类。
在一个系统中会存在很多的名词,如果这些名词需要频繁的在系统中进行使用时,则可以将这些名词抽象成类,便于后续的时候。例如在一个学生成绩管理系统中,则名词:学生、课程等则可以抽象成类。
而实际在抽象时,由于有一定的主观性,所以在系统设计时,不同人设计的系统会存在一些不同。
l 抽象类的属性和方法
把系统中的类抽象出来了以后,就可以设计每个类的内部结构了,而每个类内部最重要的结构就是属性和方法了。
抽象属性最基本的方式是——将数据抽象为属性。
抽象方法最基本的方式是——将功能抽象为方法。
在一个类内部会存在很多的数据和功能,在实际抽象时,只需要抽象自己需要的数据和功能即可。例如在学生成绩管理系统中,学生的姓名、班级和各个科目的成绩都是系统中需要使用的数据,而学生的家庭住址,联系电话则不会必须的属性,可以根据实际的需要取舍数据的值。
抽象功能时,只需要把该类在系统中需要执行的动作提取出来,然后使用方法的语法进行描述即可。
当然,面向对象设计还涉及很多其它的知识,这里讲解的只是一些基础的入门知识,更多的有关面向对象的知识可以阅读关于面向对象技术的专门书籍,并且在项目开发中逐步体会这些知识。
8.5 面向对象三大特性
面向对象技术在实际开发中有很多的特性,总结起来最核心的特性主要有三个:封装、继承和多态。
8.5.1 封装性
封装性指在实际实现时,将复杂的内部结构隐藏起来,并为这组复杂的结构取一个统一的名称进行使用。在现实世界中,大量的存在封装的例子,例如电脑的硬盘,将多组复杂的线路和存储信息的磁片封装起来,并将该组结构取名为硬盘,以后就可以使用硬盘来代表该结构,而不需要更多的了解内部的信息。
在面向对象技术中,类是典型的封装性的体现,类将一组属性和功能组合成一个统一的结构,并使用类名来代表该结构。
封装性的最大优势在于隐藏每个类的内部实现(内部结构),从而既方便项目的分解也降低了项目的难度。
例如以设计汽车为例,我们可以把汽车看作是软件开发中的整个项目,在实际设计时,首先可以将设计汽车分解为设计汽车的每个组件,然后具体对每个组件进行设计,而组件和组件的设计之间关联性很小,例如设计发动机的设计小组不需要很详细的了解轮胎设计小组的工作。而这里的每个组件可以看作实际面向对象设计中的类,每个类都把自己的内部实现隐藏起来,只通过名称使其它类了解该类的作用,并开放一些基本的功能供其它的类使用即可。
这样可以在实际设计时,每个类都更注重自身的实现,而对于其它类的实现不需要深入了解,这样可以在总体上降低交流的频率,从而降低整个项目的复杂度。
通常情况下,一般把类和类之间的关联性又称作耦合性,类和类之间的关联性比较低也称作耦合性比较低。在实际设计项目时,低耦合的项目是程序设计人员设计系统的目标之一。
8.5.2 继承性
在我们认知现实世界时,一般会把事物进行分类,而每一类内部又划分出很多的小类,生物学中将该方式体现的很彻底。例如猩猩属于动物中的哺乳类灵长目,这里的动物、哺乳类和灵长目都是一个特定的类别,和以前不同的是这些类别之间存在包含关系(is-a),换句话说,也就是哺乳类是动物类的一种,灵长目是哺乳类的一种。
其实在程序设计中,很多设计出来的类也存在这样的包含关系,这样一个类的内部会包含和其它类类似的特征和属性,如果在设计时可以以另外一个类为基础进行设计,那将是多么激动人心的特性,这个特性就是面向对象设计中的继承性。
在一个项目中,如果类和类之间存储包含关系,即一个类是另外一个类的一种,就可以使用继承。
继承性提供了全新的类设计方式,可以充分利用了已有类内部的结构和功能,极大的降低了类内部的代码重复,是设计类的一种显著的变革,对于大型的项目设计十分有用。另外很多技术的应用中也包含大量的继承成分,使整个技术体系比较固定。
示例代码如下:
//Animal.java |
这里Mammalia类就是Animal类的子类,Animal类就是Mammalia类的父类,子类和父类具有相对性,正如一个祖孙三代的家庭内部,每个人相对于不同的人扮演不同的角色一样。同时类和类之间的继承具备传递性,就如现实中的血缘关系一样。传递性可以理解为血缘关系。血缘关系也可以解除,就是通过用private来断绝父子关系。
8.5.2.2 继承说明
两个类之间如果存在了继承关系以后,将带来哪些不同呢?下面依次来进行说明:
l 子类拥有父类的所有属性
子类中继承父类中所有的属性,在父类中声明的属性在子类内部可以直接调用。说明:如果访问控制符限制则无法访问。
l 子类拥有父类的所有方法
子类中继承父类中所有的方法,在父类中声明的方法在子类内部可以直接调用。说明:如果访问控制符限制则无法访问。
l 子类不拥有父类的构造方法
子类不继承父类的构造方法,如果需要在子类内部使用和父类传入参数一样的构造方法,则需要在子类内部重新声明这些构造方法。
l 子类类型是父类类型
子类类型的对象可以自动转换为父类类型的对象,父类类型的对象则需要强制转换为子类的对象,转换的语法个基本数据类型转换的语法相同。
理解 复合数据类型 可以用基本数据类型来理解。
父类与子类可以理解为整型,但是父类是long型,而子类是short型,子类转化为父类隐式转换,而父类转化为子类必须是强制转换。
8.5.2.3 方法覆盖
前面介绍了继承的一些基础知识,现在介绍一些在使用继承时需要注意的问题。熟悉这些问题将更好的解决项目中的实际问题。
例如在实际的游戏中,会按照怪物的种类实现设计。首先设计一个基础类Monster,然后按照怪物类别设计Monster的子类,如Boss、NormalMonster等。则在实际实现时,每个怪物都有移动(move)的功能,但是在Boss和NormalMonster的移动规则存在不同。这样就需要在子类的内部重新编写移动的功能,从而满足实际的移动要求。该示例的实现代码如下:
//Monster.java |
2、 子类构造方法的书写
该项是继承时书写子类最需要注意的问题。在子类的构造方法内部必须调用父类的构造方法,为了方便程序员进行开发,如果在子类内部不书写调用父类构造方法的代码时,则子类构造方法将自动调用父类的默认构造方法。而如果父类不存在默认构造方法时,则必须在子类内部使用super关键字手动调用,关于super关键字的使用将在后续进行详细的介绍。
说明:子类构造方法的参数列表和父类构造方法的参数列表不必完全相同。
.2.2.4 需要注意的问题
除了方法覆盖以外,在实际使用继承时还有很多需要注意的问题。下面就这些问题进行一一说明。
1、 属性覆盖没有必要
方法覆盖可以重写对应的功能,在实际继承时在语法上也支持属性覆盖(在子类内部声明和父类属性名相同的属性),但是在实际使用时修改属性的类型将导致类结构的混乱,所以在继承时不能使用属性覆盖。
2、 子类构造方法的书写
该项是继承时书写子类最需要注意的问题。在子类的构造方法内部必须调用父类的构造方法,为了方便程序员进行开发,如果在子类内部不书写调用父类构造方法的代码时,则子类构造方法将自动调用父类的默认构造方法。而如果父类不存在默认构造方法时,则必须在子类内部使用super关键字手动调用,关于super关键字的使用将在后续进行详细的介绍。
说明:子类构造方法的参数列表和父类构造方法的参数列表不必完全相同。
3、 子类的构造过程
在构造子类时由于需要父类的构造方法,所以实际构造子类的过程就显得比较复杂了。其实在实际执行时,子类的构造过程遵循:首先构造父类的结构,其次构造子类的结构,无论构造父类还是子类的结构,都是首先初始化属性,其次执行构造方法。则子类的构造过程具体如下:
如果类A是类B的父类,则类B的对象构造的顺序如下:
a) 类A的属性初始化
b) 类A的构造方法
c) 类B的属性
d) 类B的构造方法
由于任何一个类都直接或间接继承自Object类,所以Object类的属性和构造方法都是首先执行的。
4、 不要滥用继承
在实际的项目设计中,继承虽然很经常使用,但是还是不能滥用,使用继承的场合以及相关问题参看下面的说明。
8.5.2.5 如何设计继承
在实际的项目中,类和类之间的关系主要有三种:
1、 没有关系
项目中的两个类之间没有关联,不需要进行消息传递,则这两个类之间就没有关系,可以互相进行独立的设计。
2、 使用关系(has-a)
如果一个类的对象是另外一个类的属性,则这两个类之间的关系是使用关系。例如把房屋(House)看作是一个类,把门(Door)看成另外一个类,则房屋有一个门,代码的实现如下:
//House.java |
则这里Door的对象是House类的属性,则Door和House类之间的关系就是使用关系,House使用Door类来制作自身。
使用关系提供了使用已有类来声明新类的方式,可以以组合的方式来构建更复杂的类,这是项目中使用类的常见方式之一。
判断是否是使用关系的依据就是:has-a,一个类具备另外一个类的对象,例如一个House有一个门。
3、 继承关系(is-a)
如果一个类是另外一个类的一种,也就是在分类上存在包含关系,则应该使用继承来实现。例如Boss是怪物的一种,则使Boss继承Monster类。
下面简单介绍一些项目中继承的设计方法。在实际设计继承时,一般有两种设计的方法:
1、 自上而下的设计
在实际设计时,考虑类的体系结构,先设计父类,然后根据需要来增加子类,并在子类的内部实现或添加对应的方法。
2、 自下而上的设计
在实际设计时,首先不考虑类的关系,每个类都分开设计,然后从相关的类中把重复的属性和方法抽象出来形成父类。
对于初学者来说,第二种设计方式相对来说比较容易实现,所以一般初学者都按照第二种设计方式进行设计,设计完成以后再实现成具体的代码。
总结:
理解继承 |
传递性(子类与父类存血缘关系)也可以用private解除血缘关系 |
构造方法,子类构造父类的结构然后才构造自己的结构(子类不继承父类的构造方法不代表不可以调用) |
子类与父类是复合数据类型,且类型相同,可以相互转换
|
Java编程那些事儿63—多态性
封装、继承与多态(方法的重载与方法的覆盖)
8.5.3 多态性
多态性是面向对象技术中最灵活的特性,主要是增强项目的可扩展性,提高代码的可维护性。
多态性依赖继承特性,可以把多态理解为继承性的扩展或者深入。
在这里把多态性分为两方面来进行介绍,对象类型的多态和对象方法的多态
按照继承性的说明,子类的对象也是父类类型的对象,可以进行直接赋值。
SuperClass sc = new SubbClass1();对这句话的理解,定义sc为superclass这种复合类型,然后再通过构造superclass的子类的构造方法给他赋值初始化
8.5.3.1 对象类型的多态
对象类型的多态是指声明对象的类型不是对象的真正类型,而对象的真正类型由创建对象时调用的构造方法进行决定。例外,按照继承性的说明,子类的对象也是父类类型的对象,可以进行直接赋值。
例如如下代码:
SuperClass sc = new SubbClass1(); |
这里声明了一个SuperClass类型的对象sc,然后使用SuperClass的子类SubbClass1的构造方法进行创建,因为子类类型的对象也是父类类型的对象,所以创建出来的对象可以直接赋值给父类类型的对象sc.除了对象的赋值以外,另外一个更重要的知识是sc对象虽然使用SuperClass声明的类型,但是内部存储的却是SubbClass1类型的对象。这个可以Java语言的中instanceof运算符进行判断。
instanceof是一个运算符,其作用是判断一个对象是否是某个类类型的对象,如果成立则表达式的值为true,否则为false.语法格式如下:
对象名 instanceof 类名
需要注意的是:这里的类名必须和声明对象时的类之间存储继承关系,否则将出现语法错误。
测试类型的代码如下:
/** * 测试对象类型的多态 */ public class TestObjectType { public static void main(String[] args) { SuperClass sc = new SubbClass1(); boolean b = sc instanceof SuperClass; boolean b1 = sc instanceof SubbClass1; System.out.println(b); System.out.println(b1); } } 该测试程序的输出结果是: true true |
由程序运行结果可以看出,sc既是SuperClass类型的对象,也是SubbClass1类型的对象,而SubbClass1的类型被隐藏起来了,这就是对象的多态。其实sc对象不仅仅在类型上是SubbClass1类型的,其存储的内容也是SubbClass1的内容,具体参看后面介绍的对象方法的多态。
对象类型的多态有很多的用途,极大的方便了对象的存储和传递,使代码很方便的进行扩展,对于已有代码不产生影响。下面介绍两个基本的使用。
1. 对象的存储
在存储一系列不同子类的对象时,可以使用父类的结构来进行声明,这样可以方便数据的存储,例如需要存储多个SubbClass1和SubbClass2的对象时,则可以声明一个SuperClass类型的数组进行存储,示例代码如下:
SuperClass sc[] = new SuperClass[3]; sc[0] = new SubbClass1(); sc[1] = new SubbClass2(); sc[2] = new SubbClass1(); |
则这里的数组sc,可以存储各个类型子类的对象,而数组中每个元素的值都是存储的对应子类的对象,而只是在名义上的类型(语法上的类型)是SuperClass类型的,这样将方便程序的控制,当增加新的子类类型时,已有的代码不需要进行改造就可以自动适应新的子类的结构。
例如新增了一个SuperClass的子类SubbClass3,则该数组的代码可以修改成如下:
SuperClass sc[] = new SuperClass[3]; sc[0] = new SubbClass1(); sc[1] = new SubbClass2(); sc[2] = new SubbClass3(); |
其它的代码都需要进行修改,就可以适应新的结构,这是多态性最主要的用途。
8.5.3.2 对象方法的多态
对象方法的多态基于方法的覆盖,也就是该对象调用的方法具体是子类的方法还是父类的方法,由创建对象时使用的构造方法决定,而不是由声明对象时声明的类型决定。
示例代码如下:
/** * 测试对象方法的多态 */ public class TestObjectMethod { public static void main(String[] args) { SuperClass sc = new SuperClass(); SubbClass1 sc1 = new SubbClass1(); SubbClass2 sc2 = new SubbClass2(); SuperClass sc3 = new SubbClass1(); testObjectTypeMethod(sc); testObjectTypeMethod(sc1); testObjectTypeMethod(sc2); testObjectTypeMethod(sc3); }
public static void testObjectTypeMethod(SuperClass sc){ sc.test(); //调用被覆盖的方法 } } 该代码的执行结果如下: SuperClass SubbClass1 SubbClass2 SubbClass1 |
则从代码的执行结果看,虽然testObjectTypeMethod方法接收的是SuperClass类型的对象,但是传入子类对象时,子类对象的内容没有丢失,所以在调用test方法时,还是调用的对应对象中对应的test方法。
这样就在功能上实现了对象的传递,从而保留了对象的内容,极大的方便了代码的扩展性。
但是,由于Java在执行程序时,在程序运行的过程中,需要判断对象调用的具体是父类的方法还是子类的方法,所以程序的执行速度会稍微有所降低。
多态 |
对象类型的多态 |
对象方法的多态(无非方法体与参数类型) |
对象类型可以分为父类对象与子类对象, |
|
主要应用就是对像的存储。也就是父类对象可以在座 |
可以用子类的构造函数进行初始化。 |
方法体不同,叫做覆盖 |
参数类型不同,叫做重载 |
Java编程那些事儿 64
访问控制符、修饰符和其它关键字
第二个使用示例:在项目中,一般为了设计的需要实现一些特定的功能,下面介绍一下使用访问控制符实现的一个功能——使一个类既不能创建对象也不能被继承。实现的方法如下:该类中只实现一个构造方法,而且将该构造方法的访问权限设置为私有的。具体实现代码如下:创建对象需要构造方法进行赋值
/** |
在该示例中,PrivateDemo类只有一个构造方法,且该构造方法为私有。按照以前的介绍,创建对象时需要调用构造方法,而private修饰的构造方法无法在类的外部进行访问,所以无法创建对象。另外,在子类的构造方法中也需要调用父类的构造方法,由于private的构造方法无法得到调用,所以该类也不可以有对应的子类。
这里说明的只是两个基本的用途,在实际的项目中,可以根据需要灵活的使用访问控制符实现对应的功能。
总之,访问控制符通过控制声明的内容的访问权限,实现对于内容的隐藏,从而降低使代码的耦合性降低,降低项目的复杂度,并且方便实际项目中代码的维护。
Java编程那些事儿65——static修饰符
8.7 修饰符 修饰符的作用是让被修饰的内容具备特定的功能,在程序中合理使用修饰符可以在语法和功能上实现很多需要的效果。Java语言中的修饰符主要有5个:static、final、native、abstract和synchronized.这里首先讲解static、final和native的作用。 8.7.1 static修饰符 static关键字的中文意思是静态的,该修饰符可以修饰成员变量,成员常量和成员方法。使用该关键字修饰的内容,在面向对象中static修饰的内容是隶属于类,而不是直接隶属于对象的,所以static修饰的成员变量一般称作类变量,而static修饰的方法一般称作类方法。另外,static还可以修饰代码块,下面进行详细的介绍。 8.7.1.1 静态变量 static修饰的变量称作静态变量。静态变量和一般的成员变量不同,一个类在加载到内存时,静态变量只初始化一次,也就是说所有对象的静态变量在内存中都只有一个存储位置,每个对象中的静态变量都指向内存中同一个地址,它是在所有的对象之间共享的数据。另外静态变量在引用时比较方便。所以一般在需要实现以下两个功能时使用静态变量: l 在对象之间共享值时 l 方便访问变量时 下面首先说一下非静态变量(没有static修饰符修饰的成员变量)在内存中如何存储的。示例代码如下:
则对象a和对象b在内存中的存储格式如下图所示:
从上面的图可以看出,非静态变量的值在每个对象中都有独立的存储空间,不同对象间这些值之间没有管理,也就是说每个对象都为内部的每个非静态的变量分配独立的存储空间,所以每个对象中非静态变量是隶属于对象,也就是说在每个对象中可能是不同的。 简单介绍了非静态变量在对象中的存储以后,下面再来看一下静态变量是如何进行存储的。示例代码如下:
则对象sv1和对象sv2在内存中存储的格式如下图所示:
对于StaticDemo类型的对象sv1和sv2来说,由于使用默认的构造方法进行构造,所以每个成员变量都被初始化为对应数据类型的默认值,int的默认值为0,char的默认值为编号为0的字符,所以sv1和sv2对象中存储的值如上图所示。 而静态变量的存储和非静态变量的存储不同,在Java虚拟机内部,第一次使用类时初始化该类中的所有静态变量,以后就不再进行初始化,而且无论创建多少个该类的对象,静态变量的存储在内存中都是独立于对象的,也就是Java虚拟机单独为静态变量分配存储空间,所以导致所有的对象内部的静态变量在内存中存储时只有一个空间。这样就导致使用任何一个对象对该值的修改都是使该存储空间中的值发生改变,而其它对象在后续引用时就跟着发生了变化。静态变量就是使用这样的方式在所有的对象之间进行数值共享的。 静态变量在实际使用时,可以通过只存储一次来节约存储空间,这个特性导致在类内部定义的成员常量一般都做成静态的,因为常量的值在每个对象中都是相同的,而且使用static修饰也便于对成员常量的引用。 在类外部访问某类中静态变量(常量)的语法格式为: 类名.成员变量(常量) 例如: StaticDemo.m 这样方便对于成员变量的访问。当然,语法上也不禁止使用:对象。成员变量,这样的语法格式进行访问,但是一般不推荐这样使用,而且有些类是无法创建对象的。 注意:static关键字不能修饰成员方法或构造方法内部的变量。 8.7.1.2 静态方法 static修饰的方法称作静态方法。静态方法和一般的成员方法相比,不同的地方有两个:一是调用起来比较方便,二是静态方法内部只能使用静态的成员变量。所以一般静态方法都是类内部的独立的功能方法。例如为了方便方法的调用,Java API中的Math类中所有的方法都是静态的,而一般类内部的static方法也是方便其它类对该方法的调用。 示例代码如下:
静态方法在类的外部进行调用时不需要创建对象,使用类名。方法名(参数)这样的语法格式进行调研,简化了代码的编写。 使用静态方法时,需要特别注意的是静态方法内部使用该类的非静态成员变量,否则将出现语法错误。 静态方法是类内部的一类特殊方法,只有在需要时才将对应的方法声明成静态的,一个类内部的方法一般都是非静态的。 8.7.1.3 静态代码块 静态代码块指位于类声明的内部,方法和构造方法的外部,使用static修饰的代码块。静态代码块在该类第一次被使用时执行一次,以后再也不执行。在实际的代码中,如果需要对类进行初始化的代码,可以写在静态代码块的内部。 示例代码如下:
静态代码块是一种特殊的语法,熟悉该语法的特点,在实际程序中根据需要使用。 | ||||
作者:陈跃峰 来源:http://blog.youkuaiyun.com/mailbomb 整理及制作: Lixn QQ:173131690 |
总结:1.static翻译成类来理解。2. 一个类在加载到内存时,静态变量只初始化一次
8.7.2.1 final数据
final修饰的数据是常量,常量既可以出现在类的内部,也可以出现在方法或构造方法的内部。在程序中常量只能赋值一次。
其它说明可以参看前面的常量介绍。
在程序中,一般类内部的成员常量为了方便调用,一般都使用static修饰符进行修饰。示例代码如下:
/** * 常量使用 */ public class Student { /**性别*/ int sex; /**男性*/ public final static int MALE = 0; /**女性*/ public final static int FEMALE = 1; } |
8.7.3 native
native关键字是“本地的”意思,native修饰的方法,只有方法的声明使用java语言实现,而方法内部的代码都是在Java虚拟机内部使用其它语言实现。
一般native的方法,都是和系统操作有关的方法,或者是基于底层实现效率比较高的方法,常见于系统类中。例如System类的arraycopy方法等。
Java编程那些事儿67——this和super
8.8.1.1 引用成员变量
在一个类的方法或构造方法内部,可以使用“this.成员变量名”这样的格式来引用成员变量名,有些时候可以省略,有些时候不能省略。首先看一下下面的代码:
/** * 使用this引用成员变量 */ public class ReferenceVariable { private int a;
public ReferenceVariable(int a){ this.a = a; }
public int getA(){ return a; }
public void setA(int a){ this.a = a; } } |
在该代码的构造方法和setA方法内部,都是用this.a引用类的成员变量。因为无论在构造方法还是setA方法内部,都包含2个变量名为a的变量,一个是参数a,另外一个是成员变量a.按照Java语言的变量作用范围规定,参数a的作用范围为构造方法或方法内部,成员变量a的作用范围是类的内部,这样在构造方法和setA方法内部就存在了变量a的冲突,Java语言规定当变量作用范围重叠时,作用域小的变量覆盖作用域大的变量。所以在构造方法和setA方法内部,参数a起作用。
这样需要访问成员变量a则必须使用this进行引用。当然,如果变量名不发生重叠,则this可以省略。
参数名称与成员变量的名称保持一致的原因:
但是为了增强代码的可读性,一般将参数的名称和成员变量的名称保持一致,所以this的使用频率在规范的代码内部应该很多。
8.8.1.2 引用构造方法
在一个类的构造方法内部,也可以使用this关键字引用其它的构造方法,这样可以降低代码的重复,也可以使所有的构造方法保持统一,这样方便以后的代码修改和维护,也方便代码的阅读。
下面是一个简单的示例:
/** * 使用this关键字引用构造方法 */ public class ReferenceConstructor { int a;
public ReferenceConstructor(){ this(0); }
public ReferenceConstructor(int a){ this.a = a; } } |
这里在不带参数的构造方法内部,使用this调用了另外一个构造方法,其中0是根据需要传递的参数的值,当一个类内部的构造方法比较多时,可以只书写一个构造方法的内部功能代码,然后其它的构造方法都通过调用该构造方法实现,这样既保证了所有的构造是统一的,也降低了代码的重复。
在实际使用时,需要注意的是,在构造方法内部使用this关键字调用其它的构造方法时,调用的代码只能出现在构造方法内部的第一行可执行代码。这样,在构造方法内部使用this关键字调用构造方法最多会出现一次。
8.8.1.3 代表自身对象
在一个类的内部,也可以使用this代表自身类的对象,或者换句话说,每个类内部都有一个隐含的成员变量,该成员变量的类型是该类的类型,该成员变量的名称是this,实际使用this代表自身类的对象的示例代码如下:
/** * 使用this代表自身类的对象 */ public class ReferenceObject { ReferenceObject instance;
public ReferenceObject(){ instance = this; }
public void test(){ System.out.println(this); } } |
在构造方法内部,将对象this的值赋值给instance,在test方法内部,输出对象this的内容,这里的this都代表自身类型的对象。
8.8.1.4 引用成员方法
在一个类的内部,成员方法之间的互相调用时也可以使用“this.方法名(参数)”来进行引用,只是所有这样的引用中this都可以省略,所以这里就不详细介绍了。
This |
This()引用构造方法 |
This代表自身对象 |
This.方法。 引用自身方法 |
This引用成员变量 |
8.8.3 注意的问题(this与super的局限性)
最后,在实际使用this和super时,除了上面介绍到的需要注意的问题以外,还需要特别注意的是,this和super都是非静态的,所以这两个关键字都无法在静态方法内部进行使用。
8.8.2.1 引用父类构造方法
在构造子类对象时,必须调用父类的构造方法。而为了方便代码的编写,在子类的构造方法内部会自动调用父类中默认的构造方法。但是如果父类中没有默认的构造方法时,则必须手动进行调用。
使用super可以在子类的构造方法内部调用父类的构造方法。可以在子类的构造方法内部根据需要调用父类中的构造方法。
使用super关键字调用父类构造方法的示例代码如下:
//文件名:SuperClass.java public class SuperClass {
public SuperClass(){}
public SuperClass(int a){} } //文件名:SubClass.java public class SubClass extends SuperClass { public SubClass(){ super(); //可省略 }
public SubClass(int a){ super(a); }
public SubClass(String s){ super(); //可省略 } } |
在该示例代码中,SubClass继承SuperClass类,在SubClass类的构造方法内部可以使用super关键字调用父类SubClass的构造方法,具体调用哪个构造方法没有限制,可以在子类内部根据需要进行调用,只是根据调用的构造方法不同传入适当的参数即可。
由于SubClass类的父类SuperClass内部有默认的构造方法,所以SubClass的构造方法内部super()的代码可以省略。
和使用this关键字调用构造方法一样,super调用构造方法的代码只能出现在子类构造方法中的第一行可执行代码。这样super调用构造方法的代码在子类的构造方法内部则最多出现一句,且不能和this调用构造方法的代码一起使用。
注意:一个类可以有多个构造方法(构造方法只起到初始化复合变量的作用,如果要想用这些初始化的变量就必须声明这个类的对象,也就是复合类型),
public class Postion {
/**
* @param args
*/
private int x;
private int y;
double distance;
public Postion(int x,int y)
{
this.x=x;
this.y=y;
}
public int getx()
{
return x;
}
public int gety()
{
return y;
}
public double getDistance(Postion p,Postion q)//构造方法起到的只是初始化的作用。如果要用必须声明这个复合类型。
{
distance=Math.sqrt((p.getx()-q.getx())*(p.getx()-q.getx())+(p.gety()-q.gety())*(p.gety()-q.gety()));
return distance;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Postion p=new Postion(3,5);
Postion p1=new Postion(4,6);
int x1=p.getx();
int y1=p.gety();
int x2=p1.getx();
int y2=p1.gety();
System.out.println("位置A是:"+x1+y1);
System.out.println("位置B是:"+x2+y2);
System.out.println("距离是:"+p.getDistance(p, p1));
}
}
方法的重载