内部类本质上都会被转换为独立的类,但一般而言,它们可以实现更好的封装,代码实现上也更为简洁。 使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。——《Think in java》

也就是说内部类拥有类的基本特征。(eg:可以继承父类,实现接口。)在实际问题中我们会遇到一些接口无法解决或难以解决的问题,此时我们可以使用内部类继承某个具体的或抽象的类,间接解决类无法多继承引起的一系列问题
内部类可以访问外部类的所有成员(包括private修饰的成员)。但是外部类不能访问内部类中的任何成员。因为,内部类隐式的持有对外部类的引用。这样通过内部类就可以提供一种代码隐藏和代码组织的机制。
内部类 - 图1

What

成员内部类


独立性 成员限制
静态内部类 不需要依赖外部类对象,仅可访问外部类的静态成员 可以包含一个正常类的所有
非静态内部类 隐式持有指向外部类对象的引用,可访问外部类的所有成员(包括private) 不能包含static成员

非静态内部类

  • .this 和 .new

通过 .this返回外部类对象;.new 返回内部类对象

  1. public class Outer {
  2. public class Inner {
  3. public Outer outer() {
  4. return Outer.this;
  5. }
  6. }
  7. public static void main(String[] args) {
  8. Outer ou = new Outer();
  9. Outer.Inner oui = ou.new Inner();
  10. Outer out = oui.outer(); //注意:ou和out是引用的同一个对象
  11. }
  12. }

静态内部类

如果不需要内部类对象与其外部类对象之间有联系,那么可以使用static将其声明为静态内部类。

普通的内部类对象隐式地保存了一个引用,指向创建它的外部类对象。而创建静态内部类对象不需要依赖外部类对象。

普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static数据和 static 字段,也不能包含静态内部类。但是静态内部类可以包含所有这些东西。

  • 存在于接口内部

如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便。
类可以作为接口的一部分,你放到接口中的任何类都自动地是 public 和 static 的。因为类是 static 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外部接口,如下:

  1. public interface ClassInInterface {
  2. void howdy();
  3. class Test implements ClassInInterface {
  4. @Override
  5. public void howdy() {
  6. System.out.println("Howdy!");
  7. }
  8. public static void main(String[] args) {
  9. new Test().howdy();
  10. }
  11. }
  12. }

局部内部类

指在方法的作用域内定义的内部类。局部内部类不能有访问说明符,因为它不是外部类的一部分;但是它可以访问当前代码块内的常量,以及此外部类的所有成员。

  • 局部内部类 vs 匿名内部类

什么情况下使用局部内部类?

  1. 需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能使用实例初始化;
  2. 需要不止一个该内部类的对象。

匿名内部类

通过 new 表达式 创建一个继承(实现)自该基类(接口)的匿名类的对象。

如果在定义一个匿名内部类时,它要使用一个外部环境(在本匿名内部类之外定义)对象,为保证数据的一致性,那么编译器会要求其(该对象)参数引用是 final (也就是说,该参数在初始化后不能被重新赋值,所以可以当作 final)的。在Java8之后,只要自由变量值在初始化后未被修改,省略 final 也没问题,编译器会自动加上,这叫做等同 final 效果(Effectively Final)。

匿名内部类与正规的继承相比有些受限,因为匿名内部类要么继承类,要么实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。

同时,匿名内部类没有类名,也就没有显示的构造方法,但可以通过初始化块儿来实现对象的显示初始化。

Why

为什么需要内部类?

每个内部类都能独立地继承自一个基类,多个内部类就可以继承多个基类,从而让Java也拥有了“多继承”的能力。而多继承在某些编程问题上还是很有作用的。

内部类可以通过private修饰,对外能够实现完全不可见,并且不可用,能够很方便的隐藏实现细节,封装性更好,让程序的结构更加合理。

如果不需要解决“多重继承”的问题,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:

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

继承内部类

因为内部类的构造器必须连接到指向其外部类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外部类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须在子类的构造函数中显示的调用外部类对象的无参构造

  1. class WithInner {
  2. class Inner {}
  3. }
  4. public class InheritInner extends WithInner.Inner {
  5. InheritInner(WithInner wi) {
  6. wi.super();
  7. }
  8. public static void main(String[] args) {
  9. WithInner wi = new WithInner();
  10. InheritInner ii = new InheritInner(wi);
  11. }
  12. }

