Thinking in Java Reading Note(7.复用类)

1.组合语法

  类中域为基本类型时能够自动被初始化为零。但是对象引用会被初始化为null

  编译器并不是简单地为每一个引用都创建默认对象。如果想初始化这些引用,可以在代码中的下列位置进行:

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

  b.在类的构造器中。

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

  d.使用实例初始化

2.继承语法

  当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在导出类对象的内部

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

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

3.代理(跳过)

4.结合使用组合和继承

  虽然编译器强制你去初始化基类,并且要求你要在构造器起始处就要这么做,但是它并不监督你必须将成员对象也初始化

  确保正确清理  

  在Java中没有C++析构函数的概念。析构函数是一种在对象被销毁时可以被自动调用的函数。其原因可能是因为在Java中,我们的习惯只是忘掉而不是销毁对象,并且让垃圾回收器在必要时释放其内存

  通常这样做是好事,但有时类可能要在其生命周期内执行一些必需的清理活动。因此,如果你想要某个类清理一些东西,就必须显式地编写一个特殊方法来做这件事,并要确保客户端程序员知晓他们必须要调用这一方法。其首要任务就是,必须将这一清理动作置于finally子句中,以预防异常的出现

  在清理方法中,还必须注意对基类清理方法和成员对象清理方法的调用顺序,以防某个子对象依赖于另一个子对象情形的发生。

  一般而言,所采用的形式应该与C++编译器在其析构函数上所施加的形式相同:首先,执行类的所有特定的清理动作,其顺序同生成顺序相反(通常这就要求基类元素仍旧存活);然后,调用基类的清理方法

  如果需要进行清理,最好是编写你自己的清理方法,但不要使用finalize()。

  名称屏蔽

  如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类的任何版本

  因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作

  

  Java SE5新增加了@Override注解,它并不是关键字,但是可以把它当做关键字使用。

  当你想要覆盖某个方法时,可以选择添加这个注解,在你不留心重载而并非覆写了该方法时,编译器就会生成一条错误的信息。这样,@Override注解可以防止你在不想重载时而意外地进行重载

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

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

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

  使成员称为public将有助于客户端程序员了解怎样去使用类,而且也降低了类开发者所面临的代码复杂度。但务必记得这仅仅是一个特例,一般情况下应该使域称为private

  在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化

  "is-a"(是一个)的关系是用继承来表达的,而"has-a"(有一个)的关系则是用组合来表达的

6.protected关键字

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

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

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

7.向上转型

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

  由于继承可以确保基类中所有的方法在导出类也同样有效,所以能够向基类发送的所有信息同样也可以向导出类发送

  再论组合与继承

  在面向对象编程中,生成和使用程序代码最有可能采用的方法就是直接将数据和方法包装进一个类中,并使用该类的对象

  也可以运用组合技术使用现有类来开发新的类

  而继承技术其实是不太常用的。应当慎用这一技术,其使用场合仅限于你确信使用该技术确实有效的情况

  到底是该用组合还是用继承,一个最清晰的判断方法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的;但如果不需要,则应当好好考虑自己是否需要继承

8.final关键字

  通常final指的是”这是无法改变的“。不想做改变可能出于两种理由:设计或效率

  final数据

  有时数据的恒定不变是很有用的。比如:

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

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

  

  对于编译期常量这种情况,编译器可以将该常量值代入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时负担。

  在Java中,这类常量必须是基本数据类型,并且以关键字final表示。在对这个常量进行定义时,必须对其进行赋值

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

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

  空白final

  Java允许生成“空白final”,所谓空白final是指被声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化

  空白final在关键字final的使用上提供了更大的灵活性。

  例如由构造器进行初始化空白final来达到创建出更具有针对性的final域。(由例子看出的技巧)

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

  final参数

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

  你可以读参数,但却无法修改参数这一特性主要用来向匿名类传递数据

  final方法

  使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖

  过去建议使用final方法的第二个原因是效率。在Java的早期实现中,如果将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法调用命令时,它会根据自己的谨慎判断,跳过插入程序代码这种正常方式而执行方法调用机制(将参数压入栈,跳至方法代码处并执行,然后跳回并清理栈中的参数,处理返回值),并且以方法体中的实际代码的副本来替代方法调用。这将消除方法调用的开销。当然,如果一个方法很大,你的程序代码就会膨胀,因而可能看不到内嵌带来的任何性能提高,因为,所带来的性能提高会因为花费于方法内的时间量而被缩减。

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

  在使用Java SE5/6时,应该让编译器和JVM去处理效率问题,只有在想要明确禁止覆盖时,才将方法设置为final的

  final和private关键字

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

  这一问题会造成混淆。因为,如果你试图覆盖一个private方法(隐含是final的),似乎是奏效的,而且编译器也不会给出错误信息

