Java基础知识(三)——面向对象(上)

时间:2022-11-16 19:40:51

 

类和对象

对一个类定义而言,可以包含三种最常见的成员:构造器、成员变量、方法,类里各成员之间的定义顺序没有任何影响,可以互相调用,但是static修饰的成员不能访问没有static修饰的成员。

static修饰的方法和成员变量,即可通过类来调用,也可通过实例来调用。没有使用static修饰的方法或成员变量只能通过实例来调用。

static修饰的成员表明他属于这个类本身,而不是属于该类的单个实例,因此通常把static修饰的成员变量称为类变量(静态变量)、类方法(静态方法)。

static真正的作用是区分成员变量、方法、内部类、初始化块这四种成员到底属于类本身还是属于实例(相当于一个标志)。

 

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

构造器是一个类创造对象的根本途径,如果一个类没有构造器,这个类通常无法创建实例。因此如果程序员没有为一个类编写构造器,则Java语言会为该类提供一个默认的构造器,反之,则不提供。

 

成员变量的语法格式:

[修饰符] 类型 成员变量名 [= 默认值];

修饰符:public、protected、private、static、final,可以与static、final组合起来修饰成员变量。

成员变量名有field意译而来,有些书籍将其称为属性。但实际上在Java世界里属性(由property翻译而来)指的是一组setter方法和getter方法。如某个类有age属性,意味着该类包含setAge()和getAge()两个方法。field也被译为字段、域。

 

方法的语法格式:

[修饰符] 方法返回值类型 方法名(形参列表){

}

修饰符:public、protected、private、static、final、abstract,其中public、protected、private最多只能出现一个,abstract和final最多出现一个,可以与static组合起来修饰方法。

方法返回值类型:返回值类型可以是任何数据类型,如果声明了返回值类型,则必须有一个有效的return语句返回一个变量或者表达式且必须与声明类型匹配。

没有返回值必须用void声明。

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

 

构造器是一个特殊的方法:

[修饰符] 构造器名 (形参列表){

}

构造器既不能定义返回值类型,也不能用void声明。如果使用或声明了,Java会把构造器当成方法处理。

实际上,构造器本身是有返回值的,当使用new关键字来调用构造器时,构造器返回该类的实例,可以把这个类的实例当成构造器的返回值。因此构造器返回值类型总是当前类,无须定义。也不要使用return来返回当前类的 对象,因为构造器的返回值是隐式的。

 

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

 

与数组类似,类也是一种一种引用数据类型,因此程序中定义的Person类型的变量实际上是一个引用,他被存放在栈内存里,指向实际的Person对象,实际的Person对象则存放在堆内存中。

 

不管是数组还是对象,当程序访问引用变量的成员变量或方法时,实际上是访问该引用变量所引用的数组、对象的成员变量或方法。

 

将p变量的值赋给p2变量,也就是将p变量保存的地址赋给p2变量,这样p2变量和p变量指向堆内存里的同一个Person对象。不管访问p2变量的成员变量和方法,还是访问p变量的成员变量和方法,将会返回相同的结果。

如果堆内存里对象没有任何变量指向该对象,那么程序将无法再访问该对象,这个对象也就成为了垃圾,Java的垃圾回收机制将会回收该对象,释放内存。

因此,如果希望垃圾回收机制回收某对象,只需切断该对象的所有引用变量和他之间的关系,即把这些引用变量赋值为null。

 

对象的this引用:

this关键字总是指向调用该方法的对象,根据this出现位置不同,this作为对象的默认引用有两种情况:

构造器中引用该构造器正在初始化的对象。

在方法中引用调用该方法的对象。

在现实世界里,对象的一个方法依赖于另一个方法情形很常见:如吃饭方法依赖于拿筷子方法,写程序方法依赖于敲键盘方法,这种依赖都是同一个对象两个方法之间的依赖。因此Java允许对象的一个成员直接调用另一个成员,可以省略this前缀。

static修饰的方法不能使用this引用。

如果调用static修饰的成员省略了主语调用者,那么默认该类作为主调;如果没有被static修饰的成员省略了主调,那么默认使用this作为主调。

不要使用对象去调用static修饰的成员变量和方法,而使用类去调用。如果确实需要在静态方法中调用普通方法,则只能重新创建一个对象。

 

大部分时候,普通方法访问其他方法、成员变量时无须使用this前缀,但如果方法里有个局部变量和成员变量同名,但程序又需要在该方法里访问这个被覆盖的成员变量,则必须使用this前缀。

 

方法:

