当前位置: 首页 > news >正文

《Java编程思想》读书笔记:第十章 内部类

目录

10.1创建内部类

10.2链接到外部类

10.3使用this与new

10.4内部类与向上转型

10.5在方法和作用域的内部类

10.6匿名内部类

10.6.1再访工厂方法

10.7嵌套类

10.7.1接口内部的类

10.7.2从多层嵌套类中访问外部类的成员

10.8为什么需要内部类

10.8.1闭包与回调

10.8.2内部类与控制框架

10.9内部类的继承

10.10内部类可以被覆盖吗

10.11局部内部类

10.12内部类标识符


可以将一个类的定义放在另一个类的定义内部,这就是内部类。
       内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。
      在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是,你将会了解到,内部类远不止如此,它了解外围类,并能与之通信,而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样。
     最初,内部类可能看起来有些奇怪,而且要花些时间才能在设计中轻松地使用它们。对内部类的需求并非总是很明显的,但是在描述完内部类的基本语法与语义之后,10.8节就应该使得内部类的益处明确显现了。
     在10.8节之后,本章剩余部分包含了对内部类语法更加详尽的探索,这些特性是为了语言的完备性而设计的,但是你也许不需要使用它们,至少一开始不需要。因此,本章最初的部分也许就是你现在所需的全部,你可以将更详尽的探索当作参考资料。

10.1创建内部类

创建内部类的方式就如同你想的一样——把类的定义置于外围类的里面:

//:innerclasses/Parcel1.java public class Parcel1 {class Contents {private int i = 11;public int value() { return i; }}class Destination {private string label ;Destination(string whereTo) { label = whereTo;}String readLabel() { return label; }}public void ship(String dest) {Contents c = new Contents();Destination d =new Destination(dest);System.out.println(d.readLabel());}public static void main(String[] args){Parcel1 p = new Parcei1();p.ship( "Tasmania" );}
}
Output :
Tasmania

     当我们在ship()方法里面使用内部类的时候,与使用普通类没什么不同。在这里,实际的区别只是内部类的名字是嵌套在Parcel1里面的。不过你将会看到,这并不是唯一的区别。
      更典型的情况是,外部类将有一个方法,该方法返回一个指向内部类的引用,就像在to()和contents()方法中看到的那样:

//:innerclasses/ Parcel2.javapublic class Parcel2 {class Contents {private int i = 11;public int value() { return i; }}class Destination {private string label ;Destination (string whereTo){label = whereTo;}}String readLabe1() { return labe1; }}  public Destination to(string s) {return new Destination(s) ;}public Contents contents() {return new Contents();}public void ship(String dest) {contents c =contents();Destination d = to(dest) ;System.out.println(d.readLabel());}public static void main(String[] args){Parcel2 p = new Parcel2();p.ship( "Tasmania" ) ;Parcei2 q = new Parcel2() ;Parcel2.Contents c = q.contents();arcel2.Destination d = q.to( "Borneo" );}
}output :
Tasmania

     如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在main()方法中那样,具体地指明这个对象的类型:OuterClassName.InnerClassName。

10.2链接到外部类

      到目前为止,内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用,但还不是最引人注目的,它还有其他的用途。当生成一个内部类的对象时,此对象与制造它的外围对象(enclosing object)之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件此外,内部类还拥有其外围类的所有元素的访问权。下面的例子说明了这点:

//:innerclasses/Sequence.javainterface Selector {boolean end();0bject current();void next();
}
public class Sequence {private Object[] items ;private int next= 0;public Sequence(int size) { items = new 0bject[size];}public void add(0bject x) {if (next < items.length)items[next++]=×;}private class SequenceSelector implements Selector {private int i = 0;public boolean end() { return i == items.length; }public 0bject current() { return items[i];}public void next() { if(i < items.length) i++;}}public Selector selector() {return new Sequenceselector();}public static void main(String[] args){Sequence sequence = new Sequence(10);for(int i =0; i < 10; i++)sequence.add(Integer.toString(i));Selector selector = sequence.selector();while(!selector.end()){System.out.print(selector.current() +" ");selector.next();}}
}Output :1 2 3 4 5 6 7 8 9

       Sequence类只是一个固定大小的Object的数组,以类的形式包装了起来。可以调用add()在序列末增加新的Object(只要还有空间)。要获取Sequence中的每一个对象,可以使用Selector接口。这是“迭代器”设计模式的一个例子,在本书稍后的部分将更多地学习它。Selector允许你检查序列是否到末尾了(end()),访问当前对象(current()),以及移到序列中的下一个对象(next())。因为Selector是一个接口,所以别的类可以按它们自己的方式来实现这个接口,并且别的方法能以此接口为参数,来生成更加通用的代码。
       这里,SequenceSelector是提供Selector功能的private类。可以看到,在main()中创建了一个Sequence,并向其中添加了一些String对象。然后通过调用selector()获取一个Selector,并用它在Sequence中移动和选择每一个元素。
         最初看到SequenceSelector,可能会觉得它只不过是另一个内部类罢了。但请仔细观察它,注意方法end()、current()和next()都用到了Objects,这是一个引用,它并不是SequenceSelector的一部分,而是外围类中的一个private字段然而内部类可以访问其外围类的方法和字段,就像自己拥有它们似的,这带来了很大的方便,就如前面的例子所示
         所以内部类自动拥有对其外围类所有成员的访向权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。幸运的是,编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外围类的对象相关联的情况下才能被创建(就像你应该看到的,在内部类是非static类时)构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时候这都无需程序员操心。

