Java内部类详解

简介

可以将一个类的定义放在另一个类的定义内部,这就是内部类。内部类允许你把一些逻辑相关的类组织在一起,并控制内部类的可见性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。

那为什么需要内部类呢?

一般来说,内部类继承自某个类或实现某个接口,内部类的代码可以操作创建它的外围类的独对象。所以可以认为内部类提供了某种进入其外围类的窗口。

那如果只需要一个对接口的引用,为什么不通过外围类来实现接口呢?确实是是这样,如果外围类能实现该接口,就应该用外围类来实现,但是外围类并不总能享用到接口带来的方便,有时需要用到接口的实现,所以,内部类最吸引人的原因是:

每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。内部类使得多重继承的解决方案变得完整,接口解决了部分问题,而内部类有效地实现了“多重继承”。但是如果我们不需要解决多重继承问题,那么我们自然可以使用其他的编码方式,但是使用内部类还能够为我们带来如下特性:

  • 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
  • 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
  • 创建内部类对象的时刻并不依赖于外围类对象的创建。
  • 内部类并没有令人迷惑的“is-a”关系,它就是一个独立的实体。

在Java中,内部类主要分为四种:静态内部类、成员内部类、匿名内部类和局部内部类,如下代码所示:

abstract class Father {
	abstract void f();
}

public class OuterClass {
	// 静态内部类
	static class innerClass1{}
	// 成员内部类
	class innerClass2{}
	
	public void f1() {
		// 局部内部类
		class innerClass3{}
		
		// 匿名内部类
		f2(new Father() {
			@Override
			void f() {
				System.out.println("hello world");
			}
		});
	}
	
	public void f2(Father father) {
		father.f();
	}
}

静态内部类和成员内部类

静态内部类是指被声明为static的内部类,它可以不依赖于外部类实例而被实例化。它可以访问外围类的所有成员,包括那些私有成员。静态内部类与其他的静态成员一样,也遵守同样的可访问性规则,即它不能访问任何外围类的非static成员变量和方法。如果它被声明为私有的,那它就只能在外围类的内部才可以被访问。

静态内部类的一种常见用法就是作为共有的辅助类,仅当与它的外部类一起使用时才有意义。

静态内部类如果去掉“static”关键字,就成为成员内部类,成员内部类为非静态内部类,它可以访问外围类的所有成员,无论是静态的还是非静态的。当我们在创建一个内部类的时候,它无形中就与外围类有了一种联系,依赖于这种联系,它可以无限制地访问外围类的元素。

成员内部类不可以定义静态的属性和方法,只有在外围类被实例化后,这个内部类才可以被实例化,我们看下面一段代码:

public class OuterClass {
	private static int num1 = 1;
	private int num2 = 1;
	
	public void f() {
		System.out.println("OuterClass.f()");
	}
	
	public Inner1 getInner1() {
		return new Inner1();
	}
	
	public class Inner1 {
		public static final int a = 1;
		public int b = 1;
		
		// 不可以定义静态属性和方法
		// public static int c = 1;
		// public static void f() {}
		
		public OuterClass getOuter() {
			// 访问外围类属性
			System.out.println(num1);
			// 访问外围类方法
			f();
			return OuterClass.this;
		}
	}
	
	public static class Inner2 {
		public void f() {
			System.out.println(num1);
			// 不可以访问外围类非静态属性
			// System.out.println(num2);
		}
	}
	
	public static void main(String[] args) {
		OuterClass outerClass = new OuterClass();
		System.out.println(outerClass);
		OuterClass.Inner1 inner1 = outerClass.getInner1();
		OuterClass.Inner1 inner2 = outerClass.new Inner1();
		System.out.println(inner1.getOuter());
		System.out.println(inner2.getOuter());
		
		OuterClass.Inner2 inner3 = new Inner2();
		inner3.f();
	}
}

运行结果:

innerclass.OuterClass@7852e922
1
OuterClass.f()
innerclass.OuterClass@7852e922
1
OuterClass.f()
innerclass.OuterClass@7852e922
1

上述代码演示了我们前面提到的注意事项。需要注意的是,由于成员内部类与外围类存在一种联系,创建成员内部类实例时,必须要通过已创建的外围类实例来创建,即类似于outclass.new InnerClass(),同时,通过成员内部类实例可以获取与其关联的外围类实例,需要通过OutClass.this的方式来获取。

那成员内部类与外围类存在的联系到底是什么呢?我们通过反编译软件来看一下:

我们将OuterClass$Inner1.class反编译如下:

public class innerclass/OuterClass$Inner1 {
     <ClassVersion=52>
     <SourceFile=OuterClass.java>

