引言

上一篇文章,我们介绍了成员内部类和静态内部类,这篇文章,我们继续看匿名内部类和局部内部类。

匿名内部类

匿名内部类的定义

匿名内部类是在方法或者作用域中定义的,应该是我们平时使用较多的内部类了,我们在创建Thread对象时,经常会这样来写:

  1. public class AnonymousExample {
  2. public static void main(String[] args) {
  3. Thread thread = new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. }
  7. });
  8. }
  9. }

给Thread的构造函数传递的就是一个匿名内部类,一般情况下,我们用这种方式来重写接口或者抽象类的方法,例如这里的run方法。
但是从广泛意义上来说,不一定非要接口或者抽象类,对于任意一个类,我都可以声明一个这个类的匿名类,例如下面的例子:

  1. public class AnonymousInnerClass {
  2. private String name;
  3. public void sayName(){
  4. System.out.println(name);
  5. }
  6. }
  7. public class AnonymousTest {
  8. public static void getInnerClass(){
  9. AnonymousInnerClass anonymousInnerClass = new AnonymousInnerClass() {
  10. @Override
  11. public void sayName() {
  12. System.out.println("i am a anonymous inner class");
  13. super.sayName();
  14. }
  15. };
  16. }
  17. }

AnonymousInnerClass本身就是一个普通的类,我通过匿名类来重写了sayName方法。
我甚至可以这样,在匿名类里面什么都不做:

  1. public static void getInnerClass(){
  2. AnonymousInnerClass anonymousInnerClass = new AnonymousInnerClass() {
  3. };
  4. }

注意这与普通的创建对象的区别,new AnonymousInnerClass()后面不是分号,而是大括号,只是大括号里面什么都没有。

为匿名内部类传递构造器参数

当匿名内部类的构造方法需要参数时,我们只需要在new后面的括号中传入参数即可:

  1. public class AnonymousInnerClass {
  2. private String name;
  3. public AnonymousInnerClass(String name) {
  4. this.name = name;
  5. }
  6. public void sayName(){
  7. System.out.println(name);
  8. }
  9. }
  10. public class AnonymousTest {
  11. public static void getInnerClass(String name){
  12. AnonymousInnerClass anonymousInnerClass = new AnonymousInnerClass(name) {
  13. };
  14. }
  15. }

AnnoymousInnerClass需要一个String类型的参数来构造对象,在getInnerClass中,我们只需要在创建时给定这个参数即可,就像创建普通的类实例一样。
需要注意的一点是,除了匿名内部类继承过来的构造方法,在匿名内部类内部不能再声明构造方法。因为匿名内部类没有名字,构造方法根本不能调用。
匿名内部类是唯一一种没有构造器的类。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

匿名内部类访问外部类字段和方法

前面讲成员内部类和静态内部类的时候,知道了成员内部类能够访问外部类的任意字段和方法,静态内部类能够访问外部类的静态字段和方法,那么匿名内部类呢?
首先,匿名类是在方法中声明的,而方法就可能是静态的或者非静态的,因此匿名类的访问权限是与方法相同的。
首先看下面的例子:

  1. public class AnonymousExample {
  2. private String name = "name";
  3. private static String aStaticName = "aStaticName";
  4. private String getName(){
  5. System.out.println(name);
  6. return name;
  7. }
  8. public static String getTheStaticName() {
  9. System.out.println(aStaticName);
  10. return aStaticName;
  11. }
  12. public void test(){
  13. new Thread(){
  14. @Override
  15. public void run() {
  16. System.out.println(name);
  17. System.out.println(aStaticName);
  18. name = "anotherName";
  19. aStaticName = "anotherStaticName";
  20. AnonymousExample.this.getName();
  21. getTheStaticName();
  22. super.run();
  23. }
  24. }.start();
  25. }
  26. public static void main(String[] args) throws InterruptedException {
  27. AnonymousExample anonymousExample = new AnonymousExample();
  28. anonymousExample.test();
  29. }
  30. }

