您的位置:首页 > 编程语言 > Java开发

Java编程思想:第七章:复用类

2018-12-10 16:30 483 查看

第七章:复用类


    复用代码是Java众多引入注目的功能之一。但要想成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,它还必须能够做更多的事情。

    组合和继承而言,其语法和行为大多是相似的。由于它们是利用现有类型生成新类型,所以这样做极富意义。


7.1 组合语法

    组合语法,只需要将对象引用置于新类中即可。

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

    编译器并不简单的为每一个引用都创建默认对象,这一点是很有意义的,因为若真要那样做的话,就会在许多情况下增加不必要的负担。如果想初始化这些引用,可以在代码中的下列位置进行:

  • 在定义对象的地方。这意味着它们总是在构造器被调用之前得到初始化。

  • 在类的构造器中。

  • 就在正要使用这些对象之前。

  • 使用实例初始化


7.2 继承语法

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

    继承使用的是一种特殊的语法。在继承过程中,需要先声明“新类与旧类相似”。这种声明是通过在类主体的左边花括号之前,书写后面紧随基类名称的关键字extends而实现的。当这么做,会自动得到基类的所有方法和域。如:class xxxx extends yyyy。

    在继承的过程中,并不一定非得使用基类的方法。也可以在子类中添加新的方法,其添加方式与在类中添加任意方法一样。


初始化基类

    由于现在涉及基类和子类,而不是只有一个类,所以要试着想像子类所产生的结果对象,会有点困惑。从外部来看,它就像是一个与基类相同接口的新类。或许还会有一些额外的方法或域。但继承并不只是复制基类的接口。当创建一个子类对象的时候,该对象包含了一个基类的子对象,这个子对象与你用基类直接创建的对象是一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在子类对象内部。

    当然,对基类子对象的准确初始化也是至关重要的,而且也仅有一种方法来保证这一点:在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的知识和能力。Java在自动在子类的构造器中插入对基类构造器的调用。

    构造过程是从基类“向外”扩散的,所以基类在子类构造器可以访问它之前,就已经完成了初始化。


带参数的构造器

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


7.3 代理

    第三种关系称为代理,Java没有提供直接的支持。这是继承与组合之间的关系。因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了改成员对象的所有方法(就像继承)。因此代理解决了此问题。

    我们使用代理可以拥有更多的控制权,因为我们可以选择只提供在成员对象中的方法的某个子集。

    尽管Java中不直接支持代理,但是很多开发工具支持代理。


7.4 结合使用组合和继承

    同时使用组合和继承是很常见的。

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


确保准确清理

    在Java中垃圾回收器负责销毁对象。但有时类可能要在其生命周期内执行一些必须的清理活动。你根本不知道垃圾回收器何时被调用,或者它是否已经被调用。因此,如果你想要某个类清理一些东西,就必须显示的编写一个特殊方法来做这个事,并要确保客户端程序员知晓他们必须要调用这个方法。

    在清理方法中,还必须注意对基类清理方法和成员对象清理方法的调动顺序,以防某个子对象依赖于另一个子对象情形的发生。首先,执行类的所有特定的清理动作,其顺序同生成顺序相反;然而,就如我们所师范的那样,调用基类的清理方法。

    许多情况下,清理并不是问题,仅需要让垃圾回收器完成该动作就行。但当必须亲自处理清理时,谬得多做努力并多加小心。因此,一旦涉及垃圾回收,能够信赖的事就不会很多了。垃圾回收器可能永远也不会被调用,即使被调用,它也可能以任何它想要的顺序来回收对象。最好的办法就是除了内存以外,不能依赖垃圾回收器去做任何事情。如果需要进行清理,最好是编写自己的清理方法。但不要使用finalize()方法。


名称屏蔽

    如果Java的基类拥有某个以被多次重载的方法名称,那么在子类中重新定义该方法的名称并不会屏蔽其在基类中的任何版本,因此,无论是改层或者它的基类中对方法进行定义,重载机制都可以正常工作。

    JavaSE5增加了@Override注解,它并不是关键字,但是可以把它当做关键字使用。当你需要覆写某个方法时,就添加这个注解。在你不留心重载而并非覆写了该方法时,编译器就会生成一条错误信息。


7.5 在组合与继承之间的选择

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

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

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

    在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。比如:用一个“交通工具”对象来构建一部"车子“是毫无意义的,因为”车子“并不包含”交通工具“,它仅是一种交通工具(is-a),这个关系是用继承来表示的,而has-a关系则是用组合来表示的。


7.6 protected关键字

    在实际项目中,经常会想要某些事物尽可能对这个世界隐藏起来,但仍然允许子类的成员访问它们。

    关键字protected就起这个作用。它指明”就类用户而言,这是private,但对于任何继承与此类的子类或其他任何位于同一个包内的类来说,它却是可以访问“。

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


