内部类是JAVA语言的主要附加部分。内部类几乎可以处于一个类内部任何位置,可以与实例变量处于同一级,或处于方法之内,甚至是一个表达式的一部分。因为很少使用,一直对内部类的理解不够深入(暴露了基础不扎实)。最近在阅读AQS源码,发现了大量内部类的使用,so,为了通顺的阅读,决定把内部类搞清楚。
第一节 什么是内部类
第二节 内部类的分类
1. 成员内部类
2. 局部内部类
为何局部变量传参必须是final的
绕开final传递局部变量
3. 静态内部类
4. 匿名内部类
第一节 什么是内部类
public class OuterClassDemo0 { class SimpleInnerClass0{ } }
SimpleInnerClass0 定义在 OuterClassDemo0 中,SimpleInnerClass0 便是一个内部类。
对于JAVA来说,每个类在被编译时都会生成一份 .class 文件,在加载入虚拟机后,都会生成一个 instanceKlass 对象作为虚拟机内类的数据结构。在这点上,内部类与外部类没有什么不同。就比如上述的 OuterClassDemo0 类,我们可以用 javac 编译一下:
可以看到,生成了两份字节码文件 OuterClassDemo0 一份,SimpleInnerClass0 一份,但不同的是 SimpleInnerClass0 编译后的文件名为OuterClassDemo0$SimpleInnerClass0 ,代表了其余 OuterClassDemo0 的从属关系。
可以看到,jvm自动为其生成了无参的构造方法。
内部类编译后生成了有参的构造方法,而传入的参数是外部类对象的引用。
第二节 内部类的分类
内部类分为一下几类:
成员内部类、局部内部类、静态内部类、匿名内部类。
1.成员内部类
成员内部类即位于外部类成员位置的内部类。内部类可以访问外部类所有成员(包括private),外部类也可以访问内部类实例的所有成员(包括private)。具体用法如下:
public class OuterClassDemo0 { //外部类私有属性 private String outerString = "i am outerString"; public void printInnerStr0(){ SimpleInnerClass0 inner=new SimpleInnerClass0(); System.out.println("外部类访问内部类私有成员 : "+inner.innerString); } //位于成员位置的内部类 class SimpleInnerClass0 { //内部类私有属性 private String innerString = "i am innerString0"; public void printOuterStr() { //内部类访问外部类私有属性 System.out.println("成员内部类访问外部类私有成员 : "+outerString); } } public static void main(String[] args){ //创建外部类对象 OuterClassDemo0 outer=new OuterClassDemo0(); //创建成员内部类对象 OuterClassDemo0.SimpleInnerClass0 simpleInner=outer.new SimpleInnerClass0(); outer.printInnerStr0(); simpleInner.printOuterStr(); } }
成员内部类可以由 private、protected、static 修饰,这对于外部类来说是不被允许的。
如果我们的内部类不想轻易的被他人访问,可以将内部类定义为 private ,这样外部就不能沟通过创建对象的方式来访问内部类。我们可以仅将内部类提供给外部类使用,并在必要的时候由外部类提供获取内部类实例的方法。
举一个实际使用时的例子:我们定义一个 个人PC的 类,实现了 Computer 接口。对于计算机,肯定有自己的 CPU ,但 CPU 也有许多的分类,属于另外一条继承链,我们也有一个 CPU 接口。
我们希望,个人PC 在创建时便已经有了自己的CPU,所以CPU 不能由别人随意创建,只能打开电脑的后壳拿本电脑的出来操作。
给 PC 内置一个 CPU 对象显然比 PC 同时实现Computer与CPU接口更加富有语义,而如果直接在 PC 内部定制CPU(内部类)会使我们的PC更加实用。
我们则可以使用内部类来解决上述的多继承以及对CPU创建和使用权限控制的问题:
//Computer接口 public interface Computer { public void getCPU(); }
//CPU接口 public interface CPU { public void getVersion(); }
public class PC implements Computer { //本电脑拥有的cpu private CPU cpu; //构造方法,创建一台电脑时,该电脑便有了自己的cpu PC() { cpu = new IntelCPU(); } public void getVersion(){ System.out.println("PC's name is : "+this.toString()+" , and CPU is : "+cpu.getVersion()); } @Override public CPU getCPU() { return cpu; } //IntelCPU内部类,实现cpu接口,定制CPU;声明为private,除所属外部类外,其它类不能通过创建对象访问该类
private class IntelCPU implements CPU {
@Override
public String getVersion() {
return "this is intel "+this.toString()+" cpu of v 1.0.0";
}
}
public static void main(String[] args){ PC pc=new PC(); pc.getVersion(); } }
还有一点需要注意的是,非static的内部类中不能有static块、方法、变量。
静态变量是要占用内存的,在编译时只要是定义为静态变量了,系统就会自动分配内存给他,而内部类是在宿主类编译完编译的。
也就是说,必须有宿主类存在后才能有内部类,这也就和编译时就为静态变量分配内存产生了冲突。
因为系统执行:运行宿主类--->静态变量内存分配--->内部类,而此时内部类的静态变量先于内部类生成,这显然是不可能的,所以不能定义静态变量
2.局部内部类
局部内部类是定义在一个方法或一个作用域里的内部类。
由于定义在方法或者作用域内,所以可以访问同作用域的局部变量,但需要注意的是,访问的局部变量必须用final修饰。同时我们也只能在这个作用域内访问这个内部类,即只能在这个作用域内创建该类的对象。
public class OuterClassDemo1 { public void test() { Object o = new Object(); int i = 1111; /** * @Author Nxy * @Date 2019/12/9 22:52 * @Description 局部内部类 */ class InnerClass { public void say() { System.out.println(o); System.out.println(i); } } } }
上面是一个局部内部类的例子,但是 o 与 i 均没有被 final 修饰但却可以被局部内部类访问。
这是因为编译器自动为我们增加了 final 修饰符(1.8之后的功能),我们不必再显式的声明为 final。
所以将上面的代码复制下来的话,会发现用1.7及之前版本编译时会报错,而用1.8环境则不会。
以下是1.8环境,我们可以发现并没有报错:
但是如果我们尝试修改 o 或者 i ,则会报错,因为它们被隐式修饰为了 final :
为什么局部变量要用 final 修饰:
因为局部变量会随着退出作用域而失效,比如方法体中的局部变量,一旦方法执行完毕,栈帧释放,那么局部变量就被清除了。
但此时,堆中的数据(内部类对象)却不会立即消失。所以此时堆中还在使用着该变量,该变量却已经不存在了。
从程序设计语言的理论上:局部内部类(即:定义在方法中的内部类),由于本身就是在方法内部(可出现在形式参数定义处或者方法体处),因而访问方法中的局部变量(形式参数或局部变量)是天经地义的.是很自然的,但是实际上却很难实现,原因是编译技术是无法实现的或代价极高(局部变量的生命周期与局部内部类的对象的生命周期的不一致性)。为了使变量的生命周期迎合堆中引用它的对象的生命周期,我们需要将局部变量声明为final。当我们用final修饰变量时,堆中存储的是变量值而不是变量的引用(标灰存疑)。
这点一直存疑,JAVA内是没有引用传递的,所有的传递都是值传递。即使我们传递的是对象的引用,也只是将引用的值传给了另一个引用而已。即:我们在内部类访问的,其实是局部变量的复制品而不是局部变量本身。final 关键字是在编译期产生作用的,不会影响运行期。
而对于堆中存储的是变量值而不是引用这一点:static与final的区别在于static会改变变量的存储位置,声明为static的变量会存储在堆中。但是局部变量是不允许声明为static的,否则在编译期就会报错。也就是说对变量来说,static只能修饰类的成员变量,而成员变量的值本身就是存储在堆中的。static真正影响存储的地方是:非static成员变量存储在堆中,而static成员变量存储在堆的方法区中。
final在编译期确保了一个传入内部类的局部变量,无论是在内部类实例中还是在内部类外,其值都不会发生改变!
代码举证:
即使在内部类中尝试改变传入的局部变量值,也会在编译期报错。
按值传递的代码举证,如果传递的是引用,判断结果应该为true:
private static Object o0=new Object(); private static void test(Object o){ o=new Object(); System.out.println(o==o0); } public static void main(String[] args){ test(o0); }
如何绕开final传递局部变量
有的时候,我们可能需要向内部类传参,但又不想参数是final的(程序逻辑上不需要内部类对象与作用域中变量值的一致),我们可以通过一些别的方法来进行传参。比如下例,定义一个 init 方法接收参数:
private void test() { Object o = new Object(); class OuterClassDemo2 extends OuterClassDemo1{ Object oi; private OuterClassDemo2 init(Object object){ this.oi=object; return this; } } OuterClassDemo2 demo2=new OuterClassDemo2().init(o); }
除上述方法外也可以将要传递的参数放入一个 final 数组,数组引用不可变但内容是可变的。也就是需要的参数除数组外都不必声明为final。
3.静态内部类
我们所知道static是不能用来修饰类的,但是成员内部类可以看做外部类中的一个成员,所以可以用static修饰,这种用static修饰的内部类我们称作静态内部类,也称作嵌套内部类。
声明为static的内部类,不需要内部类对象和外部类对象之间的联系,就是说我们可以直接引用outer.inner,即不需要创建外部类对象,也不需要创建内部类对象。 非静态内部类编译后会默认的保存一个指向外部类的引用,而静态类却没有。嵌套类和普通的内部类还有一个区别:普通内部类不能有static数据和static属性,也不能包含嵌套类,但嵌套类可以。而嵌套类不能声明为private,一般声明为public,方便调用。
单例模式就有通过静态内部类实现单例的方法,因为外部类的加载是不会引起内部类的加载的,只有在使用时内部类才可以被加载。借助这点可以实现懒人模式。
/** * @Author Nyr * @Date 2019/11/19 20:48 * @Description 单例模式-静态内部类方式 */ public class Car2 { private Car2(){} private static class InnerCar2{ private static Car2 car2=new Car2(); } public static Car2 getCar2(){ return InnerCar2.car2; } }
4. 匿名内部类
匿名内部类是一个没有名字的内部类,是内部类的简化写法。如果一个类在定义后只会被使用一次,那么就可以使用匿名内部类。
平时我们是无法new一个接口或者抽象类的,但是我们可以通过匿名内部类的方式new一个接口或抽象类,而实际是new了一个集成了接口或抽象类的匿名子类的对象。
public interface Computer { public CPU getCPU(); }
new Computer(){ @Override public CPU getCPU(){ return null; } }.getCPU();
我们也可以通过多态为这个匿名对象加一个引用,方便调用:
Computer u=new Computer(){ @Override public CPU getCPU(){ return null; } };
我们在开发的时候,会看到抽象类,或者接口作为参数。而这个时候,实际需要的是一个子类对象。如果该方法仅仅调用一次,我们就可以使用匿名内部类的格式简化。比如我们常用的Thread对象(这种写法不被推荐,应使用线程池,否则难以控制创建线程的数量):
new Thread(){ @Override public void run(){ } }.start();