5.面向对象(上)

xiaoxiao2021-03-01  29

本章要点

定义类、属性和方法创建并使用对象对象和引用方法必须属于类或对象java里方法的参数传递机制递归方法方法的重载实现良好的封装使用package和import构造器的作用和构造器重载继承的特点和用法重写父类方法super关键字的用法继承和多态向上转型和强制类型转换继承和组合的关系使用组合来实现复用构造器和初始化块的作用和区别静态初始化块

java是面向对象的程序设计语言,java语言提供了定义类,定义属性,方法等基本的功能。类可被认为是一种自定义的数据类型,可以使用类来定义变量,所有使用类定义的变量都是引用变量,它们将会引用到类的对象,对象由类负责创建。类用于描述客观世界里某一类对象的共同特征,而对象则是类的具体存在,java程序使用类的构造器来创建该类的对象。

java也支持面向对象的三大特征:封装,继承和多态,java提供了private,protected和public三个访问控制修饰符来实现良好的封装,提供了extends关键字来让子类继承父类,子类继承父类将可以继承到父类的属性和方法,除此之外,也可通过组合关系来实现这种复用,从某种程度上来看,继承和组合具有相同的功能。使用继承关系来实现复用时,子类对象可以直接赋给父类变量,这个变量具有多态性,编程更加灵活,而利用组合关系来实现复用的时候,则不具备这种灵活性。

构造器用于对类实例进行初始化操作,构造器支持重载,如果多个重载的构造器里包含了相同的初始化代码,则可以把这些初始化代码放置在普通初始化块里完成,初始化块总在构造器执行之前被调用。除此之外,java还提供了一种静态初始化块,静态初始化块用于初始化类,在类初始化阶段被执行。如果继承树里的某一个类需要被初始化时,系统将会同时初始化该类的所有父类。

5.1 类和对象

java是面向对象的程序设计语言,类是面向对象的重要内容,我们可以把类当成一种自定义数据类型,可以使用类来定义变量,这种类型的变量统称为引用型变量。也就是说,所有类是引用数据类型。

5.1.1 定义类

面向对象的程序设计过程中有两个重要概念:类(class)和对象(object,也被称为实例,instance),其中类是某一批对象的抽象,可以把类理解成某种概念:对象才是一个具体存在的实体,从这个意义上来看,我们日常所说的人,其实应该是人的对象,而不是人类。

类和对象是面向对象的核心。

[修饰符] class 类名 { 零个到多个构造器定义.. 零个到多个属性... 零个到多个方法... }

在上面的语法格式中,修饰符可以是public,final,或者完全省略这两个修饰符,类名只要是一个合法的标识符即可,但这仅仅满足的是java的语法要求;如果从程序的可读性方面来看,java类名必须是由一个或多个有意义的单词连缀而成,每个单词首字母大写,其他字母全部小写,单词与单词之间不要使用任何分隔符。

对一个类定义而言,可以包含三种最常见的成员:构造器,属性和方法,三种成员都可以定义零个或多个,如果三种成员都只定义零个,就是定义了一个空类,这没有太大的实际意义。

类里各成员之间的定义顺序没有任何影响,各成员之间可以相互调用,但需要指出的是,static修饰的成员不能访问没有static修饰的成员。

属性用于定义该类或该类的实例所包含的数据,方法则用于定义该类或该类的实例的行为特征或功能实现。构造器用于构造该类的实例,java语言通过new关键字来调用构造器,从而返回该类的实例。

构造器是一个类创建对象的根本途径,如果一个类没有构造器,这个类通常将无法创建实例。因此,java语言提供了一个功能:如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器。一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器。

定义属性的语法格式如下:

[修饰符] 属性类型 属性名 [=默认值];

属性语法格式的详细说明如下:

修饰符:修饰符可以省略,也可以是public、protected、private、static、final,其中public、protected、private三个最多只能出现其中之一,可以与static,final组合起来修饰属性。属性类型:属性类型可以是java语言允许的任何数据类型,包括基本类型和现在介绍的引用类型。属性名:属性名则只要是一个合法的标识符即可,但这只是从语法角度来说的;如果从程序可读性角度来看,属性名应该由一个或多个有意义的单词连缀而成,第一个单词首字母小写,后面每个单词首字母大写,其他字母全部小写,单词与单词之间不需使用任何分隔符。默认值:定义属性还可以定义一个可选的默认值。

提示:

