注:本博客内容是本人在看《Jave编程思想》这本书时从该书上抄录下来的一些片段。这里也强烈建议各位读者去购买这本书进行阅读学习。
可以将一个类的定义放在另一个类的定义内部,这就是内部类。
内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。
在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是,你将会了解到,内部类远不如此,它了解外围类,并能够与之通信;而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样。
一、创建内部类
创建类部内的方式就如同你想的一样——把类的定义置于外围类的里面:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/14上午12:20
**/
public class Parcel {
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) {
Parcel parcel = new Parcel();
parcel.ship("Tasmania");
}
}
输出:
Tasmania
当我们在ship()方法中使用内部类时,与使用普通类没有什么区别。在这里,实际的区别只是内部类的名字嵌套在Parcel里面。不过你将会看到,这并不是唯一的区别。
更典型的情况是,外部类将有一个方法,该方法返回一个指向内部类的引用,就像在如下示例中的to()和contents()方法中看到的那样:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/14上午12:20
**/
public class Parcel1 {
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 Destination to(String dest) {
return new Destination(dest);
}
public Contents contents() {
return new Contents();
}
public void ship(String dest) {
Contents contents = contents();
Destination destination = to(dest);
System.out.println(destination.readLabel());
}
public static void main(String[] args) {
Parcel1 parcel = new Parcel1();
parcel.ship("Tasmania");
Parcel1 parcel1 = new Parcel1();
Parcel1.Contents contents = parcel1.contents();
Parcel1.Destination destination = parcel1.to("Borneo");
}
}
输出:
Tasmania
如果想要从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在main()方法中“Parcel1.Contents”和“
Parcel1.Destination”那样,具体指明这个对象的类型:OuterClassName.InnerClassName。
二、链接到外部类
到目前为止,内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用的,但还不是最引人注目的,它还有其他用途。当生产一个内部类的对象时,此对象与制造它的外围对象(enclsoing object)之间就有了一种联系,所以它能够访问其外围对象的所有成员,而不需要特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。如下例子说明类这一点:
package innrclasses;
interface Selector {
boolean end();
Object current();
void next();
}
/**
* 功能:
* 描述:
**/
public class Sequnce {
private Object [] items;
private int next = 0;
public Sequnce(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) {
Sequnce sequnce = new Sequnce(10);
for (int i = 0; i < 10; i++) {
sequnce.add(Integer.toString(i));
}
Selector selector = sequnce.selector();
while (!selector.end()) {
System.out.print(selector.current() + " ");
selector.next();
}
}
}
输出结果:
0 1 2 3 4 5 6 7 8 9
在上述例子中的SequenceSelector初看可能觉得它不过是一个内部类罢了。但请仔细观察它,注意end()、current()和next()方法都用到了items,这是一个引用,它并不是SequenceSelector的一部分,而是外围类中的一个private字段,然而内部类可以访问其外部类的方法和字段,就像自己拥有它们似的,这带来了很大的方便。
内部类自动拥有对其外围类所有成员的访问权。这是任何做到的呢?当某个外围类对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围对象的引用。然后,在你访问此外围类的成员时,就用那个引用来选择外围类的成员。幸运的是,编译器会帮我们处理所有细节,但你可以看到:内部类的对象自能在与其外围类的对象相关联的情况下才能被创建(就像你应该看到的,在内部类是非static类时)。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时这都无需程序员操心。
三、使用.this与.new
如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时的开销。例如:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/14下午11:58
**/
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 dotThis = new DotThis();
DotThis.Inner inner = dotThis.inner();
inner.outer().f();
System.out.println(dotThis == inner.outer());
}
}
输出结果:
DotThis.f()
true
有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在new表达是中提供对其他外部类对象的引用,这就需要使用.new语法。例如:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/15上午12:09
**/
public class DotNew {
public class Inner {
private void f() {
System.out.println("DotNew.Inner.f()");
}
}
public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner ani = dn.new Inner();
ani.f();
}
}
输出结果:
DotNew.Inner.f()
要想直接创建内部类的对象,你不能按照你想像的方式,去引用外部类的名字DotNew,而是必须使用外部类的对象来创建该内部类对象,就像在上面的程序中看到的那样。这也解决了内部类名字作用域的问题,因此你不必声明(实际上你不能声明)dn.new DotNew.Inner()。
在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到创建它的外部类对象上。但是,如果你创建的是嵌套类(静态内部类),那么他就不需要对外部类对象的引用。
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/15上午12:19
**/
public class Parcel3 {
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 static void main(String[] args) {
Parcel3 p = new Parcel3();
Parcel3.Contents c = p.new Contents();
Parcel3.Destination d = p.new Destination("Jerry");
}
}
四、内部类与向上转型
当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类——某个接口的实现——能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。
先创建一个示例接口:
public interface Destination {
String readLabel();
}
public interface Contents {
int value();
}
现在COntents和Destination表示客户端程序员都可以使用的接口。(记住,接口中都有的成员都自动设置为public的)
当取得了一个指向基类或接口的引用时,甚至可能无法找出确切的类型。例如:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/15下午10:49
**/
public class Parcel4 {
private class PContents implements Contents {
private int i = 11;
@Override
public int value() {
return i;
}
}
protected class PDestination implements Destination {
String label;
private PDestination(String label) {
this.label = label;
}
@Override
public String readLabel() {
return label;
}
}
public Destination destination(String label) {
return new PDestination(label);
}
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("Jerry");
// can't access private class
//! Parcel4.PContents pc = p.new PContents();
// cant't create when constructor is private
//! Parcel4.PDestination pp = p.new PDestination();
}
}
上述示例中Parcel4增加了一些新的东西:内部类PContents是private,所以除了Parcel4,没有人能够访问它。PDestination是protected,所以只有Parcel4及其子类、还有与Parcel同一个包中的类(因为protected也给予了包访问权限)能够访问PDestination。这意味着,如果客户端程序员想了解会访问这些成员,那是要受到限制的。实际上,甚至不能向下转型称private内部类(或protected内部类,除非是继承自它的子类),因为不能访问其名字,就像在TestParcel类中看到的那样。于是,private内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型得到编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给Java编译器提供了生成高效代码的机会。
五、在方法和作用域内的内部类
通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,简单并且容易理解的。然而,内部类的语法覆盖了其他的更加难以理解的技术。例如,可以在一个方法里或者任意的作用域内定义内部类。这么做有两个理由:
- 如前所示,你实现类某个类型的接口,于是可以创建并返回对其的引用。
- 你要解决一个复杂的问题,想要创建一个来辅助你的解决方案,但是又不希望这个类是公共可用的。
在后面的例子中,先前的代码将被修改,以用来实现:
1)一个定义在方法中的类
2)一个定义在作用域的类,此作用域在方法内部
3)一个实现了接口的匿名类
4)一个匿名类,它扩展了非默认构造器的类。
5)一个匿名类,它执行字段初始化
6)一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)
第一个例子展示了在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。这被称为局部内部类。
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/15下午11:21
**/
public class Parcel5 {
public Destination destination(String label) {
class PDestination implements Destination {
private String label;
private PDestination(String label) {
this.label = label;
}
@Override
public String readLabel() {
return label;
}
}
return new PDestination(label);
}
public static void main(String[] args) {
Parcel5 p = new Parcel5();
Destination d = p.destination("Jerry");
}
}
PDestination类是destination()方法的一部分,而不是Parcel5的一部分。所以,在destination()之外不能访问PDestination。注意出现在return语句的向上转型——返回的是Destination的引用,它是PDestination的基类。当然在destination()中定义了内部类,并不意味着一旦destination()方法执行完毕,PDestination就不可用了。
你可以在同一个子目录的任意类中对某个内部类使用标识符PDestination,这不会有命名冲突。下面的例子展示了如何在任意的作用域内嵌入一个内部类:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/15下午11:32
**/
public class Parcel6 {
private void internalTracking(boolean b) {
if (b) {
class TrackingSlip {
private String id;
public TrackingSlip(String id) {
this.id = id;
}
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的作用域之外,它是不可用的;除此之外,它与普通的类一样。
六、匿名内部类
下面的例子看起来有点奇怪:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/15下午11:44
**/
public class Parcel7 {
public Contents contents() {
return new Contents() {
private int i = 11;
@Override
public int value() {
return i;
}
}; // 在这种情况下,分号是必需的
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Contents c = p.contents();
}
}
contents()方法将返回值的生成与表达这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟糕的是,看起来似乎你正要创建一个Contents对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。”
这种奇怪的语法指的是:“创建一个继承自Contents的匿名类的对象。”通过new表达式返回的引用被自动向上转型为Contens的引用。上述匿名内部类的语法是下述形式的简化形式:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/15下午11:53
**/
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。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/15下午11:59
**/
public class Wrapping {
private int x;
public Wrapping(int x) {
this.x = x;
}
public int value(){
return x;
}
}
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/15下午11:57
**/
public class Parcel8 {
public Wrapping wrapping(int x) {
return new Wrapping(x) {
@Override
public int value() {
return super.value() * 47;
}
};
}
public static void main(String[] args) {
Parcel8 parcel8 = new Parcel8();
Wrapping wrapping = parcel8.wrapping(10);
System.out.println(wrapping.value());
}
}
只需要简单地传递合适的参数给基类的构造器即可,这里是将x传进new Wrapping(x)。尽管Wrapping只是一个具有具体实现的普通类,但是它还是被其导出类当作公共“接口”来使用。
你会注意到,Wrapping拥有一个要求传递一个参数的构造器,这使得事情变得更加有趣了。
在匿名内部类末尾的分号,,并不是用来标记此内部类结构结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用分号是一样的。
在匿名类中定义字段时,还能够对其进行初始化操作:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/16上午12:10
**/
public class Parcel9 {
public Destination destination(final String dest) {
return new Destination() {
private String label = dest;
@Override
public String readLabel() {
return label;
}
};
}
public static void main(String[] args) {
Parcel9 parcel9 = new Parcel9();
Destination d = parcel9.destination("Jerry");
System.out.println(d.readLabel());
}
}
如果定义了一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的,就像你在destination()的参数中看到的那样。如果你忘记了,将会得到一个编译时错误信息。
如果只是简单地给一个字段赋值,那么次例中的方法就很好。但是如果想做一些类似构造器的行为,该怎么办呢?在匿名内部类中不可能有命名构造器(因为它根本没有名字),但是通过实例初始化,就能够达到匿名内部类创建一个构造器的效果,就像这样:
abstract class Base {
public Base(int i) {
System.out.println("Base constructor, i =" + i);
}
public abstract void f();
}
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/16上午12:19
**/
public class AnonymousConstructor {
public static Base getBase(int i) {
return new Base(i){
{
System.out.println("类部实例初始化");
}
@Override
public void f() {
System.out.println("In anonymous f()");
}
};
}
public static void main(String[] args) {
Base base = getBase(47);
base.f();
}
}
在此例中,不要求变量i一定是final的。因为i被传递给匿名类的基类的构造器,它并不会在匿名内部类被直接使用。
下例是带实例初始化的“parcel”形式。注意destination()的参数必须是final的,因为它们是在匿名内部类使用的。
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/16上午12:33
**/
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();
Destination d = parcel10.destination("Tom", 200.28F);
}
}
在实例初始化操作内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是if语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制——你不能重载实例初始化方法,所以你仅有一个这样的构造器。
匿名内部类与正规的继承相比有些限制,因为匿名内部类即可以扩展类,也可以实现接口,但是不能两者兼得。而且如果是实现接口,也只能实现一个接口。
6.1 在访工厂方法
看看在使用匿名内部类时,interfaces/Factories.java示例变得多么美妙:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/16上午12:44
**/
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
Service getService();
}
class Implementation1 implements Service {
@Override
public void method1() {
System.out.println("Implementation1.method1()");
}
@Override
public void method2() {
System.out.println("Implementation1.method1()");
}
public static ServiceFactory factory =
new ServiceFactory() {
@Override
public Service getService() {
return new Implementation1();
}
};
}
class Implementation2 implements Service {
@Override
public void method1() {
System.out.println("Implementation2.method1()");
}
@Override
public void method2() {
System.out.println("Implementation2.method1()");
}
public static ServiceFactory factory =
new ServiceFactory() {
@Override
public Service getService() {
return new Implementation2();
}
};
}
public class Factories {
public static void serviceConsumer(ServiceFactory factory) {
Service service = factory.getService();
service.method1();
service.method2();
}
public static void main(String[] args) {
serviceConsumer(Implementation1.factory);
serviceConsumer(Implementation2.factory);
}
}
输出信息:
Implementation1.method1()
Implementation1.method1()
Implementation2.method1()
Implementation2.method1()
现在用于Implementation1和Implementation2的构造器都可以是private的,并且没有任何必要去创建作为工厂的具名称。另外,你经常只需要单一的工厂对象,因此在本例中它被创建为Service实现中的一个static域。这样所产生语法更具有实际意义。
interfaces/Games.java也可以通过使用匿名内部类来进行改造:
package innrclasses;
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 is 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 is 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 g = factory.getGame();
while (g.move()) {
;
}
}
public static void main(String[] args) {
playGame(Checkers.factory);
playGame(Chess.factory);
}
}
Checkers is move 0
Checkers is move 1
Checkers is move 2
Chess is move 0
Chess is move 1
Chess is move 2
Chess is move 3
需要记住的是,在开发时,请优先考虑使用类而不是接口。如果你的设计中需要使用某个接口,你必须了解它。否则,不到迫不得已,不要将其放到你的设计中。
七、嵌套类
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static。这通常被称为嵌套类。想要理解static应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围对象。然而,当内部类是static的时候,就不是这样了。嵌套类意味着:
1)要创建嵌套类的对象,并不需要其外围类的对象。
2)不能从嵌套类的对象中访问非静态的外围类对象。
嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通内部类不能有static数据和static字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西。
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/16下午11:36
**/
public class Parcel11 {
private static class ParcelContents implements Contents {
private int i = 11;
@Override
public int value() {
return i;
}
}
protected static class ParcelDestination implements Destination {
private String label;
private ParcelDestination(String label) {
this.label = label;
}
@Override
public String readLabel() {
return label;
}
public static void f() {
System.out.println("ParcelDestination static f()");
}
static int x = 10;
static class AnotherLevel{
public static void f() {
System.out.println("AnotherLevel.f()");
}
static int x = 10;
}
}
public static Destination destination(String label) {
return new ParcelDestination(label);
}
public static Contents contents() {
return new ParcelContents();
}
public static void main(String[] args) {
Contents c = contents();
Destination d = destination("Jerry");
}
}
在上述例子的main()方法中,没有任何Parcel11的对象是必需的;而是使用选取static成员的普通语法来调用方法——这些方法返回Contents和destination的引用。
一个普通的(非static)内部类中,通过一个特殊的this引用可以链接到其外围类对象。嵌套内就没有这个特殊的this引用,这使得它类似一个static方法。
7.1 接口内部类
正常情况下,在接口内部不能放置任何代码,但是嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是public和static的。因为类也是static,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现外围接口,就像下面这样:
package innrclasses;
/**
/12/16下午11:50
**/
public interface ClassInInterface {
void howdy();
class Test implements ClassInInterface {
@Override
public void howdy() {
System.out.println("Test.howdy()");
}
public static void main(String[] args) {
new Test().howdy();
}
}
}
如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便。
7.2 从多层嵌套类中访问外部类的成员
一个内部类被嵌套多少层并不重要——它能够透明地访问所有它嵌套的外围类的所有成员,如下:
package innrclasses;
/**
* 2019/12/17上午12:12
**/
public class MultiNestingAccess {
class MNA {
private void f() {
System.out.println("MNA.f()");
}
class A {
private void g() {
System.out.println("MNA.A.g()");
}
public class B {
void h() {
f();
g();
}
}
}
}
public static void main(String[] args) {
MNA mna = new MultiNestingAccess().new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}
输出结果:
MNA.f()
MNA.A.g()
可以看到在MAN.A.B中,调用方法f()和g()不需要任何条件(即使它们被定义为private)。这个例子同时展示了如何从不同的类里创建多层嵌套类的内部类对象的基本语法。“.new”语法能够产生正确的作用域,所以不必在调用构造器时限定名。
八、为什么需要内部类
至此,我们已经看到了许多描述内部类的语法和定义,但是这并不能回答“为什么需要内部类”这个问题。那么为什么需要如此费心地增加这个基本的语言特性呢?
一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围的对象。所以可以认为内部类提供了某种进入其外围类的窗口。
内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做。”那么内部类实现一个接口与外围类实现这个接口有什么区别呢?答案是:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以,使用内部类最吸引人的原因是:
每个内部类都能够独立继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
如果没有内部类提供的,可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个类接口类(类或者抽象类)。
为了看到更多的细节,让我们考虑这样一个情形:即必须在一个类中以某种方式实现两个接口。由于接口的灵活性,你有两者选择:使用单一类,或者使用内部类:
package innrclasses;
interface A {}
interface B {}
class X implements A,B {}
class Y implements A {
// inner class
B makeB() {
return new B() {};
}
}
public class MultiInterface {
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());
}
}
当然,这里假设在这两者方式下的代码结构都确实有逻辑意义。然而遇到问题的时候,通常问题本身就能够给出某些指引,告述你是应该使用单一类,还是使用内部类。但如果没有其他限制,从实现的观点来看,前面的例子并没有什么区别,它们都能正常运作。
如果你拥有的是抽象的类或者具体的类,而不是接口,那就只能使用内部类才能实现多重继承。
package innrclasses;
class D {}
abstract class E{}
class Z extends D {
E makeE() {
return new E() {};
}
}
public 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());
}
}
如果不需要解决“多重继承”的问题,那么自然可以使用别的方式编码,而不是使用内部类。但如果使用内部类,还可以获取其他一些特性:
1)内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。
2)创建内部类对象的时刻并不依赖于外围类对象的创建
3)在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
4)内部类并没有令人迷惑的“is-a”关系;它就是一个独立的实体。
8.1 闭包与回调
闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有成员,包括private成员。
Java最引人争议的问题之一就是,人们认为Java应该包含某种类似指针的机制,以允许回调(callback)。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始化对象。稍后会看到一个非常有用的概念。如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用该指针。然而,Java更加小心,所以没有在语言中包含指针。
通过内部类提供闭包的功能是优良的解决方案,它比指针更灵活、更安全。见下例:
package innrclasses;
interface Incrementable {
void increment();
}
class 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();
}
}
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 callbackReference;
Caller(Incrementable cbn) {
callbackReference = cbn;
}
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 operation
1
1
2
Other operation
2
Other operation
3
这个例子进一步展示了外围类实现一个接口与内部类实现此接口之间的区别。就代码而言,Callee1是简单的解决方法。Callee2继承自MyIncrement,后者已经有了一个不同的incremetn()方法,并且与Incrementable接口期望的incremen()方法完全不相光。所以如果Callee2继承了MyIncrement,就不能为了Incrementable方法而覆盖increment()方法,于是只能使用内部类独立实现Incrementable。还要注意,当创建了一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。
注意,在Callee2中除了getCallbackReference()方法以外,其他成员都是private的。想要建立与外部世界的任何连接,interface Incrementable都是必需的。在这里可以看到,interface是如何允许接口与接口的实现完全独立的。
内部类Closure实现了Incrementable,以提供一个返回Callee2的“钩子”(hook)——而且是一个安全的钩子。无论谁获得此Incrementable的引用,都只能调用increment()方法。除此之外没有其他功能(不像指针那样,允许你做很多事情)。
Caller的构造函数需要一个Incrementable的引用作为参数(虽然可以在任意时刻捕获会调引用),然后在以后的某个时刻,Caller对象可以使用此引用回调Callee类。
回调的价值在于它的灵活性——可以在运行时动态决定需要调用什么方法。
8.2 内部内与控制框架
应用程序框架(application framework)就是被设计用来解决某类特定问题的一个类或一组类。要运用某个应用框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决你的特定问题(这是设计模式中模版方法的一个例子)。模版方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法动作。设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模版方法是保持不变的事物,而可覆盖的方法就是变化的事物。
控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。Java Swing库是一个控制系统,它优雅地解决库GUI问题,并使用了大量的内部类。
要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请先考虑这样一个控制框架,它的工作就是在事件“就绪”的时候执行事件。虽然“就绪”可以指任何事,但在本例中指基于时间触发的事件。接下来的问题是,对于要控制什么,控制框架不包含任何具体的信息。那些信息是在实现算法的action()部分时,通过继承来提供的。
首先,接口描述了要控制的事件。因为其默认的行为是基于时间去执行控制,所以使用抽象类替代实际的接口。下面的代码包含了某些实现:
package innrclasses.controller;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/18下午10:36
**/
public abstract class Event {
private long eventTime;
protected final long delayTime;
public Event(long delayTime) {
this.delayTime = delayTime;
start();
}
/**
* allows restarting
*/
public void start() {
eventTime = System.nanoTime() + delayTime;
}
/**
* 是否准备好
* @return
*/
public boolean isReady() {
return System.nanoTime() >= eventTime;
}
/**
* 接口定义
*/
public abstract void action();
}
当希望运行Eevent并随后调用start()方法,那么构造器就会捕获(从对象创建的时刻开始)时间,此时间是这样得来的:start()方法获取当前时间,然后加上一个延迟时间,这样就产生触发事件的时间。start()是一个独立的方法,而没有包含在构造器内,因为这样可以在事件运行以后重新启动计时器,也就是能够重复使用Event对象。例如,如果想要重复一个事件,只需要简单地在action()中调用start()方法即可。
isReady()方法告述你何时可以运行action()方法。当然,可以在导出类中覆盖isReady()方法,使得Event能够基于时间以外的其他因素而触发。
下面的文件包含了一个用来管理并触发事件的实际控制框架。Event对象被保存在List<Event>的容器对象中,通过容器的add()方法添加一个事件,remove()方法移除指定的Event。
package innrclasses.controller;
import java.util.ArrayList;
import java.util.List;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/18下午10:59
**/
public class Controller {
private List<Event> eventList = new ArrayList<>();
public void addEvent(Event event) {
eventList.add(event);
}
public void run() {
while (eventList.size() > 0) {
// make a copy so you're not modifying the list
//while you're selecting the elements in it;
for (Event event : new ArrayList<>(eventList)) {
if (event.isReady()) {
System.out.println(event);
event.action();
eventList.remove(event);
}
}
}
}
}
run()方法循环遍历eventList,寻找就绪的(isReady())、要运行的Event对象。对找到的每一个就绪的事件,使用对象的toString()打印其信息,调用其action()方法,然后从队列中移除。
注意,在目前的设计中你并不知道Event到底做了什么。这正是设计的关键所在,“使变化的事物与不变的事物相互分离”。用作者的话说,“变化向量”就是各种不同的Event对象所具有的不同的行为,而你通过创建不同的Event子类来表现不同的行为。
这正是内部类要做的事情,内部类允许:
1)控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装了起来。内部类用来表示解决问题所必需的各个不同的action()。
2)内部类能够很容易地访问外围类的任意成员,所以可以避免这种实现变得笨拙。如果没有这种能力,代码将变得令人讨厌,以至于你肯定会选择别的方法。
考虑此控制框架的一个特定实现,如控制温室的运作:控制灯光、水、温度调节器的开关,以及响铃和重启系统,每个行为都是完全不同的。控制框架的设计使得分离这些不同的代码变得非常容易。使用内部类,可以在单一的类里面产生对同一个基类Event的多种导出版本。对于温室系统的每一种行为,都继承一个新的Event内部类,并在要实现的action()中编写控制代码。
作为典型的应用程序框架,GreenHouseControls类继承自Controller:
package innrclasses.controller;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/18下午11:20
**/
public class GreenhouseControls extends Controller {
// 灯光
private boolean light = false;
public class LightOn extends Event {
public LightOn(long delayTime) {
super(delayTime);
}
@Override
public void action() {
// 控制代码放在这里进行,打开灯光
light = true;
}
@Override
public String toString() {
return "Light is on";
}
}
public class LightOff extends Event {
public LightOff(long delayTime) {
super(delayTime);
}
@Override
public void action() {
// 控制代码放在这里进行,关闭灯光
light = false;
}
@Override
public String toString() {
return "Light is off";
}
}
// 水
private boolean water =false;
public class WaterOn extends Event {
public WaterOn(long delayTime) {
super(delayTime);
}
@Override
public void action() {
// 编写控制代码
water = true;
}
@Override
public String toString() {
return "Greenhouse water is on";
}
}
public class WaterOff extends Event {
public WaterOff(long delayTime) {
super(delayTime);
}
@Override
public void action() {
water = true;
}
@Override
public String toString() {
return "Greenhouse water is off";
}
}
// 恒温器
private String thermostat = "Day";
public class ThermostatNight extends Event {
public ThermostatNight(long delayTime) {
super(delayTime);
}
@Override
public void action() {
thermostat = "Night";
}
@Override
public String toString() {
return "Thermostat on night setting";
}
}
public class ThermostatDay extends Event {
public ThermostatDay(long delayTime) {
super(delayTime);
}
@Override
public void action() {
thermostat = "Day";
}
@Override
public String toString() {
return "Thermostat on day setting";
}
}
//
public class Bell extends Event {
public Bell(long delayTime) {
super(delayTime);
}
@Override
public void action() {
// 添加事件到事件列表
addEvent(new Bell(delayTime));
}
@Override
public String toString() {
return "Bing!";
}
}
public class Restart extends Event {
private Event [] eventList;
public Restart(long delayTime, Event [] eventList) {
super(delayTime);
this.eventList = eventList;
for (Event event : eventList) {
addEvent(event);
}
}
@Override
public void action() {
for (Event event: eventList) {
event.start(); // Rerun each event
addEvent(event);
}
start();
addEvent(this);
}
@Override
public String toString() {
return "Restarting System";
}
}
public static class Terminate extends Event {
public Terminate(long delayTime) {
super(delayTime);
}
@Override
public void action() {
System.exit(0);
}
@Override
public String toString() {
return "Terminating";
}
}
}
注意,Light、Water和Thermostat都属于外围类GreenhouseControls,而这些内部类都能够自由访问外围类字段,无需限定条件或特殊许可。而且,action()方法通常都涉及某种硬件的控制。
大多数Event类看起来都很相似,但是Bell和Restart则比较特殊。Bell控制响铃,然后在事件列表中增加一个Bell对象,于是过一会它可以再次响铃。读者可能注意到内部类是多么像多重继承:Bell和Restart有Event的所有方法,并且似乎也拥有外围类GreenhouseControls的所有方法。
一个由Event对象组成的数组被递交给Restart,该数组要加到控制器上。由于Restart也是一个Event对象,所以同样可以将Restart对象添加到Restart.action()中,以使系统能够有规律地重新启动自己。
下面的类通过创建一个GreenhouseControls对象,并添加各种不同的Event对象类配置该系统。这是命令设计模式中的一个例子在eventList中每一个被封装成对象的请求:
package innrclasses.controller;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/18下午11:20
**/
public class GreenhouseControls extends Controller {
// 灯光
private boolean light = false;
public class LightOn extends Event {
public LightOn(long delayTime) {
super(delayTime);
}
@Override
public void action() {
// 控制代码放在这里进行,打开灯光
light = true;
}
@Override
public String toString() {
return "Light is on";
}
}
public class LightOff extends Event {
public LightOff(long delayTime) {
super(delayTime);
}
@Override
public void action() {
// 控制代码放在这里进行,关闭灯光
light = false;
}
@Override
public String toString() {
return "Light is off";
}
}
// 水
private boolean water =false;
public class WaterOn extends Event {
public WaterOn(long delayTime) {
super(delayTime);
}
@Override
public void action() {
// 编写控制代码
water = true;
}
@Override
public String toString() {
return "Greenhouse water is on";
}
}
public class WaterOff extends Event {
public WaterOff(long delayTime) {
super(delayTime);
}
@Override
public void action() {
water = true;
}
@Override
public String toString() {
return "Greenhouse water is off";
}
}
// 恒温器
private String thermostat = "Day";
public class ThermostatNight extends Event {
public ThermostatNight(long delayTime) {
super(delayTime);
}
@Override
public void action() {
thermostat = "Night";
}
@Override
public String toString() {
return "Thermostat on night setting";
}
}
public class ThermostatDay extends Event {
public ThermostatDay(long delayTime) {
super(delayTime);
}
@Override
public void action() {
thermostat = "Day";
}
@Override
public String toString() {
return "Thermostat on day setting";
}
}
//
public class Bell extends Event {
public Bell(long delayTime) {
super(delayTime);
}
@Override
public void action() {
// 添加事件到事件列表
addEvent(new Bell(delayTime));
}
@Override
public String toString() {
return "Bing!";
}
}
public class Restart extends Event {
private Event [] eventList;
public Restart(long delayTime, Event [] eventList) {
super(delayTime);
this.eventList = eventList;
for (Event event : eventList) {
addEvent(event);
}
}
@Override
public void action() {
for (Event event: eventList) {
event.start(); // Rerun each event
addEvent(event);
}
start();
addEvent(this);
}
@Override
public String toString() {
return "Restarting System";
}
}
public static class Terminate extends Event {
public Terminate(long delayTime) {
super(delayTime);
}
@Override
public void action() {
System.exit(0);
}
@Override
public String toString() {
return "Terminating";
}
}
public static void main(String[] args) {
GreenhouseControls gc = new GreenhouseControls();
gc.addEvent(gc.new Bell(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();
}
}
}
配置附加参数
运行结果如:
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对象中。如果提供了命令行参数,系统会以它作为毫秒数,决定什么时候终止程序。
九、内部类的继承
因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外围类对象的“秘密的”引用必须被初始化,而在导出类中不在存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:
package innrclasses;
class WithInner {
class Inner {
Inner() {
System.out.println("Inner");
}
}
}
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/19上午12:16
**/
public class InheritInner extends WithInner.Inner {
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
输出信息:
Inner
可以看到,InheritInner只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能够只是传递一个指向外围类对象的引用。此外,必须在构造器内使用如下语法:
enclosingClassReference.super();
这样才能够提供必要的引用,然后程序才能编译通过。
十、内部类可以覆盖吗?
如果创建了一个内部类,然后继承其外围类并重新定义此内部类时,会发生什么呢?也就是说,内部类可以被覆盖吗?这看起来是一个很有用的思想,但是“覆盖”内部类就好像它是外围类的一个方法,其实并不起什么作用:
package innrclasses;
class Egg {
private Yolk y;
protected class Yolk {
public Yolk() {
System.out.println("Egg.Yolk");
}
}
public Egg() {
System.out.println("New Yolk");
y = new Yolk();
}
}
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/19上午12:27
**/
public class BigEgg extends Egg{
public class Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk");
}
}
public static void main(String[] args) {
new BigEgg();
}
}
输出结果:
New Yolk
Egg.Yolk
默认的构造器是编译器自动生成的,这里是调用基类的默认构造器。你可能认为既然创建了BigEgg对象,那么所使用的因该是“覆盖后”的Yolk版本,但是从输出结果可以看到实际情况并不是这样的。
这个例子说明,当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个类部内时完全独立的两个实体,各自在自己的命名空间内。当然,明确地继承某个内部类也是可以的:
package innrclasses;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/19上午12:35
**/
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("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");
}
@Override
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
Egg2
Egg2.Yolk
BigEgg2.Yolk
BigEgg2.Yolk.f()
现在BigEgg2.Yolk通过extends Egg2.Yolk明确地继承了此内部类,并且覆盖了其中的方法。insertYolk方法允许BigEgg2将自己的Yolk对象向上转型为Egg2中的引用y。所以当g()调用y.f()时,覆盖后的新版的f()被执行。第二次调用Egg2.Yolk(),结果是BigEgge2.Yolk的构造器调用其基类的构造其。可以看到在调用g()时,新版的f()被调用了。
十一、局部内部类
前面提过,可以在代码块里创建内部类,典型的方式是在一个方法的里面创建。局部内部类不能有访问说明符,因为他不是外围类的一部分;但是它可以访问当前代码块内的常量,以及此外围类的所有成员。下面的例子对局部类与匿名内部类的创建进行了比较。
package innrclasses;
interface Counter {
int next();
}
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/12/19上午12:55
**/
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 have a constructor
System.out.println("LocalCounter");
}
@Override
public int next() {
// access local final
System.out.print(name + " ");
return count ++;
}
}
return new LocalCounter();
}
// the same thing an anonymous(匿名) inner class
Counter getCounter2(final String name) {
return new Counter() {
// 匿名内部类没有有名字的构造函数,只有实例初始化
{
System.out.println("instance initializer");
}
@Override
public int next() {
// access local final
System.out.print(name + " ");
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
instance initializer
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文件,其中包含了如何创建该类型的对象的信息(此信息产生一个“mete-calss”,叫做Class对象),你可能猜到了,内部类也必须生成一个.class文件以包含它们的Class对象信息。这些类文件的命令有严格的规则:外围类的名字,加上“$”,再加上内部类的名字。例如,LocalInnerClass.java生成的.class文件包括:
Counter.class
LocalInnerClass$1.class
LocalInnerClass$1LocalCounter.class
LocalInnerClass.class
如果内部类是匿名的,编译器会简单地生成一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需要直接将它们的名字加在其外围类标识符与“$"的后面。
所有这种命名格式简单而直接,但是它还是很健壮的,足以应对绝大多数情况。因为这是Java标准命名方式,所以产生的文件自动都是与平台无关的。(注意,为了保证你的内部类能够起作用,Java编译器会尽可能地转换它们。)