- 可以将一个类的定义放在另一个类的定义内部,这就是内部类。
- 内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。
- 内部类看起来就像是一种代码隐藏机制: 将类置于其他类的内部。
创建内部类
- 创建内部类的方式就如同你想的一样__把类的定义置于外围类的里面:
public class Parce1 {
class Contents{
private int i=11;
public int value(){return i;}
}
class Destination {
private String label;
public Destination(String label) {
this.label = label;
}
String readLabel(){return label;}
}
public void ship(String dest){
Contents contents = new Contents();
Destination destination = new Destination(dest);
System.out.println(destination.readLabel());
}
public static void main(String[] args) {
Parce1 parce1 = new Parce1();
parce1.ship("Tasmania");
}
}
//运行结果
Tasmania
- 当我们在 ship() 方法里面使用内部类的时候,与使用普通类没什么不同。
- 在这里,实际的区别只是内部类的名字是嵌套在 Parce1 里面的。
- 更典型的情况是.外部类将有一个方法,该方法返回一个指向内部类的引用,就像在 to() 和contents() 方法中看到的那样:
public class Parcel2 {
class Contents{
private int i=11;
public int value(){
return i;
}
}
class Destintion{
private String label;
public Destintion(String label) {
this.label = label;
}
String readLabel(){return label;}
}
public Destintion to(String dest){
return new Destintion(dest);
}
public Contents contents(){
return new Contents();
}
public void ship(String dest){
Contents contents=new Contents();
Destintion description=to(dest);
System.out.println(description.readLabel());
}
public static void main(String[] args) {
Parcel2 parcel2=new Parcel2();
parcel2.ship("Tasmania");
Parcel2 parcel21=new Parcel2();
//defining references to inner classes 定义内部类引用
Contents contents = parcel21.contents();
Destintion borneo = parcel21.to("Borneo");
}
}
//运行结果为
Tasmania
- 如果想从外部类的非静态方法之外的任意位置创建某个内部类对象,那么必须像在 main() 方法中那样,具体地指明这个对象的类型: OuterClassName.InnerClassName。
链接到外部类
- 到目前为止,内部类似乎还只是一种名字隐藏和组织代码的模式。
- 这些是很有用, 但还不是最引人注目的,它还有其他的用途。当生成一个内部类的对象时,此对象与制造它的外围对象之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。
- 此外, 内部类还拥有其外围类的素有元素访问权。
public interface Selector {
boolean end();
Object current();
void next();
}
class Sequence{
private Object[] item;
private int next=0;
public Sequence(int size) {
//创建一个执行大小的 数组
item=new Object[size];
}
public void add(Object object){
if (next < item.length){
item[next++]=object;
}
}
private class SequenceSelector implements Selector{
private int i=0;
@Override
public boolean end() {
return i == item.length;
}
@Override
public Object current() {
return item[i];
}
@Override
public void next() {
if (i<item.length){
i++;
}
}
}
public Selector selector(){
return new SequenceSelector();
}
public static void main(String[] args) {
Sequence sequence = new Sequence(5);
for (int i = 0; i < 5; i++) {
//自动装箱机制 把int 自动转为 object
sequence.add(i);
}
Selector selector = sequence.selector();
while (!selector.end()){
System.out.println(selector.current()+ " ");
selector.next();
}
}
}
//运行结果
0
1
2
3
4
- Sequence 类只是一个固定大小的 Object的数组,以类的形式包装了起来。可以调用 add() 在序列未增加新的 Object。
- 要获取 Sequence 中每一个对象, 可以使用 Selector 接口。这是 迭代器 设计模式的一个例子,在后面我们将更多地学习它。
- Selector 允许你检查序列是否到末尾了( end() 方法) ,访问当前对象 ( current() 方法) ,以及移到序列中的下一个对象( next() 方法)。
- 因为 Selector 是一个接口,所以别的类可以按它们自己的方式来实现这个接口,并且另的方法能以此接口为参数,来生成更加通用的代码。
- 所以 内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢? 当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向外围类对象的引用。然后在你访问此外部类的成员时,就是用哪个引用来选择外围类的成员。
- 幸运的是,编译器会帮你处理所有的细节,但你现在可以看到: 内部类的对象只能在与其外围类的对象相关联的情况下才能被创建( 就像你应该看到的, 在内部类是非 static 类时)。构建内部类对象时, 需要一个指向其外围类对象的引用, 如果编译器访问不到这个引用就会报错。 不过大多数情况下这都无需程序员操心。
使用.this 与 .new
- 如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this。
- 这样产生的引用自动地具有正确的类型, 这一点在编译器就被知晓并受到检查,因此没有任何运行时开销。
public class DotThis {
void f(){
System.out.println("DotThis f()");
}
class Inner{
DotThis outer(){
return DotThis.this;
}
}
public Inner inner(){
return new Inner();
}
public static void main(String[] args) {
DotThis dotThis = new DotThis();
DotThis.Inner inner = dotThis.inner();
inner.outer().f();
}
}
- 有时你可能想要告知某些其他对象,去创建其某个内部类的对象。
- 要实现此目的,你必须在 new 表达式中提供其他外部类对象的引用,这是需要使用.new 语法
public static void main(String[] args) {
DotThis dotThis = new DotThis();
Inner inner1 = dotThis.new Inner();
// DotThis.Inner inner = dotThis.inner();
inner1.outer().f();
}
- 要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字 DotNew,而是必须使用外部类的对象来创建该内部类对象,就像在上面的程序中所看到的那样。
- 这也解决了内部类名字作用域的问题,因此你不必声明(实际上你不能声明) doThis.new DoThis.Inner() 。
- 在拥有外部类对象之前是不可能创建内部类对象的。这就是因为内部类对象会暗暗地连接到创建它的外部类对象上。
- 但是,如果你创建的是 嵌套类(静态内部类),那么它就不需要对外部类对象的引用。
public class Parcel3 {
class Contents{
private int i=1;
public int getVal(){
return i;
}
}
class Destination{
private String label;
public Destination(String label) {
this.label = label;
}
String getLabel(){
return label;
}
}
public static void main(String[] args) {
Parcel3 parcel3 = new Parcel3();
Contents contents = parcel3.new Contents();
Destination tasmania = parcel3.new Destination("Tasmania");
}
}
内部类与向上转型
- 当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到了对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)
- 这是因为此内部类__某个接口的实现__能够完全不可见,并且不可用。所得到地只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。
//我们可以创建前一个示例的接口
interface Destination {
String readLabel();
}
interface Contents{
int getVal();
}
- 现在Contents 和 Destination 表示客户端程序员可用的接口。(记住,接口的所有成员自动被设置为 public的。)
- 当取得了一个指向基类或接口的引用时,甚至可能无法找出它确切的类型,如下
interface Destination {
String readLabel();
}
interface Contents{
int getVal();
}
class Parcel4{
private class PContents implements Contents{
private int i=11;
@Override
public int getVal() {
return i;
}
}
protected class PDestination implements Destination{
private String label;
public PDestination(String label) {
this.label = label;
}
@Override
public String readLabel() {
return label;
}
}
public Contents contents(){
return new PContents();
}
public Destination destination(String context){
return new PDestination(context);
}
}
class TestParcel{
public static void main(String[] args) {
Parcel4 parcel4 = new Parcel4();
Contents contents = parcel4.contents();
Destination tasmania = parcel4.destination("Tasmania");
String readLabel = tasmania.readLabel();
System.out.println(readLabel);
}
}
- Parcel4 增加了一些新东西: 内部类PContents 是private ,所以除了 Parcel4 ,没有人能访问它。 PDestination 是protected,所以只有Parcel4 及其子类,还有与 Parcel4 同一包中的类 (因为 protected 也给予了包访问权) 能访问 PDestination ,其他类都不能访问 PDestination。
- private 内部类给类的设计者提供了一种途径,通过这种方式完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的,原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给Java编译器提供了生成更高效代码的机会。
在方法和作用域内的内部类
- 到目前为止,我们所看到的只是内部类典型用途。通常,如果所读,写的代码包含了内部类,那么它们都是 平凡的 内部类,简单并且容易理解。然而,内部类的语法覆盖了大量其他的更加难以理解的技术。
- 例如,可以在一个方法里面或者任意的作用域内定义内部类。有以下两个理由:
- 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
- 你要解决一个复杂的问题,想要创建一个类来辅佐你的解决方案,但是又不希望这个类是公共可用的。
class Parcel5{
public Destination destination(String contents){
class PDestination implements Destination{
private String label;
public PDestination(String label) {
this.label = label;
}
@Override
public String readLabel() {
return label;
}
}
return new PDestination(contents);
}
}
class TestParcel{
public static void main(String[] args) {
Parcel5 parcel5 = new Parcel5();
Destination tasmania = parcel5.destination("Tasmania");
String readLabel = tasmania.readLabel();
System.out.println(readLabel);
}
}
- PDestination 是destination() 的一部分,而不是 Parcel5 的一部分。所以,在 destination() 之外不能访问 PDestination。
- 注意: 出现在 return 语句中的向上转型__返回的是 Destination 的引用,它是 PDestination 的基类。
// 下面的例子展示了如何在任意的作用域内嵌入一个内部类
public class Parcel6 {
private void internalTracking(boolean flag){
if (flag){
class TrackingShip{
private String id;
public TrackingShip(String id) {
this.id = id;
}
public String getId(){
return id;
}
}
TrackingShip slip = new TrackingShip("slip");
String id = slip.getId();
}
}
public void track(){
internalTracking(true);
}
public static void main(String[] args) {
Parcel6 parcel6 = new Parcel6();
parcel6.track();
}
}
- TrackingShip 类被嵌入在 if 语句的作用域内, 这并不是说该类的创建是有条件的,它其实与别的类一起编译过了。
- 然而,在定义 TrackingShip 的作用域之外,它是不可用的,除此之外,它与普通类是一样的。
匿名内部类
- 下面例子看起来有点奇怪:
interface Contents1{
int getVal();
}
public class Parcel7 {
public Contents1 contents1(){
return new Contents1() {
private int i=11;
@Override
public int getVal() {
return i;
}
};
}
public static void main(String[] args) {
Parcel7 parcel7 = new Parcel7();
Contents1 contents1 = parcel7.contents1();
contents1.getVal();
}
}
- contents1() 方法将返回值的生成与表示这个返回值的类的定义结合在一起! 另外,这个类是匿名的,它没有名字。
- 更糟糕的是 ,看起来似乎是你正要创建一个 Contents1对象。然是然后(在到达语句结束的分号之前) 你却说 等一等,我想在这里插入一个类的定义。
- 这种奇怪的语法指的是: 创建一个继承自 Contents1 的匿名类的对象。 通过 new 表达式返回的引用被自动向上转型为 Contents1 的引用。
//上述匿名内部类的语法是下述形式的简化形式
class Parcel7b{
class MyContents1 implements Contents1{
private int i=11;
@Override
public int getVal() {
return i;
}
}
public Contents1 contents1(){
return new MyContents1();
}
public static void main(String[] args) {
Parcel7b parcel7b = new Parcel7b();
Contents1 contents1 = parcel7b.contents1();
}
}
- 在这个匿名内部类中,使用了默认的构造器来生成 Contents1。
//如果你的基类需要一个有参构造器,应该怎么办呢?
class Wrapping{
private int id;
public Wrapping(int id) {
this.id = id;
}
public int getVal(){
return id;
}
}
public class Parcel8 {
public Wrapping wrapping(int x){
return new Wrapping(x){
@Override
public int getVal() {
return super.getVal()*20;
}
};
}
public static void main(String[] args) {
Parcel8 parcel8 = new Parcel8();
Wrapping wrapping = parcel8.wrapping(2);
System.out.println(wrapping.getVal());
}
}
- 只需要简单地传递合适的参数给基类的构造器即可,这里将 x 传进 new Wrapping(x)。 尽管Wrapping 只是一个具有具体实现的普通类,但它还是被其导出类当做公共接口来使用。
- 在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此这与别的地方使用的分号是一致的。
class Destination1{
private String label;
public String getLabel(){
return label;
}
}
public class Parcel9 {
public Destination1 destination1(String content){ 这里书上说要final 但从jdk1.8开始可以不用final了
return new Destination1(){
private String lebel=content;
@Override
public String getLabel() {
return lebel;
}
};
}
public static void main(String[] args) {
Parcel9 parcel9 = new Parcel9();
parcel9.destination1("Tasmania");
}
}
- 从JDK1.8 开始 可以不用final 进行修饰参数。
在访工厂方法
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 serviceFactory=new ServiceFactory() {
@Override
public Service getService() {
return new Implementation1();
}
};
}
class Impplementation2 implements Service{
private Impplementation2() {
}
@Override
public void method1() {
System.out.println("Impplementation2 method1");
}
@Override
public void method2() {
System.out.println("Impplementation2 method2");
}
public static ServiceFactory serviceFactory=new ServiceFactory() {
@Override
public Service getService() {
return new Impplementation2();
}
};
}
class Factoryes{
static void services(ServiceFactory serviceFactory){
Service service = serviceFactory.getService();
service.method1();
service.method2();
}
public static void main(String[] args) {
services(Implementation1.serviceFactory);
services(Impplementation2.serviceFactory);
}
}
//运行结果为
Implementation1 method1
Implementation1 method2
Impplementation2 method1
Impplementation2 method2
- Implementation1 和 Impplementation2 的构造器都是 private的,并且没有任何必要去创建作为工厂的具名类。
- 另外,你经常只需要单一的工厂对象,因此在本例中它被创建为 Service实现中的一个 static 域。
嵌套类
- 如果不需要内部类对象与其外围类对象之间有关系,那么可以将内部类声明为 static。这通常称为 嵌套类。
- 普通的内部类对象隐式地保存了一个引用,指向了它的外围类对象。然而,当内部类是 static 的时候,就不是这样了。
- 嵌套内部类意味着:
- 要创建嵌套类的对象,并不需要其外围类的对象。
- 不能从嵌套;类的对象中访问非静态的外围类对象。
- 嵌套类和普通类还有一个区别: 普通内部类的字段与方法,只能放在类饿的外部层次上,所以普通的内部类不能有 static 数据 和static字段,也不能包含嵌套类,但是嵌套类可以包含所有这些东西。
public class Parcel11 {
private static class ParcelContents implements Contents{
private int i=11;
@Override
public int getVal() {
return i;
}
}
protected static class ParcelDestination implements Destination{
private String label;
public ParcelDestination(String label) {
this.label = label;
}
@Override
public String readLabel() {
return label;
}
}
public static Contents contents(){
return new ParcelContents();
}
public static Destination destination(String message){
return new ParcelDestination(message);
}
public static void main(String[] args) {
Contents contents = contents();
Destination tasmania = destination("Tasmania");
}
}
- 在 main() 中,没有任何 Parcel11 的对象时必须的,而是使用选取 static 成员的普通语法来调用方法__这些方法返回对 Contents 和 Destination 的引用。
- 在一个普通的(非 static)内部类中, 通过提个特殊的 this 引用可以链接到其外围类对象。嵌套类就没有这个特殊的this 引用,这使它类似于一个 static方法。
接口内部的类
- 正常情况下,不能再接口内部放置任何代码,但嵌套类可以作为接口的一部分。
- 你放到接口中的任何类都自动地是 public 和 static 的。因为 类是static的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。
public interface ClassInterface {
void howdy();
class Test implements ClassInterface{
@Override
public void howdy() {
System.out.println("howdy()");
}
public static void main(String[] args) {
new Test().howdy();
}
}
}
//运行结果为
howdy()
- 如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类回显得很方便。
从多层嵌套类中访问外部类的成员
- 一个内部类嵌套多少层并不重要__它能透明地访问所有它嵌入的外围类的所有成员。
public class MNA {
private void f(){}
class A{
private void g(){}
public class B{
void h(){
g();
f();
}
}
}
}
class MultiNestingAccess{
public static void main(String[] args) {
MNA mna = new MNA();
MNA.A a = mna.new A();
MNA.A.B b = a.new B();
b.h();
}
}
- 可以看到 MNA.A.B 调用方法 g() 和 f() 不需要任何条件 (即使他们为 private )
- 这个例子同时展示了 如何从不同类里创建多层嵌套的内部类对象的基本语法。
- .new 语法能产生正确的作用域,所以不必在调用构造器时限定类名。
为什么需要内部类
- 一般来说 内部类继承自某个类或实现了某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入外围类的窗口。
- 每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
- 为了看到更多的细节,让我们考虑这样一情形: 即必须在一个类中以某种方式实现两个接口。由于接口的灵活性,你有了两种选择: 使用单一类,或者使用内部类。
interface A {}
interface B {}
class X implements A,B{
}
class Y implements A{
B makeB(){
return new B() {
};
}
}
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());
}
}
- 当然,这里假设在两种方式下的代码结构都确实有逻辑意义。然而遇到问题的时候,通常问题本身就能给出某些指引,告诉你是应该使用单一类,还是使用内部类。
- 但如果没有任何其他限制,从实现的观点来看,前面的例子并没有什么区别,它们都能正常运作。
//如果拥有的是抽象的类或具体的类,而不是接口,那就只能使用内部类才能实现多重继承
public class D {}
abstract class E{}
class Z extends D{
E makeE(){
return new E(){
};
}
}
class 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());
}
}
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。
- 在单个外围类中,可以让多个内部类以不同的方式实现同一接口,或继承同一个蕾。
- 创建内部类对象的时刻并不依赖于外围对象的创建。
- 内部类并没有令人迷惑的 is-a 关系,它就是一个独立的实体。
闭包与回调
- 闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 private 成员。
- Java通过内部类提供闭包的功能是优良的解决方案,它比指针更灵活,更安全。
interface Incrementable {
void increment();
}
class Calleel 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 myIncrement){
myIncrement.increment();
}
}
class Callee2 extends MyIncrement {
private int i = 0;
@Override
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 incrementable;
public Caller(Incrementable incrementable) {
this.incrementable = incrementable;
}
void go(){
incrementable.increment();
}
}
class Callbacks{
public static void main(String[] args) {
Calleel calleel=new Calleel();
Callee2 callee2=new Callee2();
MyIncrement.f(callee2);
Caller caller1 = new Caller(calleel);
Caller caller2 = new Caller(callee2.getCallbackReference());
caller1.go();
caller1.go();
caller2.go();
caller2.go();
}
}
//运行结果
Other operation
1
1
2
Other operation
2
Other operation
3
- 展示了外围类实现一个接口与内部类实现此接口之间区别。就代码而言 Calleel 是简单的解决方式。Callee2继承自 MyIncrement 后者已经有了一个不同的 increment() 方法,并且与 Incrementable接口期望的 increment() 方法完全不相同。
- 回调的价值在于它的灵活性__可以在运行时动态地决定需要调用什么方法。
内部类与控制框架
- 应用程序框架 就是被设计用以解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,已解决你特定问题。
- 模板方法包含算法的基本结构,并会调用一个或多个可覆盖的方法,以完成算法的动作。设计模式总是将变化的事物与保持不变的事物分开,在这个设计模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。
- 控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作 事件驱动系统。
public abstract class Event {
private Long eventTime;
protected final long delayTime;
public Event(long delayTime) {
this.delayTime = delayTime;
start();
}
void start(){
//System.nanoTime() :返回的是纳秒,nanoTime而返回的可能是任意时间,甚至可能是负数……
eventTime = System.nanoTime()+delayTime;
}
boolean ready(){
return System.nanoTime() >= eventTime;
}
abstract void action();
}
- 当希望运行运行 Event 并随后调用 start() 时,那么构造器就会捕获(从对象创建的时刻开始的)时间, 此时间是这样得来的: start() 获取当前时间,然后加上一个延迟时间,这样生成触发事件的时间。
- start() 是一个独立的方法,而没有包含在构造器内,因为这样就可以在事件运行以后重新启动计时器,也就是能够重复使用 Event 对象。例如,如果想要重复一个事件,只需简单地在 action() 中调用 start() 方法。
- ready() 告诉你何时可以运行 action() 方法。当然,可以在导出类中覆盖 ready() 方法,使得 Event 能够基于时间以外的其他因素而触发。
class Controller{
private List<Event> eventList=new ArrayList<Event>();
public void addEvent(Event event){
eventList.add(event);
}
void run(){
while (eventList.size() > 0){
for (Event event:new ArrayList<Event>(eventList)) {
if (event.ready()){
System.out.println(event);
event.action();
eventList.remove(event);
}
}
}
}
}
- run() 方法循环遍历 eventList ,寻找就绪的(ready()),要运行的 Event对象。对找到的每一个就绪(ready())事件,使用对象的 toString() 打印其信息,调用其action() 方法,然后从队列中移除 Event。
- 使变化的事物与不变的事物相互分离__就是各种不同的 Event 对象所具有的不同行为,而你通过创建不同的 Event子类来表现不同的行为。
- 控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装了起来。内部类用来表示解决问题所必须的各种不同的action()。
- 内部类能够很容易地访问外围类的任意成员,所以可以避免这种实现变得笨拙。如果没有这种能力,代码将变得令人讨厌,以至于你肯定会选择别的方法。
内部类的继承
-
因为内部类的构造器必须连接到指向其他外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。
-
问题在于,那个指向外围类对象的 秘密的 引用必须被初始化,而在导出类中不再存在可连接的默认对象。
-
要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:
class WithInner {
class Inner{
}
}
class InheritInner extends WithInner.Inner {
// public InheritInner() {} Won't compile 不会编译
public InheritInner(WithInner withInner) {
withInner.super();
}
public static void main(String[] args) {
WithInner inner = new WithInner();
InheritInner inheritInner = new InheritInner(inner);
}
}
- 可以看到, InheritInner 只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外,必须在构造器内使用如下语法
enclosingClassReference.super();
- 这样才提供了必要的引用,然后程序才能编译通过。
内部类可以被覆盖吗
- 如果创建了一个内部类,然后继承其外围类并重新定义此内部类时,会发生什么呢?
- 也就是说,内部类可以被覆盖吗? 这看起来似乎是个很有用的思想,但是覆盖内部类就好像它是外围类的一个方法,其实并不起什么作用。
class Egg {
private Yolk yolk;
protected class Yolk{
public Yolk(){
System.out.println("Egg Yolk");
}
}
public Egg() {
System.out.println("new Egg");
yolk=new Yolk();
}
}
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
- 默认的构造器是编译器自动生成的,这里是调用基类的默认构造器。
- 你可能认为创建的是BigEgg的对象,那么所使用的应该是 覆盖后 的Yolk 版本,但从输出中可以看到实际情况并不是这样的。
- 当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。当然,明确地继承某个内部类也是可以的:
public class Egg2 {
protected class Yolk{
public Yolk() {
System.out.println("Egg2 Yolk");
}
void f(){
System.out.println("Egg2 Yolk f()");
}
}
private Yolk yolk=new Yolk();
public Egg2() {
System.out.println("Egg2 Constructor");
}
void insertYolk(Yolk yolk){
this.yolk=yolk;
}
void g(){
yolk.f();
}
}
class BigEgg2 extends Egg2{
public class Yolk extends Egg2.Yolk{
public Yolk() {
System.out.println("BigEgg2 Yolk");
}
@Override
void f() {
System.out.println("BigEgg2 Yolk f()");
}
}
public BigEgg2() {
insertYolk(new Yolk());
}
public static void main(String[] args) {
new BigEgg2().g();
}
}
//运行结果
Egg2 Yolk
Egg2 Constructor
Egg2 Yolk
BigEgg2 Yolk
BigEgg2 Yolk f()
- 现在 BigEgg2.Yolk 通过 extends Egg2.Yolk 明确地继承了此内部类,并且覆盖了其中的方法。insertYolk() 方法允许 BigEgg2 将它自己的 Yolk 对象向上转型为 Egg2 中的引用 this.yolk。
- 所以当 g() 调用 yolk.f();时,覆盖后的新版的 f() 被执行。第二次调用 Egg2.Yolk(), 结果是 BigEgg2.Yolk 的构造器调用了其基类的构造器。可以看到 在调用 g() 的时候,新版的 f() 被调用了。
局部内部类
- 可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分,但是它可以访问当前代码块的常量,以及此外围类的所有成员。
interface Counter {
int next();
}
class LocalInnerClass{
private int count = 0;
Counter getCounter(String name){
class LocalCounter implements Counter{
public LocalCounter() {
System.out.println("LocalCounter constructor");
}
@Override
public int next() {
System.out.println(name);
return count++;
}
}
return new LocalCounter();
}
Counter getCounter2(String name){
return new Counter() {
{
System.out.println("Counter()");
}
@Override
public int next() {
System.out.println(name);
return count++;
}
};
}
public static void main(String[] args) {
LocalInnerClass lic = new LocalInnerClass();
Counter inner = lic.getCounter("local inner");
Counter counter2 = lic.getCounter2("Anonymous inner");
for (int i = 0; i < 5; i++) {
System.out.println(inner.next());
}
for (int i = 0; i < 5; i++) {
System.out.println(counter2.next());
}
}
}
//运行结果为
LocalCounter constructor
Counter()
local inner
0
local 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 返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力。
- 既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?
- 唯一的理由是: 我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能用于实例初始化。
- 所以使用局部内部类而不使用匿名内部类的另一个理由就是,需要不止一个该内部类的对象。
内部类标识符
- 由于每个类都会产生一个 .class 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个 meta-class,叫做Class对象), 你可能猜到了, 内部类也必须生成一个 .class文件以包含它们的Class 对象信息。这些类文件命名有严格的规则: 外围类的名字,加上 $ 再加上内部类名字。
Callbacks.class
Callee2$1.class
Callee2$Closure.class
- 如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外围类标识符与 $ 的后面。
- 虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。因为这是Java的标准命名方式,所以产生的文件自动都是平台无关的(注意: 为了保证你的内部类能起作用,Java 编译器会尽可能地转换它们。) 。
总结:
- 比起面向对象编程中其他的概念来,接口和内部类更深奥复杂,比如C++就没有这些。将两者结合起来,同样能够解决C++中的用多重继承所能解决的问题。
- 然而多种继承在 C++中被证明是难以相当难以使用的,相比较而言,Java接口和内部类就容易理解多了。