Java基础(四)——面向对象(上)

类可被认为是一种自定义的数据类型,可以使用类定义变量,所有使用类定义的变量都是引用变量,它们将会引用到类的对象。类用于描述客观世界里某一对象的共同特征,而对象则是类的具体存在,java程序使用类的构造器来创建该类的对象。
Java支持面向对象的三大特征:封装(encapsulation)、继承(extends,更准确地说,子类是父类的扩展)、多态(polymorphism,有继承关系的两类型,相同引用变量因指向的执行对象的类型不同,调用方法时表现出不同的特征)。Java提供了private、protected、public三个访问控制修饰符来实现良好的封装,提供了extends关键字来让子类继承父类,是实现类复用的重要手段。但若父类有的方法不适合子类时,子类需要对父类方法进行重写(override,或覆盖)。使用继承关系来实现复用时,(多态)子类对象可以直接赋给父类变量,这个变量具有多态性,编程更加灵活。
构造器用于对类实例(对象)进行初始化操作,构造器支持重载(overload),如果多个重载的构造器里包含了相同的初始化代码,则可以把这些初始化代码放在普通初始化块中完成,初始化块总在构造器执行之前被调用。除此之外,java还提供一种静态初始化块,用于初始化类,在类初始化阶段被执行。如果继承树里的某一个类需要被初始化时,系统将会同时初始化该类的所有父类。
类(class),对象(Object,也被称为实例(Instance)),属性(property)
new是构造器创建对象的过程。

定义类

1
2
3
4
5
6
[修饰符] class 类名
{
零到多个构造器定义…
零到多个成员变量…
零到多个方法…
}

修饰可以是public、final、abstract,或完全省略修饰符。类定义中三个常见的成员:构造器、成员变量、方法。
构造器:构造该类的实例,java语言通过new来调用构造器,从而返回该类的实例。构造器是一个类创建对象的根本途径,如果没有构造器,这个类通常无法创建实例,如果没有手动编写类的构造器,系统会为该类提供一个默认的构造器;
成员变量:定义该类或该类的实例所包含的状态数据;
方法:定义该类或该类的实例的行为特征或功能实现。
类里各成员间可以相互调用,但static修饰的成员不能访问没有static修饰的成员。

定义成员变量和局部变量

所有变量分为成员变量和局部变量,而成员变量又可分类变量(静态变量)和实例变量(非静态变量),局部变量又可分形参、方法局部变量和代码块局部变量。
变量命名:第一个单词首字母小写,后面单词首字母大写。
成员变量的定义:

1
2
[修饰符] 类型 成员变量名 [=默认值]
//修饰符可以省略,可以是publicprotectedprivate,static,final,其中publicprotectedprivate最多使用一个,可以与static、final组合。

如果一个方法没有返回值,则必须使用void来声明。
一旦定义方法时定义了形参列表,则调用该方法时必须传入对应的参数值,即谁调用方法,谁负责为形参赋值。
成员变量(field),属性(property)。如果某个类有age属性,意味着该类包含setAge()和getAge()两个方法。
类.类变量,实例.实例变量,(实例.类变量:也可修改类变量的值,为了程序可读性,建议不要这样访问类变量。)
Java允许成员变量和局部变量同名,但在方法里局部变量会覆盖(override)成员变量,如果要在该方法里引用被覆盖的成员变量,可以使用类.成员变量(对于static修饰的类变量)或this.成员变量(对于没有static修饰的实例变量)
注意一般情况下应避免成员变量和局部变量同名。

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

Person p1 = new Person(); //首次使用Person类,加载这个类并初始化这个类。在类的准备阶段,系统将为该类的类变量分配内存空间,并指定默认初始值(int类型初值为0)。创建Person对象,并将p1指向Person对象,并默认为Person对象中的实例变量赋初值(String类型初值为null)。
可以看出static修饰的局部变量eyeNum属于类,没有static修饰的局部变量name属于对象。

1
2
3
4
Person p2 = new Person();//代码创建第二个对象,不需要再对Person类进行初始化。
p1.name = "张三";//为p1的name实例变量赋值
p1.eyeNum = 3;//通过Person对象来修改Person的类变量。注意到Person对象根本没保存eyeNum这个变量,通过Person对象来访问Person的类变量,实际还是通过Person类来访问Person的类变量。
p2.eyeNum = 4; //通过实例访问类变量,访问的是同一块内存eyeNum,对3进行了修改