test方法是一个非静态方法,它里面的内部类可以访问外部类的任意字段和任意方法,并且可以修改它们。
对于静态方法,就有些限制了,静态方法中定义的内部类只能访问外部类的静态字段和方法:
anoynmous.png
我把上面的非静态方法加上了static,就会出现上图中的编译错误,原因是该内部类是在一个静态的上下文中。

匿名内部类访问方法内定义的变量

匿名内部类除了能访问外部类的字段和方法外,还能访问方法中定义的变量。

被访问的对象必须是final的

当匿名类访问在其外部定义的对象时,编译器就会要求其参数是final的。当省略final时,编译器也不会报错,但被引用的对象一定要保证是不变的(简单类型是值不变,引用类型是引用不变),看下面的例子:
final.png
a在test方法中定义,相对于匿名内部类来说是外部对象,在声明时没有final是没有问题的,但是之后a=19这句代码要修改a的值,就会编译报错。
对于引用类型的对象例如Map,限制是一样的,我们不能再声明之后让引用类型的变量指向其他对象实例。但是我们可以在匿名内部类中修改引用指向的对象的数据:

  1. public class AnonymousExample {
  2. public static void test(){
  3. Map<String,String> map = new HashMap<>();
  4. map.put("outer","outer");
  5. new Thread(){
  6. @Override
  7. public void run() {
  8. map.put("inner","inner");
  9. System.out.println(map.get("inner"));
  10. }
  11. }.start();
  12. }
  13. public static void main(String[] args) {
  14. test();
  15. }
  16. }

会输出:

  1. inner

为什么必须是final的

要理解被匿名内部类访问的对象为什么是final的,我们需要理解匿名内部类访问外部对象是怎么实现的。看下面这个简单的例子:

  1. public class AnonymousExample {
  2. public static void test(){
  3. int a = 10;
  4. new Thread(){
  5. @Override
  6. public void run() {
  7. System.out.println(a);
  8. }
  9. }.start();
  10. }
  11. public static void main(String[] args) {
  12. test();
  13. }
  14. }

我们需要看反编译后的代码:

  1. public class AnonymousExample
  2. {
  3. public AnonymousExample()
  4. {
  5. }
  6. public static void test()
  7. {
  8. int a = 10;
  9. (new Thread(a) {
  10. public void run()
  11. {
  12. System.out.println(a);
  13. }
  14. final int val$a;
  15. {
  16. a = i;
  17. super();
  18. }
  19. }
  20. ).start();
  21. }
  22. public static void main(String args[])
  23. {
  24. test();
  25. }
  26. }

看创建Thread的语句new Thread(a),Thread的构造器怎么会有一个int参数?类里面还多了一个val$0的final变量,还多了一个静态块,静态块里面对a进行了初始化。
根据这些,我们猜想编译器碰到访问外部对象的匿名内部类时,创建了一个新的类,这个新的类的构造方法中有对应需要访问的外部对象类型作为参数,并且有对应外部对象的变量作为自己的变量,这些变量通过静态块进行了赋值。
我不知道怎么用jad命令输出编译器自动生成的类。但是我们可以从字节码找到:
c.png
箭头指向的就是编译器生成的类的构造方法,可以看出类的名称是AnonymousExample$1。
所以编译器这样做意味着什么呢?意味着匿名内部类中访问的变量实际上是匿名内部类自己的,编译器通过上面描述的方法将外部变量copy(简单类型直接复制值,引用类型复制变量的引用)了一份到匿名类内部作为变量。既然是复制,那么复制之后原来外部的对象,不管是简单类型还是引用类型,一旦发生了变化,就会导致内部类与外部变量不一致。所以编译器限制这些变量必须是final的。
看下面的例子:

  1. public class AnonymousExample {
  2. public static void test(Map<String,String> map){
  3. int a = 10;
  4. new Thread(){
  5. @Override
  6. public void run() {
  7. System.out.println(a);
  8. System.out.println(map);
  9. }
  10. }.start();
  11. }
  12. public static void main(String[] args) {
  13. }
  14. }