属性是一种比较传统,也比较符合汉语习惯的说法。在java的官方说法里,属性被称为Field,因此有的地方也把属性翻译为字段。

定义方法的语法格式如下:

[修饰符] 方法返回值类型 方法名(形参列表) { //由零条到多条可执行语句组成的方法体 }

方法语法格式的详细说明如下:

修饰符:修饰符可以省略,也可以是public,protected,private,static,final,abstract,其中public,protected,private三个最多只能出现其中之一;abstract和final最多只能出现其中之一,它们可以与static组合起来修饰方法。方法返回值类型:返回值类型可以是java语言允许的任何数据类型,包括基本类型和引用类型;如果声明了方法返回值类型,则方法体内必须有一个有效的return语句,该语句返回一个变量或一个表达式,这个变量或表达式的类型必须与此处声明的类型匹配。除此之外,如果一个方法没有返回值,则必须使用void来声明没有返回值。方法名:方法名命名规则与属性命名规则基本相同,但通常建议方法名以英文中的动词开头。形参列表:形参列表用于定义该方法可以接受的参数,形参列表由零组到多组“参数类型 形参名”组合而成,多组参数之间以英文逗号(,)隔开,形参类型和形参名之间以英文空格隔开。一旦在定义方法时指定了形参列表,则调用该方法时必须传入对应的参数值--谁调用方法,谁负责为形参赋值。

方法体里多条可执行语句之间有严格的执行顺序,排在方法体前面的语句总是先执行,排在方法体后面的语句总是后执行。

static是一个特殊的关键字,它可用于修饰方法,属性等成员。static修饰的成员表面它是属于这个类共有的,而不是属于该类的单个实例,因为通常把static修饰的属性和方法也称为类属性,类方法。不使用static修饰的普通方法,属性则属于该类的单个实例,而不是属于该类。因为通常把不使用static修饰的属性和方法也称为实例属性,实例方法。

提示:

由于static在英文直译就是静态的意思,因此有时也把static修饰的属性好方法称为静态属性和静态方法,把不使用static修饰的属性和方法称为非静态属性和非静态方法。静态成员不能直接访问非静态成员。

构造器是一个特殊的方法,定义构造器的语法格式与定义方法的语法格式很像,定义构造器的语法格式如下:

[修饰符] 构造器名(形参列表) { //由零条到多条可执行语句组成的构造器执行体 }

构造器语法格式的详细说明如下:

修饰符:修饰符可以省略,也可以是public,protected,private其中之一。构造器名:构造器名必须和类名相同。形参列表:和定义方法形参列表的格式完全相同。

值得提出的是,构造器不能定义返回值类型声明,也不能使用void定义构造器没有返回值。如果为构造器定义了返回值类型,或使用void定义构造器没有返回值,编译时不会出错,但java会把这个所谓的构造器当成方法来处理。

5.1.2 对象的产生和使用

创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实例。

5.1.3 对象,引用和指针

5.1.4 对象的this引用

java提供了一个this关键字,this关键字是一个对象的默认引用。this关键字总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形:

构造器中引用该构造器执行初始化的对象。在方法中引用调用该方法的对象。

this关键字最大的作用就是让类中一个方法,访问该类的另一个方法或属性。

5.2 方法详解

方法是类或对象的行为特征的抽象,方法是类或对象最重要的组成部分。但从功能上来看,方法完全类似于传统结构化程序设计里的函数。但值得指出的是,java里的方法不能独立存在,所有的方法都必须定义在类里。方法在逻辑上要么属于类,要么属于对象。

5.2.1 方法的所属性

不论是从定义的语法上来看,还是从方法的功能上来,都不难发现方法和函数之间的相似性。实际上,方法确实是由传统的函数发展而来,但方法与传统的函数有着显著不同:在结构化编程语言里,函数是一等公民,整个软件由一个一个的函数组成;在面向对象编程语言里,类才是一等公民,整个系统由一个一个的类组成。因此在java语言里,方法不能独立存在,方法必须属于类或对象。

因此,如果需要定义方法,则只能在类体内定义,不能独立定义一个方法。一旦将一个方法定义在某个类体内,如果这个方法使用了static修饰,则这个方法属于这个类,否则这个方法属于这个类的对象。

java语言是静态的:一个类定义完成后,只要不再重新编译这个类文件,该类和该类的对象所拥有的方法是固定的,永远都不会改变。

永远不要把方法当成独立存在的实体,正如现实世界里由类和对象组成,而方法只能作为类和对象的附属,java语言里的方法也是一样。java语言里的方法的所属性主要体现在如下几个方面:

方法不能独立定义,方法只能在类体里定义。从逻辑意义上来看,方法要么属于该类本身,要么属于该类的一个对象。永远不能独立执行方法,执行方法必须使用类或对象作为调用者。

使用static修饰的方法属于这个类,或者说属于该类的所有实例所共有,使用static修饰的方法既可以使用类作为调用者来调用,也可以使用对象作为调用者来调用。但值得提出的是,因为使用static修饰的方法还是属于这个类的,因此使用该类的任何对象来调用这个方法将会得到相同的执行结果,与使用类作为调用者的执行结果完全相同。

不使用static修饰的方法则属于该类的对象,不属于这个类。因此不使用static修饰的方法只能使用对象作为调用者调用,不能使用类作为调用者调用。使用不同对象作为调用者来调用同一个普通方法,可能得到不同的结果。

5.2.2 方法的参数传递机制

java里方法的参数传递方式只有一种:值传递。所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。

java对于引用类型的参数传递,一样采用的是值传递方式。

5.2.3 形参长度可变的方法

数组形式的形参可以处于形参列表的任意位置,但个数可变的形参只能处于形参列表的最后。也就是说,一个方法中最多只能有一个长度可变的形参。

注意:

长度可变的形参只能处于形参列表的最后。一个方法中最多只能包含一个长度可变的形参。调用包含一个可变长度形参的方法时,这个长度可变的形参既可以传入多个参数,也可以传入一个数组。

5.2.4 递归方法

一个方法体内调用它自身,被称为方法的递归。方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。

当一个方法不断地调用它本身时,必须在某个时刻方法的返回值是确定的,即不再调用它本身。否则这种递归就变成了无穷递归,类似于死循环。因此定义递归方法时有一条最重要的规定:递归一定要向已知方向递归。

5.2.5 方法重载

java允许同一个类里定义多个同名方法,只要形参列表不同就可。如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。

在java程序中需要确定一个方法需要三个要素:

调用者,也就是方法的所属者,既可以是类,也可以是对象。方法名,方法的标识。形参列表,当调用方法时,系统将会根据传入的实参列表匹配。

方法重载的要求就是两同,一不同;同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法返回值类型,修饰符等,与方法重载没有任何关系。

5.3 成员变量和局部变量

在java语言中,根据定义变量位置的不同,可以将变量分成2大类:成员变量和局部变量。成员变量和局部变量运行机制存在较大差异。

5.3.1 成员变量和局部变量

成员变量指的是在类范围里定义的变量,也就是前面所说的属性;局部变量指的是在一个方法内定义的变量。不管是成员变量,还是局部变量,都应该遵守相同的命名规则:从语法角度来看,只要是一个合法的标识符即可,但从程序可读性角度来看,应该是多个有意义的单词连缀而成,其中第一个单词首字母小写,后面每个单词首字母大写。

成员变量被分为类属性和实例属性两种。

一个类在使用之前要经过类加载,类验证,类准备,类解析,类初始化等几个阶段。

局部变量根据定义形式的不同,又可以被分为如下三种:

形参:在定义方法签名时定义的变量,形参的作用域在整个方法内有效。方法局部变量:在方法体内定义的局部变量,它的作用域是从定义该变量的地方生效,到该方法结束时失效。代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到该代码块结束时失效。

与成员变量不同的是,局部变量除了形参之外,都必须显式初始化。

5.3.2 成员变量的初始化和内存中的运行机制

当系统加载类或创建该类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值。

5.3.3 局部变量的初始化和内存中的运行机制

局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,知道等到程序为这个变量赋初始值时,系统才会局部变量分配内存,并将初始值保存到这块内存中。

与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中的。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应内存中;如果局部变量时一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。

栈内存中的变量无须系统垃圾回收,栈内存中的变量往往是随方法或代码块的运行结束而结束的。因此,局部变量的作用域是从初始化该变量开始,直到该方法或该代码块运行完成而结束。因为局部变量只保存基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小。

5.3.4 变量的使用规则

对java初学者而言,何时应该使用类属性?何时应该使用实例属性?何时应该使用方法局部变量?何时使用应该代码块局部变量?这种选择比较困难,如果仅就程序的运行结果来看,大部分时间都可以直接使用类属性或者实例属性来解决问题,无须使用局部变量。但实际上这种做法相当错误,因为当我们定义一个成员变量时,成员变量将被放置到堆内存中去,成员变量的作用域将扩大到类存在范围或者对象存在范围,这种范围的扩大有两个害处:

增大了变量的生存时间,这将导致更大的系统开销。扩大了变量的作用域,这不利于提高程序的内聚性。

如果有如下几种情形,则应该考虑使用成员变量:

如果需要定义的变量是用于描述某个类或某个对象的固有信息的。如果在某个类中需要以一个变量来保存该类或者实例运行时的状态信息。如果某个信息需要在某个类的多个方法之间进行共享,则这个信息应该使用成员变量来保存。

即使在程序中使用局部变量,也应该尽可能地缩小局部变量的作用范围,局部变量的作用范围越小,它在内存里停留的时间就越短,程序运行性能越好。因此,能用代码块局部变量的地方,就坚决不要使用方法局部变量。

5.4 隐藏和封装

5.4.1 理解封装

封装是面向对象三大特征之一(另外两个是继承和多态),它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。

封装是面向对象编程语言对客观世界的模拟,客观世界里的属性都是被隐藏在对象内部,外界无法直接操作和修改。就如刚刚Person对象的age属性,只能随着岁月的流逝,age属性才会增加,通常不能随意修改person对象的age属性。对一个类或对象实现良好的封装,可以实现以下目的:

隐藏类的实现细节。让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对属性的不合理访问。可进行数据检查,从而有利于保证对象信息的完整性。便于修改,提高代码的可维护性。

为了实现良好的封装,需要从两个方面考虑:

将对象的属性和实现细节隐藏起来,不允许外部直接访问。把方法暴露出来,让方法来操作或访问这些属性。

因此,封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。这两个方面都需要通过使用java提供的访问控制符来实现。

5.4.2 使用访问控制符

java提供了三个访问控制符:private,protected和public,分别代表了三个访问控制级别,另外还有一个不加任何访问控制符的访问控制级别,提供了四个访问控制级别。

private-default-protected-pubic

private访问控制级别:如果类里的一个成员(包括属性和方法)使用private访问控制符来修饰,则这个成员只能在该类的内部被访问。很显然,这个访问控制符用于修饰属性最合适,使用它来修饰属性就可以把属性隐藏在类的内部。deault访问控制权限(包访问权限):如果类里的一个成员(包括属性和方法)或者一个顶级类不使用任何访问控制符修饰,我们就称它是默认访问控制,default访问控制的成员或顶级类可以被相同包下其他类访问。protected访问控制权限(子类访问权限):如果一个成员(包括属性和方法)使用protected访问控制符修饰,那么这个成员既可以被同一个包中其他类访问,也可以被不同包中的子类访问。通常情况下,如果使用protected来修饰一个方法,通常是希望其子类来重写这个方法。public访问控制权限(公共访问权限):这是一个最宽松的访问控制级别,如果一个成员(包括属性和方法)或者一个顶级类使用了public修饰,这个成员或顶级类就可以被所有类访问,不管访问类和被访问类是否处于同一包中,是否具有父子继承关系。

访问控制符用于控制一个类的成员是否可以被其他类访问,对于局部变量而言,其作用域就是它所在的方法,不可能被其他类来访问,因此不能使用访问控制符来修饰。

对于顶级类而言,它也可使用访问控制符修饰,但顶级类只能有两种访问控制级别:public和默认,顶级类不能使用private和protected修饰,因为顶级类既不处于任何类的内部,也就没有其外部类的子类了,因此private 和protected访问控制符对顶级类没有意义。

顶级类可以使用public和默认访问控制级别,使用public修饰的顶级类可以被所有类使用,如声明变量;不使用任何访问控制符修饰的顶级类只能鄂弼同一个包中的所有类访问。

提示:

如果一个java源文件里定义的所有类都没有使用public修饰,则这个java源文件的文件名可以是一切合法的文件名,但如果一个java源文件里定义了一个public修饰的类,则这个源文件的文件名必须与public类的类名相同。

JavaBean总是一个封装良好的类。

提示:

一个类常常就是一个小的模块,我们应该只让这个模块公开必须让外界知道的内容,而隐藏其他一切内容。进行程序设计时,应尽量避免一个模块直接操作和访问另一个模块的数据,模块设计追求高内聚(尽可能把模块的内部数据,功能实现细节隐藏在模块内部独立完成,不允许外部直接干预),低耦合(仅暴露少量的方法给外部使用)。正如我们日常常见的内存条,内存条里的数据及其实现细节被完全隐藏在内存条里面,外部设备(如主机板)只能通过内存条的金手指(提供一些方法供外部调用)来和内存条进行交互。

关于访问控制符的使用,存在如下几条基本原则:

类里的绝大部分属性都应该使用private修饰,除了一些static修饰的,类似全局变量的属性,才可能考虑使用public修饰。除此之外,有些方法只是用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。希望暴露出来给其他类自由调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,暴露给其他类中创建该类的对象。因为顶级类通常都希望被其他类自由使用,所以大部分顶级类都使用public修饰。

5.4.3 package和import

java引入了包(package)机制,提供了类的多层命名空间,用于解决类的命名冲突,类文件管理等问题。

java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元,如果希望把一个类放在指定的包结构下,我们应该在java源程序的第一个非注释行放如下格式的代码:

package packageName;

一旦在java源文件中使用了这个package语句,则意味着该源文件里定义的所有类都属于这个包。

java规定:位于包中的类,在文件系统中也必须有与包名层次相同的目录结构。

提醒:

不要把系统搞糊涂,系统一糊涂就是你错了。

5.4.4 java的常用包

java的核心类都放在java这个包以及其子包下,java扩展的许多类都放在javax包以及其子包下。这些实用类也就是前面所说的API(应用程序接口),Sun按这些类的功能分别放在不同的包下,下面结构包是java语言的常用包:

java.lang:这个包下包含java语言的核心类,如String,Mat,System和Thread类等,使用这个包下的类无须使用import语句导入,系统会自动导入这个包下所有类。java.util:这个包下包含了java大量工具类/接口和集合框架类/接口,例如arrays和list,set等。java.net:这个包下包含了一些java网络编程相关的类/接口。java.io:这个包下包含了一些java输入/输出编程相关的类/接口。java.text:这个包下包含了一些java格式化相关的类。java.sql:这个包下包含了java进行JDBC数据库编程的相关类/接口。java.awt:这个包下包含抽象窗口工具集的相关类/接口,这些类主要用于构建图形用户界面(GUI)程序。java.swing:这个包下包含Swing图形用户界面编程的相关类/接口,这些类可用于构建平台无关的GUI程序。

5.5 深入构造器

构造器是一个特殊的方法,这个特殊方法用于创建类的实例。java语言里构造器是创建对象的重要途径(即使使用工厂模式,反射等方式创建对象,其实质依然是依赖于构造器),因此,java;类必须包含一个或一个以上的构造器。

5.5.1 使用构造器执行初始化

构造器最大的用处就是在创建对象时执行初始化。前面已经介绍过了,当创建一个对象时,系统为这个对象的属性进行默认初始化,这种默认初始化把所有基本类型的属性设为0(对数值型属性)或false(对布尔型属性),把所有引用类型的属性设置为null。

如果我们想改变这种默认的初始化,想让系统创建对象时就为该对象各属性显式指定初始值,就可以通过构造器来实现。

注意:

如果程序员没有为java类提供任何构造器,则系统会为这个类提供一个无参数的构造器,这个构造器的执行体为空,不做任何事情。无论如何,java类至少包含一个构造器。

构造器是创建java对象的途径,是不是说构造器完全负责创建java对象?

不是!构造器是创建java对象的重要途径,通过new关键字调用构造器时,构造器也确实返回了该类的对象,但这个对象并不是完全由构造器负责创建的。实际上,当程序员调用构造器时,系统会先为该对象分配内存空间,并为这个对象执行默认初始化,这个对象已经产生了--这些操作都在构造器执行之前就完成了。也就是说,当系统开始执行构造器的执行体之前,系统已经创建了一个对象,只是这个对象还不能被外部程序访问,只能在该构造器中通过this来引用它。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回,通常还会赋给另一个引用类型的变量,从而让外部程序可以访问该对象。

一旦程序员提供了自定义的构造器,则系统不再提供默认的构造器。

通常建议为java类保留无参数的默认构造器。因此,如果为一个类编写了有参数的构造器,通常建议为该类额外编写一个无参数的构造器。

5.5.2 构造器的重载

同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。构造器重载允许java类里包含多个初始化逻辑,从而允许使用不同的构造器来初始化java对象。

构造器重载和方法重载基本相似:要求构造器的名字相同,这一点无须特别要求,因为构造器必须与类名相同,所以同一个类的所有构造器名肯定相同。为了让系统能区分不同的构造器,多个构造器的参数列表必须不同。

如果仅仅从软件功能实现上来看,这样复制,粘贴确实可以实现这个效果,但从软件工程的角度来看,这样做是相当糟糕的。在软件开发里有一个规则:不要把相同的代码段书写两次以上!因为软件是一个需要不断更新的产品。因此,尽量避免相同的代码重复出现,充分复用每一段代码,既可以让程序代码更加简洁,也可以降低软件的维护成本。

5.6 类的继承

继承是面向对象三大特征之一,也是实现软件复用的重要手段。java的继承具有单继承的特点,每个子类只有一个直接父类。

5.6.1 继承的特点

java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类,有的也称为基类,超类。父类和子类的关系,是一种一般和特殊的关系。

因为子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。

java使用sxtends作为继承的关键字,extends关键字在英文是扩展,而不是继承!这个关键字很好地体现了子类和父类的关系:子类是对父类的扩展,子类是一种特殊的父类。从这个意义上来看,使用继承来描述子类和父类的关系是错误的,用扩展更恰当。

java的子类不能获得父类的构造器。

5.6.2 重写父类的方法

子类扩展了父类,子类是一个特殊的父类。大部分的时候,子类总是以父类为基础,额外增加新的属性和方法。但有一种情况例外:子类需要重写的父类的方法。

这种子类包含与父类同名方法的现象被称为方法重写,也被称为方法覆盖(Override)。

方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同,“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等。“一大”指的子类方法的访问权限应比父类方法更大或相等,尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。

当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但还可以在子类方法中调用父类中被覆盖方法。如需要在子类方法中调用父类中被覆盖方法,可以使用super或者父类类名作为调用者来调用父类中被覆盖方法。

如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类private方法具有相同方法名,相同形参列表,相同返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。

5.6.3 父类实例的super引用

super是java提供的一个关键字,它是直接调用父类对象的默认引用。

java程序创建某个类的对象时,系统会隐式创建该类父类的对象。只要有一个子类对象存在,则一定存在一个与之对应的父类对象。在子类方法中使用super引用时,super总是指向作为该方法调用者的子类对象所对应的父类对象。其实,super和this很像,其中this总是指向到调用该方法的对象,而super则指向this指向对象的父对象。

如果在构造器中使用super引用,则super引用指向该构造器正在初始化的对象所对应的父类对象。

方法重载和方法重写在英语中分别是overload和override,经常看到有初学者或一些低水平的公司喜欢询问重载和重写的却别?其实把重载和重写放在一起比较本身没有太大的意义,因为重载主要发生在同一个类的多个同名方法之间,而重写发生在子类和父类的同名方法之间。它们之间的联系和少,除了二者都是发生在方法之间,并要求方法名相同之外,没有太大的相似之处。当然,父类方法和子类方法之间也可能发生重载,因为子类会获得父类方法,如果子类定义了一个父类方法有相同方法名,但参数列表不同的方法,就会形成父类方法和子类方法的重载。

如果子类定义了和父类同名的属性,也发生子类属性覆盖父类属性的情形。正常情况下,子类里定义的方法,子类属性直接访问该属性,都会访问到覆盖属性,无法访问父类被覆盖的属性。但在子类定义的实例方法中可以通过super来访问父类被覆盖的属性。

如果被覆盖的是类属性,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类属性。

如果子类里没有包含和父类同名的属性,则子类将可以继承到父类属性。如果在子类实例方法中访问该属性时,则无须显式使用super或父类名作为调用者。因此,如果我们在某个方法中访问名为a的属性,但没有显式指定调用者,系统查找a的顺序为:

查找该方法中是否有名为a的局部变量查找当前类中是否包含名为a的属性查找a的直接父类中是否包含名为a的属性,依次上溯a的父类,直到java.lang.Object类,如果最终不能找到名为a的属性,则系统出现编译错误。

5.6.4 调用父类构造器

子类不会获得父类的构造器,但有的时候子类构造器里需要调用父类构造器的初始化代码。

在一个构造器中调用另一个重载的构造器使用this调用来实现,在子类构造器中调用父类构造器使用super调用来实现。

创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果某个父类通过this调用了同类中重载的构造器,就会依次执行此父类的多个构造器。

多态

java引用变量有两个类型:一个是编译时的类型,一个是运行时的类型,编译时的类型由声明该变量时使用的类型决定,运行时的类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就会出现所谓的多态。

5.7.1 多态性

因为子类其实是一种特殊的父类,因此java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型,向上转型由系统自动完成。

当把一个子类对象直接赋给父类引用变量,当运行时调用该引用变量的方法时,其方法行为总是像子类方法的行为,而不是像父类方法行为,这将出现相同类型的变量,执行同一个方法时呈现出不同的行为特征,这就是多态。

注意:

引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。因此,编写java代码时,引用变量只能调用声明该变量时所用类里包含的方法。

通过引用变量来访问其包含的实例属性时,系统总是试图访问它编译时类所定义的属性,而不是它运行时类所定义的属性。

5.7.2 引用变量的强制类型转换

编写java程序时,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法,即使它实际所引用对象确实包含该方法。如果需要让这个引用变量来调用它运行时类型的方法,则必须把它强制类型转换成运行时类型,强制类型转换需要借助于类型转换运算符。

类型转换运算符是小括号,类型转换运算符的用法如下:(type)variable,这种用法可以将variable变量转换成一个type类型的变量。

除此之外,这个类型转换运算符还可以将一个引用类型变量转换成其子类类型。这种强制类型转换不是万能的,当进行强制类型转换时需要注意:

基本类型之间的转换只能在数值类型之间进行,这里所说的数值类型包括整数型,字符型和浮点型。但数值型不能喝布尔型之间进行类型转换。引用类型之间的转换只能把一个父类变量转换成子类类型,如果是两个没有任何继承关系的类型,则无法进行类型转换,否则编译时就会出现错误。如果试图把一个父类实例转换子类类型,则必须这个对象实际上是子类实例才行(即编译时类型为父类类型,而运行时类型是子类类型),否则在运行时引发ClassCastException异常。

在进行强制类型转换之前,先用instanceof运算符判断是否可以成功转换,从而避免出现ClassCastException异常,这样可以保证程序更加健壮。

注意:

当把子类对象赋给父类引用变量时,被称为向上转型(upcasting),这种转型总是可以成功的,这也从另一个侧面证实了子类是一种特殊的父类。这种转型只是表面这个引用变量的编译类型是父类,但实际执行它的方法时,依然变现出子类对象的行为方式。但把一个父类对象赋给子类引用变量时,就需要进行强制类型转换,而且还可能在运行时产生ClassCastException异常,使用instanceof运算符可以让强制类型转换更安全。

instanceof和类型转换运算符一样,都是java提供的运算符,与+,-等算术运算符的用法大致相似。

5.7.3 instanceof运算符

instanceof运算符的前一个操作数通常是一个引用类型的变量,后一个操作数通常是一个类(也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的类,或者其子类,实现类的实例。如果是,则返回true,否则返回false。

在使用instanceof运算符时需要注意:instanceof运算符前面操作数的编译时类型要么与后面的类相同,要么是后面类的父类,否则会引起编译错误。

instanceof运算符的作用是:在执行强制类型转换之前,首先判断前一个对象是否是后一个类的实例,是否可以成功地转换,从而保证代码更加健壮。

instanceof和(type)是java提供的两个相关的运算符,通常先用instanceof判断一个对象是否可以强制类型转换,然后再使用(type)运算符进行强制类型转换,从而保证程序不会出现错误。

5.8 继承与组合

继承是实现类重用的重要手段,但继承带来了一个最大的坏处:破坏封装。相比之下,组合也是实现类重用的重要方式,而采用组合方式来实现重用则能提供更好的封装性。

5.8.1 使用继承的注意点

子类扩展父类时,子类可以从父类继承得到属性和方法,如果访问权限允许,子类将可以直接访问父类的属性和方法,相当于子类可以直接复用父类的属性和方法,确实非常方便。

继承带来了高度复用的同时,也带来了一个严重的问题:继承严重地破坏了父类的封装性,前面介绍封装时提到:每个类都应该封装它内部信息和实现细节,而只暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的属性(内部信息)和方法,从而造成子类和父类的严重耦合。

从这个角度来看,父类的实现细节对子类不再透明,子类可以访问父类的属性和方法,并可以改变父类的实现细节(例如通过方法重写的方式来改变父类的方法实现),从而导致子类可以恶意篡改父类的方法。

为了保证父类良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则:

尽量隐藏父类的内部数据。尽量把父类的所有属性都设置成private访问类型,不用让子类直接访问父类的属性。不要让子类可以随意访问,修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符(该修饰符后面会有更详细的介绍)来修饰该方法:如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,可以使用prottected来修饰该方法。尽量不要在父类构造器中调用将要被子类重写的方法。

那么,到底何时需要从父类派生新的子类呢?不仅需要保证子类是一种特殊的父类,而且还需要具备以下两个条件之一:

子类需要额外增加属性,而不仅仅是属性值的改变。子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。

5.8.2 利用组合实现复用

如果需要复用一个类,除了把这个类当成基类来继承之外,还可以把该类当成另一个类的组合成分,从而允许新类直接复用该类的public方法。不管是继承还是组合,都允许在新类(对于继承就是子类)中直接复用旧类的方法。

对于继承而言,子类可以直接获得父类的public方法,程序使用子类时,将可以直接访问该子类从父类那里继承到的方法;而组合则是把旧类对象作为新类的属性嵌入,用以实现新类的功能,用户看到的是新类的方法,而不能看到嵌入对象的方法。因此,通常需要在新类里使用private修饰嵌入旧类对象。

仅从类复用的角度来看,不难发现父类的功能等同于被嵌入类,都将自身的方法提供给新类使用;子类以及组合关系里的整体类,都可复用原有类的方法,用于实现自身的功能。

继承要表达的是一种is-a的是关系,而组合表达的是has-a有关系。

5.9 初始化块

java使用构造器来对单个对象进行初始化操作,使用构造器先把整个java对象的状态初始化完成,然后将java对象返回给程序,从而让该java对象的信息更加完整。与构造器作用非常类似地是初始化块,它也可以对java对象进行初始化操作。

5.9.1 使用初始化块

初始化块是java类里可出现的第四种成员(前面依次有属性,方法和构造器),一个类里可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行,后面定义的初始化块后执行。初始化的语法格式如下:

[]{ //初始化块的可执行性代码 ... }

初始化块的修饰符只能是static,使用static修饰的初始化块被称为静态初始化块。初始化块里的代码可以包含任何可执行性语句,包括定义局部变量,调用其他对象的方法,使用分支,循环语句等。

注意:

虽然java允许一个类里定义2个普通初始化块,但这没有任何意义。因为初始化块是在创建java对象时隐式执行的,而且它们总是全部执行,因此我们完全可以把多个普通初始化块合并成一个初始化块,从而可以让程序更加简洁,可读性更强。

从上面代码可以看出,初始化和构造器的作用非常相似,他们都用于对java对象执行指定初始化操作,但他们之间依然存在一些差异。

普通初始化块,声明实例属性指定的默认值都可认为是对象的初始化代码,他们的执行顺序与源程序中排列顺序相同。

提示:

当java创建一个对象时,系统先为该对象的所有实例属性分配内存(前提是该类已经被价值过了),接着程序开始对这些实例属性执行初始化,其初始化顺序是:先执行初始化块或声明属性时指定的初始值,再执行构造器里指定的初始值。

5.9.2 初始化块和构造器

从某种程度上来看,初始化块是构造器的补充,初始化块总是在构造器执行之前执行。系统同样可使用初始化块进行对象的初始化操作。

与构造器不同的是,初始化是一段固定执行的代码,它不能接受任何参数。因此初始化块对同一个类所有对象进行的初始化处理完全相同。基于这个原因,不难发现初始化块的基本用法,如果有一段初始化块处理代码对所有对象完全相同,且无须接受任何参数,就可以把这段 初始化处理代码提取到初始化块中。

5.9.3 静态初始化块

如果定义初始化块时使用了static修饰符,则这个初始化块就变成了静态初始化块,也被称为类初始化块。静态初始化块是类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因为静态初始化块总是比普通初始化块先执行。

静态初始化块是类相关的,用于对整个类进行初始化处理,通常用于对类属性执行初始化处理,静态初始化块不能对实例属性进行初始化处理。

注意:

静态初始化块也别称为类初始化块,也属于类的静态成员,同样需要遵循静态成员不能访问非静态成员的规则,因此静态初始化块不能访问非静态成员,包括不能访问实例属性和实例方法。

系统加载并初始化某个类时,总是保证该类所有父类(包括直接父类和间接父类)全部加载并初始化。

提示:

当JVM第一个主动使用某个类时,系统会在类准备阶段为该类的所有静态属性分配内存;在初始化阶段则负责初始化这些静态属性,初始化静态属性的就是执行类初始化代码或者声明类属性时指定的初始值,他们的执行顺序与源代码中排列顺序相同。

转载请注明原文地址: https://www.6miu.com/read-3100269.html

最新回复(0)