因此访问类变量时,使用类作为主调,尽量避免使用对象作为主调。

局部变量的初始化和内存中的运行机制:
局部变量必须经过显式初始化才能使用,系统不会为局部变量执行初始化。即局部变量定义后,系统不会为其分配内存空间,需要等程序为这个变量赋初值时,系统才会为局部变量分配内存。
创建成员变量的实例时,在堆中将分配内存存储这些变量的实例
与成员变量不同,局部变量不属于任何类或实例,它总是保存在其所在方法的内存栈中。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果是引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。
内存栈中的变量无需系统垃圾回收,往往随方法或代码块运行的结束而结束。局部变量只保存基本类型或对象(或数组)的引用,因此占用的内存区通常比较小。

变量的使用规则

定义一个成员变量,成员变量将被放置到堆内存中,成员变量的作用域将扩大到类存在范围或对象存在范围。这种范围扩大有两个坏处:
增大了变量的生存时间,导致更大的内存开销;增大了变量的作用域,不利于提高程序的内聚性。

定义方法

1
2
3
4
5
[修饰符] 方法返回值类型 方法名(形参列表)
{
// 零到多条可执行语句组成的方法体
}
//修饰符可以省略,可以是publicprotectedprivate,static,finalabstract,其中publicprotectedprivate最多使用一个,finalabstract最多使用一个,可以与static组合。

如果声明了方法的返回值,则方法体内必须有一个有效的return语句;如果方法没有返回值,则必须使用void声明。

static

可用于修饰成员变量、方法等。把static修饰的成员变量和方法称为类变量(静态变量)、类方法(静态方法)。static的真正作用是区分成员变量、方法、内部类、初始化块这四种成员到底是属于类本身还是属于实例。static修饰的成员表明它属于这个类本身,不属于该类的单个实例;没有static修饰的成员属于该类的实例,没有static修饰的成员变量和方法称为实例变量、实例方法。
构造器是一种特殊的方法,与定义方法的语法格式很像:

1
2
3
4
[修饰符] 构造器名(形参){
...
}
//修饰符可以省略,也可是publicprotectedprivate其中一个。

构造器

构造器(constructor)名必须与类名相同(此处造成我之前对实例化理解的误区,因为之前用的构造器都是默认的构造器,未手动编写)。构造器:构造该类的实例,java语言通过new来调用构造器,从而返回该类的实例。构造器是一个类创建对象的根本途径,如果没有构造器,这个类通常无法创建实例,如果没有手动编写类的构造器,系统会为该类提供一个默认的构造器。构造器的最大作用是创建对象时执行初始化,一般用来给类中的实例变量赋初值,或执行创建一个完整形式的对象所需要的启动步骤。构造函数可以无形参,也可有形参。
因为构造器通常用于被其它方法调用,用于返回该类的实例,因此通常把构造器设置成public权限
构造器既不能定义返回值类型,也不能用void声明没有返回值。(不然会被java当做方法来处理,因为构造器和方法的语法格式比较相似)。其实构造器的返回值是隐式的,当使用new关键字调用构造器时,构造器返回该类的实例,所以构造器的返回值类型总是当前类,无需再定义
当没有为类提供任何构造器,系统默认为这个类提供一个无参构造器,构造器的执行体为空。Java类至少包含一个构造器。

构造器是创建对象的重要途径,但这个对象并不完全由构造器负责创建。当调用构造器时,系统会为该对象分配内存空间,并为这个对象执行默认的初始化,只是这个对象还不能被外部访问。当执行构造器的执行体时,通过this来引用。当构造器执行体结束后,这个对象(实例)作为构造器的返回值被返回,通常赋给一个引用类型的变量,从而让外部程序可以访问该对象。

构造器重载(overload)

重载Overloading是一个类中多态性的一种表现。
(1) 方法重载是让类以统一的方式处理不同类型数据的一种手段。多个同名函数同时存在,具有不同的参数个数/类型。
(2) Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义。
调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法, 这就是多态性。
(3) 重载的时候,方法名要一样,但是参数类型和个数不一样,返回值类型可以相同也可以不相同。所以无法以返回类型作为重载函数的区分标准
如果一个类中包含了多个构造器,多个构造器的形参列表不同,就形成了构造器的重载,允许使用不同的构造器初始化java对象。
多个构造器中,如果一个构造器B想要调用另一个构造器A里的变量,但构造器不能被直接访问,必须使用new关键字来调用,但使用new关键字又会新建一个对象,为了不新建一个对象,同一个类中使用this关键字来调用相应的构造器中的变量
在构造器中使用this调用给另一个重载的构造器,必须作为构造器执行体的第一条语句。

