1 匿名内部类的定义
我们通过一个程序段来给出匿名内部类的定义,如下:
interface Component {
void showLabel();
}
class Car {
public Component getComponent() {
return new Component() { // 插入函数定义
private String label = "810";
public void showLabel() { System.out.println(label); }
}; // 注意这里有个分号
}
}
public class Test {
public static void main(String[] args) {
Car c = new Car();
Component co = c.getComponent();
co.showLabel();
}
}
/* Output
810
*/
注意:getComponent() 方法将返回值的生成与表示这个返回值的类的定义结合在一起,而且这个类是没有名字的,那么这个类就是匿名内部类。
2 经典用法
在前面几篇博客中探讨了内部类、接口与经典方法之间的关系,在匿名内部类中,这种关系依旧存在着。
局部内部类可以借助接口将类定义与类的对象引用的返回结合在一起,匿名内部类同样也可以做到,正如它在定义中所说的那样,此外,两者在实现这个需求时都必须有的一个条件是:拥有可以向上转型的接口。关于这点,匿名内部类有稍稍别于局部内部类,原因如下:
我们知道向上转型可以转型为接口、抽象类或者具体类,在实现“类定义与类的对象引用的返回结合在一起”这个需求时,局部内部类可以向这三者的任意一个来转型;但是匿名内部类只能安全转向接口,在其转向抽象类或者具体类时就需要“老实些”以避免出错。
下面是一个将匿名内部类向上转型为抽象类的错误示例,如下
abstract class Component {} // 抽象类
class Car {
public Component getComponent() {
return new Component() { // 插入函数定义,实现抽象类
private String label = "810";
public void showLabel() { System.out.println(label); } // 添加了新的方法,此行会报错
}; // 注意这里有个分号
}
}
public class Test {
public static void main(String[] args) {
Car c = new Car();
Component co = c.getComponent();
co.showLabel();
}
}
解析:当运行上述代码时编译器会生成如下错误信息:
The method showLabel() is undefined for the type Component
大意是 “showLabel() 没有在 Component 类型中定义”。由此可见如果想让继承了抽象类的匿名内部类拥有方法,就需要提前让抽象类有个方法(不需要具体实现),但是可以对继承抽象类的匿名内部类随意添加数据成员。
但是如果继承了抽象类的匿名内部类不添加额外的方法,那么就不会有问题。
注意:上述代码中 return 语句结尾的分号 “;”是一定要有的,因为匿名内部类只是 return 语句的一部分,而身为节点语句,return 必须有“;”才能完整。
3 构造器
构造器无外乎两种类型,默认(无名)构造器和命名构造器,两者都可以是调用基类的构造器,在这里,简单的叙述一下这两种情况:
- 命名构造器:匿名内部类没有命名构造器,因为它没有名字!所以无法编写命名构造器。
- 默认构造器:匿名内部类常用的是自身默认的构造器,在调用基类的构造器时也是由自身的默认构造器来调用的。但是它的默认构造器是不可修改的,因为它没有名字,但是我们可以通过实例初始化来达到修改默认构造器这一效果。
注意:实例初始化就是在类中插入代码片段,就如同在真正的构造器中一样,代码块中可以含有各种语句及数据成员初始化,并且在运行时它可以起到构造器的效果(在使用前其内部类的成员前,它们都会按用户所想的被初始化过)。
实例初始化不能被重载。
实例初始化的使用示例如下:
interface In {
void show();
}
public class Test {
public static In getIn() {
return new In() {
{ System.out.println("实例初始化"); } // 实例初始化,是一个代码块
public void show() {
System.out.println("getIn.show()");
}
};
}
public static void main(String[] args) {
In i = getIn();
i.show();
}
}
/* Output
实例初始化
getIn.show()
*/
解析:从上述代码的运行结果中可以看出实例初始化的代码块在创建内部类时就已经运行完毕了。
我们将得到匿名内部类的方法设计成 static 类型是为了可以通过外部类的类名直接调用这个方法(此处因为是在含有 static 方法外围类中调用该方法,所以连外围类名都省略了),而非只有通过创建外部类对象才能使用这个方法,java 类库中存有大量的 static 方法,这种设计思想在 Java 中是极为常见的。
4 传递参数
匿名内部类在向外界寻求参数时与其它类不同,在这里主要叙述两种情况(也是正常参数使用的两大方面):
- 传递参数给基类的构造器:只需要将参数传递给返回匿名内部类的方法即可(注意:在编写定义时,必须要在在匿名内部类定义前的 return 语句中的 new BaseClass() 的括号中也写上该形参,这样才能正确传递,否则报错),创建内部类,将参数作为方法的实参,匿名内部类构造器会自动将它传递给基类的构造器。
示例如下:
class Base { // 基类
private int x;
Base(int x) {
this.x = x;
System.out.println(x);
}
}
public class Test {
public static Base getBase(int x) { // 参数 x 作为 getBase() 的实参
return new Base(x) { // 这里也必须写上参数
};
}
public static void main(String[] args) {
Base b = getBase(810); // 传递实参
}
}
/* Output
810
*/
- 在匿名内部类内中使用的参数:想要在匿名内部类中使用外部传递的参数,就必须在在定义时在形参前面加上 final,如果没有 final 限定,编译时会报错。
示例如下:
interface In {
void show();
}
public class Test {
public static In getIn1(final String s) { // 必须加上 final 限定
return new In() { // 此处不需要再写形参
public void show() {
System.out.println(s);
}
};
}
public static In getIn2(String s) {
return new In() {
public void show() {
System.out.println(s);
}
};
}
public static void main(String[] args) {
In i1 = getIn1("必须加上final限定");
i1.show();
In i2 = getIn2("不加final限定"); // 运行时此行会报错
i2.show();
}
}
解析:在上面的代码中,Test 外围类中含有 2 个匿名内部类,返回它们的方法分别是 getIn1() 和 getIn2()。我们在 getIn1() 的内部类中使用外来参数并指定其为 final,这种方式在运行时会成功;在 getIn2() 的内部类中直接使用外来参数不指定其为 final,运行时就会报错。
上述程序运行结果如下
必须加上final限定
Exception in thread “main” java.lang.Error: Unresolved compilation problem:
Cannot refer to the non-final local variable s defined in an enclosing scope
5 可视度
匿名内部类与其它内部类一样,都可以自由的访问外围的数据成员及方法;但反过来则不行,外围类依旧对内部类的一切不可见,这点与其它内部类也一样。
6 匿名内部类的缺陷
匿名内部类的继承与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备,而且如果是实现接口,也只能实现一个接口。
备注
更多细节可以看 Bruce Eckel 所著的《Java 编程思想》,本博文多为该书的学习心得,欢迎大家一起探讨。