     public static final int a = 1 (java.lang.Integer);
     public int b;
     synthetic final innerclass.OuterClass this$0;

     public OuterClass$Inner1(innerclass.OuterClass arg0) { // <init> //(Linnerclass/OuterClass;)V
         <localVar:index=0 , name=this , desc=Linnerclass/OuterClass$Inner1;, sig=null, start=L1, end=L2>

         L1 {
             aload0 // reference to self
             aload1
             putfield innerclass/OuterClass$Inner1.this$0:innerclass.OuterClass
             aload0 // reference to self
             invokespecial java/lang/Object.<init>()V
         }
         L3 {
             aload0 // reference to self
             iconst_1
             putfield innerclass/OuterClass$Inner1.b:int
             return
         }
         L2 {
         }
     }

     public getOuter() { //()Linnerclass/OuterClass;
         <localVar:index=0 , name=this , desc=Linnerclass/OuterClass$Inner1;, sig=null, start=L1, end=L2>

         L1 {
             getstatic java/lang/System.out:java.io.PrintStream
             invokestatic innerclass/OuterClass.access$0()I
             invokevirtual java/io/PrintStream.println(I)V
         }
         L3 {
             aload0 // reference to self
             getfield innerclass/OuterClass$Inner1.this$0:innerclass.OuterClass
             invokevirtual innerclass/OuterClass.f()V
         }
         L4 {
             aload0 // reference to self
             getfield innerclass/OuterClass$Inner1.this$0:innerclass.OuterClass
             areturn
         }
         L2 {
         }
     }
}

从上述代码中可以看到,成员内部类Inner1中,存在一个OuterClass类的变量this$0,该变量被synthetic所修饰,这个修饰符表示由java编译器生成的(除了像默认构造函数这一类的)方法,或者类等信息,具体介绍可以看这一篇博客。在Inner1的默认构造方法中存在一个外围类参数arg0,这就是创建内部类的外围类对象了,该对象会被赋值给this$0,也就是说,成员内部类存在一个有java编译器生成的外围类的对象。那静态内部类是不是就没有了呢,我们看一下OuterClass$Inner2.class的反编译代码:

public class innerclass/OuterClass$Inner2 {
     <ClassVersion=52>
     <SourceFile=OuterClass.java>

     public OuterClass$Inner2() { // <init> //()V
         <localVar:index=0 , name=this , desc=Linnerclass/OuterClass$Inner2;, sig=null, start=L1, end=L2>

         L1 {
             aload0 // reference to self
             invokespecial java/lang/Object.<init>()V
             return
         }
         L2 {
         }
     }

     public f() { //()V
         <localVar:index=0 , name=this , desc=Linnerclass/OuterClass$Inner2;, sig=null, start=L1, end=L2>

         L1 {
             getstatic java/lang/System.out:java.io.PrintStream
             invokestatic innerclass/OuterClass.access$0()I
             invokevirtual java/io/PrintStream.println(I)V
         }
         L3 {
             return
         }
         L2 {
         }
     }
}

静态内部类中确实是没有的,这也就说明了为什么创建成员内部类必须要先创建外围类实例了,因为外围类实例创建成员内部类时,需要将自己传给内名内部类的构造方法中,而静态内部类就不需要了。

那什么时候用静态内部类,什么时候用成员内部类呢?

建议如果声明成员类不要求访问外部实例,就要始终把static修饰符放在它的声明中,使它成为静态内部类。

匿名内部类

匿名内部类没有名字,它不是外围类的一个成员,它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。匿名内部类可以出现在代码中任何允许存在表达式的地方。

匿名内部类不使用class、extends、implements等关键字,没有构造方法,它必须继承其他类或者实现其他接口。匿名内部类的好处就是代码更加紧凑简洁,但会带来易读性下降的问题。它一般用于GUI编程中实现事件处理等。在使用匿名内部类时,要注意以下原则:

  • 匿名内部类没有构造方法。
  • 匿名内部类是没有访问修饰符的。
  • 匿名内部类不能定义静态成员、方法和类。
  • 只能创建匿名内部类的一个实例。
  • 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是同时只能继承一个类或者实现一个接口。
  • 匿名内部类访问外部定义的对象需要是final的。

匿名内部类没有构造方法,那我们怎么初始化变量呢?答案是用构造代码块啦,如下所示:

abstract class MyClass{
	abstract void f();
}

public class OuterClass {
	public int a = 1;
	
	public MyClass getMyClass(final int num) {
		final OuterClass outerClass = new OuterClass();
		System.out.println(outerClass);
		
		return new MyClass() {
			int myNum;
			
			{
				myNum = num;
			}
			
			@Override
			void f() {
				System.out.println(myNum);
				System.out.println(outerClass);
				System.out.println(outerClass.a);
			}
		};
	}
	
