Java编程思想 - 第七章、复用类

| 分类 Java  | 标签 Java编程思想 

###本章主题

既然是讨论代码的复用,那么肯定涉及到了组合和继承的概念,另外代理也是一种常用的代码复用手段。

  1. has a:组合
  2. is a:继承
  3. 中庸之道:代理

然后就是如何使用这三个法则选择对应的解决方案。对于更复杂的要求(比如实现C++的多继承问题),接口就会出来打酱油了- -

###1. 组合、继承、代理

理清这三者之间的关系是我们做出设计的第一步。在《JAVA编程思想》中,作者建议慎重使用继承,当我们确定要不要使用继承时,有一个很清晰的方法:

是否需要向上转型

在前面我们说过,组合解决has a问题,继承解决is a问题。而这里之所以要说向上转型,其实是为后面的多态做铺垫。这样,我们就可以完成面向接口编程。你只需要定义一个基类,就可以通过调用相同的方法传递不同的消息给不同的对象,极大的简化了程序的复杂度,完成了代码的复用。就像马士兵的“Sping教程”里的例子,运用MVC模型,一个数据库接口,可以操纵mysql/oricle等任何数据库。

tips:

  • 为了继承,一般的规则是将所有的数据成员都设置为private,而将所有的方法都设置为public。这样,当不同的包下的类继承该类时,就可以获得该类所有的方法,和包内、包外没有区别。如果不加修饰符,就是限制包内访问,那么包外继承的时候,只能获得public修饰的方法,这样内外的方法不一致,就会出现问题了。当然,特殊情况需要特殊考虑。

###2. final用法

根据上下文环境,JAVA的关键字final的含义存在着细微的区别,但通常它指的是“这是无法改变的”。不能做改变可能处于两种理由:设计和效率。由于这两个原因相差很远,所以可能会被误用。这里简单总结一下它们应用的场景。具体的,JAVA中的final有三种使用场景,分别为数据、方法和类:

  • final数据:
    1. 一个永不改变的编译时常量: 因为编译期常量可以带入用到它所在的表达式,就可以免去运行时的工作。由于编译的时候是无法初始化自定义类的,所以必须为8种基本类型,而且在定义的时候初始化。对于引用来说,用final修饰就表示这个引用(数组也是一种引用)只能指向这个对象,不能指向别的对象(但对象本身可以改变,只是引用不可改变)
    2. 一个运行时被初始化的变量,而你不希望它被改变,比如空白final的使用:可以通过在定义处或者在构造函数中初始化,而在构造函数中初始化可以使不同对象的对象拥有不同的值(比如身份证号,唯一且不能改变)。这样就保证final数据在使用前被初始化且无法改变的特征。
  • final方法使用的原因有2:
    1. 把方法锁定,防止任何继承的子类修改它的含义(比如查看某一重要数据的方法,任何方法无法改变)
    2. 效率:早期JAVA会将final方法转为内嵌调用。类似C++的内敛函数(inline),但随着JAVA性能的提升,这个做法基本废除了
  • final类:表明了你不打算继承该类,而且也不允许别人这样做。你对该类的设计并不需要改变,或者是处于安全的考虑,你不希望它有子类。

tips:

  • 类中所有的private方法都隐式指定为final的,因为无法取用private方法,所以无法覆盖它。就好像构造函数是隐式指定为static类似。
  • 在JAVA中,如果基类有一个方法是private的,那么,在子类中可以使用完全相同的函数(包括所有东西)。因为基类的private方法子类是看不到的,所以你定义一个完全相同的函数子类也会认为是新函数,当然不会有问题。但是如果你使用@Override注解,就明确告诉编译器这个方法是覆盖基类的方法,但因为子类“看不到”基类的这个private方法,@Override注解就会报错,说你必须覆盖一个基类存在的方法。

###3. 重载、覆盖、隐藏

这个在《高质量C/C++编程指南》中详细讲了,但是好像和JAVA有一定的出入。保险起见,还是先把C++和JAVA的都搞清楚吧,防止混淆。

  • 高质量C/C++编程指南
  • JAVA没有所谓的名称屏蔽(隐藏)。如果JAVA的基类拥有某个已被多次重载的方法名称,那么在子类中重新定义该方法名称并不会隐藏在基类中的任何版本。可以通过super调用父类的这些方法。

###4. 初始化

在第五章讲到了初始化,但是因为没有涉及到继承,所以还不算完整。这里就详细说一下在有继承的情况下是如何完成初始化的。【P96页和P147页】

  1. 在执行xx.main的时候,因为main是static方法,就触发了该类的加载(因为Java采用了动态加载技术,只有使用的时候才会被加载)。于是Java解释器调用类加载器在CLASSPATH中找xx.class,如果xx继承自aa,那么编译器注意到xx有一个基类aa(通过extends得到),就开始加载基类,不管你是否打算产生一个该基类的对象(因为需要将该类加载进内存,有其子必有其父是也)。如果该基类继承自其它类,就会以此类推。
  2. 根基类中的static初始化会被执行,然后是第二个基类,以此类推(如果没有调用new,到这里就结束了)
  3. 如果使用new创建对象,就会在堆上为该对象分配足够的存储空间
  4. 存储空间先清空,然后进行默认初始化,基本类型给默认值,引用类型给null。其实这一块是通过将对象内存设为二进制零值一举生成的,说成两个过程是便于理解,本质是分配内存的同时进行设置默认值
  5. 从根基类开始,执行每个类定义处的域初始化
  6. 从根基类的构造器开始执行,一直到底
  7. Over

上一篇     下一篇