Java内部类:下

来源:互联网 发布:数控编程什么软件好用 编辑:程序博客网 时间:2024/06/05 19:47

1. 为什么需要内部类?

一般来说,内部类继承自某个类或者实现某个接口,内部类的代码操作创建它的外围类的对象,所以可以认为内部类提供了某种进入其外围类的窗口。

内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做”。内部类实现一个接口与外围类实现一个接口的区别在于,后者不能享守接口带来的方面,有时需要用到接口的实现。使用内部类最吸引人的原因在于:

每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(类或者抽象类)。

假设必须在一个类中以某种方式实现两个接口。由于接口的灵活性,你有两种选择:使用单一类或者使用内部类,看下面的例子:

interface A {}interface B {}class X implements A, B {}class Y implements A {    B makeB() {        // Anonymous inner class:        return new B() {};    }}public class MultiInterfaces {    static void takesA(A a) {}    static void takesB(B b) {}    public static void main(String[] args) {        X x = new X();        Y y = new Y();        takesA(x);        takesA(y);        takesB(x);        takesB(y.makeB());    }}

当然,这里假设在两种方式下的代码结构都确实有逻辑意义。然而遇到问题的时候,通常问题本身就能给出某些指引,告诉你是应该使用单一类,还是使用内部类。但如果没有任何其他限制,从实现的观点来看,这两种方式并没有什么区别,它们都能正常运作。

如果拥有的是抽象类或者具体的类,而不是接口,那就只能使用内部类才能实现多重继承,如下代码:

class D {}abstract class E {}class Z extends D {    E makeE() {        return new E() {};    }}public class C22_MultiImplementation {    static void takesD(D d) {}    static void takesE(E e) {}    public static void main(String[] args) {        Z z = new Z();        takesD(z);        takesE(z.makeE());    }}

如果不需要解决“多重继承的问题”,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:
1) 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。
2) 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
3) 创建内部类对象的时刻并不依赖于外围类对象对的创建。
4) 内部类并没有令人迷惑的”is a”关系;它就是一个独立的实体。

(1) 闭包与回调

闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括private成员。
Java最引人争议的问题之一就是,人们认为Java应该包含某种类似指针的机制,以允许回调(callback)。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。
通过内部类提供闭包的功能是优良的解决方案,它比指针更灵活、更安全。

interface Incrementable {    void increment();}// 简单的实现了Incrementableclass Callee1 implements Incrementable {    private int i = 0;    @Override    public void increment() {        i++;        System.out.println(i);    }}class MyIncrement {    public void increment() {        System.out.println("Other Operation");    }    static void f(MyIncrement mi) {        mi.increment();    }}// 如果你的类必须通过另一个方式实现Incrementable,你必须使用内部类class Callee2 extends MyIncrement {    private int i = 0;    public void increment() {        super.increment();        i++;        System.out.println(i);    }    private class Closure implements Incrementable {        @Override        public void increment() {            // 使用外围类的方法            Callee2.this.increment();        }    }    Incrementable getCallbackReference() {        return new Closure();    }}class Caller {    private Incrementable callbackReference;    Caller(Incrementable cbh) {        callbackReference = cbh;    }    void go() {        callbackReference.increment();    }}public class Callbacks {    public static void main(String[] args) {        Callee1 c1 = new Callee1();        Callee2 c2 = new Callee2();        MyIncrement.f(c2);        Caller caller1 = new Caller(c1);        Caller caller2 = new Caller(c2.getCallbackReference());        caller1.go();        caller1.go();        caller2.go();        caller2.go();    }}结果:Other Operation112Other Operation2Other Operation3

这个例子进一步展示了实现一个外围类与内部类实现此接口之间的区别。就代码而言,Callee1是简单的解决方式。Callee2继承自MyIncrement,后者已经有了一个不同的increment()方法,并且与Incrementable接口期望的increment()方法完全不相关。所以如果Callee2继承了MyIncrement,就不能为了Incrementable的用途而覆盖increment()方法,于是只能使用内部类独立地实现Incrementable。还要注意,当创建一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。
注意,在Callee2中除了getCallbackReference()以外,其他成员都是private的。要想建立与外部世界的任何连接,interface Incrementable都是必须的,在这里可以看到,interface是如何允许接口与接口的实现完全独立的。
内部类Closure实现了Incrementable,以提供一个返回Callee2的“钩子”(hook),而且是一个安全的钩子,无论谁获得此Incrementable的引用,都只能调用increment(),除此之外没有其他功能。

