zoukankan      html  css  js  c++  java
  • Java学习笔记---复用类

    复用代码是Java众多引人注目的功能之一,仅仅能够复制并加以改变是不够的。

    通过创建新类来复用代码,此方法的窍门在于使用类而不破坏现有程序代码:

    方法一:只需在新的类中产生现有类的对象。新的类由现有类的对象组成,这叫组合,该方法只是复用了现有代码的功能,而非它的形式。

    方法二:按照现有类的类型来创建新类,无需改变现有类的形式,采用现有类的形式并在其中添加新代码,这叫继承。

    • 组合语法
    • 继承语法
    • 代理
    • 结合使用组合和继承
    • 在组合和继承之间选择
    • protected关键字
    • 向上转型
    • final关键字
    • 初始化及类的加载

    1.组合语法

    只需将对象引用置于新类中即可: 

    这里有一个特殊的函数:toString()。每一个非基本类型的对象都有一个toString()方法,而且当编译器需要一个String而你却只有一个对象时,该方法便会被调用

    类中域为基本类型时能够将自动被初始化为零,但是对象引用会被初始化为null,而且,如果你试图为它们调用任何方法,都会得到一个异常--运行时错误。但是,很方便的是,在不抛出异常的情况下仍旧可以打印一个null引用。

    初始化引用:

    1).在定义对象的地方。这意味着它们总是能够在构造器被调用之前被初始化。

    2).在类的构造器中

    3).就在正要使用这些对象之前,这种方式称为:惰性初始化。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担。

    4).使用实例初始化

    例子:

    如果没有在定义处初始化,那么除非发生了不可避免的运行期异常,否则将不能保证信息在发送给对象引用之前已经被初始化。

    这里,当toString()被调用时,它将填充s4的值,以确保所有的域在使用之时已被妥善初始化。 

    2.继承语法

     当创建一个类时总是在继承,因此,除非已明确指出要从其他类中继承,否则就是隐式地从Java的标准根类Object进行继承。

    组合的语法比较平实,但是继承使用的是一种特殊的语法。在继承过程中,需要先声明“新类与旧类相似”。这种声明通过在类的左边花括号之前,书写后面紧跟基类名称的关键字extends而实现。这样做,会自动得到基类种所有的域和方法:

    (一个编译单元中,只能有一个类是public的,且类名和文件名必须相同)

    Cleanser和Detergent均含有main()方法,可以为每个类都创建一个main()方法,这种在每个类中都设置一个main()方法的技术可使每个类的单元测试都变得简便易行。而且在完成单元测试之后,无需删除main(),可以将其留待下次测试。

    即使一个程序中含有多个类,也只有命令行所调用的那个类的main()方法会被调用。即使Cleanser只具有包访问权限,其public main()仍然是可访问的。

    在这里,Detergent.main()明确调用了Cleanser.main(),并将命令行获取的参数传递给了它,当然也可以向其传递任意的String数组。

    Cleaner中的所有方法都必须是public的,为了继承,一般的规则是将所有的数据成员指定为private,将所有的方法指定为public。当然也可以调整规则。

    在Cleaser的接口中有一组方法:append(),dilute(),apply(),scrub()和toString()。由于Detergent是由关键字extends从Cleanser导出的,所以它可以在其接口中自动获得这些方法,尽管并不能看到这些方法在Detergent中的显式定义。因此,可以将继承是做是对类的复用。

    如scrub()中所示,使用基类中定义的方法及对它进行修改是可行的。在继承类中的scrub()中,若想使用基类继承来的方法scrub(),不能直接调用scrub(),这会形成递归,为解决此问题,Java用super关键字表示父类的意思,当前类就是从超类继承来的。为此,表达式super.scrub()将调用基类版本的scrub()方法。

    在继承中,不一定非得使用基类的方法,也可以在导出类中添加新方法,其添加方式域类中添加任意方法一样,即对其加以定义即可。foam()方法就是例子。

    初始化基类

    继承类从外部看,就像是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。但继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者的区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部

    仅有一种方法来保证基类子对象的正确初始化:在导出类构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。Java会自动在导出类的构造器中插入对基类构造器的调用:

    例子:

    构建过程是从基类“向外”扩散的,所以基类在导出类构造器中可以访问它之前,就已经完成了初始化。即使没有给Cartoon()创建构造器,编译器也会自动合成一个默认的构造器,该构造器将调用基类的构造器。

    带参数的构造器

    如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须使用关键字super显式地编写调用基类构造器的语句,并且配以适当的参数列表。

    如果不在BoardGame()中调用基类构造器,编译器会报错:无法找到符合Game()形式的构造器。而且,调用基类构造器必须是你在导出类构造器中做的第一件事(位于构造器的第一行,如果导出类复写方法,想要调用基类方法可以不需要在第一行调用父类方法)。

    3.代理

     前面已经介绍了组合、继承;代理是要介绍的第三种关系,Java并没有提供对他的直接支持。这是继承和组合的中庸之道,因为将一个成员对象置于所要构造的类中(组合),但与此同时,在新类中暴露了该成员对象的所有方法(继承)。

    太空船控制模块

    构造太空船的一种方式是使用继承

    然而,SpaceShip并非真正的SpaceShipControls类型。更准确的讲,SpaceShip包含SpaceShipControls,与此同时,SpaceShipControls的所有方法在SpaceShip中都暴露了出来

    代理可以解决这个问题:

    上面的操作,通过在飞船代理中定义一个飞船控制对象(组合),调用飞船控制中的方法,而其接口由此也就与使用继承得到的接口相同了。但是使用代理可以拥有更多的控制力,因为我们可以选择只提供在成员对象中的方法的某个子集 

    4.结合使用组合和继承

    通常使用组合和继承是很常见的事,下例展示同时使用这两种技术,并配以必要的构造器初始化,来创建更加复杂的类:

    虽然编译器要求初始化基类,并且要求要在构造器起始处就要这么做,但是它并不监督你必须将成员对象也初始化,因此这一点必须要时刻注意。

    在这个例子中,类字段处的成员对象定义,并没有对其进行初始化,只是进行了定义;在构造器中,才开始给这些成员对象进行初始化。第一个初始化的是基类,后面是各种成员对象。

    确保真确清理

    在Java中,习惯只是忘掉而不是销毁对象,并且让垃圾回收器在必要时释放其内存。但是,我们并不知道垃圾回收器何时将会被调用,或者它是否会被调用。因此,若想要某个类清理一些东西,就必须显式地编写一个特殊方法来做这件事,并要确保客户端程序员知晓他们必须要调用这一方法。其首要任务就是,必须将这一清理动作置于finally子句之中,以访异常的出现

    下面例子是能在屏幕上绘制图案的计算机辅助设计系统示例:

    此系统的一切都是某种Shape(Shape自身就是一种Object,因为Shape继承自根类Object)。每个类都覆写Shape的dispose()方法,并运用super来调用该方法的基类版本。尽管对象生命期中任何被调用的方法都可以做一些必需的清理工作,但是Circle、Triangle和Line这些特定的Shape类仍然都带有可以进行“绘制”的构造器。每个类都有自己的dispose()方法将未存于内存之中的东西恢复到对象存在之前的状态。

    在main()中,关键字try表示,下面的块(使用大括号括起来的范围)是所谓的保护区,这意味着它需要被特殊处理。其中一项特殊处理就是无论try块是怎样退出的,保护区后的finally子句中的代码总是要被执行的。这里finally子句标识的是“无论发生什么事,一定要为x调用dispose”。

    在清理方法dispose中,需注意对基类清理方法和成员对象清理方法的调用顺序,以防某个子对象依赖于另一个子对象情形的发生。清理顺序:其顺序同生成顺序相反,最后调用基类的清理方法。

    垃圾回收器可能永远也无法被调用,即使被调用,它也可能以任何它想要的顺序来回收对象。最好的办法是除了内存以外,不能依赖垃圾回收器去做任何事情。如需进行清理,最好是编写你自己的清理方法,但不要使用finalize().使用finally子句,但不使用finalize()方法

    名称屏蔽

    若Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类的任何版本(这与C++不同)。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作。

     这叫重载,不是覆写,覆写的话应该是函数名和参数都相同

    可以看到,虽然Bart引入了一个新的重载方法,但是在Bart中Homer的所有重载方法都是可用的。使用与基类完全相同的特征签名及返回类型来覆盖具有相同名称的方法,是一件极其平常的事。

    Java SE5新增加了@override注解,它并不是关键字,但是可以把它当作关键字使用。当你想要覆写某个方法时,可以添加这个注解,在你不留心重载而并非覆写该方法时(因为添加了@override),编译器就会报错。

    这是重载,而非覆写,但是添加了@override注解

    {CompileTimeError}标签把该文件从本书的Ant构建中排除了出来,但是如果手工编译该文件,就会收到下面的错误消息:

    这样,@override注解可以防止你在不想重载时而以外进行了重载。 

    5.在组合和继承之间选择

     组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承是隐式地做。

    组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所有嵌入对象的接口。为取此效果,需要在新类中嵌入一个现有类的private对象

    有时,允许类的用户直接访问(将成员对象设置为public)新类中的组合成分是极具意义的,特别是成员对象自身都隐藏了具体实现,那么这种做法还是安全的。(当用户能够了解到你正在组装一组部件时,会使得端口更加易于理解)

    例子:

     

    在这个例子中,car的组合也是问题分析的一部分(而不仅仅是底层设计的一部分),所以使成员成为public将有助于客户端程序员了解怎样取使用类,而且也降低了类开发者所面临的代码复杂度。这只是一个特例,一般情况下还是应该使用private。

    在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅是一种交通工具(is-a关系)。“is-a”(是一个)的关系是用继承来表达的,而“has-a”(有一个)的关系则是用组合来表达的。 (还有一个关系是is-like-a关系,指的是继承之后又添加了一些方法)

    6.protected关键字

    介绍完了继承,现在protected关键字具有了意义。理想中,仅靠private就已经足够了,但在实际中,经常会想要将某些事物尽可能对这个世界隐藏起来,但仍然允许导出类的成员访问它们

    protected关键字指明“就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的”。(protected也提供了包内访问权)

    尽管可以创建protected域,但最好的方式还是将域保持为private;你应当一直保留"保留更改底层实现"的权利。然后通过protected方法来控制类的继承者的访问权限。

     

    change()可以访问set(),这是因为它是protected的。还应注意Orc的toString()方法的定义方式,它依据toString()的基类版本而定义。

    7.向上转型

    “为新的类提供方法”,并不是继承技术中最重要的方面,其最重要的方面是用来表现类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”概括。

    这个描述并非只是一种解释继承的华丽的方式,这直接是由语言所支撑的。例如:假设有一个称为Instrument的代表乐器的基类和一个称为Wind的导出类。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的消息同样也可以向导出类发送。如果Instrument类具有一个play()方法,那么Wind乐器也将同样具备。这意味着我们可以准确地说Wind对象也是一种类型的Instrument。

    编译器是如何支持这一概念的:

     

    在此例中,tune()方法可以接受Instrument引用。但在Wind.main()中,传递给tune()方法的是一个Wind引用。鉴于Java对类型的检查十分严格,接受某种类型的方法同样可以接受另一种类型就会显得很奇怪,但这里之所以可以正常运行,是因为Wind对象同样也是一种Instrument对象,而且也不存在任何tune()方法是可以通过Instrument来调用,同时又不存在于Wind之中(Wind也继承了Instrument中的tune()方法)。tune()中,程序代码可以对Instrument和它所有导出类起作用,这种Wind引用转换为Instrument引用的动作,称为向上转型

    向上转型:子类引用转换为父类引用,参数接受的是父类,但给的引用是子类

     

    为什么称为向上转型

    8.final关键字

    final数据

    9.初始化及类的加载

    ---恢复内容结束---

    复用代码是Java众多引人注目的功能之一,仅仅能够复制并加以改变是不够的。

    通过创建新类来复用代码,此方法的窍门在于使用类而不破坏现有程序代码:

    方法一:只需在新的类中产生现有类的对象。新的类由现有类的对象组成,这叫组合,该方法只是复用了现有代码的功能,而非它的形式。

    方法二:按照现有类的类型来创建新类,无需改变现有类的形式,采用现有类的形式并在其中添加新代码,这叫继承。

    • 组合语法
    • 继承语法
    • 代理
    • 结合使用组合和继承
    • 在组合和继承之间选择
    • protected关键字
    • 向上转型
    • final关键字
    • 初始化及类的加载

    1.组合语法

    只需将对象引用置于新类中即可: 

    这里有一个特殊的函数:toString()。每一个非基本类型的对象都有一个toString()方法,而且当编译器需要一个String而你却只有一个对象时,该方法便会被调用

    类中域为基本类型时能够将自动被初始化为零,但是对象引用会被初始化为null,而且,如果你试图为它们调用任何方法,都会得到一个异常--运行时错误。但是,很方便的是,在不抛出异常的情况下仍旧可以打印一个null引用。

    初始化引用:

    1).在定义对象的地方。这意味着它们总是能够在构造器被调用之前被初始化。

    2).在类的构造器中

    3).就在正要使用这些对象之前,这种方式称为:惰性初始化。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担。

    4).使用实例初始化

    例子:

    如果没有在定义处初始化,那么除非发生了不可避免的运行期异常,否则将不能保证信息在发送给对象引用之前已经被初始化。

    这里,当toString()被调用时,它将填充s4的值,以确保所有的域在使用之时已被妥善初始化。 

    2.继承语法

     当创建一个类时总是在继承,因此,除非已明确指出要从其他类中继承,否则就是隐式地从Java的标准根类Object进行继承。

    组合的语法比较平实,但是继承使用的是一种特殊的语法。在继承过程中,需要先声明“新类与旧类相似”。这种声明通过在类的左边花括号之前,书写后面紧跟基类名称的关键字extends而实现。这样做,会自动得到基类种所有的域和方法:

    (一个编译单元中,只能有一个类是public的,且类名和文件名必须相同)

    Cleanser和Detergent均含有main()方法,可以为每个类都创建一个main()方法,这种在每个类中都设置一个main()方法的技术可使每个类的单元测试都变得简便易行。而且在完成单元测试之后,无需删除main(),可以将其留待下次测试。

    即使一个程序中含有多个类,也只有命令行所调用的那个类的main()方法会被调用。即使Cleanser只具有包访问权限,其public main()仍然是可访问的。

    在这里,Detergent.main()明确调用了Cleanser.main(),并将命令行获取的参数传递给了它,当然也可以向其传递任意的String数组。

    Cleaner中的所有方法都必须是public的,为了继承,一般的规则是将所有的数据成员指定为private,将所有的方法指定为public。当然也可以调整规则。

    在Cleaser的接口中有一组方法:append(),dilute(),apply(),scrub()和toString()。由于Detergent是由关键字extends从Cleanser导出的,所以它可以在其接口中自动获得这些方法,尽管并不能看到这些方法在Detergent中的显式定义。因此,可以将继承是做是对类的复用。

    如scrub()中所示,使用基类中定义的方法及对它进行修改是可行的。在继承类中的scrub()中,若想使用基类继承来的方法scrub(),不能直接调用scrub(),这会形成递归,为解决此问题,Java用super关键字表示父类的意思,当前类就是从超类继承来的。为此,表达式super.scrub()将调用基类版本的scrub()方法。

    在继承中,不一定非得使用基类的方法,也可以在导出类中添加新方法,其添加方式域类中添加任意方法一样,即对其加以定义即可。foam()方法就是例子。

    初始化基类

    继承类从外部看,就像是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。但继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者的区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部

    仅有一种方法来保证基类子对象的正确初始化:在导出类构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。Java会自动在导出类的构造器中插入对基类构造器的调用:

    例子:

    构建过程是从基类“向外”扩散的,所以基类在导出类构造器中可以访问它之前,就已经完成了初始化。即使没有给Cartoon()创建构造器,编译器也会自动合成一个默认的构造器,该构造器将调用基类的构造器。

    带参数的构造器

    如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须使用关键字super显式地编写调用基类构造器的语句,并且配以适当的参数列表。

    如果不在BoardGame()中调用基类构造器,编译器会报错:无法找到符合Game()形式的构造器。而且,调用基类构造器必须是你在导出类构造器中做的第一件事(位于构造器的第一行,如果导出类复写方法,想要调用基类方法可以不需要在第一行调用父类方法)。

    3.代理

     前面已经介绍了组合、继承;代理是要介绍的第三种关系,Java并没有提供对他的直接支持。这是继承和组合的中庸之道,因为将一个成员对象置于所要构造的类中(组合),但与此同时,在新类中暴露了该成员对象的所有方法(继承)。

    太空船控制模块

    构造太空船的一种方式是使用继承

    然而,SpaceShip并非真正的SpaceShipControls类型。更准确的讲,SpaceShip包含SpaceShipControls,与此同时,SpaceShipControls的所有方法在SpaceShip中都暴露了出来

    代理可以解决这个问题:

    上面的操作,通过在飞船代理中定义一个飞船控制对象(组合),调用飞船控制中的方法,而其接口由此也就与使用继承得到的接口相同了。但是使用代理可以拥有更多的控制力,因为我们可以选择只提供在成员对象中的方法的某个子集 

    4.结合使用组合和继承

    通常使用组合和继承是很常见的事,下例展示同时使用这两种技术,并配以必要的构造器初始化,来创建更加复杂的类:

    虽然编译器要求初始化基类,并且要求要在构造器起始处就要这么做,但是它并不监督你必须将成员对象也初始化,因此这一点必须要时刻注意。

    在这个例子中,类字段处的成员对象定义,并没有对其进行初始化,只是进行了定义;在构造器中,才开始给这些成员对象进行初始化。第一个初始化的是基类,后面是各种成员对象。

    确保真确清理

    在Java中,习惯只是忘掉而不是销毁对象,并且让垃圾回收器在必要时释放其内存。但是,我们并不知道垃圾回收器何时将会被调用,或者它是否会被调用。因此,若想要某个类清理一些东西,就必须显式地编写一个特殊方法来做这件事,并要确保客户端程序员知晓他们必须要调用这一方法。其首要任务就是,必须将这一清理动作置于finally子句之中,以访异常的出现

    下面例子是能在屏幕上绘制图案的计算机辅助设计系统示例:

    此系统的一切都是某种Shape(Shape自身就是一种Object,因为Shape继承自根类Object)。每个类都覆写Shape的dispose()方法,并运用super来调用该方法的基类版本。尽管对象生命期中任何被调用的方法都可以做一些必需的清理工作,但是Circle、Triangle和Line这些特定的Shape类仍然都带有可以进行“绘制”的构造器。每个类都有自己的dispose()方法将未存于内存之中的东西恢复到对象存在之前的状态。

    在main()中,关键字try表示,下面的块(使用大括号括起来的范围)是所谓的保护区,这意味着它需要被特殊处理。其中一项特殊处理就是无论try块是怎样退出的,保护区后的finally子句中的代码总是要被执行的。这里finally子句标识的是“无论发生什么事,一定要为x调用dispose”。

    在清理方法dispose中,需注意对基类清理方法和成员对象清理方法的调用顺序,以防某个子对象依赖于另一个子对象情形的发生。清理顺序:其顺序同生成顺序相反,最后调用基类的清理方法。

    垃圾回收器可能永远也无法被调用,即使被调用,它也可能以任何它想要的顺序来回收对象。最好的办法是除了内存以外,不能依赖垃圾回收器去做任何事情。如需进行清理,最好是编写你自己的清理方法,但不要使用finalize().使用finally子句,但不使用finalize()方法

    名称屏蔽

    若Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类的任何版本(这与C++不同)。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作。

     这叫重载,不是覆写,覆写的话应该是函数名和参数都相同

    可以看到,虽然Bart引入了一个新的重载方法,但是在Bart中Homer的所有重载方法都是可用的。使用与基类完全相同的特征签名及返回类型来覆盖具有相同名称的方法,是一件极其平常的事。

    Java SE5新增加了@override注解,它并不是关键字,但是可以把它当作关键字使用。当你想要覆写某个方法时,可以添加这个注解,在你不留心重载而并非覆写该方法时(因为添加了@override),编译器就会报错。

    这是重载,而非覆写,但是添加了@override注解

    {CompileTimeError}标签把该文件从本书的Ant构建中排除了出来,但是如果手工编译该文件,就会收到下面的错误消息:

    这样,@override注解可以防止你在不想重载时而以外进行了重载。 

    5.在组合和继承之间选择

     组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承是隐式地做。

    组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所有嵌入对象的接口。为取此效果,需要在新类中嵌入一个现有类的private对象

    有时,允许类的用户直接访问(将成员对象设置为public)新类中的组合成分是极具意义的,特别是成员对象自身都隐藏了具体实现,那么这种做法还是安全的。(当用户能够了解到你正在组装一组部件时,会使得端口更加易于理解)

    例子:

     

    在这个例子中,car的组合也是问题分析的一部分(而不仅仅是底层设计的一部分),所以使成员成为public将有助于客户端程序员了解怎样取使用类,而且也降低了类开发者所面临的代码复杂度。这只是一个特例,一般情况下还是应该使用private。

    在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅是一种交通工具(is-a关系)。“is-a”(是一个)的关系是用继承来表达的,而“has-a”(有一个)的关系则是用组合来表达的。 (还有一个关系是is-like-a关系,指的是继承之后又添加了一些方法)

    6.protected关键字

    介绍完了继承,现在protected关键字具有了意义。理想中,仅靠private就已经足够了,但在实际中,经常会想要将某些事物尽可能对这个世界隐藏起来,但仍然允许导出类的成员访问它们

    protected关键字指明“就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的”。(protected也提供了包内访问权)

    尽管可以创建protected域,但最好的方式还是将域保持为private;你应当一直保留"保留更改底层实现"的权利。然后通过protected方法来控制类的继承者的访问权限。

     

    change()可以访问set(),这是因为它是protected的。还应注意Orc的toString()方法的定义方式,它依据toString()的基类版本而定义。

    7.向上转型

    “为新的类提供方法”,并不是继承技术中最重要的方面,其最重要的方面是用来表现类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”概括。

    这个描述并非只是一种解释继承的华丽的方式,这直接是由语言所支撑的。例如:假设有一个称为Instrument的代表乐器的基类和一个称为Wind的导出类。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的消息同样也可以向导出类发送。如果Instrument类具有一个play()方法,那么Wind乐器也将同样具备。这意味着我们可以准确地说Wind对象也是一种类型的Instrument。

    编译器是如何支持这一概念的:

     

    在此例中,tune()方法可以接受Instrument引用。但在Wind.main()中,传递给tune()方法的是一个Wind引用。鉴于Java对类型的检查十分严格,接受某种类型的方法同样可以接受另一种类型就会显得很奇怪,但这里之所以可以正常运行,是因为Wind对象同样也是一种Instrument对象,而且也不存在任何tune()方法是可以通过Instrument来调用,同时又不存在于Wind之中(Wind也继承了Instrument中的tune()方法)。tune()中,程序代码可以对Instrument和它所有导出类起作用,这种Wind引用转换为Instrument引用的动作,称为向上转型

    向上转型:子类引用转换为父类引用,参数接受的是父类,但给的引用是子类

    为什么称为向上转型

    原因是以传统的类继承图的绘制方法为基础:将根置于页面的顶端,然后逐渐向下。由导出类转型成基类,在继承图上是向上移动的

    向上转型是从一个较专用类型向较通用类型转换,所以总是很安全的。也就是说,导出类是基类的一个超集。它可能比基类含有更多的方法,但它必须至少具备基类中所含有的方法

    在向上转型的过程中,类接口中唯一可能发生的事情是丢失方法,而不是获取它们。这也是为什么编译器在“未曾明确表示转型”或“未曾指定特殊标记”的情况下,仍然允许向上转型。

    也可以执行与向上转型相反的向下转型,后面介绍。

    再讨论组合与继承

    面向对象编程中,生成和使用程序代码最有可能采用的方法就是直接将数据和方法包装进一个类中,并使用该类的对象。也可以运用组合技术使用现有类来开发新的类;而继承技术不太常用

    对于继承,应当慎用,其使用场合仅限于你确信使用该技术确实有效的情况。

    如何判断使用组合还是继承?问问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的;但如果不需要,则应当好好考虑是否需要继承。

    向上转型是决定是否使用继承的标准。前面例子就是使用向上转型的典型例子。

    8.final关键字

    根据上下文环境,Java的final关键字存在着细微的区别,但通常指的是“这是无法改变的”。

    不想改变的原因:设计或效率。

    final数据

    每种编程语言都存在某种方法告知编译器,一块数据是恒定不变的。数据恒定不变有其作用:

    1).一个永不改变的编译时常量

    2).一个在运行时被初始化的值,而不希望它被改变

    对于编译时常量,编译器可以将该常量代入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时的负担。在Java中,这类常量必须是基本数据类型,并且以关键字final表示。在对这个常量进行定义时,必须对其进行赋值(因为值不可改变)

    一个既是static又是final的域占据一段不能改变的存储空间

    当对对象引用而不是基本类型运用final时,其含义有点令人迷惑。对于基本类型,final使数据恒定不变;而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其本身是可以被修改的,Java并未提供任何对象恒定不变的途径(但可以自己编写类以取得使对象恒定不变的效果)。这一限制同样适用数组,它也是对象。

    下面例子示范final域,其中既是static又是final的域(编译器常量)将用大写表示,并适用下划线分隔各个单词:

    valueone和VAL_TWO都是带有编译时数字的final基本类型,故其二者均可作为编译器常量,且没有重大区别。VAL_THREE是一种更加典型的对常量进行定义的方式:定义为public,则可以用于包外;

    定义为static,则意味着只有一份;定义为final,说明是一个常量;

    某数据是final,并不意味着在编译时就可以知道它的值。上面例子:i4和INT_5就说明了这点。编译器对编译时数值一视同仁(并且它们可能因优化而消失)。

    如果又是static又是final,则数据不可通过多次创建对象而改变。

    对于v2,它是final对象引用,他无法在指向其他对象,但是可以改变对象里面的数据。

    空白final

    Java允许生成“空白final”,空白final是指被声明为final但又未给定初值的域,无论什么情况,编译器都确保空白final在使用前必须被初始化,但是空白final在关键字final的使用上提供了更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却又保持其恒定不变的特性

     

    必须在域的定义处或者每个构造器中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因。若没在定义处初始化,则必须在构造器中进行初始化,这也就是前面所说:确保空白final在使用前必须被初始化。

    final参数

    Java允许在参数列表中以声明的方式将参数指明为final。这意味着,你无法在方法中更改参数引用所指向的对象:

    方法f()和g()展示了当基本类型的参数被指明的参数被指明为final时所出现的结果:你可以读参数,但无法修改参数。这一特性主要用来向匿名内部类传递数据

    final方法

    使用final方法的原因有两个:

    1).锁定方法,以防任何类修改他的含义;确保在继承中使方法行为保持不变,并且不会被覆盖。

    2).效率,在Java早期实现中,将方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法,它会根据自己的判断,跳过插入程序代码这种正常方式而执行方法调用机制(将参数压入栈,跳至方法代码处并执行,然后调回并清理栈中的参数,并处理返回值),并且以方法体中的实际代码的副本来代替方法调用。这将消除方法调用的开销。但是,如果一个方法很大,代码就会膨胀,因而可能看不到内嵌带来的任何性能提高,因为所带来的性能提高会因为话费于方法内的时间量而被压缩。在最近的Java SE5/6中,虚拟机可以探测到这些情况,并优化去掉这些效率反而降低的额外的内嵌调用,因此不再需要使用final方法来进行优化。

    final和private关键字

    类中所有的private方法都隐式地指定为final,由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但这并不能给该方法增加任何额外的意义。

    (所有的protected方法都具有包访问权限)

    这里有个问题,如果试图覆盖一个private方法(隐含是final的),似乎是奏效的,而且编译器也不会给出错误信息,如下:

    “覆盖”只有在某方法是基类的接口的一部分时才会出现。即,必须能将一个对向上转型为它的基本类型并调用相同的方法。在这个例子中,可以向上转型为它的基本类型,但是却无法调用相同的方法。因此这里不能称为覆盖,只是生成了一个方法名相同的新方法。

    如果方法是private,那它就不是基类的接口的一部分。它仅是一些隐藏于类中的程序代码,只不过具有相同的名称而已,但如果在导出类中以相同的名称生成一个public、protected或包访问权限方法的话,该方法就不会产生在积累中出现的“仅具有相同名称”的情况。此时,你并没有覆盖该方法,仅仅是生成了一个新的方法。

    由于private方法无法触及而且能有效隐藏,所以除了把它看成是因为它所属的类的组织结构的原因而存在外,其他任何事物都不需要考虑到它。

    总结:当方法为private时,已经隐式的将其指定为final,private方法无法被复写;如果导出类出现和private方法名相同,参数也相同的情况只是为该类添加了一个新的方法。 

    final类

    当将某个类整体定义为final时(通过将关键字final置于它的定义之前),表明该类无法被继承。该类永远不可做任何变动。且没有子类:

    例子:

    final类的域可以根据个人意愿选择为是或不是final。不论类是否被定义为final,相同的规则都适用于定义为final的域。然而,由于final类禁止继承,所以final类中所有的方法都隐式指定为final,因为无法覆盖它们。在final类中可以给方法添加final修饰词,但这不会增添任何意义。

    有关final的忠告

    在设计类时,将方法指明是final的,应该说是明智的。

    但是,要预见类是如何被复用的一般是困难的,特别是对于一个通用类更是如此。如果将方法指定为final,可能会妨碍其他程序员在项目中通过继承来复用你的类,而这只是因为你没有想到它会以哪种方式被运用。

    9.初始化及类的加载

    在许多传统语言中,程序是作为启动过程的一部分立刻被加载的。然后是初始化,紧接着程序开始运行。这些语言的初始化过程必须小心控制,以确保定义为static的东西,其初始化顺序不会造成麻烦。例如,C++中,如果某个static期望另一个static在被初始化之前就能有效地使用它,那么就会出现问题。

    Java不会出现这种问题,Java采用了一种不同的加载方式。加载是众多变得更加容易的动作之一,因为Java中的所有事物都是对象。每个类的编译都存在于它自己的独立的文件中。该文件只在需要使用程序代码时才会被加载。一般来说,类的代码在初次使用时才加载。这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载

    初次使用之处也是static初始化发生之处。所有的static对象和static代码段都会在加载时依程序中的顺序(即,定义类时的书写顺序)而一次初始化。当然,定义为static的东西只会被初始化一次。

    继承与初始化

    例子:

     

    在Beetle上运行Java时,所发生的第一件事情就是试图访问Beetle.main()(一个static方法),于是加载器开始启动并找出Beetle类的编译代码(在名为Beetle.class的文件之中)。在对它进行加载的过程中,编译器注意到它有一个基类(这是由关键字extends得知的),于是它继续进行加载。不管你是否打算产生一个该基类的对象,这都要发生。

    如果该基类还有自身的基类,那么第二个基类就会被加载,如此类推。接下来,根基类中的static初始化(此例中为Insect)即会被执行,然后是下一个导出类,以此类推。这种方式很重要,因为导出类的static初始化可能会依赖于基类成员能否被正确初始化。

    至此为止,必要的类都已加载完毕,对象就可以被创建了。首先对象中所有的基本类型都会被设为默认值。对象引用被设为null,这是通过将对象内存设为二进制零值而一举生成的。然后基类的构造器会被调用。在本例中,它是被自动调用的。但也可以用super来指定对基类构造器的调用(正如在Beetle()构造器中的第一步操作)。基类构造器和导出类的构造器一样,以相同的顺序来经历相同的过程。在基类构造器完成之后,实例百年变量按其次序被初始化。最后,构造器的其余部分被执行。

    总结

    跟前面的介绍过的初始化过程一样,首先是static域,然后是非static域,最后是构造器;

    如果有继承,则每次执行上面的过程(static域,非static域,构造器)都要先在基类中执行,然后回到导出类执行。

    每次执行构造器时,都会初始化非static域。

    在包含继承和static域和mian()方法的类中,会先执行域中的static,发现有基类,就先转到基类执行static域,然后返回导出类执行static域初始化。

    在main()方法中,需要生成对象,如果有继承,需要现在基类中执行构造器,执行构造器之前会先执行类中的非static域,和分配static方法内存;

    执行完基类中的构造器后,执行导出类的构造器,同样的需要先对非static域进行初始化,然后执行构造器。

  • 相关阅读:
    zookeeper03
    微服务网关Zuul
    微服务网关概述
    服务熔断Hystrix高级
    服务熔断Hystrix入门
    微服务架构的高并发问题
    服务注册和发现总结
    服务调用Feign高级
    服务调用Feign入门
    负载均衡Ribbon高级
  • 原文地址:https://www.cnblogs.com/ifreewolf/p/11228841.html
Copyright © 2011-2022 走看看