反编译后的代码:

  1. public class AnonymousExample
  2. {
  3. public AnonymousExample()
  4. {
  5. }
  6. public static void test(Map map)
  7. {
  8. int a = 10;
  9. (new Thread(a, map) {
  10. public void run()
  11. {
  12. System.out.println(a);
  13. System.out.println(map);
  14. }
  15. final int val$a;
  16. final Map val$map;
  17. {
  18. a = i;
  19. map = map1;
  20. super();
  21. }
  22. }
  23. ).start();
  24. }
  25. public static void main(String args1[])
  26. {
  27. }
  28. }

此时,编译器生成的类构造方法应该有了两个参数,分别是int和map类型。
上面这两个例子中的test方法都是静态的,当test方法不是静态的时候,编译器创建匿名类AnonymousExample$1的方式可能有所不同,这里不再展开,具体可以参考《java语言规范》15.9.5章节的内容。
既然编译器需要为匿名内部类生成新的类型,那么对于访问不同外部对象的内部类,是不是会生成多个内部类呢?看下面的例子:

  1. public class AnonymousExample {
  2. public static void test(Map<String,String> map){
  3. int a = 10;
  4. SimpleObject simpleObject = new SimpleObject();
  5. new Thread(){
  6. @Override
  7. public void run() {
  8. System.out.println(a);
  9. System.out.println(simpleObject);
  10. }
  11. }.start();
  12. new Thread(){
  13. @Override
  14. public void run() {
  15. System.out.println(a);
  16. System.out.println(map);
  17. }
  18. }.start();
  19. }
  20. public static void main(String[] args) {
  21. }
  22. }

两个Thread访问的外部数据的类型不同,看字节码:
c2.png
确实是的,两个新类AnonymousExample$1和AnonymousExample$2。
上述应该就是匿名内部类比较关键的知识点了,在我们平时的使用中,匿名内部类通常就是用来创建某个接口或者类的实例,然后重写某个方法。

局部内部类

局部内部类的定义

局部内部类与匿名内部类很相似,都是定义在方法或者作用域内的类,只不过局部内部类有自己的名字,可以定义构造方法,下面就是一个局部内部类:

  1. public class AnonymousExample {
  2. private String name = "name";
  3. private static String aStaticName = "aStaticName";
  4. private String getName(){
  5. System.out.println(name);
  6. return name;
  7. }
  8. public static String getTheStaticName() {
  9. System.out.println(aStaticName);
  10. return aStaticName;
  11. }
  12. public void test(){
  13. String aLocalName = "aLocalName";
  14. class LocalInnerClass{
  15. private int i;
  16. private String localInnerName;
  17. public LocalInnerClass(int i, String localInnerName) {
  18. this.i = i;
  19. this.localInnerName = localInnerName;
  20. }
  21. public void test(){
  22. System.out.println(name);
  23. System.out.println(aStaticName);
  24. System.out.println(aLocalName);
  25. }
  26. }
  27. new LocalInnerClass(1,"localInnerClass1").test();
  28. }
  29. public static void main(String[] args) throws InterruptedException {
  30. AnonymousExample anonymousExample = new AnonymousExample();
  31. anonymousExample.test();
  32. }
  33. }

LocalInnerClass在方法test中被定义,除了出现的位置外,它与普通类没有区别。注意局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的,它只能在定义它的方法中被使用。

局部内部类访问外部类字段和方法

局部内部类访问外部类的字段和方法的规则与匿名内部类是相同的,这里不再赘述。

局部内部类访问方法内定义的变量

这个规则与匿名类也是相同的,都要求变量是final的,这里不再赘述。

小结

匿名内部类和局部内部类都是定义在方法或者作用域内的类,并且两者的使用方式和很多访问规则都是相同的,下一篇文章,我们来看内部类的作用。