您的位置:首页 > 其它

第十章 内部类(上)

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

               return label;

          }

     }

     public void ship(String dest){

          Contents c = new Contents();

          Destination d = new Destination(dest);

          System.out.println(d.readLable());

     }

     public static void main(String[] args) {

          Parcel1 p = new Parcel1();

          p.ship("Tasmaina");

     }

    运行结果:Tasmaina

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

          Parcel2 q = new Parcel2();

          Parcel2.Contents c = q.contents();

          Parcel2.Destination d = q.to("Borneo");

     }
}

运行结果:Tasmaina

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

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

     Object current();

     void next();

}

public class Sequence {

     private Object [] items;

     private int next = 0;

     public Sequence(int size){

          items = new Object[size];

     }

     public void add(Object x){

          if(next<items.length){

               items[next++]=x;

          }

     }

     private class SequenceSelector implements Selector{

          private int i = 0;

          @Override

          public boolean end(){

               return i==items.length;

          }

          @Override

          public Object current() {

               return items[i];

          }

          @Override

          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();

          }

     }
}
       运行结果:0 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:
     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 inner = dt.inner();

          inner.outer().f();

     }
}
     运行结果:DotThis.f()

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

     public static void main(String[] args) {

          DotNew dn=new DotNew();

          DotNew.Inner inner = dn.new Inner();

     }
}

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

          Parcel3.Contents contents = p.new Contents();

          Parcel3.Destination destination = p.new Destination("Tasmania");

     }
}
 