创建对象

(即实例化)的根本途径是构造器,使用new关键字来调用某个类的构造器即可创建这个类的实例。没有构造器系统将为他提供一个默认构造器,(此处导致我总以为实例化时对类实例,其实实例化是对构造器实例,而构造器是与类同名的无返回值的特殊方法)默认构造器总是没有参数的。

1
2
3
Person p; //定义一个Person类型的变量p
//通过new关键字调用Person类的构造器,返回一个Person类实例(即在内存中创建实例(对象),但还不能被外界访问);将该实例赋给p变量(此时对象能够被外界访问)。
p = new Person();

简写为:

1
Person p = new Person(); //定义p变量并为p赋值

类或实例访问变量或方法的语法是:类.类型量/类方法;实例.实例变量/实例方法。
有static修饰的成员既可以用类调用,也可以用实例调用(尽量不要这样作,容易混淆);没有static修饰的成员只能有用实例(对象)调用。
大部分的时候,定义一个类是为了重复创建该类的实例,类定义了多个实例的共同特征。因此类不是具体存在,实例(对象)才是具体存在。
创建Person对象时,必然有内存来存储Person对象的实例变量。当一个对象创建以后,这个对象将保存在堆内存中。Java中不允许直接访问堆内存中的对象(或数组),只能通过该对象的引用操作该对象(数组)。
如果希望垃圾回收机制回收某个对象(或数组),只需要切断该对象(或数组)的所有引用变量和它之间的关系,即将引用变量赋值为null。

this关键字

this总是指向调用该方法的对象。
this可以代表任何关键字,当this出现在某个方法体中时,它所代表的对象是不确定的,但它只能代表当前类的实例;只有当这个方法被调用时,它所代表的对象才能确定下来:谁在调用这个方法,this就代表谁。
this最大的作用是:让类中的一个方法,访问该类里的另一个方法或实例变量(常用于构造器中)。大部分的时候,一个方法访问该类中的其它成员变量或方法时不加this前缀的效果是完全一样的,但省略this前缀只是一种假象,实际上这个this依然存在。一般来说,如果调用static成员(包括成员变量、方法),默认使用该类作为主调;如果调用非static成员,默认使用this作为主调。
对于static修饰的方法,可以使用类直接调用该方法,注意不能使用this来引用,static成员不能访问非static成员。如果确实需要在静态方法中访问另一个普通方法,需要重新创建一个对象。
大多数情况下,普通方法访问其他方法、成员变量时无需使用this前缀,但方法里有个变量(局部变量)和成员变量同名,但程序又需要在这个方法里访问被覆盖的成员变量,就需要使用this前缀。

1
2
3
4
5
6
7
8
9
public class ThisInConstructor{
public int foo;
public ThisInConstructor(){
int foo;//方法中的变量(局部变量)
//与要访问的(即非static的成员变量)变量foo同名了
this.foo = 6; //this表示调用该方法的对象。
//对调用该方法的对象的成员变量赋值,即ThisInConstructor中的成员变量foo.
}
}

方法的参数传递:值传递

Java中的方法都必须定义在类中。如果这个方法使用了static修饰,则该方法属于类,没有static修饰的方法属于该类的对象,不属于该类本身。所有的方法必须使用“类.方法”或“对象.方法”来调用。同一个类的一个方法调用另一个方法时,如果被调用的是static方法,则默认使用类作为主调者;如果被调用的是普通方法,则默认使用this作为主调者。
如果声明方法时包含了形参声明,则调用方法时必须给这些形参指定参数值,调用方法时实际传给形参的参数值称为实参。
Java方法中的参数传递方式只用一种:值传递。即将实际参数的副本(复制品)传入方法内,而参数本身不会受影响。

程序从main()方法开始执行,main()方法创建了一个DateWrap对象,并用dw引用变量来指向DataWrap对象。接着程序通过引用来操作DataWrap对象,把该对象的a、b两个成员变量分别赋值为5、9。
接下来main()方法中开始调用swap()方法。系统分别为main()方法和swap()方法开辟出两个栈区,用于存放main()和swap()方法的局部变量。调用swap()方法时,dw变量作为实参传入swap()方法,采用值传递:把main()方法中dw变量的值赋给swap()方法里的形参,从而完成swap()方法里dw形参的初始化。注意,main()方法中存的dw是DataWrap对象的引用,将dw传入swap()方法,dw也是指向DataWrap对象的引用。

