Java 内部类使用详解

3,032 阅读9分钟

这个系列是帮助复习 Java 的基础知识的,但是并不会按照一个特定的顺序。现在开始复习下内部类的相关知识。

0. 简介

内部类的定义很简单,它其实就是在一个类里面定义了另一个类,但是这个定义还是有很多细节需要掌握的。

1. 非静态内部类

1.1 定义

非静态内部类就是在一个类的内部里面定义了一个没有用 static 修饰的类。

1.2 访问控制符

内部类的访问控制符 访问范围
private 同一个类
default 同一个类,同一包中
protected 同一个类,子类,同一包中
public 任何位置

1.3 非静态内部类访问外部类元素

非静态内部类是可以访问外部类的任何实例变量和方法,包括 private 修饰的成员变量。现在用一个例子来说明一下,代码如下:

public class Outer {

	private int a = 10;
	
	private void innerCall() {
		
		System.out.println("Inner Call");
		
	}

	private class Inner {

		public void printInfo() {
			System.out.println("a = " + a);
			innerCall();
		}

	}

	public void test() {

		Inner inner = new Inner();
		inner.printInfo();

	}

	public static void main(String[] args) {

		Outer outer = new Outer();
		outer.test();
		
	}

}

输出结果:

a = 10
Inner Call

从输出的结果就可以看到,非静态内部类是可以访问到外部类的任何实例变量和方法的。 那为什么内部类可以直接访问外部类的实例变量和方法呢?因为内部类里面是持有外部类的实例引用,一个非静态内部类创建时必然会有其外部类实例的引用。也就是说上面的 printInfo() 方法也可以写成如下:

public void printInfo() {
    System.out.println("a = " + Outer.this.a);
    Outer.this.innerCall();
}

上述代码中的 Outer.this 就是非静态内部类持有外部类的实例引用。

1.3.1 非静态内部类方法访问某个变量的检查顺序

当非静态内部类的方法访问某个变量是按一定顺序来查找的,顺序如下

  1. 在该方法的局部变量找
  2. 方法所在的内部类找
  3. 内部类所在的外部类找
  4. 如果都没有则编译报错

举个例子,代码如下:


public class Outer {

	private int a = 10;

	private class Inner {

		private int a = 9;

		public void printInfo(int a) {
			System.out.println("a = " + a);
		}

	}

	public void test() {

		Inner inner = new Inner();
		inner.printInfo(8);

	}

	public static void main(String[] args) {

		Outer outer = new Outer();
		outer.test();

	}

}

以上代码的输出结果就是 a = 8。 如果把 printInfo(int a) 的形参 a 去掉,则输出的结果为 a = 9。 再把 Inner 类中的成员变量 a 去掉,则输出的结果为 a = 10。 各位可以自己尝试一下,这里就不再讲解。

1.4 外部类访问非静态内部类的元素

外部类不能直接访问非静态内部类的实例变量和方法,如果想要访问的话,必须要创建非静态内部类的实例进而调用该实例的成员。如下代码所示:



public class Outer {

	private int a = 10;

	private class Inner {

		public void printInfo() {
			System.out.println("a = " + a);
		}

	}

	public void test() {

		//这句会编译错误
		printInfo();

	}

	public static void main(String[] args) {

		Outer outer = new Outer();
		outer.test();

	}

}


以上代码会编译错误,如果想要访问的话只能创建 Inner 的实例访问该方法,这里有两种方式,一种就是在 test() 方法里面创建 Inner 对象,另一种就是在 main 方法创建,以下两种方法都用代码试一下:

public class Outer {

	private int a = 10;

	private class Inner {

		public void printInfo() {
			System.out.println("a = " + a);
		}

	}

	public void test() {

		new Inner().printInfo(); 

	}

	public static void main(String[] args) {

		Outer outer = new Outer();
		outer.new Inner().printInfo();
		outer.test();

	}

}

从以上代码可以看到,test() 方法里面直接 new Inner() 即可调用 Inner 方法。不过在 main 方法里要先创建 Outer 对象才可以再调用 new Inner()。

1.4.1 外部类的静态方法不可以创建非静态内部类

根据 Java 的规则,静态成员是不可以访问非静态成员的。非静态内部类其实就是类实例中的一个成员,所以在外部类的静态方法不可以创建非静态内部类,现用代码举例:


public class Outer {

	private int a = 10;

	private class Inner {

		public void printInfo() {
			System.out.println("a = " + a);
		}

	}

	public static void main(String[] args) {

		//这句代码会编译报错
		Inner inner = new Inner();

	}

}

1.4.2 非静态内部类不可以创建静态成员

同样的,非静态内部类不可以创建静态代码块,静态方法和静态成员变量。代码举例如下:

public class Outer {

	private int a = 10;

	private class Inner {
		
		
		//以下三个静态声明都会编译报错
		static {
			
		}
		public static int a = 0;
		public static void printInfo() {
			System.out.println("a = " + a);
		}

	}

	public static void main(String[] args) {


	}

}

1.5 非静态内部类子类的规则

非静态内部类的子类必须要存在外部类的实例引用。代码举例如下:

public class SubInner extends Outer.Inner {

	public SubInner(Outer outer) {
		outer.super();
	}	
	
}

这里解释一下为什么非静态内部类的子类需要外部类的实例引用,因为由以上就可以知道非静态内部类一定会有外部类的实例引用,所以该子类也应该要有外部类的实例引用,只是这个外部类的引用是创建该子类的时候传入的。

2. 静态内部类

2.1 定义

静态内部类就是在一个类的内部里面定义了一个使用用 static 修饰的类。

2.2 静态内部类访问外部类

静态内部类不可以访问外部类的非静态成员,但是可以访问外部类的静态成员。代码如下:

public class Outer {

	private int a = 10;
	private static int b = 10;

	private static class Inner {
		
		public void printInfo() {
			
			//这句会编译错误
			System.out.println("a = " + a);
			
			System.out.println("b = " + b);
		}

	}

	public static void main(String[] args) {


	}

}

可以看到当静态内部类 Inner 访问外部类的非静态成员变量 a 时,代码会编译错误,但访问外部类的静态成员变量 b 时,则不会报错。

2.3 外部类访问静态内部类

外部类可以使用静态内部类的类名调用静态内部类的类成员,也可以使用静态内部类的实例调用内部类的的实例成员。代码举例如下:


public class Outer {

	private int a = 10;
	private static int b = 10;

	private static class Inner {
		
		
		public static void staticPrintInfo() {
			
			System.out.println("静态方法");
		
		}
		
		public void printInfo() {
			
			System.out.println("实例方法");
		}

	}
	
	private static void test() {
		
		Inner.staticPrintInfo();
		new Inner().printInfo();
		
	}

	public static void main(String[] args) {

		Outer outer = new Outer();
		outer.test();
		

	}

}

要注意的是,Inner 实例不可以调用 staticPrintInfo() 方法。

其实这里的难点就是要理解内部类与外部类成员之间的访问,现在把这些关系总结为一张思维导图来帮助理解。

内部类与外部类关系.png

上图的非就是非静态的意思。

3. 局部内部类

3.1 定义

局部内部类就是把一个类放在方法内定义。 因为局部内部类的上一级程序单元是方法,所以不能使用 static 修饰。 并且不能使用任何访问控制符修饰,因为它的作用域只能在方法内。 这里就不再细说这个概念,因为在实际开发中很少使用。

4. 匿名内部类

4.1 定义

匿名内部类就是一种没有名字的内部类,它只会使用一次。 匿名内部类必须必须是继承一个父类或者实现一个接口,但最多只能继承一个父类或实现一个接口。

4.2 实现接口的匿名内部类

代码如下:

interface Person {
	
	void eat();
	
}

public class AnonymousInnerClass {
	
	public void test(Person p) {
		p.eat();
	}

	public static void main(String[] args) {
		
		AnonymousInnerClass a = new AnonymousInnerClass();
		
		a.test(new Person() {
			
			@Override
			public void eat() {
				System.out.println("eat someting");
				
			}
		});
		
	}
}

可以看到 test() 方法需要一个 Person 的对象,但是可以使用匿名内部类直接实现 Person 接口的方法即可使用。

4.3 继承抽象类的匿名内部类

通过继承抽象类来创建的匿名内部类,可以使用抽象类当中的任何构造器,并且也可以重写抽象父类当中的普通方法。代码如下:

abstract class Person {
	
	private String name = "Zede";

	public Person() {

	}

	public Person(String name) {
		this.name = name;
	}

	public abstract void eat();

	public String getName() {
		return name;
	}
}

public class AnonymousInnerClass {

	public void test(Person p) {
		p.eat();
		System.out.println(p.getName());
	}

	public static void main(String[] args) {

		AnonymousInnerClass a = new AnonymousInnerClass();

		a.test(new Person() {

			@Override
			public void eat() {
				System.out.println("eat someting");

			}
		});

		a.test(new Person("Zede") {

			@Override
			public void eat() {
				System.out.println("eat someting");

			}
			
			
			@Override
			public String getName() {
				return "xiaoming";
			}
			
		});

	}

}

输入结果为:

eat someting
Zede
eat someting
xiaoming

可以看到的是,继承抽象类的匿名内部类可以使用任何该抽象类的任何方法并且可以重写该抽象类的普通方法。

4.4 继承普通类的匿名内部类

class Person {
	
}

public class AnonymousInnerClass {

	public static void main(String[] args) {

		new Person() {
			public void eat() {
				System.out.println("eat someting");
			}
		}.eat();

	}

}

上述代码可以看到,在 main() 方法中直接创建一个 Person 类型 的匿名内部类,这个匿名内部类其实就是继承了一个 Person 类,然后在这个内部类当中创建 eat() 方法。运行结果如下:

eat someting

4.5 匿名内部类访问局部变量

当匿名内部类访问局部变量的时候,这个局部变量必须使用 final 修饰。 但是在 Java 8 开始会自动帮这个局部变量使用 final 修饰。举例代码如下:

abstract class Person {

	public abstract void getAge();
}

public class AnonymousInnerClass {

	public void test(Person p) {
		p.getAge();
	}

	public static void main(String[] args) {

		int age = 18;
		
		//这段代码会编译报错
		age = 20;

		AnonymousInnerClass a = new AnonymousInnerClass();

		a.test(new Person() {

			@Override
			public void getAge() {

				System.out.println("age: " + age);

			}
		});

	}

}

可以看到上面那段代码因为 age 已经被匿名内部类使用了,所以会被自动使用 final 修饰,如果改变了 age 的值就会编译报错。

为什么匿名内部类访问的局部变量要用 final 修饰呢?在解答这个问题之前,首先看一下上面的代码编译出来的 class 文件:

AnonymousInnerClass.class:

public class AnonymousInnerClass
{
  public void test(Person p)
  {
    p.getAge();
  }
  
  public static void main(String[] args)
  {
    int age = 18;
    
    AnonymousInnerClass a = new AnonymousInnerClass();
    
    a.test(new Person()
    {
      public void getAge()
      {
        System.out.println("age: " + this.val$age);
      }
    });
  }
}

AnonymousInnerClass$1.class:

class AnonymousInnerClass$1
  extends Person
{
  AnonymousInnerClass$1(int paramInt) {}
  
  public void getAge()
  {
    System.out.println("age: " + this.val$age);
  }
}

可以看到 AnonymousInnerClass$1 的生命周期是对象级别的,而变量 age 的生命周期是方法级别的,也就是说局部变量 age 在方法结束后就会被销毁,而 AnonymousInnerClass$1 还可能继续存在。这时候 AnonymousInnerClass$1 访问的 age 变量可能已经被销毁了。所以在匿名内部类中会将引用的局部变量复制一个副本到自己的内部当中,所以匿名内部类访问的其实就是这个局部变量的副本。而且为了副本和原始变量一致所以要用 final 修饰这个局部变量。

5. 内部类一些问题

这节会介绍一些注意事项。

5.1 如果内部类不需要访问外部类的实例,最好把这个内部类变成静态内部类

如果一个内部类不需要访问外部类的任何实例,这个内部类最好是静态内部类。为什么呢?因为非静态内部类会持有外部类的对象引用,所以每次创建非静态内部类的时候,就会与外部类实例进行关联,这种操作是需要耗费时间的。 Android 的一些 Adapter 就会使用 ViewHolder 的内部类。ViewHolder 中并没有引用到外部类的任何实例,所以 ViewHolder 类最好使用 static 修饰。