10.3使用this与new

      如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。下面的示例展示了如何使用.this :

//:innerclasses/DotThis.java
public class DotThis {void f() { System.out.println(" DotThis.f ()"); }public class Inner{public DotThis outer() {return DotThis.this ;}}public Inner inner() { return new Inner(); }public static void main(string[] args){DotThis dt = new DotThis();DotThis.Inner dti = dt.inner();dti.outer().f();}
}*output :
DotThis.f()

     有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在new表达式中提供对其他外部类对象的引用这是需要使用.new语法,就像下面这样:

//:innerclasses/DotNew .java
public class DotNew {public class Inner {}public static void main(String[] args) {DotNew dn = new DotNew();DotNew.Inner dni = dn.new Inner();}
}

     要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字DotNew,而是必须使用外部类的对象来创建该内部类对象,就像在上面的程序中所看到的那样。这也解决了内部类名字作用域的问题,因此你不必声明(实际上你不能声明) dn.new DotNew.Inner()。
     在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会喑暗地连接到创建它的外部类对象土。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用。
   下面你可以看到将.new应用于Parcel的示例:

//:innerclasses/Parcel3.java
public class Parce13 {class contents {private int i= 11;public int value() { return i; }}class Destination {private string label;Destination(string whereTo) { label = whereTo; }string readLabel() { return label; }}public static void main(string[] args) {Parcel3 p = new Parcel3();Parce13.contents c = p.new Contents();Parcel3.Destination d = p.new Destination( "Tasmania" );}
}

10.4内部类与向上转型

    当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类——某个接口的实现——能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。

我们可以创建前一个实例的接口;

//: innerclasses/Destination.java
public interface Destination {string readLabel();
}
//: innerclasses/contents.java
public interface contents {int value();
}

     现在Contents和Destination表示客户端程序员可用的接口。(记住,接口的所有成员自动被设置为public的。)
    当取得了一个指向基类或接口的引用时,甚至可能无法找出它确切的类型,看下面的例子:

//:innerclasses/TestParce1.java
class Parcel4 {private class PContents implements Contents {private int i = 11;public int value() { return i; }}protected class PDestination implements Destination{private String label;private PDestination(string whereTo) { label = whereTo;}public string readLabel() { return label; }}public Destination destination(String s) {return new PDestination(s);}public Contents contents() {return new PContents();}
}
public class TestParcel {public static void main(String[] args){Parcel4 p = new Parcel4();Contents c = p.contents();Destination d = p.destination("Tasmania") ; //! Parce14.PContents pc = p.new PContents();}
}

    Parcel4中增加了一些新东西:内部类PContents是private,所以除了Parcel4,没有人能访问它。PDestination是protected,所以只有Parcel4及其子类、还有与Parcel4同一个包中的类(因为protected也给予了包访问权)能访问PDestination,其他类都不能访问PDestination。这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上,甚至不能向下转型成private内部类(或protected内部类,除非是继承自它的子类),因为不能访问其名字,就像在TestParcel类中看到的那样。于是,private内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给Java编译器提供了生成更高效代码的机会。

10.5在方法和作用域的内部类

     到目前为止,读者所看到的只是内部类的典型用途。通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,简单并且容易理解。然而,内部类的语法覆盖了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作用域内定义内部类。这么做有两个理由:

  1. 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
  2. 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。

在后面的例子中,先前的代码将被修改,以用来实现:

  1. 一个定义在方法中的类。
  2. 一个定义在作用域内的类,此作用域在方法的内部
  3. 一个实现了接口的匿名类。
  4. 一个匿名类,它扩展了有非默认构造器的类。
  5. 一个匿名类,它执行字段初始化。
  6. 一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)。

第一个例子展示了在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。这被称作局部内部类:
 

//: innerclasses/Parce15.java
public class Parce15 {public Destination destination(String s){class PDestination implements Destination {private String label;private PDestination(String whereTo){label = whereTo;}public String readLabel() { return label; }}return new PDestination(s);}public static void main(String[] args) {Parcel5 p = new Parcel5();Destination d =p.destination("Tasmania" );}
}

    PDestination类是destination()方法的一部分,而不是Parcel5的一部分。所以,在destination()之外不能访问PDestination。注意出现在return语句中的向上转型——返回的是Destination的引用,它是PDestination的基类。当然,在destination()中定义了内部类PDestination,并不意味着一旦dest()方法执行完毕,PDestination就不可用了。
     你可以在同一个子目录下的任意类中对某个内部类使用类标识符PDestination,这并不会有命名冲突。
    下面的例子展示了如何在任意的作用域内嵌入一个内部类:

//:innerclasses/Parcel6.java
public class Parce16 {private void internalTracking(boolean b) {if(b){class Trackings1ip {private String id;Trackings1ip(String s) {id = s;}String getslip() { return id; }}Trackingslip ts = new Trackings1ip("slip");String s = ts.getslip();}//! Trackingslip ts = new Trackingslip("x");}public void track() { internalTracking(true); }public static void main(string[] args){Parcel6 p =new Parcei6();p.track();}
}

    TrackingSlip类被嵌入在if语句的作用域内,这并不是说该类的创建是有条件的,它其实与别的类一起编译过了。然而,在定义TrackingSlip的作用域之外,它是不可用的,除此之外,它与普通的类一样。

10.6匿名内部类

    下面的例子看起来有点奇怪

//:innerclasses/Parcel7.java
public class Parcel7 {public Contents contents() {return new Contents() {private int i = 11;publie int value() { return i; }};} public static void main(String[] args) {Parcel7 p= new Parcel7();Contents c = p.contents();}
}

     contents()方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个Contents对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。”
     这种奇怪的语法指的是:“创建一个继承自Contents的匿名类的对象。”通过new表达式返回的引用被自动向上转型为对Contents的引用。上述匿名内部类的语法是下述形式的简化形式:

//:innerclasses/Parce17b.java
public class Parcel7b {class HyContents impiements Contents {private int i = 11;public int value() { return i ; }}public Contents contents() { return new MyContents(); }public static void main(String[] args) {Parcel7b p = new Parcel7b();Contents c = p.contents();}
}

    在这个匿名内部类中,使用了默认的构造器来生成Contents。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:

//:innerclasses/Parce18.java
public class Parce18 {public wrapping wrapping(int x) {return new Wrapping(x) {public int value() {return super.value() * 47;}};}public static void main(string[] args) {Parcel8 p = new Parcel8():Wrapping w = p.wrapping(10);}
}

    只需简单地传递合适的参数给基类的构造器即可,这里是将x传进new Wrapping(X)。尽管Wrapping只是一个具有具体实现的普通类,但它还是被其导出类当作公共“接口”来使用:

//:innerclasses/wrapping.java
public class wrapping {private int i;public Wrapping(int x){ i = x; }public int value(){ return i; }
}

    你会注意到,Wrapping拥有一个要求传递一个参数的构造器,这使得事情变得更加有趣了。在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的。
       在匿名类中定义字段时,还能够对其执行初始化操作:

//:innerclasses/Parcel9.java
public class Parcel9 {public Destination destination(final string dest) {return new Destination() {private string label = dest;public string readLabel() { return label; } };}public static void main(string[] args) {Parcel9 p =new Parcel9();Destination d = p.destination ( "Tasmania") ;}
}

     如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的,就像你在destination()的参数中看到的那样。如果你忘记了,将会得到一个编译时错误消息。
     如果只是简单地给一个字段赋值,那么此例中的方法是很好的。但是,如果想做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!)但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:

//:innerclasses/Anonymousconstructor.java
import static net.mindview.util.Print.*;
abstract class Base {public Base(int i) {print("Base constructor. i = " + i ) ;}public abstract void f();
}
public class Anonymousconstructor {public static Base getBase(int i) {return new Base(i) {{ print ( "Inside instance initializer"); } public void f() {print ("In anonymous f()");}};}public static void main(string[] args) {Base base = getBase(47);base.f();}
}*output :
Base constructor. i =47
Inside instance initializer
In anonymous f()

      在此例中,不要求变量i一定是final的。因为i被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用
     下例是带实例初始化的“parcel”形式。注意destination()的参数必须是final的,因为它们是在匿名类内部使用的。

//:innerclasses/Parcel10.java
public class Parcel10 {public Destinationdestination(final String dest, final float price) {return new Destination() { private int cost;{cost =Math.round(price);if(cost > 100)System.out.println("Over budget! ");}private String label = dest;public String readLabel() { return 1abel; }};}public static void main(String[] args) {Parce10 p = new Parce11();Destination d = p.destination ( "Tasmania" , 101.395F);}
}*output:
Over budget!

     在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是if语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制——你不能重载实例初始化方法,所以你仅有一个这样的构造器
      匿名内部类与正规的继承相比有些受限因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口

10.6.1再访工厂方法

看着在使用匿名内部类时,interfaces/Factories.java示例变得多么美好呀