因此当swap()方法中交换dw参数所引用的DataWrap对象的两个成员变量a、b的值后,main()方法中dw变量所引用的DataWrap对象的两个成员变量a、b的值也换了。
总结:值传递传递的是副本;而引用类型传递传递的是地址,因此可以操作地址所指的同一对象

方法的形参个数可变(varargs)

允许为方法指定数量不确定的形参,在最后一个形参类型的最后增加三个点:… ,多个参数值被当成数组传入。
个数可变的形参实质是数组类型的形参。调用该方法时,既可以传入多个形参,也可以传入一个数组。
(1)以可变个数形参来定义方法

1
2
3
4
5
Public static void test(int a, String… books)
{

}
//调用方法,实参无需是数组

test(5, “形参”, “个数可变”)
(2)以数组形参来定义方法

1
2
3
4
5
6
7
Public static void test(int a, String[] books)
{

}
//调用方法,传入的实参必须为数组

test(5, new String[]{“形参”, “个数可变”});

递归(recursive)方法

一个方法体内调用它自身。注意在某个时刻方法的返回值是确定的,即不再调用它自身,否则这种递归将变成无穷递归,陷入死循环。
递归一定要向已知方法递归。如对 ,求 :若已知 , ,则采用 的形式,因为小的一端已知;若已知 , ,则采用 的形式,因为大的一端已知。
递归:如 , , ,代码如下:

方法重载(overload)

方法重载:Java允许同一个类中定义多个同名方法,只要形参列表不同就行。即同一类中方法名相同,参数列表不同。返回值可以相同,也可以不同。因此不同用返回值来判断方法是否重载。
注意不推荐重载形参个数可变的方法,会使程序可读性变差。

封装(encapsulation)

前面的通过对象直接访问成员变量可能会存在某些问题,如将某个Person类的age成员变量直接设为1000,这显然是不实际的。
封装:将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部的信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
优点:让使用者只能通过预定义的方法来访问数据,从而可以在该方法里添加控制逻辑,限制成员变量的不合理访问。把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。

访问控制符

Java提供三种访问控制符,即四种访问控制方式:private、default、protected、public

访问控制方式 访问权限
private 当前类访问权限。用于修饰成员变量最合适,将成员变量隐藏在该类的内部。
default 包访问权限。默认即什么访问控制符都不加,被default修饰的成员或外部类只能被相同包下的其它类访问。
protected 子类访问权限。被protected修饰的成员既可以被同一包内的其它类访问,也可以被不同包的子类访问。通常情况下,如果使用protected来修饰一个方法,通常是希望其子类来重写这个方法。
public 公共访问权限。可以被所有类访问,不管是不是在同一个包中,不管是否具有父子继承关系。

访问控制符是用于控制一个类的成员是否能被其它类访问,而对于局部变量而言,其作用域就是其所在的方法,不可能被其它类访问,因此不能使用访问控制符来修饰
对外部类来说,只由default和public两种控制级别。因为外部类没有内部类,没有所在类的内部、所在类的子类两个范围,private和protected对外部类没有意义。
如果一个java文件中所有类都没有使用public修饰,则源文件名可以是一切合法字符(即可以文件名与类名不一致);如果一个java文件中定义了一个public类,则源文件名必须使public修饰的类名(即源文件名必须与类名相同)。

该类的成员变量name和age只能在类内部进行操作和访问,person类之外只能通过各自对应的setter和getter方法(如getName()、setName()、getAge()、setAge())来操作和访问它们。(即将原实例变量首字母大写,并在前面分别加set和get,就变成了setter和getter方法名。)
使用setter方法允许增加自己的控制逻辑,从而保证Person对象的两个实例变量不会出现与实际不符合的情况。
访问控制符使用的几个基本原则:

  • 类里大部分成员变量都应使用private修饰,只有一些public、全局变量的成员变量才考虑用public修饰。有些方法只用于辅助实现该类的其它方法,这些方法称为工具方法,也用private修饰。
  • 如果某个类主要用作其它类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界使用,则应当用protected修饰这些方法。
  • 希望暴露出来供其他类自由调用的方法应该使用public修饰。因此类的构造器通过public修饰,允许其在其它地方创建该类的实例。因为外部类通常希望被其它类使用,所以用public修饰。