7.7 向上转型

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

    这个描述并非只是一种解析继承的华丽的方式,这直接是由语言所支撑的。

    Java对类型的检测是非常严格的,接收某种类型的方法同样可以接收另外一种类型就显得很奇怪。除非你认识到子类也是一种基类对象。这种子类引用转换为基类引用,称为向上转换。


为什么称为向上转换

    历史原因,并且是传统的继承图的绘制法为基础的:将根植于页面的顶部,然后逐渐向下,如下:

    由子类型转基类型,在继承图上是先上移动的,因此一般称为:向上转型。由于向上转型是一个专用类型向通用类型转换的,所以总是很安全的。也就是说,子类是基类的一个超集。它可能比基类含有更多的方法,但它必须至少具备基类中所含有的方法。在向上转型的过程中,类接口中唯一可能发生的事情是丢失方法,而不是获得它们。这就是为什么编译器在“未曾明确表示转型”或“未曾指定特殊标记”的情况下,仍然允许向上转型的原因。


再论组合和继承

    到底是改用结合或继承?

  • 问一问自己是否需要从新类向基类进行向上转型,如果必须向上转型,则继承是必要的。但如果不需要,则应当好好考虑自己是否需要继承。


7.8 final关键字

    根据上下文环境,Java的关键字final的含义存在细微的区别,别通常它指的是“这是无法改变的”,不想做改变可能处于两种原因:

  • 设计

  • 效率

    由于这两个愿意相差很远,所以final有可能被误用。


final数据

    许多编程语言都有某种方法,来向编译器告知一块数据时永恒不变的。有时数据的永恒不变很有用的,比如:

  • 一个永不改变的编译时常量。

  • 一个在运行时被初始化的值,而你不希望它被改变。

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

    一个即使static又是final的域只占据一段不能改变的存储空间。

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

    我们不能因为某数据时final的就认为在编译器可以知道它的值。看起来,使引用称为final没有使基本类型称为final的用处大。


空白final

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

    必须在域定义处或每个构造器中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因所在。


final参数

    Java允许在参数列表中以声明的方式将参数指明为final,这意味着你无法在方法中更改参数引用所指向的对象。你可以读参数,但却无法修改参数。这一特性主要用于向匿名内部类传递参数。


final方法

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

  • 把方法锁定,以防止任何继承类修改它的含义。这是出于设计的考虑。想要确保在继承中使方法行为不变,并且不会被覆盖。

  • 效率,在Java的早期实现中,如果将一个方法声明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法调用命令时,它会根据自己的判断,跳过插入程序代码这种正常方式而执行方法调用机制,并且以方法体中的实际代码的副本来替代方法调用。这将消除方法调用的开销。当然,一个方法很大,你的程序代码就会膨胀,因为可能看不到内嵌带来的任何性能提高,因为,所带来的性能提高会因为花费与方法内的时间量而被缩减。

    在最近的Java版本中,虚拟机(特别是hotspot)可以检测到这种情况,并优化去掉这小效率反而降低额外的内嵌调用,因此不再需要引用final方法来进行优化了。


final与private关键字

    类中所有的private方法都是隐式的指定为final的。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final,但这并不能给该方法增加任何额外的意义。这问题可能造成混乱。因此,如果你试图覆盖一个private方法(隐含是final),似乎是有效的,而且编译器也不会出错误信息的。因为这个private方法就是该子类的private方法来对待了,和基类的对应的方法没有任何关系了,所以编译器允许这么做。


final类

    当类定义为final时,就表明你不打算继承该类,而且也不允许别人这样做。由于filan类禁止继承,所以final类中的所有方法都隐式指定为final的,因为无法覆盖他们。

    在final类中可以给方法添加final修饰符,但这不会添加任何意义。


有关final的忠告

    在类设计时,把方法声明为final是比较明智的。而且这就防止其他使用该类的程序员了,他们根本不能覆盖次方法。所以使用时考虑场景。


7.9 初始化及类的加载

    Java采用不同的加载方式,以为Java中所有都是对象。请记住,每一个类的编译代码都存在于它自己的独立的文件中。该文件只在需要使用程序代码时才会被加载。一般来说,可以说“类的代码在初始化使用时才加载”。这通常是值加载发生于创建类的第一个实例对象之前,但是当访问static域或者方法时,也会发生加载。

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


继承与初始化

    了解继承在内的初始化过程,以对所发生的的一切有个全局性的把握,是很有意义的。

    如果编译器注意到它有一个基类,于是它就继续进行加载。不管你是否打算产生一个基类的对象,这都要发生。如果该基类也有基类,那么第二个基类也会被加载,如此类推。接下来,根基类中的static初始化即会被执行,然后是下一个子类,依次类推。这种方式很重要,因为子类的static初始化可能会依赖于基类成员是否准确初始化。

    到此为止,必要的类都已加载完毕,对象就可以创建了。首先,对象中的所有基本类型都设值为默认值,对象引用设值为null:这是通过将对象内存设为二进制值来完成的。然后,基类的构造器会被调用,在基类构造器完成之后,实例变量初始化。最后构造器的区域部分被执行。


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息