Thinking in Java学习杂记(第7章)

将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定,就叫做“早期绑定”。而Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。后期绑定意味着绑定是在运行期间进行,以对象的类型为基础。

Java中提供了一种名为"抽象方法"的机制。它属于一种不完整的方法,只有一个声明,没有方法主体。抽象方法声明时采用的语法如下:
abstract void X();
包含了抽象方法的一个类叫做“抽象类”。如果一个类里包含了一个或多个抽象方法,类就必须指定成abstract(抽象)。否则,编译器将会向我们报告出一条出错信息。

如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基础类中所有抽象方法提供方法定义。如果不这样做,则派生类也会是抽象的,而且编译器会强迫我们用abstract关键字来标志那个类。即使不包括任何abstract方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例,这种能力就会显得非常重要。

"interface"(接口)关键字使抽象的概念更深入了一层。可以将其视为一个“纯”抽象类,它允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主体。接口也包含了基本数据类型的数据成员,但它们都默认为static和final。接口只提供一种形式,并不提供实施细节。

与类的继承使用extends关键字不同,为了实现接口,要使用implements关键字。

在实现了一个接口之后,就获得了一个普通类,可用标准方式对其进行扩展。可决定将一个接口中的方法声明明确定义为"public"。但是,即使不明确定义,它们也会默认为public。所以在实现一个接口的时候,来自接口的方法必须定义成public。此处需要注意的是,为了满足向上造型(upcasting),我们对于子类的访问限制必须小于等于基类的限制。

对于类来说,Java只能但继承,但是却可以同时实现多个接口。具体的方法就是将所有接口名置于implments关键字之后,并用逗号分割它们。可根据需要使用多个接口,而且每个接口都会成为一个独立的类型,可对其进行上溯造型。当我们需要同时继承类与实现接口时,需要将继承类写在实现的接口之前。
class A extends B implements C, D{}
接口同样也是可以继承的并且能够多继承
interface A extends B, C{}

接口中定义的字段会自动具有static和final属性。它们不能时“空白final”,但可初始化成非常数表达式。由于字段是static的,所以它们会在首次装载类之后、以及首次访问任何字段之前获得初始化。

字段并不是接口的一部分,而是保存在那个接口的static存储区域中。

在Java中,可将一个类定义置入另一个类。这就叫做“内部类”。利用它可对那些逻辑上相互联系的类进行分组,并可控制一个类在另一个类里的“可见性”。同时,若想在除外部类非static方法内部之外的任何地方生成内部类的一个对象,必须将那个对象的类型设为“外部类名.内部类名”。

public class Test {
    public Test(){
        System.out.println("construct Test");
    }
    class B{
        public B(){
            System.out.println("construct B");
        }
    }
    public B constructB(){
        return new B();
    }
    public static void main(String[] args) {
        B b = new Test().constructB();
    }
}
// output
/*
construct Test
construct B
*/

在需要隐藏具体实现时,使用内部类是非常有效的。因为,在上溯造型之后,不能通过造型后的字段或方法来访问内部类。这就有效的避免了客户程序的非法访问。

为了能够更好地隐藏细节,Java提供了匿名内部类的设施。使用内部匿名类之前,我们首先需要定义一个名字相同的实体类,以便我们能够在其上进行个性化修改。

public class Test {
    public Test(){
        System.out.println("construct Test");
    }

    public static void main(String[] args) {
        Person p=new Person(){
            public void eat(){
                System.out.println("eat apple");
            }
        };
        p.eat();
    }
}
class Person{
    public void eat(){
        System.out.println("eat egg");
    }
}
// output
/*
eat apple
*/

需要注意的是:匿名类不能拥有构建器,同时,若试图定义一个匿名内部类,并想使用在匿名内部类外部定义一个对象,则编译器要求外部对象为final属性。

在static应用于内部类时,需记住内部类的对象默认持有创建它的那个封装类的一个对象的句柄。static内部类意味着:

  1. 为创建一个static内部类的对象,我们不需要一个外部类对象。
  2. 不能从static内部类的一个对象中访问一个外部类对象。

但是,由于static成员只能位于一个类的外部级别,所以内部类不可拥有static数据或static内部类。倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为static。为了能正常工作,同时也必须将内部类设为static。

public class Test {
    public Test(){
        System.out.println("construct Test");
    }

    private static class innerTest{
        public innerTest(){
            System.out.println("construct innerTest");
        }
    }
    public static innerTest getInnerTest(){
        return new innerTest();
    }

    public static void main(String[] args) {
        getInnerTest();
    }
}
// output
/*
construct innerTest
*/