包(package)

处理类的重名,引入了包(package)机制,它提供类的多层命名空间,用于解决类的命名冲突、类文件管理等问题。为了避免不同公司类名的重复,Oracle公司建议使用公司Internet域名倒写来作为包名。如公司域名为123.com,则建议类放在com.123包或其子包下。实际开发中,还会在com.123包以下建立子包:com.123—项目名(子包)—模块(子包)—组件(子包)—…
Java将一组功能相关的类放在同一package下,形成逻辑上的类库单元。在java中添加包,需要在源文件中指定,而不是单纯地将类文件放在包文件夹下。还要保证class文件在对应的路径下。建议如图存放项目文件:

同一包的不同类可以放在计算机上不同位置,只要在CLASSPATH中指定了相应的路径,进行搜索,就能找到这些.class文件。

如果希望将某个类放在指定的包结构下,应在java源文件第一行声明:
package packageName;
一个源文件只能指定一个package文件,该文件中的全部类将位于该包下。
同一包下的类可以自由访问,而无需添加包前缀。不同包下使用类需要import其它包的完整路径(包的全名)。即使是父包和子包,它们在用法上不存在任何关系,仍需使用包的全名。
如果创建其它包下的类的实例,则在调用构造器时也应当使用包前缀。如在com.456类中使用com.123:com.123 a = new com.456();但这种写法太复杂,下面用import关键字来导入其它包,然后使用其它包下的类的实例。

import关键字

为了简化引入了import关键字,用在package语句之后、类定义之前。

1
2
import package.subpackage.... className;
import package.subpackage.... *;//*代表导入包下所有类,不能代表包。(一般不要这么干)

静态导入:导入某个类的静态成员变量、方法或某个类的全部静态成员变量、方法。

1
2
import static package.subpackage....  className.fielName|methodName;
import static package.subpackage.... className.*;

类的继承(extends)

继承是实现软复用的重要手段。Java的继承具有单继承的特点,即每个子类只有一个直接父类。
Java的继承通过extends关键字来实现,实现继承的类称为子类,被继承的类称为父类。(有的也称为基类、超类)。因为子类是一种特殊的父类,所以父类的范围总是比子类大得多,可以认为父类是大类,子类是小类。
但extends的意思是扩展,即子类与父类的关系是:子类是对父类的扩展(extends),父类派生(derive)出了子类。子类能获得父类的全部成员变量和方法,但Java的子类不能继承父类的构造器(但可以通过super关键字调用)
继承的语法格式如下:

1
2
3
修饰符 class SubClass extends SuperClass{
//类定义部分
}

只是在原来的类定义后面加了extends SuperClass。

Apple1类为空类,只有mian()方法,但创建了Apple1对象后,引用变量a可以指向Apple1对象的weight和info(),子类Apple1类继承了父类Fruit类的成员变量和方法。
class Fruit extends Plant{…} // 间接继承
class Apple extends Plant{…}
如果java中没有显示定义这个类的直接父类,则默认父类为java.lang.Object类。因此java.lang.Object类是所有类的父类,要么是直接父类,要么是间接父类。因此所有对象都可以调用java.lang.Object类定义的实例方法。
重写(override,或覆盖)父类方法
大部分情况下,子类总是以父类为基础,额外增加新的成员变量和方法。但有一种例外,如所有鸟类都有飞翔方法,鸵鸟是鸟类的子类,但鸵鸟不能飞,父类的方法不适合子类,这就需要重写鸟类方法。

方法重写(override)或方法覆盖

方法重写(override)或方法覆盖:子类包含与父类同名方法。
方法重写要遵循“两同两小一大”规则,即方法的方法名、形参列表要相同;子类的返回值类型应 父类的返回值类型,子类方法声明抛出的异常类 父类方法声明抛出的异常类;子类方法的访问权限 父类方法的访问权限
覆盖方法和被覆盖方法,要么都是类方法(用static修饰),要么都是实例方法(没有static修饰),两者必须相同。如果要在子类中访问被覆盖的父类方法,可以使用父类类名.方法(如果是类方法(用static修饰的))或super.方法(如果是实例方法(没有static修饰的))
如果父类方法具有private访问权限,则对其子类依然是隐藏的,子类无法访问,即使在自乐中定义的相同的方法,也是重新定义了一个新方法,不是方法重写,是无法访问父类方法的。
如果子类变量具有private访问权限,则不能通过对象直接访问子类变量,(总之就是不能访问了,只能在类里调用),但此时可以访问父类变量(若父类变量此时是可能访问的(如public的)),则可以通过强制将子类实例变量转换为公有的父类实例变量,从而访问:

((Parent)d).tag)强制将私有的子类实例变量转换为公有的父类实例变量。
注意重载(overload):同一个类的多个同名方法之间;重写(override):发生在子类与父类之间,重写导致了多态。

super限定

当创建子类对象(实例)时,系统不仅会为子类实例变量分配内存,还会为父类实例变量分配内存,即使子类中定义了与父类同名的实例变量。为了访问被覆盖的同名实例变量或实例方法,使用super.实例变量/方法访问

super用于限定该对象调用它从父类继承得到的实例变量和方法,如果子类定义了和父类同名的实例变量,则子类会隐藏父类实例变量,需要用super来访问被隐藏的父类实例变量。

因为不能在static(如main()方法是static方法)中直接使用super(static成员不能访问非static成员),即不能直接调用父类的a,如sc.super.a,所以在子类中定义方法来访问父类的a。
创建SubClass对象时会为其分配两块内存,一块用于存SubClass类中定义的实例变量a,一块用于存从BaseClass类中继承的实例变量a。
如果被覆盖的是类变量,则使用父类名.类变量可以访问被覆盖的类变量。

调用父类构造器

子类继承不会获得父类的构造器,但类似于构造器重载中,一个构造器可以在第一行通过this调用另一个构造器,在子类构造中使用super在第一行调用父类构造器。

  • 子类构造器执行体第一行使用super显示调用父类构造器时,系统将根据super调用里传入的实参列表调用父类对应的构造器;
  • 子类构造器执行体第一行使用this显示调用本类中重载的构造器时,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中的另一个构造器即会调用父类构造器;
  • 子类构造器中既没有super,也没有this时,系统会在执行子类构造器之前,隐式地调用父类无参构造器。

调用子类构造器来初始化对象时,父类构造器总会在子类构造器之前执行,一直往上推,即最先执行的是java.lang.Object类构造器。

this最大的作用是:让类中的一个方法,访问该类里的另一个方法或实例变量。一般用于访问覆盖的同名变量,若访问的是static成员,则使用类作为主调;若访问的是非static成员,则使用this作为主调。若没有同名,则不需要加前缀(不过一般构造器执行对象初始化,类中还是有同名的)。

多态(polymorphism)

多态(polymorphism)现象的直接原因:子类对父类方法的重写(override)
Java引用变量有两个类型:一种是编译时类型,一种是运行时类型。编译时类型由声明该变量时使用的类型决定;运行时类型由实际赋值给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态。
BaseClass a = new SubClass();
编译类型是BaseClass(父类),运行类型是SubClass(子类)
子类是一种特殊的父类,允许将子类对象直接赋给父类引用变量,而无需任何类型转换,称为向上转型(upcasting);而将父类对象赋给子类引用变量时,需要强制类型转换。

1
2
3
4
5
BaseClass bc = new SubClass();
BaseClass a = “Hello”;
if( a instanceof SubClass){
SubClass sc = (SubClass) a;
}

多态:相同类型的引用变量,调用同一方法时表现出的多种不同的行为特征(即多态表现在方法上,而不表现在变量上)。当运行时调用该父类引用变量时,其方法总是表现出子类方法的特征。(将子类对象赋给父类引用对象,父类引用变量调用同名的被子类覆盖的方法时出现的现象。)而实例变量则不存在多态特征,因为通过引用变量访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量

引用变量的强制类型转换

引用变量(即定义的父类变量的类型)只能调用它编译时(即赋值的子类变量的类型)类型的变量,若要调用它运行时类型的变量(即向下转型:父—>子),则需要对引用变量进行强制类型转换
注意强制转换符用法:基本类型只能在数值类型(如整型、字符型、浮点型)间转换,如整型不能和布尔类型转换。引用类型的转换只能在具有继承关系(即父类与子类间)的两类型间转换。考虑到强制类型转换可能出现异常,通常先用instanceof运算符来判断是否可以转换成功。

