内部类
- 所谓内部类就是将一个类定义在另一个类的定义内部。还记得组合的定义是:成员是另外一个类的对象。有点类似,那却不同。那么问题来了,为什么要使用有内部类?使用内部类有什么好处?如何使用内部类?听我娓娓道来。
创建内部类
public class Outer {
String name;
void method() {
Inner in = new Inner();
}
class Inner {
}
}
class other {
void method() {
Outer o = new Outer();
Outer.Inner i = o.new Inner();
}
}
- 看起来除了Inner 类定义的位置是在Outer内部以外好像并没有其他区别。不过这里可以用任意的访问权限修饰符进行修饰,作用和修饰方法成员类似,后续会有例子。还可以用static或final去修饰。final不必多说,而static修饰会在后面进行讨论,此处先讨论非static的内部类。
链接到外部类
- 我们知道组合是对象中包含其他对象的引用,那内部类对象会有怎样的特性呢。上面的例子我们在其他类中创建了一个
Outer.Inner
对象。这个很好理解,Inner是default的,所以在同包下可以访问,它又是附属于Outer的,所以必须带上命名空间,否则就无法和普通类区别了。我们使用.new语法创建了一个Inner对象。那为什么不能直接这样去创建对象呢?
Outer.Inner i = new Outer.Inner();//错误
- 其实上述写法只要在Inner是static的情况下,就可以通过编译。一个很重要的思想,如果一个成员或者方法不是static的,那就必须创建对象后,才能去使用它。这句话对内部类也同样适用,我们可以认为非static内部类的对象是外围类的一个“可选择成员”。因为存在这种先后关系,内部类对象会暗暗地连接到创建它的外部类对象上。有两个问题需要考虑:
- 怎么通过外围类对象去访问内部类对象?
- 怎么通过内部类对象去访问外围类对象?
- 首先,通过外围类对象如何创建内部类对象我们已经知道了,我们可以通过外围类对象去创建任意个数的内部类对象。因此我们好像无法直接通过外围类对象去获得某个内部类对象的引用(数量未知)。但反过来就不一样了,由于所有的内部类对象都是由一个固定的内部类对象创建的,因此我们就可以通过.this语法去获得外围类对象的引用:
public class Main {
public static void main(String args[]) {
Outer o = new Outer();
Outer.Inner i = o.new Inner();
i.run();
i.outerRun();
i.outerRun2();
}
}
class Outer {
void run() {System.out.println("Outer run");}
private void run2() {System.out.println("Outer run2");}
class Inner {
void outerRun() {Outer.this.run();}
void run() {System.out.println("inner run");}
void outerRun2() {run2();}
}
}
- 这里使用了
Outer.this
这样的语法,还记得普通方法里面的this吗,通常我们可以省略this。只要没有歧义,内部类也支持这种省略。我们可以说内部类具有外围类的一切信息。
内部类与向上转型
- 问题又来了,private的内部类有什么用?此外,为什么要使用内部类,我直接用两个类进行组合不好吗?
- 这两个问题先放着,继续讨论内部类。我们知道对象可以向上转型,那么内部类对象也是可以这样的。我们可以让内部类实现一些接口,或者继承一个父类,这样的话,内部类的意义就体现出来了。
public class Main {
public static void main(String args[]) {
Outer o = new Outer();
Animal a = o;
a.run();
RunImpl r = o.getRun();
r.run();
EatImpl e = o.getEat();
e.eat();
Sleeper s = o.getSleep();
s.sleep();
}
}
abstract class Animal {
abstract void run();
}
abstract class Sleeper {
abstract void sleep();
}
class Outer extends Animal {
private class Inner extends Sleeper implements RunImpl, EatImpl {
public void run() {
System.out.println("inner run");
}
public void eat() {
System.out.println("inner eat");
}
void sleep() {
System.out.println("inner sleep");
}
}
RunImpl getRun() {
return new Inner();
}
EatImpl getEat() {
return new Inner();
}
Sleeper getSleep() {
return new Inner();
}
void run() {
System.out.println("animal run");
}
}
interface RunImpl {
void run();
}
interface EatImpl {
void eat();
}
- 对外而言,Inner类是完全不可见的,但是我们却可以通过接口去间接使用它。由于Inner类具有Outer 类的所有信息,我们完全可以认为Outer 类实现了RunImpl和EatImpl接口。不仅如此,我们还可以认为Outer 类继承了Sleeper类,这样解决了多重继承的问题。有了这个方法以后我们写代码就可以更加方便了。这体现了一点内部类的意义。
在方法和作用域内的内部类
- 上面一些使用内部类的方法都是比较简单的,我们还可以在方法或者任意的作用域内定义内部类。这样做是有原因的:
- 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
- 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。
- 在后面的例子中,先前的代码将被修改,以用来实现:
- 一个定义在方法中的类。
- 一个定义在作用域内的类,此作用域在方法的内部。
- 一个实现了接口的匿名类。
- 一个匿名类,它扩展了有非默认构造器的类。
- 一个匿名类,它执行字段初始化。
- 一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)。
定义在方法中的类
RunImpl getRun2() {
final int i = 5;
class temp implements RunImpl {
private int j = i;
public void run() {
System.out.println("temp run");
}
}
return new temp();
}
RunImpl r2 = o.getRun2();
r2.run();
- 这里有一个疑问,那就是:定义在方法内的类(其实下面的例子都有这个特性),想要引用方法作用域内的局部变量,则该变量必须是常量。这里说一下我的看法。按照我们的逻辑,在定义内部类时传入的形参值应该是在执行该类前的最后一次值。比如下面这个例子:
RunImpl getRun2() {
int a = 1;
class temp implements RunImpl {
public void run() {
System.out.println(a);
a++;
}
}
a++;
return new temp();
}
- 但是还记着之前讲过的生命周期吗?在getRun2()内,a是一个局部变量,它会随着方法的结束而销毁。但是内部类却不会被销毁(不然你怎么调用它的run()呢?)。我们可以说内部类的生命周期是长于方法中的局部变量的。那我们在内部类中引用一个已经被销毁的局部变量算哪门子事?其实在这里编译器悄悄在内部类的域中增加了一个成员,然后初始化它为局部变量的值。这样的话,我们就可以在内部类中去使用它,且不用担心它被销毁的问题了。但这种隐藏操作,开发人员是看不见的。在内部类中的变量和方法中的变量本质上是不同的,只是他们的值或者指向的对象是相同的。那么如果该变量不是final的,也就意味着它可以在方法内被修改。但是在方法的作用域内修改又不会影响内部类中那个隐藏的成员。这个特性很容易造成错误。所以Java干脆就禁止这种修改,规定其必须为final。大家有兴趣可以看看这边博文。
定义在作用域内的类
RunImpl getRun3(boolean flag) {
if (flag) {
class temp implements RunImpl {
public void run() {
System.out.println("temp run");
}
}
return new temp();
}
return null;
}
- 和上一个例子相同,temp类出了if的作用域后就不能再使用了。不过有个地方需要注意,这里的语义不是说如果flag为true就定义temp类。无论flag为什么,temp类始终会被编译,将其定义在if作用域内意味着它只能在该作用域内被使用。
匿名类(剩余的内部类样例)
- 如果一个内部类只在方法内被使用(事实上由于作用域的限制,其他地方也根本不可能使用它),那我们就可以写的更加简洁。看一个例子:
RunImpl getRun4() {
return new RunImpl() {
public void run() {
System.out.println("runImpl run");
}
};
}
Animal getRun5() {
return new Animal() {
private String test = "1";
public void run() {
System.out.println("animal run" + test);
}
};
}
- 这种语法指的是,创建一个继承自Animal(或者是实现了RunImpl接口)的匿名类的对象。通过new表达式返回的引用被自动向上转型为对Animal(RunImpl)的引用。
- 上述两个例子都使用了默认的构造器,同样也可以使用父类的其他构造器来生成匿名类:
abstract class Animal {
String type;
abstract void run();
public Animal() {}
public Animal(String type) {this.type = type;}
}
Animal getRun6() {
return new Animal("miaoch") {
public void run() {
System.out.println(super.type + " run");
Outer.this.run();
}
};
}
- 至于为什么只能借用父类的构造器,而不是自己重新定义构造器的原因是:它是匿名的,完全没办法编写构造器(不过也有一些类似的方法,下面会讨论)。
- 如果是实现接口的话,就不能借用父类的(因为没有父类)非默认构造器了。但是我又的确需要传入某个值,那该怎么办呢?我们可以通过传入final局部变量,再使用类中的字段初始化的第二种方法。
RunImpl getRun4() {
final String s = "2";
final int i = 3;
return new RunImpl() {
private String s2;
private int i2;
{
s2 = s;
i2 = i > 2 ? 2 : i;
run();
}
public void run() {
System.out.println(s2);
System.out.println(i2);
}
};
}
嵌套类
- 如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static,这通常称为嵌套类。嵌套类意味着:
- 要创建嵌套类的对象,并不需要其外围类的对象。
- 不能从嵌套类的对象中访问非静态的外围类对象。
- 我们来看一个嵌套类的例子:
class Outer extends Animal {
private static int a = 1;
private int b = 2;
private static class temp2 {
static int c = 3;
void method() {
System.out.println(a);
System.out.println(b);
}
}
}
- 普通的内部类是不能有static成员或方法的,这是因为普通的内部类对象需要依附于外围类对象。如果没有外围类对象,那static成员或方法也是无法直接访问的,这没有体现static存在的意义。但是嵌套类就没有这个限制,我们可以在嵌套类中定义static成员或方法。
- 再来说一下此处请求b报错的原因。其实原因很简单,b是一个非static的字段,而嵌套类对象的存在不需要外围类对象为前提(但是b的存在需要外围类对象为前提),所以嵌套类不能访问外围类的非static成员或方法。
- 当有多层嵌套类的时候,我们可以通过类名.嵌套类名.嵌套类名去编写代码。
class Outer extends Animal {
class A {
class B {
}
}
static class C {
static class D {
}
}
static class E {
class F {
}
}
}
Outer o = new Outer();
Outer.A o_a = o.new A();
Outer.A.B o_a_b = o_a.new B();
Outer.C o_c = new Outer.C();
Outer.C.D o_c_d = new Outer.C.D();
Outer.E o_e = new Outer.E();
Outer.E.F o_e_f = o_e.new F();
接口中的嵌套类
- 之前我们知道接口中的方法都是public的,接口中的成员都是static和final和public的。如果在接口中写入一个内部类,无论有没有static关键字,都会被认为是嵌套类(也是public的,但不是final的)。嵌套类就不过多说明了。下面是一个用来测试的例子:
public class intface {
void run() {
System.out.println("run");
}
static class Temp {
public static void main(String args[]) {
new intface().run();
}
}
}
一个关于内部类的综合例子
import java.util.ArrayList;
import java.util.List;
/**
* 这是一个抽象事件类,利用ready()判断是否可以开始执行事件程序action()
*/
abstract class Event {
private long eventTime;
protected final long delayTime;
public Event(long delayTime) {
this.delayTime = delayTime;
start();
}
public void start() {
eventTime = System.currentTimeMillis() + delayTime;
}
public boolean ready() {
return System.currentTimeMillis() >= eventTime;
}
public abstract void action();
public abstract String toString();
}
/**
* 这是一个控制器类,用于控制多个事件的执行
*/
class Controller {
private List<Event> eventList = new ArrayList<Event>();
public void addEvent(Event e) {
eventList.add(e);
}
public void run() {
while (eventList.size() > 0) {
List<Event> copyList = new ArrayList<Event>(eventList);
for (Event e : copyList) {
if (e.ready()) {
System.out.println(e);
e.action();
eventList.remove(e);
}
}
}
}
}
/**
* 这是一个扩展的控制器
*/
class GreenHouseControls extends Controller {
private boolean light;
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";
}
}
public class Bell extends Event {
public Bell(long delayTime) {
super(delayTime);
}
public void action() {
addEvent(new Bell(delayTime));
}
public String toString() {
return "Bing!";
}
}
public class Restart extends Event {
private Event[] eventlist;
public Restart(long delayTime, Event[] eventlist) {
super(delayTime);
this.eventlist = eventlist;
}
public void action() {
for (Event e: eventlist) {
e.start();
addEvent(e);
}
start();
addEvent(this);
}
public String toString() {
return "Restart system";
}
}
public static class Terminate extends Event {
public Terminate(long delayTime) {
super(delayTime);
}
public void action() {
System.exit(0);
}
public String toString() {
return "Terminating";
}
}
}
public class GreenHouseController {
public static void main(String args[]) {
GreenHouseControls gc = new GreenHouseControls();
gc.addEvent(gc.new Bell(1000L));
Event[] eventList = new Event[] {
gc.new LightOn(200L),
gc.new LightOff(400L)
};
gc.addEvent(gc.new Restart(2000L, eventList));
gc.addEvent(new GreenHouseControls.Terminate(10*1000L));
gc.run();
}
}
内部类的继承
- 先讲一下内部类继承内部类。我们都知道内部类(非嵌套类)都需要外围类对象作为依附。因为创建子内部类对象需要先创建父内部类对象,父内部类对象存在的条件是外围类对象必须存在。而子内部类存在的条件也是外围类对象必须存在,所以我们在创建子内部类对象时得先创建外围类对象,这样就没有什么问题。
- 但是当一个非内部类要去继承一个内部类时(或者说只要他们不是同属于一个外围类),问题就出现了。我要想创建该非内部子类的对象,我就必须先创建外围类:
public class Test extends A1.A2 {
public Test() {
}
}
class A1 {
class A2 {
}
}
- 这样必然是无法通过编译的。问题就出在必须先创建A1对象。那么我该如何创建A1对象呢?或者说我该如何让某个A1对象成为父类A2对象的外围类呢?
public class Test extends A1.A2 {
public Test(A1 a1) {
a1.super(1);
}
public static void main(String args[]) {
A1 a = null;
A1 b = new A1();
new Test(b).run();
}
}
class A1 {
private int i = 123;
class A2 {
A2 (int i) {
System.out.println("A2构造器: " + i);
}
A2 () {
}
void run() {
System.out.println("通过特殊构造器建立了外围类关联: " + A1.this.i);
}
}
}
---------------
A2构造器: 1
通过特殊构造器建立了外围类关联: 123
内部类可以被覆盖吗
- 自然是不行的,它与变量和方法是不一样的。假设你意图在子类中重新定义某个内部类,那他们也不过是存在于两个命名空间里面的名字刚好相同的类而已。有兴趣的同学可以做做例子,我这里就省略了。