通常,我们不会在一个接口中设置任何代码,但static内部类可以成为接口的一部分。由于类是静态的,所以它不会违反接口的规则--static内部类只位于接口的命名空间内部。

interface Test {
    static class Inner{
        public Inner(){
            System.out.println("construct inner");
        }
    }
}

class A implements Test{
    public static void main(String[] args) {
        Inner inner = new Inner();
    }
}
// output
/*
construct inner
*/

有些时候,我们想告诉其他某些对象创建它某个内部类的一个对象,为达到这个目的,必须在new表达式中提供指向其他外部类对象的一个句柄。

public class Test {
    public Test(){
        System.out.println("construct Test");
    }
    class Inner{
        public Inner(){
            System.out.println("construct inner");
        }
    }

    public static void main(String[] args) {
        Test t = new Test();
        Inner inner = t.new Inner();
    }
}
// output
/*
construct Test
construct inner
*/

为直接创建一个内部类对象,必须利用外部类的一个对象来说生成内部类的一个对象。即,除非拥有外部类的一个对象,否则不可能创建内部类的一个对象。这是由于内部类的对象已同创建它的外部类的对象“默默”地连接到一起。然而,如果生成一个static内部类,就不需要指向外部类对象的一个句柄。

由于内部类构建器必须同封装类对象的一个句柄联系到一起,所以从一个内部类继承的时候,情况会变得更加复杂。这儿的问题是封装类的“秘密”句柄必须获得初始化,而且在衍生类中不再有一个默认的对象可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种连接。

public class Test extends A.B{
    Test(A a){
        a.super();
    }
    public static void main(String[] args) {
        new Test(new A());
    }
}

class A{
    A(){
        System.out.println("construct A");
    }
    class B{
        B(){
            System.out.println("construct B");
        }
    }
}
// output
/*
construct A
construct B
*/

我们能否像重写函数一样重写内部类呢?这一概念实际并不能做任何事情。

public class Test extends A{
    public class B{
        public B(){
            System.out.println("construct B*");
        }
    }

    public static void main(String[] args) {
        new Test();
    }
}

class A{
    A(){
        System.out.println("construct A");
        new B();
    }
    class B{
        B(){
            System.out.println("construct B");
        }
    }
}
// output
/*
construct A
construct B
*/

这个例子揭示出当我们从外部类继承的时候,没有任何额外的内部类继续下去。当然,如果我们需要覆盖内部类的方法,我们也可以直接继承内部类。

我们知道每个类都会生成一个.class文件,用于容纳与如何创建这个类型的对象有关的所有信息(这种信息产生了一个名为Class对象的元类),同样,内部类也必须生成相应的.class文件,用来容纳与它们的Class对象有关的信息。这些文件或类的名字遵守一种严格的形式:先是封装类的名字,再跟随一个$,再跟随内部类的名字。如,假设一个类A,内部包含类B,则会生成的.class文件包括: A.class和 A$B.class

如果内部类是匿名的,那么编译器会简单地生成数字,把它们作为内部标识符使用。若内部类嵌套与其它内部类中,则它们的名字会简单地追加在一个$以及外部标识符后面。

内部类的存在让我们能够更好地学习控制框架。一个“应用程序框架”是指一个或一系列类,它们专门设计用来解决特定类型的问题。为应用应用程序框架,我们可以从一个或多个类继承,并覆盖其中的部分方法。在覆盖方法中编写的代码用于定制由那些应用程序框架提供的常规方案,以便解决自己的实际问题。“控制框架”属于应用程序框架的一种特殊类型,受到对事件响应的需要的支配,主要用来响应事件的一个系统叫做“由事件驱动的系统”。

在进行事件监听的时候,我们需要将发生变化的东西同没有发生变化的东西区分开,“改变的意图”造成了各类Event对象的不同行动。通过创建不同的Event子类,可以表达出不同的行动。这正是内部类大显身手的地方。它们允许我们做两件事情:

  1. 在单独一个类里表达一个控制框架应用的全部实施细节,从而完整地封装与那个实施有关的所有东西。内部类用于表达多种不同类型action(),它们用于解决实际的问题。除此之外,通过为内部类添加private关键字,可以将实施细节完全隐藏起来,可以安全地修改。
  2. 内部类使我们具体的实施变得更加巧妙,因为能方便地访问外部类的任何成员。

对于一个复杂的对象,构建器的调用将遵守下面的顺序:

  1. 调用基础构建器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。
  2. 按声明顺序调用成员初始化模块。
  3. 调用衍生构建器的主体。

Thinking in Java学习杂记(5-6章)

posted @ 2020-04-06 15:14  范中豪  阅读(220)  评论(0编辑  收藏  举报