	public static void main(String[] args) {
		OuterClass outerClass = new OuterClass();
		MyClass myClass = outerClass.getMyClass(10);
		myClass.f();
	}
}

myNum通过构造代码块进行了初始化,其中,匿名内部类访问了外部变量num和outerClass,这两个变量都声明为了final,上述代码是在JDK1.7中运行的,必须要final参数,否则无法编译,但是在JDK1.8中,可以不声明为final,具体可以看在线文档:

https://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html#accessing-members-of-an-enclosing-class

这是JDK1.8新增的Effectively final功能,虽然我们不必再添加final修饰符,但我们在代码中也是不能修改外部变量的,如下所示:


那为什么外部变量要声明为final呢?我们先来看一下生成的字节码文件,除了MyClass.class和OuterClass.class两个文件,还有一个OuterClass$1.class文件,这就是生成的匿名内部类了:

反编译结果如下:

class innerclass/OuterClass$1 extends innerclass/MyClass {
     <ClassVersion=52>
     <SourceFile=OuterClass.java>

     int myNum;
     synthetic final innerclass.OuterClass this$0;
     private synthetic final innerclass.OuterClass val$outerClass;

     OuterClass$1(innerclass.OuterClass arg0, int arg1, innerclass.OuterClass arg2) { // <init> //(Linnerclass/OuterClass;ILinnerclass/OuterClass;)V
         <localVar:index=0 , name=this , desc=Linnerclass/OuterClass$1;, sig=null, start=L1, end=L2>

         L1 {
             aload0 // reference to self
             aload1 // reference to arg0
             putfield innerclass/OuterClass$1.this$0:innerclass.OuterClass
             aload0 // reference to self
             aload3
             putfield innerclass/OuterClass$1.val$outerClass:innerclass.OuterClass
         }
         L3 {
             aload0 // reference to self
             invokespecial innerclass/MyClass.<init>()V
         }
         L4 {
             aload0 // reference to self
             iload2 // reference to arg1
             putfield innerclass/OuterClass$1.myNum:int
             return
         }
         L2 {
         }
     }

     f() { //()V
         <localVar:index=0 , name=this , desc=Linnerclass/OuterClass$1;, sig=null, start=L1, end=L2>

         L1 {
             getstatic java/lang/System.out:java.io.PrintStream
             aload0 // reference to self
             getfield innerclass/OuterClass$1.myNum:int
             invokevirtual java/io/PrintStream.println(I)V
         }
         L3 {
             getstatic java/lang/System.out:java.io.PrintStream
             aload0 // reference to self
             getfield innerclass/OuterClass$1.val$outerClass:innerclass.OuterClass
             invokevirtual java/io/PrintStream.println(Ljava/lang/Object;)V
         }
         L4 {
             getstatic java/lang/System.out:java.io.PrintStream
             aload0 // reference to self
             getfield innerclass/OuterClass$1.val$outerClass:innerclass.OuterClass
             getfield innerclass/OuterClass.a:int
             invokevirtual java/io/PrintStream.println(I)V
         }
         L5 {
             return
         }
         L2 {
         }
     }
}

可以看到,该类有如下两个属性:

int myNum;
synthetic final innerclass.OuterClass this$0;

这说明匿名内部类将外部变量备份了一份,内部类中的属性和外部方法的参数两者实际不是同一个东西,所以他们两者是可以任意变化的,而然这从程序员的角度来看这是不可行的,毕竟站在程序的角度来看这两个根本就是同一个,如果内部类该变了,而外部方法的形参却没有改变这是难以理解和不可接受的,所以为了保持参数的一致性,就规定使用final来避免形参的不改变。

简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。同时,拷贝引用也解决了匿名内部类的生命周期可能比外部的类长的问题,比如在上述代码中,getMyClass(int)方法返回之后,栈帧中对应的outClass变量就会被销毁,即匿名内部类的生命周期可能比外部的类长,那匿名内部类访问外部局部变量有可能是访问不到的。通过拷贝引用也解决了上述问题。

局部内部类

局部内部类是定义在一个代码块中的类,它的作用范围为其所在的代码块,它就像局部变量一样,不能被public、protected、private及static修饰,局部内部类同样只能访问方法中定义为final的变量。若局部内部类定义在一个静态方法中,它就成为了局部静态内部类,局部静态内部类与静态内部类特性基本相同,局部内部类与成员内部类特性基本相同。

参考资料

Bruce Eckel:《Java编程思想》

Joshua Bloch:《Effective Java》

java提高篇(十)—–详解匿名内部类

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值