//:innerclasses/Factories.java
import static net.mindview.util.Print.* ;
interface Service {void method1();void method2();
}
interface serviceFactory {Service getService();
}
class Implementation1 implements Service {private Implementationio {}public void method1() { print( " Implementation1 method1");}public void method2() { print( " Implementation1 method2");}public static ServiceFactory factory =new ServiceFactory() {public Service getservice() {return new Implementation1o ;};
}
class Implementation2 implements service {private Implementation2() {}public void method1() { print("Implementation2 method1");}public void method2() { print("Implementation2 method2");}public static Servicefactory factory =new ServiceFactory() {public Service getservice() {return new Implementation2();};
)
public class Factories ipublic static void serviceConsumer(ServiceFactory fact){Service s = fact.getservice();s.method1();s.method2();}public static void main(String[] args) {serviceConsumer( Implementation1.factory ) ;serviceConsumer( Implementation2.factory ) ;}
}Output:
implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2

     现在用于Implementation1和Implementation2的构造器都可以是private的,并且没有任何必要去创建作为工厂的具名类。另外,你经常只需要单一的工厂对象,因此在本例中它被创建为Service实现中的一个static域。这样所产生语法也更具有实际意义。
      interfaces/Games.java示例也可以通过使用匿名内部类来改进:

//: innerclasses/Games. java
import static net.mindview.uti1.Print .* ;interface Game { boolean move(); }
interface GameFactory { Game getGame(); }class Checkers implements Game {private Checkers() {}private int moves = 0;private static final int HOVES = 3;public boolean move() {print ( "Checkers move " +moves) ;return ++moves !=HOVES;}public static GameFactory factory = new Gamefactory(){public Game getGame() { return new checkers(); }};
}
class chess implements Game {private Chess() {}private int moves = 0;private static final int HOvEs = 4 ;public boolean move() {print ("chess move " + moves);return ++moves != MOVES;}public static GameFactory factory = new GameFactory() {public Game getGame() { return new Chess(); }};
}
public class Games {public static void playGame (GameFactory factory) {Game s = factory.getGame() ;while(s.move());}public static void main(String[] args) {playGame(Checkers.factory) :playGame(Chess.factory );}
}output:
Checkers move 0
Checkers move 1
Checkers move 2
Chess move 0
Chess move 1
Chess move 2
Chess move 3

请记住在第9章最后给出的建议:优先使用类而不是接口。如果你的设计中需要某个接口,你必须了解它。否则,不到迫不得已,不要将其放到你的设计中。

10.7嵌套类

     如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static这通常称为嵌套类°。想要理解static应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而,当内部类是static的时,就不是这样了。嵌套类意味着:

  1. 要创建嵌套类的对象,并不需要其外围类的对象
  2. 不能从嵌套类的对象中访问非静态的外围类对象

嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西:

//: innerclasses/Parcel11.javapublic class Parcel11 {private static class ParcelContents implements Contents {private int i = 11;public int value() { return i; }}protected static class ParcelDestination implements Destination {private string label ;private ParceiDestination(string whereTo){label = whereTo;}public string readLabel() { return label ; }public static void f() {}static int x = 10;static class AnotherLevel {public static void f() {}static int x = 10;} }public static Destination destination(String s){return new ParcelDestination(s) ;}public static contents contents() {return new Parcelcontents();}public static void main(string[] args) {Contents c = contents();Destination d = destination ( "Tasmania" );}
}

     在main()中,没有任何Parcel11的对象是必需的;而是使用选取static成员的普通语法来调用方法——这些方法返回对Contents和Destination的引用。
      就像你在本章前面看到的那样在一个普通的(非static)内部类中,通过一个特殊的this引用可以链接到其外围类对象。嵌套类就没有这个特殊的this引用,这使得它类似于一个static方法。

10.7.1接口内部的类

    正常情况下,不能在接口内部放置任何代码,但嵌套类可以作为接口的一部分你放到接口中的任何类都自动地是publicstatic的。因为类是static的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口,就像下面这样:

//:innerclasses/classInInterface.javapublic interface classInInterface {void howdy();class Test implements classInInterface {public void howdy() {System.out.printin("Howdy ! ");}public static void main(String[] args) {new Test().howdy();}}
}*output:
Howdy !

    如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便
      我曾在本书中建议过,在每个类中都写一个main()方法,用来测试这个类。这样做有一个缺点,那就是必须带着那些已编译过的额外代码。如果这对你是个麻烦,那就可以使用嵌套类来放置测试代码。

//:innerclasses/TestBed.javapublic class TestBed {public void f(){ System.out.println("f()");} public static class Tester {public static void main(String[] args){TestBed t = new TestBed();t.f();}}
}*Output:
f()

    这生成了一个独立的类TestBed$Tester(要运行这个程序,执行java TestBed$Tester即可,在Unix/Linux系统中必须转义$)。可以使用这个类来做测试,但是不必在发布的产品中包含它,在将产品打包前可以简单地删除TestBed$Tester.class。