class WithFinals {
    // Identical to private alone:
    private final void f() {
        System.out.println("WithFinals.f()");
    }

    // Also automatically "final":
    private void g() {
        System.out.println("WithFinals.g()");
    }
}

class OverridingPrivate extends WithFinals {
    // attempt to override:
    private final void f() {
        System.out.println("OverridingPrivate.f()");
    }

    private void g() {
        System.out.println("OverridingPrivate.g()");
    }
//     @Override: compiler finds error - does NOT override
//     @Override private final void f() { System.out.println("OverridingPrivate.f()"); }
//     @Override private void g() { System.out.println("OverridingPrivate.g()"); }
}

class OverridingPrivate2 extends OverridingPrivate {
    // attempt to override:
    public final void f() {
        System.out.println("OverridingPrivate2.f()");
    }

    public void g() {
        System.out.println("OverridingPrivate2.g()");
    }
    // use @Override so compiler with say "does NOT override or implement"
//     @Override public final void f() { System.out.println("OverridingPrivate2.f()"); }
//     @Override public void g() { System.out.println("OverridingPrivate2.g()"); }
}

public class FinalOverridingIllusionEx {
    public static void main(String[] args) {
        OverridingPrivate2 op2 = new OverridingPrivate2();
        op2.f();
        op2.g();
        // You can upcast:
        OverridingPrivate op = op2;
        // But you can't call the methods:
        //! op.f(); // f() has private access in OverridingPrivate
        //! op.f();
        // Same here:
        WithFinals wf = op2;
        //! wf.f(); // f() has private access in WithFinals
        //! wf.g();
    }
}

  “覆盖”只有在某方法是基类的接口的一部分时才会出现。即,必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private,它就不是基类的接口的一部分。它仅是一些隐藏于类中的程序代码,只不过是具有相同的名称而已。但如果在导出类以相同的名称生成一个public,protected或包访问权限方法的话,该方法就不会产生在基类中出现的“仅具有相同名称”的情况。此时你并没有覆盖该方法,仅是生成了一个新的方法。由于private方法无法触及而且能有效隐藏,所以除了把它看成是因为它所归属的类的组织结构的原因而存在外,其他任何事物都不需要考虑到它

  final类

  当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,出于某种考虑,你对该类的设计永不需要做任何变动,或者出于安全考虑,你不希望它有子类

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

9.初始化及类的加载

  在许多传统语言中,程序是作为启动过程的一部分立刻被加载的。然后是初始化,紧接着程序开始运行。这些语言的初始化过程必须小心控制,以确保定义为static的东西,其初始化顺序不会造成麻烦。

  Java就不会出现这个问题,因为它采用了一种不同的加载方式。加载是众多变得更加容易的动作之一,因为Java中的所有事物都是对象

  请记住,每个类的编译代码都存在于它自己的独立的文件中。该文件只在需要使用程序代码时才会被加载。一般来说,可以说:”类的代码在初次使用时才加载。“这通常是指加载发生于创建类的第一个对象之时,但是访问static域或static方法时,也会发生加载

  注:构造器也是static方法,尽管static关键字没有显式的写出来。因此确切的讲,类是在任何static成员被访问时加载的

  继承与初始化

  了解包括继承在内的初始化全过程,以对所发生的一切有个全局性的把握,是很有益的。请看下例:

//: reusing/Beetle.java
// The full process of initialization.
import static net.mindview.util.Print.*;

class Insect {
  private int i = 9;
  protected int j;
  Insect() {
    print("i = " + i + ", j = " + j);
    j = 39;
  }
  private static int x1 =
    printInit("static Insect.x1 initialized");
  static int printInit(String s) {
    print(s);
    return 47;
  }
}

public class Beetle extends Insect {
  private int k = printInit("Beetle.k initialized");
  public Beetle() {
    print("k = " + k);
    print("j = " + j);
  }
  private static int x2 =
    printInit("static Beetle.x2 initialized");
  public static void main(String[] args) {
    print("Beetle constructor");
    Beetle b = new Beetle();
  }
} /* Output:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*///:~

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

  

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

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

  

猜你喜欢

转载自www.cnblogs.com/Miromiaosang/p/8922052.html