疯狂Java讲义(五)----第二部分

1.调用父类构造器

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

        从上面程序中不难看出,使用super调用和使用this 调用也很像,区别在于super调用的是其父类的构造器,而this调用的是同一个类中重载的构造器。因此,使用super调用父类构造器也必须出现在子类构造器执行体的第一行所以this 调用和 super 调用不会同时出现(这里指的是的是调用构造启动情况)

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

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

不管上面哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器……依此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器

 如果ClassB显式调用ClassA 的构造器,而该构造器又调用了ClassA类中重载的构造器,则会看到ClassA两个构造器先后执行的情形。

 2.多态

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

 

         因为子类其实是一种特殊的父类,因此 Java 允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型(upcasting),向上转型由系统自动完成。
        当把一个子类对象直接赋给父类引用变量时,例如上面的BaseClass ploymophicBc =new SubClass();,,这个ploymophicBc引用变量的编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。

        上面的main()方法中注释了ploymophicBc.sub();,这行代码会在编译时引发错误。虽然ploymophicBc引用变量实际上确实包含 sub()方法(例如,可以通过反射来执行该方法),但因为它的编译时类型为BaseClass,因此编译时无法调用sub()方法。

 3.instanceof运算符

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

        上面程序通过Object hello = "Hello";代码定义了一个hello变量,这个变量的编译时类型是Object类,但实际类型是String。因为Object类是所有类、接口的父类,因此可以执行hello instanceof String和 hello instanceof Math等。
        但如果使用String a = "Hello";代码定义的变量a,就不能执行 a instanceof Math,因为a的编译类型是String,String类型既不是Math类型,也不是Math类型的父类,所以这行代码编译就会出错。
        instanceof运算符的作用是:在进行强制类型转换之前,首先判断前一个对象是否是后一个类的实例,是否可以成功转换,从而保证代码更加健壮。
        instanceof和(type)是Java提供的两个相关的运算符,通常先用instanceof判断一个对象是否可以强制类型转换,然后再使用(type)运算符进行强制类型转换,从而保证程序不会出现错误。

4.继承与组合

(1)利用继承实现复用:

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

使用继承的注意点:

         当系统试图创建Sub对象时,同样会先执行其父类构造器如果父类构造器调用了被其子类重写的方法,则变成调用被子类重写后的方法。当创建Sub对象时,会先执行Base类中的Base构造器,而Base构造器中调用了test()方法——并不是调用①号test()方法,而是调用②号test()方法,此时Sub对象的name 实例变量是null,因此将引发空指针异常
        如果想把某些类设置成最终类,即不能被当成父类,则可以使用final修饰这个类,例如JDK提供的java.lang.String类和java.lang.System类。除此之外,使用private修饰这个类的所有构造器,从而保证子类无法调用该类的构造器,也就无法继承该类。对于把所有的构造器都使用private 修饰的父类而言,可另外提供一个静态方法,用于创建该类的实例。

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

  • 子类需要额外增加属性,而不仅仅是属性值的改变。例如从Person类派生出Student子类, Person类里没有提供 grade(年级)属性,而Student类需要grade属性来保存Student对象就读的年级,这种父类到子类的派生,就符合Java继承的前提。
  • 子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。例如从Person 类派生出Teacher类,其中Teacher类需要增加一个teaching()方法,该方法用于描述Teacher对象独有的行为方式:教学。

(2)利用组合实现复用:

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

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

 

         对于上面定义的三个类:Animal、Wolf和Bird,它们对应的UML图如图5.20所示。
        从图5.20中可以看出,此时的Wolf 对象和Bird对象由Animal 对象组合而成,因此在上面程序中创建Wolf对象和 Bird对象之前先创建Animal对象,并利用这个Animal对象来创建Wolf对象和 Bird对象。运行该程序时,可以看到与前面程序相同的执行结果。

        到底该用继承?还是该用组合呢?继承是对已有的类做一番改造,以此获得一个特殊的版本。简而言之,就是将一个较为抽象的类改造成能适用于某些特定需求的类。因此,对于上面的Wolf和 Animal的关系,使用继承更能表达其现实意义。用一个动物来合成一匹狼毫无意义:狼并不是由动物组成的。反之,如果两个类之间有明确的整体、部分的关系,例如Person类需要复用Arm类的方法(Person对象由Arm对象组合而成),此时就应该采用组合关系来实现复用,把Arm作为Person类的组合成员变量,借助于Arm的方法来实现Person的方法,这是一个不错的选择
        总之,继承要表达的是一种“是(is-a)”的关系,而组合表达的是“有 ( has-a)”的关系。

5.使用初始化块

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

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

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

 

 从图5.21中可以看出,如果两个构造器中有相同的初始化代码,且这些初始化代码无须接收参数,就可以把它们放在初始化块中定义。通过把多个构造器中的相同代码提取到初始化块中定义,能更好地提高初始化代码的复用,提高整个应用的可维护性。

 6.静态初始化块

        如果定义初始化块时使用了static修饰符,则这个初始化块就变成了静态初始化块,也被称为类初始化块(普通初始化块负责对对象执行初始化,类初始化块则负责对类进行初始化)。静态初始化块是类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行因此静态初始化块总是比普通初始化块先执行。
        静态初始化块是类相关的,用于对整个类进行初始化处理,通常用于对类变量执行初始化处理。静态初始化块不能对实例变量进行初始化处理

        下面程序创建了三个类: Root、Mid和 Leaf,这三个类都提供了静态初始化块和普通初始化块,而且Mid类里还使用this调用重载的构造器,而Leaf使用super显式调用其父类指定的构造器。

 

 输出结果:

         从图5.23来看,第一次创建一个Leaf对象时,因为系统中还不存在 Leaf类,因此需要先加载并初始化Leaf类,初始化Leaf类时会先执行其顶层父类的静态初始化块,再执行其直接父类的静态初始化块,最后才执行Leaf本身的静态初始化块。
        一旦Leaf类初始化成功后,Leaf类在该虚拟机里将一直存在,因此当第二次创建Leaf实例时无须再次对Leaf类进行初始化。
        普通初始化块和构造器的执行顺序与前面介绍的一致,每次创建一个Leaf对象时,都需要先执行最顶层父类的初始化块、构造器,然后执行其父类的初始化块、构造器……最后才执行Leaf类的初始化块和构造器。

静态初始化块和声明静态成员变量时所指定的初始值都是该类的初始化代码,它们的执行顺序与源程序中的排列顺序相同。看如下代码。

 上面程序中定义了两次对a静态成员变量进行赋值,执行结果是a值为9,这表明static int a = 9;这行代码位于静态初始化块之后执行。如果将上面程序中粗体字静态初始化块与static int a= 9;调换顺序,将可以看到程序输出6,这是由于静态初始化块中代码再次将a的值设为6。

散称知识点:

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/120874982