引言
上一篇文章,我们介绍了成员内部类和静态内部类,这篇文章,我们继续看匿名内部类和局部内部类。
匿名内部类
匿名内部类的定义
匿名内部类是在方法或者作用域中定义的,应该是我们平时使用较多的内部类了,我们在创建Thread对象时,经常会这样来写:
public class AnonymousExample {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
}
});
}
}
给Thread的构造函数传递的就是一个匿名内部类,一般情况下,我们用这种方式来重写接口或者抽象类的方法,例如这里的run方法。
但是从广泛意义上来说,不一定非要接口或者抽象类,对于任意一个类,我都可以声明一个这个类的匿名类,例如下面的例子:
public class AnonymousInnerClass {
private String name;
public void sayName(){
System.out.println(name);
}
}
public class AnonymousTest {
public static void getInnerClass(){
AnonymousInnerClass anonymousInnerClass = new AnonymousInnerClass() {
@Override
public void sayName() {
System.out.println("i am a anonymous inner class");
super.sayName();
}
};
}
}
AnonymousInnerClass本身就是一个普通的类,我通过匿名类来重写了sayName方法。
我甚至可以这样,在匿名类里面什么都不做:
public static void getInnerClass(){
AnonymousInnerClass anonymousInnerClass = new AnonymousInnerClass() {
};
}
注意这与普通的创建对象的区别,new AnonymousInnerClass()后面不是分号,而是大括号,只是大括号里面什么都没有。
为匿名内部类传递构造器参数
当匿名内部类的构造方法需要参数时,我们只需要在new后面的括号中传入参数即可:
public class AnonymousInnerClass {
private String name;
public AnonymousInnerClass(String name) {
this.name = name;
}
public void sayName(){
System.out.println(name);
}
}
public class AnonymousTest {
public static void getInnerClass(String name){
AnonymousInnerClass anonymousInnerClass = new AnonymousInnerClass(name) {
};
}
}
AnnoymousInnerClass需要一个String类型的参数来构造对象,在getInnerClass中,我们只需要在创建时给定这个参数即可,就像创建普通的类实例一样。
需要注意的一点是,除了匿名内部类继承过来的构造方法,在匿名内部类内部不能再声明构造方法。因为匿名内部类没有名字,构造方法根本不能调用。
匿名内部类是唯一一种没有构造器的类。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。
匿名内部类访问外部类字段和方法
前面讲成员内部类和静态内部类的时候,知道了成员内部类能够访问外部类的任意字段和方法,静态内部类能够访问外部类的静态字段和方法,那么匿名内部类呢?
首先,匿名类是在方法中声明的,而方法就可能是静态的或者非静态的,因此匿名类的访问权限是与方法相同的。
首先看下面的例子:
public class AnonymousExample {
private String name = "name";
private static String aStaticName = "aStaticName";
private String getName(){
System.out.println(name);
return name;
}
public static String getTheStaticName() {
System.out.println(aStaticName);
return aStaticName;
}
public void test(){
new Thread(){
@Override
public void run() {
System.out.println(name);
System.out.println(aStaticName);
name = "anotherName";
aStaticName = "anotherStaticName";
AnonymousExample.this.getName();
getTheStaticName();
super.run();
}
}.start();
}
public static void main(String[] args) throws InterruptedException {
AnonymousExample anonymousExample = new AnonymousExample();
anonymousExample.test();
}
}
test方法是一个非静态方法,它里面的内部类可以访问外部类的任意字段和任意方法,并且可以修改它们。
对于静态方法,就有些限制了,静态方法中定义的内部类只能访问外部类的静态字段和方法:
我把上面的非静态方法加上了static,就会出现上图中的编译错误,原因是该内部类是在一个静态的上下文中。
匿名内部类访问方法内定义的变量
匿名内部类除了能访问外部类的字段和方法外,还能访问方法中定义的变量。
被访问的对象必须是final的
当匿名类访问在其外部定义的对象时,编译器就会要求其参数是final的。当省略final时,编译器也不会报错,但被引用的对象一定要保证是不变的(简单类型是值不变,引用类型是引用不变),看下面的例子:
a在test方法中定义,相对于匿名内部类来说是外部对象,在声明时没有final是没有问题的,但是之后a=19这句代码要修改a的值,就会编译报错。
对于引用类型的对象例如Map,限制是一样的,我们不能再声明之后让引用类型的变量指向其他对象实例。但是我们可以在匿名内部类中修改引用指向的对象的数据:
public class AnonymousExample {
public static void test(){
Map<String,String> map = new HashMap<>();
map.put("outer","outer");
new Thread(){
@Override
public void run() {
map.put("inner","inner");
System.out.println(map.get("inner"));
}
}.start();
}
public static void main(String[] args) {
test();
}
}
会输出:
inner
为什么必须是final的
要理解被匿名内部类访问的对象为什么是final的,我们需要理解匿名内部类访问外部对象是怎么实现的。看下面这个简单的例子:
public class AnonymousExample {
public static void test(){
int a = 10;
new Thread(){
@Override
public void run() {
System.out.println(a);
}
}.start();
}
public static void main(String[] args) {
test();
}
}
我们需要看反编译后的代码:
public class AnonymousExample
{
public AnonymousExample()
{
}
public static void test()
{
int a = 10;
(new Thread(a) {
public void run()
{
System.out.println(a);
}
final int val$a;
{
a = i;
super();
}
}
).start();
}
public static void main(String args[])
{
test();
}
}
看创建Thread的语句new Thread(a),Thread的构造器怎么会有一个int参数?类里面还多了一个val$0的final变量,还多了一个静态块,静态块里面对a进行了初始化。
根据这些,我们猜想编译器碰到访问外部对象的匿名内部类时,创建了一个新的类,这个新的类的构造方法中有对应需要访问的外部对象类型作为参数,并且有对应外部对象的变量作为自己的变量,这些变量通过静态块进行了赋值。
我不知道怎么用jad命令输出编译器自动生成的类。但是我们可以从字节码找到:
箭头指向的就是编译器生成的类的构造方法,可以看出类的名称是AnonymousExample$1。
所以编译器这样做意味着什么呢?意味着匿名内部类中访问的变量实际上是匿名内部类自己的,编译器通过上面描述的方法将外部变量copy(简单类型直接复制值,引用类型复制变量的引用)了一份到匿名类内部作为变量。既然是复制,那么复制之后原来外部的对象,不管是简单类型还是引用类型,一旦发生了变化,就会导致内部类与外部变量不一致。所以编译器限制这些变量必须是final的。
看下面的例子:
public class AnonymousExample {
public static void test(Map<String,String> map){
int a = 10;
new Thread(){
@Override
public void run() {
System.out.println(a);
System.out.println(map);
}
}.start();
}
public static void main(String[] args) {
}
}
反编译后的代码:
public class AnonymousExample
{
public AnonymousExample()
{
}
public static void test(Map map)
{
int a = 10;
(new Thread(a, map) {
public void run()
{
System.out.println(a);
System.out.println(map);
}
final int val$a;
final Map val$map;
{
a = i;
map = map1;
super();
}
}
).start();
}
public static void main(String args1[])
{
}
}
此时,编译器生成的类构造方法应该有了两个参数,分别是int和map类型。
上面这两个例子中的test方法都是静态的,当test方法不是静态的时候,编译器创建匿名类AnonymousExample$1的方式可能有所不同,这里不再展开,具体可以参考《java语言规范》15.9.5章节的内容。
既然编译器需要为匿名内部类生成新的类型,那么对于访问不同外部对象的内部类,是不是会生成多个内部类呢?看下面的例子:
public class AnonymousExample {
public static void test(Map<String,String> map){
int a = 10;
SimpleObject simpleObject = new SimpleObject();
new Thread(){
@Override
public void run() {
System.out.println(a);
System.out.println(simpleObject);
}
}.start();
new Thread(){
@Override
public void run() {
System.out.println(a);
System.out.println(map);
}
}.start();
}
public static void main(String[] args) {
}
}
两个Thread访问的外部数据的类型不同,看字节码:
确实是的,两个新类AnonymousExample$1和AnonymousExample$2。
上述应该就是匿名内部类比较关键的知识点了,在我们平时的使用中,匿名内部类通常就是用来创建某个接口或者类的实例,然后重写某个方法。
局部内部类
局部内部类的定义
局部内部类与匿名内部类很相似,都是定义在方法或者作用域内的类,只不过局部内部类有自己的名字,可以定义构造方法,下面就是一个局部内部类:
public class AnonymousExample {
private String name = "name";
private static String aStaticName = "aStaticName";
private String getName(){
System.out.println(name);
return name;
}
public static String getTheStaticName() {
System.out.println(aStaticName);
return aStaticName;
}
public void test(){
String aLocalName = "aLocalName";
class LocalInnerClass{
private int i;
private String localInnerName;
public LocalInnerClass(int i, String localInnerName) {
this.i = i;
this.localInnerName = localInnerName;
}
public void test(){
System.out.println(name);
System.out.println(aStaticName);
System.out.println(aLocalName);
}
}
new LocalInnerClass(1,"localInnerClass1").test();
}
public static void main(String[] args) throws InterruptedException {
AnonymousExample anonymousExample = new AnonymousExample();
anonymousExample.test();
}
}
LocalInnerClass在方法test中被定义,除了出现的位置外,它与普通类没有区别。注意局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的,它只能在定义它的方法中被使用。
局部内部类访问外部类字段和方法
局部内部类访问外部类的字段和方法的规则与匿名内部类是相同的,这里不再赘述。
局部内部类访问方法内定义的变量
这个规则与匿名类也是相同的,都要求变量是final的,这里不再赘述。
小结
匿名内部类和局部内部类都是定义在方法或者作用域内的类,并且两者的使用方式和很多访问规则都是相同的,下一篇文章,我们来看内部类的作用。