10.7.2从多层嵌套类中访问外部类的成员

     一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外围类的所有成员,如下所示:
 

//:innerc1asses/HultiNestingAccess.javaclass MNA{private void f(){}class A {private void g(){public class B{void h(){ g();f();}}}
}
public class HultiNestingAccess {public static void main(Atring[] args) {MNA mna = new HNA();HNA.A mnaa = mna.new A() ;HNA.A.B mnaab =mnaa.new B():mnaab.h();}
}

    可以看到在MNA.A.B中,调用方法g()和f()不需要任何条件(即使它们被定义为private)。这个例子同时展示了如何从不同的类里创建多层嵌套的内部类对象的基本语法。".new”语法能产生正确的作用域,所以不必在调用构造器时限定类名。

10.8为什么需要内部类

       至此,我们已经看到了许多描述内部类的语法和语义,但是这并不能回答“为什么需要内部类”这个问题。那么,Sun公司为什么会如此费心地增加这项基本的语言特性呢?
        一般说来,内部类继承自某个类或实现某个接口内部类的代码操作创建它的外围类的对象所以可以认为内部类提供了某种进入其外围类的窗口
        内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做。”那么内部类实现一个接口与外围类实现这个接口有什么区别呢?答案是:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以,使用内部类最吸引人的原因是:
       每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
      如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(译注:类或抽象类)。
      为了看到更多的细节,让我们考虑这样一种情形:即必须在一个类中以某种方式实现两个接口。由于接口的灵活性,你有两种选择:使用单一类,或者使用内部类;
 

//:innerclasses/MultiInterfaces.java
package innerclasses;interface A {}
interface B {}class X implements A,B {}class Y implements A {B makeB() {return new B() {};}
}public class HultiInterfaces {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());}
}

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

//: innerclasses/Multi Implementation.java
package innerclasses;class D {}
abstract class E {}
class Z extends D {E makeE() { return new E() {}; }
}public class Nulti1mplementation {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”关系﹔它就是一个独立的实体

      举个例子,如果Sequence.java不使用内部类,就必须声明“Sequence是一个Selector”,对于某个特定的Sequence只能有一个Selector。然而使用内部类很容易就能拥有另一个方法reverseSelector(),用它来生成一个反方向遍历序列的Selector。只有内部类才有这种灵活性。

10.8.1闭包与回调

     闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括private成员。
     Java最引人争议的问题之一就是,人们认为Java应该包含某种类似指针的机制,以允许回调(callback)。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的
对象。稍后将会看到这是一个非常有用的概念。如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用该指针。然而,读者应该已经了解到,Java更小心仔细,所以没有在语言中包括指针。
     通过内部类提供闭包的功能是优良的解决方案,它比指针更灵活、更安全。见下例:
 

//:innerclasses/callbacks.java
package innerclasses;
import static net.mindview.util.Print.* ;interface Incrementable {void increment();
}class Callee1 implements Incrementable {private int i = 0;public void increment() {i++;print(i);} 
}class MyIncrement {public void increment() { print( "Other operation"); }static void f(MyIncrement mi) { mi.increment(); }
}class Callee2 extends MyIncrement {private int i =0;public void increment() {super.increment();i++;print(i);}private class closure implements Incrementable {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 calleri =new Caller(c1) ;Caller caller2 =new Caller(c2.getcallbackReference());Caller1.go();Caller1.go();Caller2.go();Caller2.go();}
}*output:
Other operation
1
1
2
Other operation
2
Other operation
3

      这个例子进一步展示了外围类实现一个接口与内部类实现此接口之间的区别。就代码而言,Callee1是简单的解决方式。Callee2继承自MyIncrement,后者已经有了一个不同的increment()方法,并且与Incrementable接口期望的increment()方法完全不相关。所以如果Callee2继承了MyIncrement,就不能为了Incrementable的用途而覆盖increment()方法,于是只能使用内部类独立地实现Incrementable。还要注意,当创建了一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。
      注意,在Callee2中除了getCallbackReference()以外,其他成员都是private的。要想建立与外部世界的任何连接,interface Incrementable都是必需的。在这里可以看到,interface是如何允许接口与接口的实现完全独立的。
      内部类Closure实现了Incrementable,以提供一个返回Callee2的钩子”(hook)——而且是一个安全的钩子。无论谁获得此Incrementable的引用,都只能调用increment(),除此之外没有其他功能(不像指针那样,允许你做很多事情)。
     Caller的构造器需要一个Incrementable的引用作为参数(虽然可以在任意时刻捕获回调引用),然后在以后的某个时刻,Caller对象可以使用此引用回调Callee类。
      回调的价值在于它的灵活性——可以在运行时动态地决定需要调用什么方法。这样做的好处在第22章可以看得更明显,在那里实现GUI功能的时候,到处都用到了回调。

10.8.2内部类与控制框架

     在将要介绍的控制框架(control framework)中,可以看到更多使用内部类的具体例子。应用程序框架( application framework)就是被设计用以解决某类特定问题的一个类一组类要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决你的特定问题。模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模板方法保持不变的事物,而可覆盖的方法就是变化的事物
     控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。在第22章将会看到,Java Swing库就是一个控制框架,它优雅地解决了GUI的问题,并使用了大量的内部类。
      要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请考虑这样一个控制框架,它的工作就是在事件“就绪”的时候执行事件。虽然“就绪”可以指任何事,但在本例中是指基于时间触发的事件。接下来的问题就是,对于要控制什么,控制框架并不包含任何具体的信息。那些信息是在实现算法的action()部分时,通过继承来提供的。

     首先,接口描述了要控制的事件。因为其默认的行为是基于时间去执行控制,所以使用抽象类代替实际的接口下面的例子包含了某些实现:
 

//:innerelasseslcontroller/Event.java
package innerclasses.controlier;public abstract class Event {private long eventTime;protected final long delayTime;public Event(long delayTime) {this.delayTime = delayTime ;start();}public void start() { eventTime = System.nanoTime() +delayTime;}public boolean ready() {return System.nanoTime() >= eventTime;}publie abstract void action();
}

     当希望运行Everit并随后调用start()时,那么构造器就会捕获从对象创建的时刻开始的)时间,此时间是这样得来的:start()获取当前时间,然后加上一个延迟时间,这样生成触发事件的时间。start()是一个独立的方法,而没有包含在构造器内,因为这样就可以在事件运行以后重新启动计时器,也就是能够重复使用Event对象。例如,如果想要重复一个事件,只需简单地在action()中调用start()方法。
    ready()告诉你何时可以运行action()方法了。当然,可以在导出类中覆盖ready()方法,使得Event能够基于时间以外的其他因素而触发。
    下面的文件包含了一个用来管理并触发事件的实际控制框架。Event对象被保存在List<Event>类型(读作“Event的列表”)的容器对象中,容器会在第11章中详细介绍。目前读者只需要知道add()方法用来将一个Object添加到List的尾端size()方法用来得到List中元素的个数,foreach语法用来连续获联List中的Eventremove()方法用来从List中移除指定的Event
 