instanceof运算符

语法:引用类型变量 instanceof 类、接口 // 判断前一个引用变量是否是后面的类,或其子类、实现类的实例,若是则返回true,否则返回false
注意引用类型必须与后面类类型相同或有继承关系,否则编译出错。

1
2
3
4
5
6
7
8
Object obj = “Hello”; // obj引用变量在编译时时Object类,实际运行时是String类。
if(obj instanceof String){ //父类类型引用变量 -> 子类类型
//将Object类类型(父类)引用变量转换为String类型(子类)(Object类(父类)对象 -> String类(子类))
String Obj2Str = (String) obj;
}
if(obj instanceof Comparable){// 字符串是Comparable接口的实例
//…
}

继承中子类可以直接访问父类的成员变量和方法,这破坏了父类的封装性,造成子类和父类严重耦合(如前面的方法重写,还改写了父类的方法,Bird,Ostrich)。每个类都应该封装它内部信息和实现细节,只暴露必要的方法给其它类使用。
设计父类应遵循的规则:

  • 尽量隐藏父类的成员变量,设为private(当前类访问权限),不要让子类直接访问父类成员变量。
  • 不要让子类可以随意修改父类方法。
    父类中那些辅助其他工具的方法,应使用private修饰,让子类无法访问该方法;
    父类中那些需要被外部调用的方法,必须使用public修饰,而为了不让子类重写该方法,使用final(将类设置成最终方法,不能被子类重写)修饰;
    父类中希望被子类重写的方法,而不让其他类访问,使用protected(子类访问权限)修饰。
  • 尽量不要在父类构造器中调用将要被子类重写的方法。(当系统创建子类对象时,会先执行父类构造器,当父类构造器中调用了被子类重写的方法时,则变成调用被子类重写的方法。)
    何时从父类派生出子类:
  • 子类需要额外增加属性,而不仅仅是属性值的改变;(新的成员变量)
  • 子类需要增加自己独有的行为方式。(新的方法)

组合

复用一个类,可以用继承:对已有的类进行改造,从而创建一个特殊的版本(如Person类 -> Student类(子类是一种父类));可以用组合:两个类之间有明确的整体、部分关系(如Person类 –> Arm类(父组合有子组合))。继承要表达的是is-a,组合要表达的是has-a

初始化块

Java使用构造器来对单个对象进行初始化操作,完成对java对象的状态初始化,然后将java对象返回给程序,从而让该对象的信息更加完整。初始化块与构造器的作用非常类似。
与构造器不同的是,初始化块是一段固定代码,他不能接收任何参数。则如果有一段处理代码对所有对象都相同,且无须接收任何参数,则可提取到初始化块中。初始化块是一种假象,编译java程序时,它们会被“还原”到构造器中。
注意与构造器相似,都会上溯到java.lang.Object类,然后根据父类一级一级地进行初始化。
分静态初始化块(用static修饰,类初始化块,负责对类进行初始化)和普通初始化块(没有static修饰,负责对实例(对象)进行初始化)。语法如下:

1
2
3
static {  // 静态(类)初始化块
//…
}

或块

1
2
3
{ //初始化
//…
}

当创建一个对象,若没加载,则从顶级父类加载静态初始化块,若类已经被加载过,系统先为所有的实例变量分配内存,开始对这些实例变量初始化,初始化顺序是:先执行初始化块中或声明实例变量时指定的初值值(普通初始化块),再执行构造器中指定的初始值。

静态初始化块

静态初始化块是类相关的,对整个类进行初始化,在类初始化阶段进行初始化,因此静态初始化块总比普通初始化块先执行;初始化块是与对象相关的,对实例进行初始化。
未加载类:从顶级父类执行静态初始化块;
加载过后进行初始化,从顶级父类执行普通初始化块,构造器,然后子类普通初始化块,构造器。

第一次创建一个Leaf对象时,因为系统中还没有Leaf类,因此需加载并初始化(编译时)Leaf类,初始化Leaf类需从顶层父类到Leaf类的静态初始化块依次执行。
(一旦Leaf类初始化成功后,它会在虚拟机中一直存在,因此在第二次对Leaf类实例化(运行时)时,无需再对Leaf类初始化。)
每创建一个Leaf类对象,都需要先执行顶层父类的初始化块、构造器,然后执行直接父类的初始化块、构造器…最后才执行Leaf类的初始化块,构造器。