深入构造器
构造器是一个特殊的方法,用于创建实例时执行初始化。
使用构造器执行
当创建一个对象时,系统为这个对象的实例变量进行默认初始化,这种默认的初始化把所有基本类型的实例变量设为0或false,把所有引用类型的实例变量设为Null。
如果程序员没有为Java类提供任何构造器,则系统会为这个类提供一个无参数的构造器,这个构造器的执行体为空,不做任何事情。无论如何,Java类至少包含一个构造器。
通常把构造器设置为public访问权限,从而允许系统中任何位置的类来创建该类的对象。
构造器重载
同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载,构造器重载允许Java类里包含多个初始化逻辑,从而允许使用不同的构造器来初始化Java对象。
类的继承
继承的特点
Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类。父类与子类的关系,是一种一般和特殊的关系。Java类只能有一个直接父类。
修饰符 class SubClass extends SuperClass
{
//类定义部分
}
java.lang.Object类是所有类的父类,要么是直接父类,要么是其间接父类。因此所有的java对象都可以调用java.lang.Object类定义的实例方法。
重写父类的方法
子类包含与父类同名方法你的现象被称为方法重写(Override),也称为方法覆盖。
方法的重写遵循“两同两小一大”规则
“两同”:方法名、形参列表相同;
“两小”:子类方法返回值类型应比父类返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等。
“一大”:子类方法的访问权限应比父类方法的访问权限更大或相等。
覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个类方法,一个是实例方法。
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。如果需要在子类方法最后调用父类中被覆盖的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。
如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。
super限定
如果需要在子类方法中调用父类被覆盖的实例方法,则可使用super限定来调用父类被覆盖的实例方法。super用于限定该对象调用它从父类继承得到的实例变量或方法。this和super均不能出现在static修饰的方法中。
如果在某个方法中访问名为a的成员变量,但没有显式指定调用者,则系统查找a的顺序为:
查找该方法中是否有名为a的局部变量。
查找当前类中是否包括名为a的成员变量。
查找a的直接父类中是否包含名为a的成员变量,依次上溯a的所有父类,直到java.lang.Object类,如果最终不能找到名为a的成员变量,则系统出现编译错误。
如果被覆盖的是类变量,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量。
当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。
调用父类构造器
在一个构造器中调用另一个重载的构造器使用this来完成,在子类构造器使用super调用来完成。this和super调用构造器必须出现在构造器执行体的第一行。
子类构造器调用父类构造器分如下几种情况:
子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。
子类构造器执行体的第一行代码使用this显式调用本类中重载构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时即会调用父类构造器。
子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
创建任何java对象,最先执行的总是java.lang.Object类的构造器。即,创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果某个父类通过this调用了同类中重载的构造器,就会依次执行此父类的多个构造器。
多态
多态性
Java引用变量有两个类型:编译时类型,运行时类型。
编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态。
BaseClass bc = new BaseClass();
SubClass sc = new SubClass();
BaseClass polymorphicBc = new SubClass();
上面程序显式创建了三个引用变量,对于前两个引用变量bc和sc,它们编译时类型和运行时类型完全相同,因此调用它们的成员变量和方法非常正常,完全没有任何问题。但第三个引用变量polymorphic则比较特殊,它的编译时类型是BaseClass,而运行时类型是SubClass,当调用该引用变量的test()方法(BaseClass类中定义了该方法,子类SubClass覆盖了父类的该方法)时,实际执行的是SubClass类中覆盖后的test()方法,这就可能出现多态了。
因为子类其实是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型(upcasting),向上转型由系统自动完成。
相同类型的变量、调用同一方法时呈现出多种不同的行为特征,这就是多态。
polymorphicBc.sub();这行代码会在编译时引发错误。虽然polymorphicBc引用变量实际上确实包含sub()方法,但因为它的编译时类型为BaseClass,因此编译时无法调用sub方法。
与方法不同的是,对象的实例变量则不具备多态性。比如上面的polymorphicBc引用变量,程序中输出它的book实例变量时,并不是输出SubClass类里定义的实例变量,而是输出BaseClass类的实例变量。
引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。因此,编写Java代码时,引用变量只能调用声明该变量时所用类里包含的方法。例如,通过Object p = new Person()代码定义了一个变量p,则这个p只能调用Object类的方法,而不能调用Person类里定义的方法。
通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量。
引用变量的强制类型转换
编写Java程序时,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法,即使它实际所引用的对象却是包含该方法。如果需要让这个引用变量调用它运行时类型的方法,则必须把它强制类型转换成运行时类型,强制类型转换需要借助于类型转换运算符。
当进行强制类型转换时需要注意:
基本类型之间的转换只能在数值类型之间进行,这里所说的数组类型包括整数性、字符型和浮点型。但数值型和布尔类型直接不能进行类型转换。
-
引用类型直接的转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,则无法进行类型转换,否则编译时就会出现错误。如果试图把一个父类实例转换成子类实例,则这个对象必须实际上是子类实例才行(即编译时类型为福类型,而运行时类型是子类类型),否则将在运行时引发ClassCastException异常。
进行强制类型转换时可能出现异常,因此进行类型转换之前应先通过instanceof运算符来判断是否可以成功转换,从而避免出现ClassCastException异常,这样可以保证程序更加健壮。
instanceof运算符
instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是返回true,否则返回false。
instanceof运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。
instanceof和(type)是Java提供的两个相关的运算符,通常先用instanceof判断一个对象是否可以强制类型转换,然后再使用(type)运算符进行强制类型转换,从而保证程序不会出现错误。
继承与组合
使用继承的注意点
为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则。
尽量隐藏父类的内部结构。尽量把父类的所有变量都设置成private访问类型,不要让子类直接访问父类的成员变量。
不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符(该修饰符后面会有更详细的介绍)来修饰该方法。如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。
尽量不要在父类构造器中调用将要被子类重写的方法。 将引发空指针异常。
如果想把某些类设置成最终类,既不能被当成父类,则可以使用final修饰这个类;使用private修饰这个类的所有构造器,从而保证子类无法调用该类的构造器,也就无法继承该类的实例。
何时需要从父类派生新的子类:
子类需要额外增加属性,而不仅仅是属性值的改变。
子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。
利用组合实现复用
对于继承而已,子类可以直接获得父类的public方法,程序使用子类时,将可以直接访问该子类从父类那里继承到的方法;而组合则是把旧类对象作为新类的成员变量组合进来,用以实现新类的功能,用户看到的是新类的方法,而不能看到被组合对象的方法。
继承要表达的是一种“是(is-a)”的关系,而组合表达的是“有(has-a)”的关系。
初始化块
使用初始化块
初始化块是Java类里可出现的第4种成员(前面依次有成员变量、方法和构造器),一个类里可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行,后面定义的初始化块后执行。
[修饰符]
{
//初始化块的可执行性代码
}
初始化块的修饰符只能是static,使用static修饰的初始化块被称为静态初始化块。初始化块里的代码可以包含任何可执行性语句,包括定义局部变量、调用其他对象的方法,以及使用分支、循环语句等。
当创建Java对象时,系统总是先调用该类里定义的初始化块,如果一个类里定义了2个普通初始化块,则前面定义的初始化块先执行,后面定义的初始化块后执行。初始化块只在创建Java对象时隐式执行,而且在执行构造器之前执行。
初始化块和构造器
与构造器不同的是,初始化块是一段固定执行的代码,它不能接收任何参数。
静态初始化块
如果定义初始化块时使用了static修饰符,则这个初始化块就变成了静态初始化块,也被称为类初始化块(普通初始化块负责对对象执行初始化,类初始化块则负责对类进行初始化)。静态初始化块是类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此静态初始化块总是比普通初始化块先执行。
静态初始化块也被称为类初始化块,也属于类的静态成员,同样需要遵循静态成员不能访问非静态成员的规则,因此静态初始化块不能访问非静态成员,包括不能访问实例变量和实例方法。
系统在类初始化阶段执行静态初始化块时,不仅会执行本类的静态初始化块,而且还会一直上溯到java.lang.Object类(如果它包含静态初始化块),先执行java.lang.Object类的静态初始化块(如果有),然后执行其父类的静态初始化块······最后才执行该类的静态初始化块。
系统在创建一个Java对象时,不仅会执行该类的普通初始化块和构造器,而且系统会一直上溯到java.lang.Object类,先执行java.lang.Object类的初始化块,开始执行java.lang.Object的构造器,一次向下执行其父类的初始化块,开始执行其父类的构造器······最后才执行该类的初始化块和构造器,返回该类的对象。
当JVM第一次主动使用某个类时,系统会在类准备阶段为该类的所有静态成员变量分配内存;在初始化阶段则负责初始化这些静态成员变量,初始化静态成员变量就是执行类初始化代码或者声明类成员变量时指定的初始值,它们的执行顺序与源代码中的排列顺序相同。