可以看到,InheritInner 只继承自内部类,而不是外部类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外部类对象的引用。此外,必须在子类的构造函数中显示的调用外部类对象的无参构造,这样才提供了必要的引用,然后程序才能编译通过。

内部类标识符

由于编译后每个类都会产生一个 .class 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个”meta-class”,叫做 Class 对象)。
你可能猜到了,内部类也必须生成一个 .class 文件以包含它们的 Class 对象信息。这些类文件的命名有严格的规则:外部类的名字,加上 “$” ,再加上内部类的名字。
如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外部类标识符与 “$” 的后面。

闭包

含有自由变量的代码块称为闭包(closure)。或者称作 词法定界(lexically scoped ,基于词法作用域的),也有用术语 变量捕获 variable capture 称呼的。

  • 自由变量:一个函数的“自由变量”即 非函数内部定义的变量,这种变量一般处于函数运行时上下文。
  • Java中的闭包

Java中,当非成员内部类持有了自由变量(成员变量除外,因为成员变量位于堆中,可被所有匿名内部类共用),该非成员内部类就形成了闭包。因为它自动拥有一个指向此外部类对象的引用,便可访问外部类对象(创建该内部类的作用域)的信息,在此作用域内,内部类有权操作所有的成员,包括 private 成员。

  • Java支持闭包的方式

匿名内部类持有的自由变量(成员变量除外)必须是final修饰。
在Java8之后,只要自由变量值在初始化后未被修改,省略 final 也没问题,编译器会自动加上,这叫做等同 final 效果(Effectively Final)。

  • 闭包的价值

在于可以作为函数对象或者匿名函数,持有上下文数据,作为第一级对象进行传递和保存。闭包广泛用于回调函数函数式编程中。

  • 为什么匿名内部类持有的自由变量(成员变量除外)必须是final修饰的?

答:保证了该局部变量的数据一致性,从而解决了闭包内外变量生命周期不一致的问题。
匿名内部类在使用局部变量时,JVM会通过该匿名内部类的初始化代码块将该局部变量拷贝一份儿传入匿名内部类,并以该匿名内部类成员变量的形式存在。即,jvm层面,局部变量与备份到匿名内部类的成员变量是两个互不干扰的变量, 为了保证在语义上的一致,需要加上final关键字修饰,来避免调用者以为是直接操作了外部变量。通过final修饰使其局部变量初始化后就不能被修改了,也就保证该匿名内部类 内外数据的一致性。
而使用final修饰该局部变量表面上看似乎就起到了延长其生命周期的效果,但个人认为,更准确的是保证了该局部变量的数据一致性,因为这是他的直接作用,同时final关键字本身并没有延长变量生命周期的能力,这样的解释更贴切,避免产生误解。

  • 非成员内部类在使用局部变量时为什么要拷贝?直接使用不就不存在数据一致性问题了吗?
    • 原因一:

局部变量位于栈帧中,其生命周期与方法一致,当方法执行完毕后出栈,该局部变量也随之消失。而匿名内部类位于堆中,生命周期更长(由GC控制)。因此方法出栈后,匿名内部类再次使用该局部变量时,就会出问题:内部类访问了一个已经不存在的变量。因此使用值传递的形式将该变量传递进非成员内部类中。

  1. - **原因二:**

在JVM的运行时数据区域中,局部变量存在于虚拟机栈上,也就意味着这个变量无法线程间进行共享,当开启新线程时,也只能通过值传递的方式,传递到线程内部。同时,你会发现新线程也是创建了新的内部类。

  • 为什么匿名内部类访问成员变量就无需加final?

因为成员变量位于堆内存中,所以变量可以共享,也就无需拷贝成员变量到内部类中,直接可以使用(通过this. 的形式),也就不存在数据一致性问题了。

  • 如果匿名内部类中需要修改自由变量(成员变量除外)该怎么办?

变量持有基本数据类型是无法修改的,因为基本数据类型时栈上分配,Java是值传递,为了保证数据一致性必须使用final修饰;而变量持有对象,是堆中分配,虽然该变量依然必须final修饰,但是它持有的对象内部的属性是可以改变的,只是该对象的地址是不能改变的。
综上所述,可以将持有的基本类型包装为对象,然后就可以在闭包中改变其属性了。