嵌套类的定义
嵌套类指定义在另一个类的内部的类,为它的外围类提供服务。
嵌套类有4种:非静态成员类、局部类、匿名类和静态成员类。前3种是内部类。
1 非静态成员类
非静态成员类没有加关键字static。
例如:
1 class Circle {
2 double radius = 0;
3
4 public Circle(double radius) {
5 this.radius = radius;
6 }
7
8 class Draw { //内部类
9 public void drawSahpe() {
10 System.out.println("drawshape");
11 }
12 }
13 }
类Draw像是类Circle的一个成员,Circle称为外围类。非静态成员类可以访问外围类的所有属性和方法。
1 class Circle {
2 private double radius = 0;
3 public static int count =1;
4 public Circle(double radius) {
5 this.radius = radius;
6 }
7
8 class Draw { //内部类
9 public void drawSahpe() {
10 System.out.println(radius); //外围类的private成员
11 System.out.println(count); //外围类的静态成员
12 }
13 }
14 }
当非静态成员类拥有和外围类同名的成员时,会发生隐藏现象,即默认情况下访问的是非静态成员类的成员。如果要访问外围类的同名成员,需要以下面的形式进行访问:
外围类.this.属性
外围类.this.方法
在外围类中如果要访问非静态成员类的成员,必须先创建一个非静态成员类的对象,再通过指向这个对象的引用来访问:
1 class Circle {
2 private double radius = 0;
3
4 public Circle(double radius) {
5 this.radius = radius;
6 getDrawInstance().drawSahpe(); //必须先创建非静态成员类的对象,再进行访问
7 }
8
9 private Draw getDrawInstance() {
10 return new Draw();
11 }
12
13 class Draw { //内部类
14 public void drawSahpe() {
15 System.out.println(radius); //外围类的private成员
16 }
17 }
18 }
非静态成员类依赖外围类,也就是说,如果要创建非静态成员类的对象,前提是必须存在一个外围类的对象。创建非静态成员类对象的一般方式如下:
1 public class Test {
2 public static void main(String[] args) {
3 //第一种方式:
4 Outter outter = new Outter();
5 Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建
6
7 //第二种方式:
8 Outter.Inner inner1 = outter.getInnerInstance();
9 }
10 }
11
12 class Outter {
13 private Inner inner = null;
14 public Outter() {
15
16 }
17
18 public Inner getInnerInstance() {
19 if(inner == null)
20 inner = new Inner();
21 return inner;
22 }
23
24 class Inner {
25 public Inner() {
26
27 }
28 }
29 }
从语法上讲,非静态成员类与静态成员类之间唯一的区别是静态成员类的声明中包含修饰符static。每个非静态成员类的对象都隐含着与一个外围类的对象相关联。在非静态成员类的对象方法内部,可以调用外围实例上的方法,或者通过修饰过的this构造获得外围类对象的引用。如果嵌套类的对象可以在它的外围类对象之外独立存在,这个嵌套类必须是静态成员类。当非静态成员类的对象被创建时,它和外围类对象之间的关联关系随之建立,而且这种关联关系以后不能被修改。同时,这种关联关系需要消耗非静态成员类对象的空间,增加了构造的时间开销,会导致外围类对象在符合垃圾回收时得以保留。
常见用法是定义一个Adapter,允许外围类对象被看作是另一个不相关的类的对象。例如,Map接口实现类中使用非静态成员类来实现集合视图,由Map的keySet、entrySet和Values方法返回。同样地,Set和List接口的实现类使用非静态成员类来实现迭代器:
2 局部类
局部类是定义在一个方法或者一个作用域里面的类,是4种嵌套类中用得最少的类。在任何“可以声明局部变量”的地方,都可以声明它,并且遵守同样的作用域规则。它有名字,可以被重复地使用,仅当在非静态环境中定义时才有外围实例,不能包含静态成员。
1 class People{
2 public People() {
3
4 }
5 }
6
7 class Man{
8 public Man(){
9
10 }
11
12 public People getWoman(){
13 class Woman extends People{ //局部类
14 int age =0;
15 }
16 return new Woman();
17 }
18 }
局部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。
3 匿名类
匿名类没有名字,不是外围类的成员,在使用时被声明和实例化。它可以出现在任何允许存在表达式的地方,仅当出现在非静态环境中才有外围实例。
第一种常见用法是动态地创建方法对象。例如,在编写事件监听的代码时使用匿名类不但方便,而且使代码更加容易维护。下面是一段Android事件监听代码:
1 scan_bt.setOnClickListener(new OnClickListener() {
2
3 @Override
4 public void onClick(View v) {
5 // TODO Auto-generated method stub
6
7 }
8 });
9
10 history_bt.setOnClickListener(new OnClickListener() {
11
12 @Override
13 public void onClick(View v) {
14 // TODO Auto-generated method stub
15
16 }
17 });
为两个按钮设置监听器,使用了匿名类:
1 new OnClickListener() {
2
3 @Override
4 public void onClick(View v) {
5 // TODO Auto-generated method stub
6
7 }
8 }
下面这种写法也可以:
1 private void setListener()
2 {
3 scan_bt.setOnClickListener(new Listener1());
4 history_bt.setOnClickListener(new Listener2());
5 }
6
7 class Listener1 implements View.OnClickListener{
8 @Override
9 public void onClick(View v) {
10 // TODO Auto-generated method stub
11
12 }
13 }
14
15 class Listener2 implements View.OnClickListener{
16 @Override
17 public void onClick(View v) {
18 // TODO Auto-generated method stub
19
20 }
21 }
因为它既冗长又难以维护,所以一般使用匿名类来编写事件监听代码。
第二种常见用法是创建过程对象,例如Runnable、Thread或者TimerTask实例。
第三种常见用法是在静态工厂方法内部。
匿名类不能有访问修饰符和static修饰符。匿名类是唯一没有构造器的类。因为它没有构造器,所以匿名类的使用范围非常有限,大部分匿名类用于接口回调。匿名类在编译的时候由系统自动起名为Outter$1.class。匿名类用于继承其他类或者实现接口,不需要增加额外的方法,只是对继承方法的重写或者抽象方法的实现。
4 静态成员类
静态成员类是定义在另一个类里面的类,加了关键字static,是最简单的嵌套类。静态成员类不依赖外围类,是外围类的静态成员,不能访问外围类的非静态属性或方法。因为没有外围类实例时,可以创建静态成员类的实例,如果允许访问外围类的非静态成员,那么就与外围类的非静态成员依赖外围类实例相矛盾。如果一个嵌套类不是静态成员类,则该嵌套类不可以定义静态成员(包括属性和方法)。但是非静态成员类可以定义static final类型属性。
1 public class Test {
2 public static void main(String[] args) {
3 Outter.Inner inner = new Outter.Inner();
4 }
5 }
6
7 class Outter {
8 public Outter() {
9
10 }
11
12 static class Inner {
13 public Inner() {
14
15 }
16 }
17 }
对于非私有静态成员类而言,常见用法是作为公有的辅助类,仅当与它的外围类一起使用时才有意义。例如,作为计算器Calculator类的公有静态成员类,Operation枚举支持加减操作,可以通过Calculator.Operation.PLUS和Calculator.Operation.MINUS来引用这些操作。
对于私有静态成员类而言,常见用法是代表外围类所代表的对象的组件。例如,作为Map接口实现类的私有静态成员类,键值对Entry与Map实例关联,但是其方法(getKey、getValue和setValue)不需要访问Map实例。
深入理解内部类
1 为什么非静态成员类可以无条件访问外围类的成员?
编译器在编译时会将非静态成员类单独编译成一个字节码文件,下面是Outter.java的代码:
1 public class Outter {
2 private Inner inner = null;
3 public Outter() {
4
5 }
6
7 public Inner getInnerInstance() {
8 if(inner == null)
9 inner = new Inner();
10 return inner;
11 }
12
13 protected class Inner {
14 public Inner() {
15
16 }
17 }
18 }
编译之后,生成两个字节码文件:
反编译Outter$Inner.class文件结果:
1 E:WorkspaceTestincomcxh est2>javap -v Outter$Inner
2 Compiled from "Outter.java"
3 public class com.cxh.test2.Outter$Inner extends java.lang.Object
4 SourceFile: "Outter.java"
5 InnerClass:
6 #24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
7 t2/Outter
8 minor version: 0
9 major version: 50
10 Constant pool:
11 const #1 = class #2; // com/cxh/test2/Outter$Inner
12 const #2 = Asciz com/cxh/test2/Outter$Inner;
13 const #3 = class #4; // java/lang/Object
14 const #4 = Asciz java/lang/Object;
15 const #5 = Asciz this$0;
16 const #6 = Asciz Lcom/cxh/test2/Outter;;
17 const #7 = Asciz <init>;
18 const #8 = Asciz (Lcom/cxh/test2/Outter;)V;
19 const #9 = Asciz Code;
20 const #10 = Field #1.#11; // com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
21 est2/Outter;
22 const #11 = NameAndType #5:#6;// this$0:Lcom/cxh/test2/Outter;
23 const #12 = Method #3.#13; // java/lang/Object."<init>":()V
24 const #13 = NameAndType #7:#14;// "<init>":()V
25 const #14 = Asciz ()V;
26 const #15 = Asciz LineNumberTable;
27 const #16 = Asciz LocalVariableTable;
28 const #17 = Asciz this;
29 const #18 = Asciz Lcom/cxh/test2/Outter$Inner;;
30 const #19 = Asciz SourceFile;
31 const #20 = Asciz Outter.java;
32 const #21 = Asciz InnerClasses;
33 const #22 = class #23; // com/cxh/test2/Outter
34 const #23 = Asciz com/cxh/test2/Outter;
35 const #24 = Asciz Inner;
36
37 {
38 final com.cxh.test2.Outter this$0;
39
40 public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
41 Code:
42 Stack=2, Locals=2, Args_size=2
43 0: aload_0
44 1: aload_1
45 2: putfield #10; //Field this$0:Lcom/cxh/test2/Outter;
46 5: aload_0
47 6: invokespecial #12; //Method java/lang/Object."<init>":()V
48 9: return
49 LineNumberTable:
50 line 16: 0
51 line 18: 9
52
53 LocalVariableTable:
54 Start Length Slot Name Signature
55 0 10 0 this Lcom/cxh/test2/Outter$Inner;
56
57
58 }
第11行到35行是常量池的内容,第38行的内容如下:
1 final com.cxh.test2.Outter this$0;
一个指向外围类对象的指针,也就是说,编译器会默认为非静态成员类添加一个指向外围类对象的引用。看内部类的构造器:
1 public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
虽然定义的内部类的构造器是无参构造器,但是编译器还是会默认添加一个参数,该参数类型为指向外围类对象的一个引用,所以非静态成员类中的Outter this$0指针指向了外围类对象,可以在非静态成员类中随意访问外围类的成员。这间接说明非静态成员类依赖外围类,如果没有创建外围类对象,则无法初始化Outter this$0引用,也就无法创建非静态成员类的对象了。
2 为什么局部类和匿名类只能访问局部final变量?
先看这段代码:
1 public class Test {
2 public static void main(String[] args) {
3
4 }
5
6 public void test(final int b) {
7 final int a = 10;
8 new Thread(){
9 public void run() {
10 System.out.println(a);
11 System.out.println(b);
12 };
13 }.start();
14 }
15 }
它会被编译成两个class文件:Test.class和Test$1.class。默认情况下,编译器会为匿名类和局部类起名为Test$x.class(x为正整数)。
如果把变量a和b前面的任何一个final去掉,这段代码编译通不过。
当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没结束,那么在Thread的run方法中继续访问变量a就不可能了?Java采用复制 的方式来解决这个问题。这段代码的字节码反编译结果:
在run方法中有一条指令:
1 bipush 10
该指令将操作数10压栈,表示使用的是一个本地局部变量。这是编译器在编译时默认执行的,如果这个变量的值在编译时可以确定,则编译器默认会在匿名类或者局部类的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。于是,匿名类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,独立于方法中的局部变量。
举例:
1 public class Test {
2 public static void main(String[] args) {
3
4 }
5
6 public void test(final int a) {
7 new Thread(){
8 public void run() {
9 System.out.println(a);
10 };
11 }.start();
12 }
13 }
反编译结果:
匿名类Test$1的构造器有两个参数,一个是指向外围类对象的引用,一个是int型变量,test方法中的形参a传入后创建匿名类中的拷贝(变量a的拷贝)。如果在编译时可以确定局部变量的值,则直接在匿名类里面创建一个拷贝;如果在编译时无法确定局部变量的值,则通过构造器传参的方式来进行拷贝。
在run方法中访问的变量a不是test方法中的局部变量a,解决了生命周期不一致的问题。因为run方法中访问的变量a和test方法中的变量a不是同一个变量,所以在run方法中改变变量a的值时会造成数据不一致性。为了解决这个问题,Java编译器限定变量a必须有final修饰符。
但是Java8中已经取消了这个限制,并很好地解决了数据不一致性问题。
3 静态成员类有特殊之处吗?
静态成员类不依赖外围类,也就是说,可以在不创建外围类对象的情况下创建内部类对象。静态成员类不持有指向外围类对象的引用。
使用嵌套类的好处:
1 嵌套类能独立地继承一个接口的实现类,所以无论外围类是否继承了某个接口的实现类,都不会影响嵌套类,有利于实现多继承。
2 隐蔽地组织相关的类。
3 有利于编写事件驱动程序。
4 有利于编写线程代码。
最重要的是第一点:嵌套类可以弥补Java单继承的缺陷。
内部类相关的笔试面试题
1 根据注释填写(1),(2),(3)处的代码
1 public class Test{
2 public static void main(String[] args){
3 // 初始化Bean1
4 (1)
5 bean1.I++;
6 // 初始化Bean2
7 (2)
8 bean2.J++;
9 //初始化Bean3
10 (3)
11 bean3.k++;
12 }
13 class Bean1{
14 public int I = 0;
15 }
16
17 static class Bean2{
18 public int J = 0;
19 }
20 }
21
22 class Bean{
23 class Bean3{
24 public int k = 0;
25 }
26 }
对于非静态成员类,必须先创建外围类对象,才能创建内部类对象。而静态成员类可以直接创建内部类对象。
创建非静态成员类对象的一般形式为: 外围类类名.内部类类名 xxx = 外围类对象名.new 内部类类名();
创建静态成员类对象的一般形式为: 外围类类名.内部类类名 xxx = new 外围类类名.内部类类名();
因此,(1),(2),(3)处的代码分别为:
1 Test test = new Test();
2 Test.Bean1 bean1 = test.new Bean1();
1 Test.Bean2 b2 = new Test.Bean2();
1 Bean bean = new Bean();
2 Bean.Bean3 bean3 = bean.new Bean3();
2 下面这段代码的输出结果是什么?
1 public class Test {
2 public static void main(String[] args) {
3 Outter outter = new Outter();
4 outter.new Inner().print();
5 }
6 }
7
8
9 class Outter
10 {
11 private int a = 1;
12 class Inner {
13 private int a = 2;
14 public void print() {
15 int a = 3;
16 System.out.println("局部变量:" + a);
17 System.out.println("内部类变量:" + this.a);
18 System.out.println("外围类变量:" + Outter.this.a);
19 }
20 }
21 }
结果:
1 3
2 2
3 1
嵌套类的继承
嵌套类很少被继承。当被继承时,要注意两点:
1 非静态成员类的引用方式必须为Outter.Inner。
2 构造器中必须有指向外围类对象的引用,并通过这个引用调用super()。
下面这段代码摘自《Java编程思想》:
1 class WithInner {
2 class Inner{
3
4 }
5 }
6 class InheritInner extends WithInner.Inner {
7
8 // InheritInner() 是不能通过编译的,一定要加上形参
9 InheritInner(WithInner wi) {
10 wi.super(); //必须有这句调用
11 }
12
13 public static void main(String[] args) {
14 WithInner wi = new WithInner();
15 InheritInner obj = new InheritInner(wi);
16 }
17 }
参考资料
《Java编程思想》
《Effective Java 中文第二版》 第22条:优先考虑静态成员类 P94-96