在结构化编程语言里,整个软件有一个一个函数组成;在面向对象编程语言里,整个系统由一个个类组成。因此在Java语言里,方法不能独立存在,方法必须属于类或对象。

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

同一个类的一个方法调用另外一个方法时,如果被调用方法是普通方法,则默认使用this作为调用者。如果被调方法是静态方法,则默认使用类作为调用者。

Java语言里方法的附属性主要体现在如下体验在几个方面:

方法不能独立定义,方法只能在类体里定义。

从逻辑意义上来看,方法要么属于该类本身,要么属于该类的一个对象。

永远不能独立执行方法,执行方法必须使用类或对象作为调用者。

 

使用static修饰的方法属于这个类本身,使用static修饰的方法既可以使用类作为调用者来调用,也可是使用对象作为调用者来调用。但因为使用static修饰的方法还是属于这个类的,因此使用该类的任何对象来调用这个方法时将会得到相同的执行结果,这是由于底层依然是使用这些实例所属的类作为调用者。

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

 

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

 

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

 

方法的重载:

Java程序中确定一个方法的三个要素:

调用者,也就是方法的所属者,既可以是类,也可是对象。

方法名

形参列表,当调用法时,系统将会根据传入的实参列表匹配。

方法重载的要求就是两同一不同:同一个类中方法名相同,参数列表不同。

 

Java调用方法时可以忽略方法返回值,如果采用 f();调用方法,则不能判断调用的是int f(){};还是void f(){};。

 

成员变量和局部变量:

成员变量被分为类变量和实例变量两种,有static修饰的就是类变量。

其中类变量总该类的准备阶段起就开始存在,直到系统完全销毁这个类,类变量的作用于与这个类的生存范围相同,而实例变量则从该类的实例创建起就开始存在,直到系统完全消失这个实例,实例变量的作用域与对应实例的生存范围相同。

 

类变量也可以通过类的实例(实例.类变量)访问,但是由于这个实例并不拥有这个类变量,因此他访问的并不是这个实例的变量,依然是访问它对应类的类变量。也就是说,如果通过一个实例修改了类变量的值,由于这个类变量并不属于他,而是属于它对应的类。因此,修改的依然是类的变量,与通过该类来修改类变量的结果完全相同,这回倒是该类的其他实例来访问这个类变量时,也将会获得这个被修改过的值。

类变量的作用域比实例变量的作用域更大;实例变量随实例的存在而存在,而类变量随类的存在而存在。同一个类的所有实例访问类变量时,实际*问的是该类本身的同一个变量,也就是说,访问了同一片内存区。

 

Java不允许通过实例来访问static修饰的成员变量,因此看到类似情况都可以将它替换成通过类本身来访问static成员变量的情形。

 

在同一个类里,成员变量的作用范围是整个类内有效,一个类里不能定义两个同名的成员变量,即使一个是类变量,一个是实例变量也不行。一个方法里不能定义两个同名的方法局部变量,方法局部变量与形参也不能同名。同一个方法中不同代码块内的代码块局部变量可以同名。如果先定义代码块局部变量,后定义方法局部变量,前面定义的代码块局部变量与后面定义的方法局部变量也可以同名。

Java里允许局部变量和成员变量同名,如果方法里的局部变量和成员变量同名,局部变量会覆盖成员变量,如果需要在这个方法里引用被覆盖的成员变量,则可以使用this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。

 

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

  局部变量不属于任何类或实例,因此他总是保存在其所在方法的栈内存中。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对于的内存中,如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过改地址引用到改变量实际引用的对象或数组。栈内存中的变量无须系统垃圾回收,往往随着方法或代码块的运行结束而结束。

 

隐藏和封装:

Java提供了三个访问控制符:private、protected、public,分别代表了3个访问控制级别,另外还有一个不加任何访问控制级别,一共4个访问控制级别。

 

private(当前类访问权限):如果类里的一个成员(包括成员变量、方法和构造器等)使用private访问控制符来修饰,则这个成员只能在当前类的内部被访问。很显然,这个访问控制符用于修饰成员变量最合适,使用他来修饰成员变量就可以把成员变量隐藏在该类的内部。

default(包访问权限):如果一个类里成员或者一个外部类不适应任何访问控制符修饰,就称他是包访问权限,default访问控制的成员或外部类可以被相同包下的其他类访问。

protected(子类访问权限):如果一个成员使用protected访问控制符修饰,那么这个成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。在通常情况下,如果使用protected修饰一个方法,通常是希望其子类来重写这个方法。