//:innerclasses/controller/Controller.java
package_ innerclasses.controller ;
import java.util.*;public class Controller {private List<Event> eventList =new ArrayList<Event>():public void addEvent(Event c) { eventList.add(c); }public void run() {while (eventList.size()>0)for(Event e : new ArrayList<Event>(eventList))if(e.ready()) {System.out.println(e):e.action();eventList.remove(e);}}
}

   run()方法循环遍历eventList,寻找就绪的(ready())、要运行的Event对象。对找到的每一个就绪的(ready())事件,使用对象的toString()打印其信息,调用其action()方法,然后从队列中移除此Event。

    注意,在目前的设计中你并不知道Event到底做了什么。这正是此设计的关键所在,“使变化的事物与不变的事物相互分离”。用我的话说,“变化向量”就是各种不同的Event对象所具有的不同行为,而你通过创建不同的Event子类来表现不同的行为。
这正是内部类要做的事情,内部类允许:

  1. 控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装了起来。内部类用来表示解决问题所必需的各种不同的action()。
  2. 内部类能够很容易地访问外围类的任意成员,所以可以避免这种实现变得笨拙。如果没有这种能力,代码将变得令人讨厌,以至于你肯定会选择别的方法。

    考虑此控制框架的一个特定实现,如控制温室的运作:控制灯光、水、温度调节器的开关,以及响铃和重新启动系统,每个行为都是完全不同的。控制框架的设计使得分离这些不同的代码变得非常容易。使用内部类,可以在单一的类里面产生对同一个基类Event的多种导出版本。对于温室系统的每一种行为,都继承一个新的Event内部类,并在要实现的action()中编写控制代码。
     作为典型的应用程序框架,GreenhouseControls类继承自Controller:

//:innerclasses/GreenhouseControls.java
import innerclasses.controlier.*;public class GreenhouseControls extends Controller {private boolean light = false;public class Lighton extends Event {public LightOn (long delayTime) { super(delayTime); }public void action() {light = true;}public string toString() { return "Light is on"; }}public class Lightoff extends Event {public LightOff(long delayTime) { super(delayTime); }public void action() {light = false;}public string toString() { return "Light is off": }}private boolean water = false :public class waterOn extends Event {public Wateron(long delayTime) { super(delayTime); }public void action() {water = true;}public string toString() {return "Greenhouse water is on" ;}}public class wateroff extends Event {public waterOff(long delayTime) { super(delayTime);}public void action() {water =false;}public string toString() {return "Greenhouse water is off";}}private string thermostat ="Day" ;public class ThermostatNight extends Event {public ThermostatNight (long delayTime) {super (delayTime);}public void action() {thermostat ="Night" ;}public string toString( {return "Thermostat on night setting" ;}}public class ThermostatDay extends Event {public ThermostatDay (long delayTime) {super (delayTime);}public void action() {thermostat = "Day" ;}public String toString( {return "Thermostat on day setting";}}public class Bell extends Event {public Bell (long delayTime) { super (delayTime); }public void action() {addEvent(new Beli(delayTime));}public string toString() { return "Bing!"; }}public class Restart extends Event {private Event[] eventList;public Restart (long delayTime,Event[] eventList) {super (delayTime) ;this.eventiist = eventList ;for (Event e : eventList)addEvent(e);}public void action() {for(Event e : eventList){e.start();addEvent(e);}start();addEvent(this);}public string toString() {return "Restarting system " ;}}public static class Terminate extends Event {public Terminate (long delayTime) { super(delayTime);} public void action() { System.exitc(0);} public string toString() { return"Terminating" ; }}
}

      注意,light、water和 thermostat都属于外围类GreenhouseControls,而这些内部类能够自由地访问那些字段,无需限定条件或特殊许可。而且,action()方法通常都涉及对某种硬件的控制。
     大多数Event类看起来都很相似,但是Bell和Restart则比较特别。BelI控制响铃,然后在事件列表中增加一个Bell对象,于是过一会儿它可以再次响铃。读者可能注意到了内部类是多么像多重继承:Bell和Restart有Event的所有方法,并且似乎也拥有外围类GreenhouseContrlos的所有方法。
     一个由Event对象组成的数组被递交给Restart,该数组要加到控制器上。由于Restart()也是一个Event对象,所以同样可以将Restart对象添加到Restart.action()中,以使系统能够有规律地重新启动自己。
    下面的类通过创建一个GreenhouseControls对象,并添加各种不同的Event对象来配置该系统。这是命令设计模式的一个例子在eventList中的每一个被封装成对象的请求:
 

//:innerclasses/GreenhouseController.java
import innerclasses.controller.* ;public class Greenhousecontroller {public static void main(string[] args) {Greenhousecontrols gc = new Greenhousecontrols();gc.addEvent(gc.new Be1l(900));Event[]eventList = {gc.new ThermostatNight(0),gc.new Lighton (200),gc.new Lightoff (400),gc.new wateron (600),gc.new wateroff (800) ,gc.new ThermostatDay (1400)};gc.addEvent(gc.new Restart(2000,eventList));if(args.length == 1)gc.addEvent(new Greenhousecontrols.Terminate (new Integer(args [0])));gc.run();}
}*output:
Bing!
Thermostat on night setting
Light is on
Light is off
Greenhouse water is on
Greenhouse water is off
Thermostat on day setting
Restarting system
Terminating

     这个类的作用是初始化系统,所以它添加了所有相应的事件。Restart事件反复运行,而且它每次都会将eventList加载到GreenhouseControls对象中。如果提供了命令行参数,系统会以它作为毫秒数,决定什么时候终止程序(这是测试程序时使用的)。当然,更灵活的方法是避免对事件进行硬编码,取而代之的是从文件中读取需要的事件(第12章的练习会要求读者照此方法修改这个例子)。
     这个例子应该使读者更了解内部类的价值了,特别是在控制框架中使用内部类的时候。在第18章中,读者将看到内部类如何优雅地描述图形用户界面的行为。到那时,读者应该就完全信服内部类的价值了。

10.9内部类的继承

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

//:innerclasses/InheritInner.javaclass WithInner {class Inner {}
}public class InheritInner extends withInner.Inner //!InheritInner() {}InheritInner (withInner wi) {wi.super();}public static void main(String[] args){WithInner wi = new WithInner();InheritInner ii = new InheritInner(wi);}
}

    可以看到,InheritInner只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外,必须在构造器内使用如下语法:

enclosingClassReference.super():

这样才提供了必要的引用,然后程序才能编译通过 

10.10内部类可以被覆盖吗

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

//:innerclasses/BigEgg.java
import static net.mindview.util.Print.*;class Egg {private Yolk y;protected class Yolk {public Yolk() { print ("Egg.Yolk()");}}public Egg() {print ("New Egg()");y =new Yolk();}
}
public class BigEgg extends Egg {public class Yolk {public Yolk() { print ("BigEgg.Yolk("); }}public static void main(String[] args){new BigEgg();}
}Output :
New Egg()
Egg.Yolk()

   默认的构造器是编译器自动生成的,这里是调用基类的默认构造器。你可能认为既然创建了BigEgg的对象,那么所使用的应该是“覆盖后”的Yolk版本,但从输出中可以看到实际情况并不是这样的。
     这个例子说明,当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。当然,明确地继承某个内部类也是可以的:

