1.内部类的定义和性质
内部类,顾名思义,就是在一个类里面定义一个类,但是内部类的一个特殊之处在于,它能够获取到其外部类对象的所有成员,包括private字段。
这时因为当外部类对象在创建一个内部类对象的时候,编译器又开始悄悄咪咪的把外部类对象的引用传入给内部类对象,于是内部类就可以通过这个外部类对象的引用对外部类的所有成员进行访问。在内部类中,可以通过外部类类名.this的方式获取到外部类对象的引用,见下面的例子。
基于这个原因,在创建内部类对象的时候,不能使用外部类类名进行创建,而必须使用外部类的对象来进行创建。下面是栗子:
public class Outer{
public class Inner{
public Outer getOuter(){
//可以在内部类中通过外部类类名.this获取到外部类的引用
return Outer.this;
}
}
public static void main(string[] args){
//编译不通过,不能通过外部类类名创建内部类,因为Inner非静态
//Inner inner = new Outer.Inner();
//编译通过,通过外部类对象创建内部类
Outer outer = new Outer();
Inner inner = outer.new Inner();
}
}
这个时候你是不是隐约感觉到了什么不对!在java基础整理1当中我们提过,static关键字可以使类的成员不再与单个具体对象相互关联,而是与类本身关联,那么我们如果用static关键字对内部类进行修饰会怎么样呢?答案是在创建一个static的内部类时,你并不需要一个外部类对象,这时我们称这个内部类为嵌套类(或静态内部类),下文中static内部类我将一律以嵌套类(或静态内部类)对其进行称呼,而内部类则指代非static的内部类
2.匿名内部类
在匿名内部类之前,我们首先了解一下在方法和作用域中的内部类
在方法中定义的内部类其实很好理解,即在方法体当中,内部类可以访问,如下
//在方法中定义的内部类
public class InnerClassInMethod {
public interface Destination{
String readLabel();
}
//一个返回Destination的方法
public Destination methodA(String s){
//在其中定义一个内部类, 该内部类仅可在methodA(String s)的方法体中访问
class InnerDestination implements Destination{
private String label;
private InnerDestination(String dest){
label = dest;
}
public String readLabel(){
return label;
}
}
return new InnerDestination(s);
}
}
在作用域中定义的内部类与在方法中定义类似,在定义内部类的整个作用于当中,内部类都是可以访问的
//在作用域中定义的内部类
public class InnerClassInField {
public interface Destination{
String readLabel();
}
//一个返回Destination的方法
public void methodB(Boolean b){
if(b){
//在其中定义一个内部类, 该内部类仅可在methodA(String s)的方法体中访问
class InnerDestination implements Destination{
private String label;
private InnerDestination(String whereTo){
label = whereTo;
}
public String readLabel(){
return label;
}
}
//可以在if()的作用域中对内部类进行调用
InnerDestination innerDestination = new InnerDestination("在作用域中可以调用");
}
//在这里就不能对内部类进行调用
//InnerDestination innerDestination = new InnerDestination("在作用域外则不能调用");
}
}
匿名内部类则是以上情况的简化形式,当匿名内部类需要使用一个从外部定义的对象,那么这个对象必须声明为final的,否则会出现编译时错误消息,下面我们将InnerClassInMethod.java改变为匿名内部类的
//InnerClassInMethod.java的匿名内部类形式
public class AnonymousClassInMethod {
public interface Destination{
String readLabel();
}
//一个返回Destination的方法
public Destination methodA(final String s){
//使用匿名内部类返回Destination对象,因为需要使用外部String对象s,因此s被定义为final
return new Destination() {
private String label = s;
@Override
public String readLabel() {
return label;
}
};
}
}
3.嵌套类(静态内部类)
前面我们在第1部分中最后有提到,在创建内部类时,外部类对象会将自己的引用传给内部类持有。但是当我们用static关键字修饰内部类时,内部类对象就不会与外部类的对象相联系,即不再持有外部类对象的引用,这时该内部类我们称之为嵌套类。
嵌套类有几个性质:
- 创建嵌套类时不需要外部类的对象
- 嵌套类不能访问外部类的非静态对象
- 嵌套类可以包含static数据和字段,而普通的内部类不能拥有这些
因为嵌套类不具有对外部类对象的引用,以上性质就比较好理解了
4.为什么要用内部类(重点!!!)
由于java的继承机制使得子类只能具有一个父类,这个限制会使得一些编程问题变得难以解决
但是内部类的存在使得这个问题变得容易起来。首先我们思考一下内部类的特点,往往在我们的编程当中,外部类可以继承某个类或某个接口,内部类也可以独立的继承某个类或某个接口,而同时由于内部类能够访问其外部类的对象,因此内部类能够访问自己所继承的以及其外部类对象所继承的所有方法,于是该内部类可以看做继承了两个类。因此实际上,内部类是一种多重继承的实现方式。
以下介绍两种内部类的应用场景
a. 回调 + “钩子”
/** 一个带有increment()方法的接口*/
interface Incrementable{
void increment();
}
/** 第一个回调类,实现Incrementable接口 */
class Callee1 implements Incrementable{
private int i = 0;
@Override
public void increment() {
i++;
System.out.println(i);
}
}
/**一个同样含有increment()但做着不同工作的类*/
class OtherOptionClass{
public void increment(){
System.out.println("other operation");
}
}
/**
* 第二个回调类
* 若因为某些原因必须继承OtherOptionClass,但又想实现Incrementable
* 由于两者都具有increment()方法,则可以通过内部类来实现
*/
class Callee2 extends OtherOptionClass{
private int i = 0;
/** 继承并覆盖OtherOptionClass中的increment方法 */
@Override
public void increment(){
super.increment();
i++;
System.out.println(i);
}
/** 定义内部类,内部类实现Incrementable接口 */
private class Closure implements Incrementable{
@Override
public void increment() {
/** 调用外部类的方法 */
Callee2.this.increment();
}
}
/** 定义一个方法以获取到Closure对象 */
Incrementable getCallbackReference(){
return new Closure();
}
}
/** 调用者类,需要通过传入Incrementable对象来构建实例,在适当的时候调用其方法,即回调 */
class Caller{
Incrementable callBackReference;
Caller(Incrementable cbr){
callBackReference = cbr;
}
/** 在方法中执行回调 */
void go(){
callBackReference.increment();
}
}
/** 主类 */
public class Callbacks {
public static void main(String[] args) {
/** 创建两个被回调对象 */
Callee1 c1 = new Callee1();
Callee2 c2 = new Callee2();
/** 构建两个调用者对象,c1传入自身,c2传入自己的内部类*/
Caller caller1 = new Caller(c1);
Caller caller2 = new Caller(c2.getCallbackReference());
/** 在方法中执行回调 */
caller1.go();
caller1.go();
caller2.go();
caller2.go();
}
}
/** Output:
1 ->caller1.go();
2 ->caller1.go();
other operation ->caller2.go();
1
other operation ->caller2.go();
2
*/
如上例,构建Callee对象,将其作为构造参数传入Caller对象中,这样可以让Caller对象在恰当的时候决定操作Callee类,这就是程序设计时常用到的回调。
在这个例子当中,定义了两个回调类,第二个回调类需要继承一个类和一个接口,但是它们具有不同作用但具有相同方法标签的increment()方法,这个时候,我们可以考虑使用内部类对接口进行继承,并在接口方法实现时调用外部类Callee2的increment()方法,从而达成了外部类Callee2对类和接口的increment()同时进行了继承。
在本例中内部类Closure仅可通过Callee2获取,通过它可以转发调用Callee2对象的increment()方法,这被称为钩子,另外,在该类的外部获取到内部类Closure,也仅能通过它调用Callee2对象的increment(),因此这个是个安全的钩子。
b. 内部类与控制框架
控制框架如控制一个温室的运行:控制灯、水、温度的开关,响铃等操作。
- 首先可以定义一个模板事件类Event,在这个类中具有一个抽象方法action(); (应用模板模式)
- 然后搭建框架抽象类Controller,该类维护一个Event列表,可以添加删除Event,并通过run()方法逐条执行Event。
- 实现具体的框架类GreenHouseControls,具有私有变量light、water、temperate等,并定义多个内部类如LightOn、LightOff等继承自Event,实现各自的action()方法 (应用命令模式,不同的Event自行决定如果和实现)
- 实现一个main()方法,创建GreenHouseControls实例,操作之~
这个例子虽然也可以不用内部类实现,例如可以单独定义多个操作类继承自Event,然后在构造方法中传入具有light、water、temperate等的GreenHouseControls对象,通过这个对象引用修改其参数。
但是如果使用内部类,就可以将这些所有的细节全部封装起来,此外内部类能够很容易访问外部类GreenHouseControls的变量
该例子详情请见《java编程思想第四版》p207 - p211