public(公共访问权限):这是一个最宽松的访问控制级别,如果一个成员或一个外部类使用public访问控制符修饰,那么这个成员或外部类就可以被所有类访问,不管访问类和被访问类是否处于同一个包中,是否具有父子继承关系。

 

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

对于外部类而言,只能有两种访问控制级别:public和default,外部类不能使用private和protected修饰,因为外部类没有处于任何类的内部,也就没有其所在类的内部、所在类的子类两个范围,因此private和protected没有意义。

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

 

Java类里实例变量的setter和getter方法有非常重要的意义。例如,某个类里包含了一个名为abc的实例变量,则其对应的setter和getter方法名为setAbc()和getAbc()。如果一个Java类的每个实例变量都被使用private修饰,并为每个实例变量都提供了public修饰的setter和getter方法,那么这个类就是一个符合JavaBean规范的类。JavaBean总是一个封装良好的类。

关于访问控制符的使用原则:

类里绝大部分成员变量都应该使用private修饰,只有一些static修饰的、类似全局变量的成员,才可能考虑使用public修饰。除此之外,有些方法只用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。

如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不是想被外界直接调用,则应该使用protected修饰这些方法。

希望暴露出来给其他类*调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,从而允许在其他地方闯将爱哦类的实例。因为外部类通常都希望被其他类*使用,所以大部分外部类都使用public修饰。

 

 

package、import和import static:

当虚拟机在装载带包名的类是会先搜索CLASSPATH环境变量制定的目录,然后在这目录中按与包层次对应的目录结构去查找class文件。

不要以为只要把生成的class文件放在某个目录下,这个目录就成了这个类的包名。这是错误的,不是有了目录结构,就等于有了包名。为Java类添加包必须在Java源文件中通过package语句指定,单靠目录名是没办法指定的。Java的包机制需要:源文件里使用package语句指定包名;class文件必须放在对应的路径下。

 

为了简化编程,Java引入了import关键字,import可以向某个Java文件中导入指定包层次下某个类或全部类,import语句应该在package语句之后、类定义之前。

Java默认为所有原文件导入java.lang包下所有的类,因此前面在Java程序中使用String、System类时都无需使用import语句来导入这些类。但对于Arrays类,其位于java.util包下,则必须使用import语句来导入该类。

JDK1.5以后增加了一种静态导入的语法,它用于导入指定类的某个静态成员变量、方法或全部的静态成员变量、方法。

静态导入使用import static语句,静态导入也有两种语法,分别用于导入指定类的单个静态成员变量、方法和全部静态成员变量、方法:

import static package.subpackeage…ClassName.fieldName|methodName;

导入指定类的全部静态成员变量、方法:

import static package.Subpackage…ClassName.*;

即:使用import可以省略写包名,而使用import static则可以连类名都省略。

Java的常用包:

Java的核心类都放在java包以及其子包下,Java扩展的许多类都放在javax包以及其子包下。这些实用类就是API(应用程序接口)。

java.lang:这个包下包含了Java语言的核心类,如String、Math、System和Thread类等,使用这个包下的类无须使用import语句导入,系统自动导入。

java.util:这个包下包含了Java的大量工具类/接口和集合框架类/接口,例如Arrays和List、Set等。

java.net:这个包下包含了了一些Java网络编程相关的类/接口。

java.io:这个包下包含了一些java输入/输出编程相关的类/接口。

java.text:这个包下包含了了一些Java格式化相关的类。

java.sql:这个包下包含了Java进行JDBC数据库编程的相关类/接口。

java.awt:这个包下包含了抽象窗口工具集(Abstract Window Toolkits)的相关类/接口,这些类主要用于构建图形用户界面(GUI)程序。

java.swing:这个包下包含了Swing图形用户界面编程相关类/接口,这些可用于构建平台无关的GUI程序。

 

构造器:

构造器是一个特殊的方法,用于创建实例时执行初始化。当创建一个对象时,系统为这个对象的实例变量进行默认初始化,这种默认初始化把所有基本类型实例变量设为0或false,把所有引用类型的实例变量设置为null。

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

如果程序员没有为Java类提供任何构造器,则系统会为这个类提供一个无参数的构造器,该构造器执行体为空。

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

如果用户希望该类保留无参数的构造器,或者希望有多个初始化过程,则可以为该类提供多个构造器,即构造器重载。

 

若方法B完全包含了方法A,则可以在方法B中调用方法A。

但构造器必须使用new关键字来调用,但一旦使用new关键字来调用构造器,将会导致系统重新创建一个对象。为了在构造器B中调用构造器A中的初始化代码,又不会重新创建一个Java对象,可以使用new关键字来调用相应的构造器。

 