//:innerclasses/BigEgg2.java
import static net.mindview.util.Print.*;class Egg2 {protected class Yolk {public Yolk() ( print("Egg2.Yolk("); }public void f() { print ("Egg2.Yoik.f()") ; }}private Yolk y = new Yolk();pub1ic Egg2() { print( "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() { print("BigEgg2.Yolk()");}public void f() { print("B1gEgg2.Yoik.f()"); }}public BigEgg2(){ insertYolk(new Yolk()); }public static void main(String[] args) {Egg2 e2 = new BigEgg2(;e2.g();}
}Output :
Egg2.Yolk()
NewEgg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()

   现在BigEgg2.Yolk通过extends Egg2.Yolk明确地继承了此内部类,并且覆盖了其中的方法。insertYolk()方法允许BigEgg2将它自己的Yolk对象向上转型为Egg2中的引用y。所以当g()调用y.f()时,覆盖后的新版的fO被执行。第二次调用Egg2.Yolk(),结果是BigEgg2.Yolk的构造器调用了其基类的构造器。可以看到在调用g()的时候,新版的f()被调用了。

10.11局部内部类

     前面提到过,可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符因为它不是外围类的一部分,但是它可以访问当前代码块内的常量,以及此外围类的所有成员下面的例子对局部内部类与匿名内部类的创建进行了比较。

//:innerclasses/LocalInnerciass.java1
import static net.mindview.util.Print.*;interface Counter {int next();
}public class LocalInnerClass {private int count = 0;Counter getCounter(final String name){ class LocalCounter implements Counter {public Localcounter() {print("Localcounter()");}public int next() {printnb(name); return count++;}}return new LocalCounter();}Counter getcounter2(final string name) {return new Counter() {{print ("counter()");}public int next() {printnb(name) ; return count++;}};}public static void main(Stringn args) {LocalInnerclass lic = new LocalInnerciass();Counterc1 = lic.getCounter ("Local inner  ") .c2 = lic.getCounter2 (" Anonymous inner ");for (int i = 0; i < 5; i++)print(c1.next());for(int i = 0; i < 5; 1++)print(c2.next());}
}Output:
LocalCounter()
Counter()
Local inner 0
Loeal inner 1
Local inner 2
Local inner 3
Local inner 4
Anonymous inner 5
Anonymous inner 6
Anonymous inner 7
Anonymous inner 8
Anonymous inner 9

     Counter返回的是序列中的下一个值。我们分别使用局部内部类匿名内部类实现了这个功能,它们具有相同的行为和能力。既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能用于实例初始化
    所以使用局部内部类而不使用匿名内部类的另一个理由就是:需要不止一个该内部类的对象

10.12内部类标识符

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

counter.class
LocalInnerclass$1.class
Localinnerciass$1LocalCounter.class
LocalInnerclass.class

    如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外围类标识符与“$”的后面。
     虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况°。因为这是Java的标准命名方式,所以产生的文件自动都是平台无关的。(注意,为了保证你的内部类能起作用,Java编译器会尽可能地转换它们。)
 

相关文章:

  • IMX675-AAQR-C 索尼图像传感器 属于索尼 Starvis 2 系列,主打 高灵敏度、低噪声,适用于工业检测、安防监控、机器视觉等场景 提供数据手册
  • 从零开始学Python游戏编程40-碰撞处理2
  • fps项目总结:生成武器子弹丧尸攻击
  • pyinstaller打包paddleocr发生错误解决
  • 【5】GD32 基础通信外设:USART、I2C、SPI
  • 正则表达式三剑客之——awk命令
  • OCR(Optical Character Recognition),光学字符识别
  • 使用 Python 项目管理工具 uv 快速创建 MCP 服务(Cherry Studio、Trae 添加 MCP 服务)
  • 通道降维方式
  • 一款好的私有云产品推荐——优刻得私有云(UCloudStack Pro)产品白皮书
  • 单机无穷大系统暂态稳定性仿真Matlab模型
  • 数据库-子查询、关联查询 和 TCL 语言
  • 智慧医疗领域TMI期刊2025年3月研究热点解析
  • 嵌入式:Linux系统应用程序(APP)启动参数及其规则详解
  • 【网络入侵检测】基于源码分析Suricata的PCAP模式
  • 计算器(WEB)
  • 流动式起重机Q2证考试有哪些科目?
  • C++与Python编写二进制转十进制
  • 机器人行业研究系列报告
  • 方案精读:77页2024 集团企业IT技术架构规划方案【附全文阅读】
  • 在县中,我看到“走出去”的渴望与“留下来”的惯性
  • 临沂文旅集团被诉侵权,原告每年三百余起类案
  • 我国翻译从业人员达680.8万人,行业总需求仍在上升
  • 安徽临泉一小区交付后多楼层现裂缝,专家组论证称不影响安全
  • 载人登月总体进展顺利
  • 神二十瞄准明日17时17分发射