目录
创建内部类
到外部类的链接
使用.this和.new
内部类和向上转型
在方法和作用域中的内部类
匿名内部类
嵌套类
接口中的类
从多嵌套的内部类中访问外部人员
本笔记参考自: 《On Java 中文版》
定义在另一个类中的类称为内部类。利用内部类,将逻辑上存在关联的类组织在一起,并且可以控制一个类在另一个类中的可见性。
创建内部类
创建内部类的方式就是把类定义在一个包围它的类中。
public class Parcel_1 {class Contents {private int i = 1;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) {Parcel_1 p = new Parcel_1();p.ship("一串字符串");}
}
程序执行,输出:一串字符串 。
在上述程序中,ship()方法进行了内部类对象的创建,这与使用普通类并无什么区别。除了这种使用内部类的方式外,在外部类中设置一个方法,用来返回一个指向内部类的引用,这种形式也很常见:
public class Parcel_2 {class Contents {private int i = 1;public int value() {return i;}}class Destination {private String label;Destination(String whereTo) {label = whereTo;}String readLabel() {return label;}}public Destination toDest(String s) {return new Destination(s);}public Contents toCon() {return new Contents();}public void ship(String dest) {Contents c = toCon();Destination d = toDest(dest);System.out.println(d.readLabel());}public static void main(String[] args) {Parcel_2 p_1 = new Parcel_2();p_1.ship("第二串字符串");Parcel_2 p_2 = new Parcel_2();// 定义指向内部类的引用Parcel_2.Contents c = p_2.toCon();Parcel_2.Destination d = p_2.toDest("这是一个输入");}
}
在外部类的非静态方法之外的任何地方创建内部类的对象,其对象类型的指定需要遵循以下格式:
OuterClassName.InnerClassName
到外部类的链接
对于一个负责创建内部类对象的特定外围类对象而言,内部类对象会获得一个隐藏的指向外围类的引用。
当创建一个内部类时,这个内部类的对象中会隐含一个链接,这个链接用于创建该对象的外围对象。通过这一链接,无需任何条件就可以直接访问外围对象的成员。除此之外,内部类还拥有对外围对象所有元素的访问权。
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;@Overridepublic boolean end() {return i == items.length;}@Overridepublic Object current() {return items[i];}@Overridepublic 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();}System.out.println();}
}
程序执行的结果是:
通过sequence中的每一个对象,可以使用Selector接口。这就是一个迭代器设计模式的例子。因为Selector是一个接口,其他类可以使用自己的方式去实现这一接口,而其他方法可以通过Selector这个接口去创建更加通用的代码。
注意,上述程序中,private字段items并不是内部类SequenceSelector的一部分,但是内部类的end()、current()和next()方法都使用到了该引用。这就是因为内部类可以访问外围对象的所有方法和字段。
使用.this和.new
要在内部类中生成外部类对象的引用,可以使用 外部类的名字+.this :
public class DotThis {void f() {System.out.println("这是外部类DoThis的f()");}public class Inner {public DotThis outer() {return DotThis.this;// 若直接使用this,得到的是一个Inner类的引用}}public Inner inner() {return new Inner();}public static void main(String[] args) {DotThis dt = new DotThis();DotThis.Inner dti = dt.inner();dti.outer().f();}
}
程序执行,输出:这是外部类DoThis的f() 。
若要创建内部类的对象,我们还需要使用其外部类的对象。此时会使用到.new语法:
public class DotNew {public class Inner {}public static void main(String[] args) {DotNew dn = new DotNew();DotNew.Inner dni = dn.new Inner();}
}
通过这种方式,解决了内部类的名字作用域问题。也因此,不需要使用 dn.new DotNew.Inner() 这种更加冗余的方式(不过这种方式也确实不被允许使用)。
.new的使用例(部分代码重复多次,因此这次放入图片):
内部类的对象会隐式地连接到用于创建它的外部类对象。
在之后会出现,嵌套类(static修饰的内部类)不需要指向外部类对象的引用。
内部类和向上转型
内部类在进行向上转型,特别是转型为接口时有其独特的优势。因为内部类(即接口的实现)对外部而言是不可见、不可用的,这会方便隐藏实现:外部类只会获得一个指向基类或接口的引用。
还是引用之前的例子,假设现在存在两个接口Destination和Contents:
正如图中所示的,这两个接口可以让客户程序员进行使用。若客户程序员得到的是一个指向这些接口(指向基类同理)的引用,那么他们就无法从这个引用中得知其确切的类型:
class Parcel_4 {private class PContents implements Contents { // 访问权限为private,无法从外部直接访问private int i = 1;@Overridepublic int value() {return i;}}protected final class PDestination implements Destination {private String label;private PDestination(String whereTo) {label = whereTo;}@Overridepublic 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) {Parcel_4 p = new Parcel_4();Contents c = p.contents();Destination d = p.destination("这是第四串字符串");// 注意:不能访问private类// Parcel_4.PContents pc = p.new PContents();}
}
在Parcel_4中,内部类PContents是private的,这表示只有Parcel_4有权对其进行访问。另外,PDestination是protected的,这表示其的访问权限同样是受限的。
不能向下转型为访问权限是private的内部类(若无继承关系,也无法向下转型为protected的内部类)。
private内部类为类的设计者提供了一种方式,这种方式可以完全阻止任何与类型有关的编码依赖,并且可以完全隐藏实现细节。
在方法和作用域中的内部类
内部类可以在一个方法或是任何一个作用域内创建。有两个理由支持这种做法:
- 像上述例子中展示的,需要实现某种接口,以便创建和返回一个引用;
- 为解决一个复杂问题,在自己的解决方案中创建了一个类用于辅助,但不希望这个类被公开。
局部内部类
修改之前的例子,现在创建一个局部内部类。这种类是一个完整的类,它存在于一个方法的作用域中:
public class Parcel_5 {public Destination destination(String s) {final class PDestination implements Destination {private String label;private PDestination(String whereTo) {label = whereTo;}@Overridepublic String readLabel() {return label;}}return new PDestination(s);}public static void main(String[] args) {Parcel_5 p = new Parcel_5();Destination d = p.destination("这也是一个字符串");}
}
在上述程序中,PDestination类是destination()方法的一部分,而不是Parcel_5的一部分。因此,PDestination在destination()外是无法访问的。另外,尽管PDestination类在是destination()进行了定义,但即使destination()方法已经返回,PDestination的对象依旧会是合法的。
在同一子目录下的每一个类中,都可以使用类标识符PDestination来命名内部类,这不会产生命名冲突。
接下来的例子会展示如何如何将内部类嵌入到一个条件判断的作用域中:
public class Parcel_6 {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("可以使用");String s = ts.getSlip();}// 超出if的作用域,无法使用内部类// TrackingSlip ts = new TrackingSlip("不能使用");}public void track() {internalTracking(true);}public static void main(String[] args) {Parcel_6 p = new Parcel_6();p.track();}
}
上述程序中,虽然内部类被布置到了if语句中,但这并不表示这个内部类的创建是有条件的,它会与其他代码一起被编译。
匿名内部类
一个内部类可以是匿名的:这种类通常会与方法返回值的创建结合在一起,在值被返回之前插入一个类的定义。
public class Parcel_7 {// Contents是之前声明的接口,它的方法未被定义public Contents contents() {return new Contents() { // 在进行返回时,插入类的定义private int i = 1;@Overridepublic int value() {return i;}}; // 必要的分号}public static void main(String[] args) {Parcel_7 p = new Parcel_7();Contents c = p.contents();}
}
这段代码看起来是在准备创建一个Contents的对象,但返回值却被插入了一个类的定义:
return new Contents() { // ...
};
这种语法的意思是“创建一个继承自Contents的匿名类的对象”。在这里,通过new表达式返回的引用会被自动向上转型为一个Contents引用。上述的匿名内部类的语法是以下代码的缩写:
---
另外,上面展示的匿名内部类中,Contents是用无参构造器创建的。若基类需要的是一个带有参数的构造器,那么:
首先,这个匿名内部类的基类构造器需要带有参数:
public class Wrapping { // 基类Wrappingprivate int i;public Wrapping(int x) { // 含参构造器i = x;}public int value() {return i;}
}
尽管Wrapping只是一个带有实现的普通类,但它也是其子类的公共“接口”。
然后是匿名内部类的创建:
public class Parcel_8 {public Wrapping Wrapping(int x) {return new Wrapping(x) { // 需要将合适的参数传递给基类构造器@Overridepublic int value() {return super.value() * 12;}}; // 这个分号标记表达式的结束,但它刚好包含了这个匿名类}public static void main(String[] args) {Parcel_8 p = new Parcel_8();Wrapping w = p.Wrapping(10);}
}
上述程序中,return语句末尾的分号标记着表达式的结束,但它并不会标记类体的结束。
---
若正在构建一个匿名类,并且这个匿名类一定需要使用这个匿名类外部定义的对象,此时,编译器会要求被使用的参数引用使用final修饰,或是“实际上的最终变量”(这种变量在初始化后不再改变,因此被视为final)。
public class Parcel_9 {public Destination destination(final String dest) {return new Destination() {private String label = dest;@Overridepublic String readLabel() {return label;}};}public static void main(String[] args) {Parcel_9 p = new Parcel_9();Destination d = p.destination("这里是Parcel_9");}
}
在上述程序中,方法destination()的参数可以不用加上final,但通常会把final写上作为提示。
---
由于匿名类没有名字,所以也不可能有命名的构造器。但如果我们必须对匿名类执行某个类似于构造器的动作,这应该怎么办?借助实例初始化,就可以在效果上为匿名内部类创建一个构造器:
abstract class Base {Base(int i) {System.out.println("这是Base的构造器,i = " + i);}public abstract void f();
}public class AnonymousConstructor {public static Base getBase(int i) {return new Base(i) {{ // 进行实例初始化System.out.println("内部类的实例初始化");}@Overridepublic void f() {System.out.println("匿名类的f()");}};}public static void main(String[] args) {Base base = getBase(10);base.f();}
}
程序执行的结果是:
在这里,传入匿名类的变量i并不一定需要是最终变量,尽管i被传入匿名类的基类构造器,但匿名类内部没有直接使用到它。而下方的程序中,由于匿名类使用了参数,所以被使用的参数必须是最终变量:
public class Parcel_10 {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("太贵了吧!");}private String label = dest;@Overridepublic String readLabel() {return label;}};}public static void main(String[] args) {Parcel_10 p = new Parcel_10();Destination d = p.destination("买什么好呢?", 120);}
}
实例初始化操作中包含了一段if语句,这段if语句不能作为字段初始化的一部分来执行。在效果上,实例初始化部分就是匿名内部类的构造器。但因为我们无法重载实例初始化的部分,所以只能有一个这样的构造器。
与普通的继承相比,匿名构造器只能扩展一个类,或是实现一个接口,且二者不能兼得。
嵌套类
将内部类设置为static的,这就变成了嵌套类。这种类不同与普通的匿名类:
- 不需要一个外部类对象来创建嵌套类对象;
- 无法从嵌套类对象内部访问非static的外部类对象。
除此之外,嵌套类内部还能存放其他嵌套类,或者static数据及static字段。这些是普通内部类无法做到的:
public class Parcel_11 {// 嵌套类:带有static的内部类private static class ParceContents implements Contents {private int i = 1;@Overridepublic int value() {return i;}}protected static final class ParceDestination implements Destination {private String label;private ParceDestination(String whereTo) {label = whereTo;}@Overridepublic 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 ParceDestination(s);}public static Contents contents() {return new ParceContents();}public static void main(String[] args) {Contents c = contents();Destination d = destination("不知道写什么,随便写点");}
}
普通内部类(即非static的)可以使用特殊的this引用创建向外部类对象的连接。而嵌套类没有特殊的this引用,这使得它和static方法类似。
接口中的类
嵌套类可以是接口的一部分,因为类是static的,所以被嵌套的类只是被放到了这个接口的命名空间里。甚至于,可以在嵌套类中实现包围它的接口:
public interface ClassInterface {void howdy();class Test implements ClassInterface {@Overridepublic void howdy() {System.out.println("在嵌套类内部实现了外围接口的方法");}}public static void main(String[] args) {new Test().howdy();}
}
程序执行的结果是:
当需要创建一个接口的所有不同实现的公用代码时,一个嵌套在接口中的类会很有用。
有时,为了测试一个独立的类,会用到一个单独的main()。这种main()就可以被放入到嵌套类中,在交付产品时将其删去即可。
从多嵌套的内部类中访问外部人员
一个类被嵌套了多少层都不重要,因为它可以透明地访问包含它的所有类的所有成员:
class MNA {private void f() {}class A {private void g() {}public class B {void h() {g();f();}}}
}public class MultiNestingAcess {public static void main(String[] args) {MNA mna = new MNA();MNA.A mnaa = mna.new A();MNA.A.B mnaab = mnaa.new B();mnaab.h();}
}
不需要在调用构造器时限定类的名字,因为.new语法会寻找到正确的作用域。