类的继承:

Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类。

extends关键字意思是扩展,而不是继承。子类对父类时一种扩展,子类是一种特殊的父类。Java中子类可获得父类的全部成员变量和方法,但是子类继承父类的构造器。

 

如果定义一个Java类时并未显式制定这个类的直接父类,则这个类默认扩展java.lang.Object类。因此,java.lang.Object类是所有类的父类,要么是其直接父类,要么是其间接父类。因此所有的Java对象都可以调用java.lang.Object类所定义的实例方法。

 

重写父类方法:

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

 

当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。如果需要在子类方法中调用父类中背负高的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。

 

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

重载主要发生在同一个类的多个同名方法之间,而重写发生在子类和父类的同名方法之间。

父类方法和子类方法之间也可能发生重载,因为子类会获得父类方法,如果子类定义了一个与父类方法有相同的方法名,但参数列表不同的方法,就会形成父类方法和子类方法的重载。

 

super限定:

super是Java提供的一个关键字,用于限定该对象调用从他父类继承得到的实例变量或方法。super也不能出现在static修饰的方法中,static修饰的方法是属于类的,该方法的调用者是一个类,而不是对象,super限定没有意义。

如果构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。

如果子类定义了和父类同名的实例变量,只发生子类实例变量隐藏父类实例变量的情形。在正常情况下,子类里定义的方法直接访问该实例变量默认会访问到子类中定义的实例变量,无法访问到父类中被隐藏的实例变量。在子类定义的实例方法中可以通过super来访问父类中被隐藏的实例变量。

 

如果在某个方法中访问名为a的成员变量,但没有显式指定调用者,则系统查找a的顺序为:

查找该方法中是否有名为a的局部变量

查找当前类中是否包含名为a的成员变量

查找a的直接父类中是否包含名为a的成员变量,依次上溯a的所有父类,知道java.lang.Object类,如果最终找不到,则编译报错。

如果被覆盖的是类变量,在子类方法中则可以通过父类名作为调用者来访问被覆盖的类变量(不必用super)。

 

当程序创建一个子类对象是,系统不仅会为该类中定义的实例变量分配内存,也会为他从父类继承得到的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。即,当系统创建一个Java对象时,如果Java类有两个父类(一个直接父类A,一个间接父类B),假设A类中定义了2个实例变量,B类中定义了3个实例变量,当前类中定义了1个实例变量,那么这个Java对象将会保存2+3+1个实例变量。

 

如果在子类里定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量,但不是完全覆盖,即系统在创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间。

 

为了在子类方法中访问父类中定义的、被隐藏的实例变量,或为了在子类方法中调用父类中定义的、被覆盖的(Override)的方法,可以通过super.作为限定来调用这些实例变量和实例方法。

 

调用父类构造器:

子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码,类似于前面所介绍的一个构造器调用另一个重载的构造器。

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

使用super调用和使用this调用很像,区别在于super调用的是其父类的构造器,而this调用的是同一个类中重载的构造器,所以super和this不会同时出现。

 

不管是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次。子类构造器调用父类构造器分三种情况:

1.子类构造器执行体的四一行使用super显示调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。

2.子类构造器执行体的第一行代码使用this显示调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时即会调用父类构造器。

3.子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。

无论哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行,而且执行父类构造器时,系统会再次上溯执行其父类构造器,依次类推,创建任何对象,最先执行的总是java.lang.Object类的构造器。

 

 

多态(Polymophism):

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

 

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

把一个子类对象直接赋给父类引用变量时,如BaseClass ploymophicBc = new SubClass();,这个ploymophicBc引用变量的编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的特征,这就是多态。

 

引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。因此,编写Java代码时,引用变量只能调用声明该变量时所用类里包含的方法。例如Object p = new Person()代码定义了一个p变量,则这个p只能调用Object类的方法,而不能调用Person类里定义的方法。

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

 

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

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

此外,这个类型转换符还可以将一个引用类型变量转换成其子类类型。但不是万能的,需要注意:

基本类型之间转换只能在数值类型之间进行,但数值类型不能和布尔类型之间进行类型转换。

引用类型之间转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,则无法进行类型转换,否则会编译出错。如果试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行(即编译时是父类类型,运行时是子类类型),否者会引发ClassCastException异常。

 

当把子类对象赋给父类引用变量时,被称为向上转型,这种转型总是可以成功的,这也侧面证实了子类是一种特殊的父类。这种转型只是表明这个引用变量的编译时类型师傅类,但实际执行他的方法时,依然表现出子类对象的行为方式。但把父类对象赋给子类引用变量时,就需要强制类型转换。

 

