目录
一、成员内部类
二、静态内部类
三、匿名内部类
四、局部内部类
五、Q&A
内部类就是把一个类的定义放在另一个类的定义内部
广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类
一、成员内部类
1、成员内部类的形式
成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式:
class Outer {
class Inner {
}
}
Inner 类为 Outer 类的成员内部类,Inner像是Outer类的一个成员,Outer称为外部类。在这种定义方式下,成员内部类对象依赖外部类对象而存在,即在创建一个普通内部类对象时首先需要创建其外部类对象,成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
2、成员内部类访问外部类
class Outer {
private double d;
public static int i;
public Outer(double d) {
this.d= d;
}
class Inner {
public void print(){
System.out.println(d);
System.out.println(i);
}
}
}
当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
外部类.this.成员变量
外部类.this.成员方法
public class Outer{
private int outField = 1;
public static int outStaticField = 2;
public Outer() {
System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
Inner innerObj = new Inner();
}
public class Inner {
private int innerField1=3;
private int outField = 4;
// static int innerfield2 = 5; // 编译错误,普通内部类中不能定义 static 属性
public Inner() {
System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
System.out.println("内部类的 outField 字段的值为: " + outField);
System.out.println("外部类的 outField 字段的值为: " + Outer.this.outField);
System.out.println("外部类的 outStaticField 字段的值为: " + outStaticField);
}
}
public static void main(String[] args) {
Outer outerObj = new Outer();
}
}
3、外部类访问成员内部类
成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner();
public class Outter{
private Inner inner = null;
public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}
public class Inner {
private int innerField=1;
}
public static void main(String[] args) {
//第一种方式:
Outter outterObj = new Outter();
Outter.Inner inner = outterObj.new Inner();
System.out.println(inner.innerField);
//第二种方式:
Outter.Inner inner1 = outterObj.getInnerInstance();
System.out.println(inner1.innerField);
}
}
可见内部类对象可以访问外部类对象中所有访问权限的字段,而外部类对象也可以通过内部类的对象引用来访问内部类中定义的所有访问权限的字段。
内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限,而外部类只能被public和包访问两种权限修饰。由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
二、静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。一个类的静态成员独立于这个类的任何一个对象存在,只要在具有访问权限的地方,我们就可以通过 类名.静态成员名
Outter.Inner inner = new Outter.Inner();
public class Test {
public static void main(String[] args) {
Outter.Inner inner = new Outter.Inner();
}
}
class Outter {
public Outter() {
}
static class Inner {
public Inner() {
}
}
}
创建静态内部类对象的一般形式为: 外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()
创建成员内部类对象的一般形式为: 外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()
三、匿名内部类
匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法
1、如通过,FutureTask对象,传入Callable子类对象,重写call方法创建线程:
FutureTask<Integer> task = new FutureTask<Integer>(
new Callable<Integer>() {
public Integer call() throws Exception {
}
}
});
上述例子new了一个已经存在的类 / 抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。
2、通过实现Runnable接口,重写run方法,创建任务体对象来创建线程
public class Test {
public static void main(String[] args) {
Runnable r1=new Runnable() {
public void run() {
}
};
Thread t1 = new Thread(r1);
}
}
上述例子直接 new 一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象并赋值给前面的Runnable 类型的引用;
四、局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内,而且不在定义类的定义域之内便无法使用,其提供的功能使用匿名内部类都可以实现,而本身匿名内部类可以写得比它更简洁,因此局部内部类用的比较少。
class People{
public People() {
}
}
class Man{
public Man(){
}
public People getWoman(){
class Woman extends People{
int age =0;
}
return new Woman();
}
同样的,在局部内部类里面可以访问外部类对象的所有访问权限的字段,而外部类却不能访问局部内部类中定义的字段,因为局部内部类的定义只在其特定的方法体 / 代码块中有效,一旦出了这个定义域,那么其定义就失效了,就像代码注释中描述的那样,即外部类不能获取局部内部类的对象,因而无法访问局部内部类的字段。
五、Q&A
1、为什么成员内部类可以无条件访问外部类的成员?
通过分析编译文件可以得到,内部类内有一个指向外部类对象的指针,也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用。虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。
2、为什么局部内部类和匿名内部类只能访问局部final变量?
public class Test {
public static void main(String[] args) {
}
public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
}
这段代码会被编译成两个class文件:Test.class和Test1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outter1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outterx.class(x为正整数)。
上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,Java采用了复制的手段来解决这个问题。
在run方法中的a使用的是本地局部变量,这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。
下面看另一个例子:
public class Test {
public static void main(String[] args) {
}
public void test(final int a) {
new Thread(){
public void run() {
System.out.println(a);
};
}.start();
}
}
通过分析编译文件,匿名内部类的构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。
从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。
3、为什么在Java中需要内部类?
1.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,
2.方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
3.方便编写事件驱动程序、线程代码
内部类的存在使得Java的多继承机制变得更加完善