10.4  内部类与向上转型
       当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类——某个接口的实现——能够完全不可见,而且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。
       我们可以创建前一个示例的接口:
     public interface Destination {
     String readLabel();
}
       
     public interface Contents {
     int value();
}

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

          private int i=11;

          @Override

          public int value() {

               return i;

          }

     }

     protected class PDestination implements Destination{

          private String label;

          private PDestination(String whereTo){

               label=whereTo;

          }

          @Override

          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");

//         The type Parcel4.PContents is not visible

//         p.new PContents();

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

10.5   在方法和作用域内的内部类
       到目前为止,读者所看到的只是内部类的典型用途。通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,简单并且容易理解。然而,内部类的语法覆盖了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作用域内定义内部类。这么做有两个理由:
如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。
      在后面的例子中,先前的代码将被修改,以用来实现:
一个定义在方法中的类。
一个定义在作用域内的类,此作用域在方法的内部。
一个实现了接口的匿名类。
一个匿名类,它扩展了有非默认构造器的类。
一个匿名类,他执行字段初始化。
一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)        
    第一个例子展示了在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。这被称作局部内部类:
     public class Parcel5 {
     public Destination destination(String s){

          class PDestination implements Destination{

               private String label;

               private PDestination(String whereTo){

                    label=whereTo;

               }

               @Override

               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,这并不会有命名冲突。
       下面的例子展示了如何在任意的作用域内嵌入一个内部类:
     public class Parcel6 {
     private void internalTracking(boolean b){

          if(b){

               class TrackingSlip{

                    private String id;

                    TrackingSlip(String s){

                         id=s;

                    }

                    String getSlip(){

                         return id;

                    }

               }

               TrackingSlip ts = new TrackingSlip("slip");

               String s = ts.getSlip();

          }

//          Can't use it here! Out of scope:

//          !TrackingSlip ts = new TrackingSlip("slip");

     }

     public void track(){

          internalTracking(true);

     }

     public static void main(String[] args) {

          Parcel6 p = new Parcel6();

          p.track();

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

10.6  匿名内部类
        下面的例子看起来有点奇怪:
     public class Parcel7 {
     public Contents contents(){

          return new Contents(){

               private int i=11;

               public int value(){

                    return i;

               }

          };

     }

     public static void main(String[] args) {

          Parcel7 p = new Parcel7();

          Contents c = p.contents();

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

          private int i=11;

          @Override

          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。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:
     public class Parcel8 {
     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只是一个具有具体实现的普通类,但它还是被其导出类当作公共“接口”来使用:
     public class Wrapping {
     private int i;

     public Wrapping(int x){

          i=x;

     }

     public int value(){

          return i;

     };
}
       你会注意到,Wrapping拥有一个要求传递一个参数的构造器,这使得事情变得更加有趣了。
        在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用分号是一致的。
       在匿名类中定义字段时,还能够对其执行初始化操作:
     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()的参数中看到的那样。如果你忘记了,将会得到一个编译时错误的消息。 
       如果只是简单地给一个字段赋值,那么此例中的方法是很好的。但是,如果想做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没有名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:
     abstract class Base{
     public Base(int i){

          System.out.println("Base constructor. i="+i);

     }

     public abstract void f();

}

public class AnonymousConstructor {

     public static Base getBase(int i){

          return new Base(i){

               {System.out.println("Inside instance initializer");}

               public void f(){

                    System.out.println("In anonymous f()");

               }

          };

     }

public static void main(String[] args) {

     Base base = getBase(47);

     base.f();

}
}
   运行结果:
Base constructor. i=47
Inside instance initializer
In anonymous f()

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

               @Override

               public String readLabel() {

                    return label;

               }

          };

     }

public static void main(String[] args) {

     Parcel10 parcel10 = new Parcel10();

     parcel10.destination("Tasmania", 101.395f);

}
}
运行结果:Over budget!

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

10.6.1    再访工厂方法
        看看在使用匿名内部类时,interfaces/Factories.java示例变得多么美妙呀:
     interface Service {
     void method1();

     void method2();

}

interface ServiceFactory{

     Service getService();

}

class Implementation1 implements Service{

     private Implementation1(){}

     @Override

     public void method1() {

          System.out.println("Implementation1.method1()");

     }

     @Override

     public void method2() {

          System.out.println("Implementation1.method2()");

     }

     public static ServiceFactory factory=new ServiceFactory() {

          @Override

          public Service getService() {

               return new Implementation1();

          }

     };

}

class Implementation2 implements Service{

     private Implementation2(){}

     @Override

     public void method1() {

          System.out.println("Implementation2.method1()");

     }

     @Override

     public void method2() {

          System.out.println("Implementation2.method2()");

     }

     public static ServiceFactory factory=new ServiceFactory() {

          @Override

          public Service getService() {

               return new Implementation2();

          }

     };

}

public class Factories {

     public 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);

}
}
运行结果:
Implementation1.method1()

Implementation1.method2()

Implementation2.method1()
Implementation2.method2()
     

        现在用于Implementation1和Implementation2的构造器都可以是private的,并且没有任何必要去创建作为工厂的具名类。另外,你经常只需要单一的工厂对象,因此在本例中它被创建为Service实现中的一个static域。这样所产生语法也更具有实际意义。
     interface Game{
     boolean move();

}

interface GameFactory{

     Game getGame();

}

class Checkers implements Game{

     private Checkers(){}

     private int moves=0;

     private static final int MOVES=3;

     @Override

     public boolean move() {

          System.out.println("Checkers move"+moves);

          return ++moves!=MOVES;

     }

     public static GameFactory factory=new GameFactory() {

          @Override

          public Game getGame() {

               return new Checkers();

          }

     };

}

class Chess implements Game{

     private Chess(){}

     private int moves=0;

     private static final int MOVES=4;

     @Override

     public boolean move() {

          System.out.println("Chess move"+moves);

          return ++moves!=MOVES;

     }

     public static GameFactory factory=new GameFactory() {

          @Override

          public Game getGame() {

               return new Chess();

          }

     };

}

public class Games {

     public static void playGame(GameFactory factory){

          Game game = factory.getGame();

          while(game.move())

               ;

     }

     public static void main(String[] args) {

          playGame(Checkers.factory);

          playGame(Chess.factory);

     }
}
运行结果:
Checkers move0

Checkers move1

Checkers move2

Chess move0

Chess move1

Chess move2
Chess move3

     请记住在第九章最后给出的建议:优先使用类而不是接口。如果你的设计中需要某个接口,你必须了解它。否则,不到迫不得已,不要将其放到你的设计中。
      
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息