Java内部类

270 阅读6分钟

什么是内部类:

一个类内部除了有属性和方法,还可以有其他类,内部类也可以在方法或代码块中。 可以简单的理解为 花括号里 { } 里面再定义一个类,那么这个类就称为:内部类。 因为理论上内 部类可以在类任意位置,所以代码块也好,普通方法也可以。

内部类的作用:

内部类可以很好的实现类的隐藏,内部类可以拥有外部类的全部元素的访问权限。通过内部类可

以实现多重继承(外部类继承一个,内部类继承一个,然后内部类就可以调用到继承类的方法和

属性)。可以避免修改接口,而实现同一个类中两种同名方法的调用。

1.增强封装,把内部类放在外部类当中,不允许其它类访问这个内部类。

2.增加了代码一个维护性,不涉及到其他类。

3.内部类可以直接访问外部类当中的成员

根据类是否定义在方法内部分为:成员内部类(实例内部类、静态内部类)和局部内部类(匿名内部类)

内部类分为四种:

实例内部类:直接定义在类当中的一个类,在类前面没有任何一个修饰符

静态内部类:在内部类前面加上一个static

局部内部类:定义在方法中的内部类

匿名内部类:属于局部的一种特殊情况

实例内部类: image.png

静态内部类:(静态的特点是无需实例化可以直接调用,静态类和静态方法皆是如此)

image.png

局部内部类:

image.png

image.png

匿名内部类:

根据字面意思就知道没有命名的内部类,那么匿名内部类就是没有构造器的,因为我们都知道类的构造器需要跟类同样的命名。

image.png image.png image.png image.png

局部内部类和匿名内部类

局部内部类是嵌套在方法和作用域内的,对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。

而匿名内部类也可以说是局部内部类的一种,有时候一个类只使用一次,就可以用匿名内部类,告诉GC只用一次就可以回收了,同时也可以简化代码和方便地定义回调

需要注意的是局部内部类和匿名内部类引用外部变量时,外部的变量需要是final 的:

abstract class InnerClass {
public abstract void print();
}
public class Outer {
public  void test1(final String s1) {// 参数必须是final
    //成员内部类
    InnerClass c = new InnerClass() {
        public void print() {
            System.out.println(s1);
        }
    };
    c.print();
}

public  void test2(final String s2) {// 参数必须是final
    //匿名内部类
     new Outer() { //名字可以跟外部类一样
        public void print() {
            System.out.println(s2);
        }
    }.print();
}    
public static void main(String[] args) {
    Outer o=new Outer();
    o.test1("inner1");
    o.test2("inner2");

}
}

为什么匿名内部类和局部内部类引用外部的变量必要是final的呢?

直接看编译出来的源码吧

InnerClass:

    abstract class InnerClass
    {
      public abstract void print();
    }

Outer.class:

`import java.io.PrintStream;

public class Outer
{
  public void test1(String paramString)
  {
Outer.1 local1 = new InnerClass(this, paramString) {
  public void print() {
    System.out.println(this.val$s1);//引用了s1变量
  }

};
local1.print(); }

  public void test2(String paramString) {
new Outer(this, paramString) {//名字可以一样
  public void print() {
    System.out.println(this.val$s2);
  }
}
.print();
  }

 public static void main(String[] paramArrayOfString)
  {
Outer localOuter = new Outer();
localOuter.test1("inner1");
localOuter.test2("inner2");
  }
}

局部内部类Outer$1.class:

import java.io.PrintStream;
class 1 extends InnerClass
{
  public void print()
  {
    System.out.println(this.val$s1);//引用了s1变量
  }
}

匿名内部类Outer$2.class:

import java.io.PrintStream;
class 2 extends Outer
{
  public void print()
  {
    System.out.println(this.val$s2);//引用了s2变量
  }
}

看源码两个内部类各编译出了一个独立的class文件,也就是说Outer1Outer1和Outer2的生命周期是对象级别的,而变量s1、s2是方法级别的,方法运行完了变量就销毁了,而局部内部类对象Outer1Outer1和Outer2还可能一直存在(只能没有人再引用该对象时,它才会被GC回收),它不会随着方法运行结束就马上死亡。这时可能会出现了一种"荒唐"的结果:局部内部类对象inner_object要访问一个已不存在的局部变量s1、s2!

也有人说:当方法调用完了,内部类也不可能再被访问到了,照理内部类对象也应该成为了垃圾。
别忘了Java还有反射,而且在多线程的情况下完全有可能主线程的方法运行结束,而内部类还在运行,例如:

public void execute() {
  final int s = 10;
       class InnerClass {
          public void execute() {
              new Thread() {
              @Override
               public void run() {
                 try {
                        Thread.currentThread().sleep(2000);
                        System.out.println(s);
                 } catch (final InterruptedException e) {
                       e.printStackTrace();
                  }
               }
             }.start();
           }
         }
        new InnerClass().execute();
        System.out.println("主方法已经over");
    } 

为什么把变量定义为final就能避免上述问题?

stackoverflow里最高票的答案说到,当主方法结束时,局部变量会被cleaned up 而内部类可能还在运行。当局部变量声明为final时,当使用已被cleaned up的局部变量时会把局部变量替换成常量:

也就是说当变量是final时,编译器会将final局部变量"复制"一份,复制品直接作为局部内部中的数据成员.这样,当局部内部类访问局部变量时,其实真正访问的是这个局部变量的"复制品"。因此:当运行栈中的真正的局部变量死亡时,局部内部类对象仍可以访问局部变量(其实访问的是"复制品")。而且,由于被final修饰的变量赋值后不能再修改,所以就保证了复制品与原始变量的一致。给人的感觉:好像是局部变量的"生命期"延长了。
这就是java的闭包。

闭包是个什么东西呢?
Ruby之父松本行弘在《代码的未来》一书中解释的最好:闭包就是把函数以及变量包起来,使得变量的生存周期延长。闭包跟面向对象是一棵树上的两条枝,实现的功能是等价的。

Java中闭包带来的问题
在Java的经典著作《Effective Java》、《Java Concurrency in Practice》里,都提到:匿名函数里的变量引用,也叫做变量引用泄露,会导致线程安全问题,因此在Java8之前,如果在匿名类内部引用函数局部变量,必须将其声明为final,即不可变对象。(Python和Javascript从一开始就是为单线程而生的语言,一般也不会考虑这样的问题,所以它的外部变量是可以任意修改的)。
而java8的lambda 表达式之所以不用写final,是因为Java8这里加了一个语法糖:在lambda表达式以及匿名类内部,如果引用某局部变量,则直接将其视为final。本质并没有改变。