2. 内部类的继承

因为内部类的构造器必须指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外围类对象的“秘密的”引用必须被初始化,而在导出类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来说清楚它们之间的关联。

class WithInner {    class Inner {}}public class InheritInner extends WithInner.Inner {    // Won't compile    // InheritInner() {}    InheritInner(WithInner wi) {        wi.super();    }    /*     * 可以看到,InheritInner只继承内部类,而不是外围类。     * 但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类的引用。     * 此外,必须在构造器内使用如下语法:     * enclosingClassReference.super();     * 这样才提供了必要的引用,然后程序才能编译通过。     */    public static void main(String[] args) {        WithInner wi = new WithInner();        InheritInner ii = new InheritInner(wi);    }}

3. 内部类可以被覆盖吗?

如果创建了一个内部类,然后继承其外围类并重新定义此内部类时,会发生什么呢?也就是说,内部类可以被覆盖吗?“覆盖”内部类就好像它是外围类的一个方法,其实并不起什么作用。

class Egg {    private Yolk y;    protected class Yolk {        public Yolk() {            System.out.println("Egg.Yolk()");        }    }    public Egg() {        System.out.println("New Egg()");        y = new Yolk();    }}public class BigEgg extends Egg {    public class Yolk {        public Yolk() {            System.out.println("BigEgg.Yolk()");        }    }    public static void main(String[] args) {        new BigEgg();    }}结果:New Egg()Egg.Yolk()

默认的构造器是编译器自动生成的,这里是调用基类的默认构造器。虽然“覆盖”了York这个类,但是并不起作用。这说明,当继承了某个外围类的时候,内部类不会发生什么神奇的变化,这两个内部类是完全独立的两个实体,各自在自己的命名空间内。当然,明确地继承某个内部类是可以的:

class Egg2 {    protected class Yolk {        public Yolk() {            System.out.println("Egg2.Yolk()");        }        public void f() {            System.out.println("Egg2.Yolk.f()");        }    }    // 在构造器之前初始化,调用内部类构造器    private Yolk y = new Yolk();    public Egg2() {        System.out.println("New Egg2()");    }    public void insertYolk(Yolk yy) {        y = yy;    }    public void g() {        y.f();    }}public class BigEgg2 extends Egg2 {    public class Yolk extends Egg2.Yolk {        public Yolk() {            System.out.println("BigEgg2.Yolk()");        }        public void f() {            System.out.println("BigEgg2.Yolk.f()");        }    }    public BigEgg2() {        insertYolk(new Yolk());    }    public static void main(String[] args) {        Egg2 e2 = new BigEgg2();        e2.g();    }}结果:Egg2.Yolk()New Egg2()Egg2.Yolk()BigEgg2.Yolk()BigEgg2.Yolk.f()

4. 局部内部类

可以在类中创建匿名内部类,静态内部类,普通内部类,也可以在代码块里创建内部类,典型的方式就是在一个方法体的内部里面创建。局部内部类不能有访问说明符,因为它不是外部类的一部分,但是它可以访问当前代码块内的常量(Java8中传入到内部类的值不一定是final类型的,只要初始化后不被修改即可),以及此外围类的所有成员。
下面是创建局部内部类和创建匿名内部类的比较:

interface Counter {    int next();}public class LocalInnerClass {    private int count = 0;    Counter getCounter(final String name) {        // A local inner class:        class LocalCounter implements Counter {            public LocalCounter() {                // Local inner class can have a constructor                System.out.println("LocalCounter()");            }            public int next() {                System.out.println(name); // Access local final                return count++;            }        }        return new LocalCounter();    }    // The same thing with an anonymous inner class:    Counter getCounter2(final String name) {        return new Counter() {            // Anonymous inner class cannot have a named            // constructor, only an instance initializer:            {                System.out.println("Counter()");            }            public int next() {                System.out.println(name); // Access local final                return count++;            }        };    }    public static void main(String[] args) {        LocalInnerClass lic = new LocalInnerClass();        Counter c1 = lic.getCounter("Local inner "), c2 = lic.getCounter2("Anonymous inner ");        for (int i = 0; i < 5; i++)            System.out.println(c1.next());        for (int i = 0; i < 5; i++)            System.out.println(c2.next());    }}结果:LocalCounter()Counter()Local inner 0Local inner 1Local inner 2Local inner 3Local inner 4Anonymous inner 5Anonymous inner 6Anonymous inner 7Anonymous inner 8Anonymous inner 9

局部内部类和匿名内部类的区别:局部内部类的名字在方法外是不可见的,局部内部类可以重载构造器,而匿名内部类只能用于实例初始化,没有构造器。

所以当我们需要不止一个内部类的对象时,应该选择局部内部类而不是匿名内部类。

5. 内部类标识符

内部类标识符

由于每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个“meta-class”,叫做Class对象),内部类也会生成一个.class文件以包含它们的Class对象信息。这些类的命名有严格的规则:外围类的名字,加上”$“,再加上内部类的名字。例如,LocalInnerClass.java生成的.class文件包括:

Counter.class  LocalInnerClass$1.class   LocalInnerClass$1LocalCounter.class  LocalInnerClass.class

如果是匿名内部类,编译器会简单的产生一个数字作为其标识符。如果内部类嵌套在别的内部类中,只需将它们的名字加在其外部类标识符与$的后面。

6. 总结

内部类:

声明在一个类的内部的类,我们称为内部类。
如果类A声明在类B的内部,我们称类A为类B的内部类,类B为类A的外围类。
如果一个类没有声明在另外一个类的内部,我们称这样的类为顶层类。
顶层类可以是public与默认访问权限,内部类可以是任意的访问权限

内部类作用:
(1). 如果两个类之间的关系非常密切,我们就可以将一个类声明在一个类的内部,从而增强了两类之间的关系。(内部类可以让两个类的关系更加密切)
(2). 内部类可以提供更好的访问权限的控制(内部类可以是任意的访问权限)
(3). 内部类与外围类可以自由的访问彼此的成员,没有访问权限的限制(包括private访问权限的成员)

内部类的分类:
(1). 静态成员类:
使用static修饰,类似于类中声明的静态成员变量.
静态成员类不依赖于外围类的对象,我们如果想要创建静态成员类的对象,完全没有必要创建外围类的对象。
在外围类的内部,可以通过静态成员类的名称直接进行访问。
在外围类的外部,需要通过”外围类.静态成员类”进行访问。
因为静态成员类不依赖与外围类的对象,因此,静态成员在特征上非常类似于顶层类。只不过是声明在一个类的内部而已。
静态成员不能访问外围类的实例成员(可以访问外围类的静态成员)。静态成员类内部可以声明与静态环境相关的内容(静态成员,静态初始化块等)。

(2). 实例成员类
与静态成员类相同,实例成员类也是作为外围类的一个成员。实例成员类不使用static修饰,类似于类中声明的实例成员变量。
实例成员类依赖于外围类的对象,我们想要创建实例成员类的对象,则必须首先创建外围类的对象。
因为实例成员类依赖于外围类的对象,因此,实例成员类不能声明任何与静态环境相关的内容(包括静态成员变量,静态方法,静态初始化块,静态成员类等,静态成员类可以声明以上的内容)
例外:实例成员类可以声明静态编译期间的常量,因为编译时,字节码就是使用常数值来替换静态成员变量(宏替换),实际上根本不存在该静态常量的值。

(3). 局部类:
声明在方法,语句块等局部范围中的类
局部类类似于方法中声明的局部变量。
局部类不能使用访问权限修饰符,也不能使用static修饰
如果局部类处于实例上下环境中,局部类可以访问外围类的静态成员和实例成员。
如果局部类处于静态上下文环境中,局部类仅能访问外围类的静态成员,不能访问外围类的实例成员。
局部类不能声明静态成员。
局部类的作用域与局部变量相同,即从局部类声明的位置起,到局部类所在的最小语句块结束。
因为局部类仅在局部范围内有效,局部对象也不能在方法外继续有效,为了能够让局部对象可以在方法外继续有效,我们可以使用局部类实现一个接口,然后将局部类的对象作为接口类型返回,这样就可以在方法外继续使用局部类的对象。
局部类对局部变量的访问:
在JDK1.8之前,局部类只能访问声明为final的局部变量。
从JDK1.8起,局部变量也可以访问非final的局部变量,前提是该局部变量的值在作用域内没有放生改变(final等效的局部变量)。

(4). 匿名内部类:
就是没有名字的内部类,所以匿名内部类只能使用一次,他通常用来简化代码编写。但使用匿名内部类还有一个前提条件:必须继承一个父类或实现一个接口。

参考文献:
Bruce Eckel-《Thinking in Java 4th Edition》Chapter 10 : Inner Classes
参考文章:
《Java编程思想》第四版之内部类学习——神奇而又实用
JAVA内部类的作用(推荐阅读)

原创粉丝点击