在强制类转换之前,先用instanceof运算度判断是否可以成功转换,从而避免出现ClassCastException异常。

 

instanceof运算符:

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

需要注意:instanceof运算符前面操作数的编译时类型要么与后面类相同,要么与后面的类具有父子继承关系,否则会编译出错。

 

继承与组合:

继承是实现复用的重要手段,但继承带来了一个最大坏处:破坏封装(在继承关系中,子类可以直接访问父类的成员变量和方法)。相比之下,组合也是实现类复用的重要方式,而采用组合方式来实现类复用则能提供更好的封装性。

为保证父类具有良好的封装性,不会被子类随意改变,设计父类原则:

1.尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置为private。

2.不要让子类可以随意访问、修改父类方法。父类中那些仅为辅助其他的工具方法,应该使用private修饰。如果父类的方法需要被外部类调用,则必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符。如果希望某个方法被子类重写,但不希望其他类*访问,则使用protected修饰。

3.尽量不要在父类构造器中调用将要被子类重写的方法。

 

如果想把某些类设置为最终类,即不能被当为父类,则可以使用final修饰。此外,使用private修饰这个类的所有构造器,总而保证子类无法调用该类的构造器,也就无法继承该类。对于把所有的构造器都使用private修饰的父类而言,可提供另外一个静态方法,用于创建该类的实例。

 

想要从父类派生新的子类,需要具备以下两个条件之一:

1.子类需要额外增加属性,而不仅仅是属性值改变。

2.子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。

 

利用组合实现复用:

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

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

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

 

系统设计与组合设计的系统开销不会有本质区别。

 

大部分时候,继承关系中从多个子类里抽象出共有父类的过程,类似于组合关系中从多个整体类里提取被组合类的过程;继承关系中从父类派生子类的过程,则类似于组合关系中把被组合类组合到整体类的过程。

总之,继承要表达的是一种 “是(is a)”关系,二组合表达的是“有(has a)”关系

 

初始化块:

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

 

初始化块是Java类里可出现的第4种成员(成员变量、方法、构造器),一个类里可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行。

[修饰符] {

//初始化可执行性代码(任何可执行语句)

}
初始化块的修饰符只能是static,使用static修饰的初始化块被称为静态初始化块。

初始化块虽然是Java类的一种成员,但他没有名字,没有标识,因此无法通过类、对象来调用初始化块。初始化块只在创建Java对象时隐式执行,而且在执行构造器之前执行。

 

初始化块和构造器的作用非常相似,他们都用于对Java对象执行指定的初始化操作,但也有差异。

当Java创建一个对象时,系统先为该对象的所有实例变量分配内存(前提是该类已经被加载过了),接着程序开始对这些实例变量执行初始化,其初始化执行顺序是:先执行初始化块或声明实例变量时指定的初始值(这两个地方指定初始值的执行顺序允许与他们在源代码中的排列顺序相同),再执行构造器里指定的初始值。

 

从某种程度上来看,初始化块是构造器的补充,初始化块总是在构造器执行之前执行。与构造器不同的是,初始化块是一段固定执行的代码,他不能接受任何参数。因此,初始化块对同一个类的所有对象进行的初始化处理完全相同。因此,如果有一段初始化处理代码对所有对象完全相同,且无须接受任何参数,就可以吧这段初始化处理代码提取到初始化块中。

 

实际上初始化块是一个假象,使用javac命令编译Java类后,该Java类中初始化块会消失——初始化块中代码会被“还原”到每个构造器中,且位于构造器所有代码前面。

与构造器类似,创建一个Java对象时,不仅会执行该类普通初始化块和构造器,而且系统会一直上溯到java.lang.Object类的初始化块和构造器,依次向下执行其父类的初始化块和构造器,最后才执行该类的初始化块和构造器,返回该类对象。

 

使用static修饰的初始化块被称为静态初始化块(类初始化块):

静态初始化块是和类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此静态初始化块比普通初始化块先执行。

静态初始化块,也属于类的静态成员,同样需要遵循静态成员不能访问非静态成员(非静态初始化块、实例变量、实例方法)的规则。

 

当JVM第一次主动适应某个类时,系统会在类准备阶段为该类所有静态成员变量分配内存。在初始化阶段则负责初始化这些静态成员变量,初始化静态成员变量就是执行类初始化或者声明类成员变量时指定的初始值,他们的执行顺序与源程序顺序相同。