在Java种类的使用是十分基础的,但是常规类往往无法完全满足我们的需求,因此发展出更加丰富特性类,其中最常见的特性类之一就是内部类。内部类其实又可以分成:成员内部类、局部内部类,匿名内部类。可以参考下面这张图片。
从上图我们可以知道,内部类的基本分类。也因此本次我们通过以下几个方面进行:
1、成员内部类
2、局部内部类
3、匿名内部类
4、内部类常见的问题
4.1、内部类和外部类可以互相拿取对方成员变量问题
4.2、内部类和多重继承问题
4.3、内部类和内存泄漏问题
4.4、避免内部类内存泄漏的小建议
4.5、 内部类调取外部变量必须是final的问题
通过对他们的性质与使用,进行场景的分析与探索。
1、成员内部类
当我们将普通的一个类进行构造的时候,在类内部,成员范围的位置进行另一个类的构造,则为成员内部类,如下图:
因类Draw对于类Circle来说,像一个成员一样,因此叫做成员内部类。而我们也很容易分辨,成员内部类也可以像成员一样,分为静态与非静态两种。二者的最大区别,使用上来说在于对象的初始化,根源在于是否依赖于是否需要依赖于外部类。我们也可以从静态修饰符static的作用与差别进行分析。
先说非静态成员内部类(常简称为:普通内部类),无论你是否在代码中进行构造器传入外部类的对象,成员内部类对象的构造与初始化都需要依赖于外部类对象。这也就能够比较好地解释,为什么非静态成员内部类能够自由地获取到外部类的成员变量。我们可以先看看非静态成员内部类的初始化方式:
以上图为例,我看到有一些帖子上有写着类似 Outter.Inner inner=new Outter.Inner(); 这种做法,本人亲测,你在敲代码的时候编译器就直接报错,不需要等到运行。由此,我们知道非静态成员内部类的构造初始化必须依赖于外部类的对象,那当内部类的成员变量与外部类的成员变量发成重名的时候,会不会造成覆盖甚至错误呢?答案是不会,因为内部类要引用外部类的成员变量的时后,其实严谨的写法是要通过外部类对象来获取的。以简单例子说明一下:
上图的结果就是打印出:a = a(这里就不分行了,节约空间)、A.a = 0、AA.aa = 1。这也恰好佐证了上述,非静态成员内部类持有外部类对象的结论。
这里我们简单讲一下静态成员内部类,其实他和非静态成员内部类的区别基本跟普通的静态、非静态成员变量的区别是一样的。静态修饰下,不依赖于外部类对象,所以静态成员内部类是完全独立于外部类对象而存在的,也因此他不能访问外部类的非静态成员变量(因为他没有持有外部类对象),而可以访问外部类的所有静态成员变量。但是无论静态与非静态成员变量,都具有一个共同点,就是外部类依然可以访问静态内部类对象的所有访问权限的成员。总的来说,静态内部类对象不依赖其外部类对象存在,而其余的内部类对象必须依赖其外部类对象而存在。
2、局部内部类
局部内部类,其实就是定义在方法体或者作用域内的一个类,它跟成员内部类的区别仅在于作用范围(即局部内部类能够访问仅限于该方法体内/作用域内),如下图:
3、匿名内部类
由于其方便且易于维护,这是我们很常用的一种类,尤其是在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了,如下:
匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围很有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为 Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需增加额外的方法,只是对继承方法的实现或是重写。而跟外部类的联系强度来说,在匿名内部类中可以使用外部类的属性,但是外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。
4、内部类常见的问题
4.1、为什么非静态内部类可以访问外部类所有访问权限修饰的字段(即包括了 private 权限的,可是在定义上,private 权限的字段只能被当前类本身访问),同时,外部类也可以访问内部类的所有访问权限修饰的字段?
我们知道,在非静态内部类编译的时候会放入一个外部类的对象(也就是非静态内部类依赖于外部类)。而在非静态内部类访问外部类私有成员 / 外部类访问内部类私有成员 的时候,对应的外部类 / 外部类会生成一个静态方法,用来返回对应私有成员的值,而对应外部类对象 / 内部类对象通过调用其内部类 / 外部类提供的静态方法来获取对应的私有成员的值。当然这个值也是能够随着它原主的变化而变化的。
4.2、内部类和多重继承问题
犹豫Java具有单继承的特点,因此在某个程度上来说,内部类继承其他父类,是多继承的一种实现。但是,这个代价就是破坏了Java类的结构,一般来说,建议一个 .java 文件只包含一个类,除非两个类之间有非常明确的依赖关系(比如说某种汽车和其专用型号的轮子),或者说一个类本来就是为了辅助另一个类而存在的(比如说上篇文章介绍的 HashMap 类和其内部用于遍历其元素的 HashIterator 类),那么这个时候使用内部类会有较好代码结构和实现效果。而在其他情况,将类分开写会有较好的代码可读性和代码维护性。
当然,虽然不建议内部类继承,但是并不是禁止。真的要使用的情况下需要注意一下,如下图:
4.3、内部类和内存泄露问题
上面我们解释过了,非静态内部类会持有外部类对象的引用,如果我们使用不当,当内部类对象没被JVM回收的情况下,外部类对象自然也不会被回收,也因此造成内存泄漏。例如:
A、在Activity声明一个线程内部类Y:
B、静态内部类持有外部类引用情况:
我们知道静态内部类的生命周期比较长,迟迟无法释放,一旦持有外部类对象引用,就会很容易造成内存泄漏。
4.4、避免内部类内存泄漏的小建议 (摘自参考博客)
那么我们在日常开发中怎么合理的使用内部类来避免产生内存泄露呢?这里给出一点我个人的理解:
1、能用静态内部类就尽量使用静态内部类,从上文中我们也知道了,静态内部类的对象创建不依赖外部类对象,即静态内部对象不会持有外部类对象的引用,自然不会因为静态内部类对象而导致内存泄露,所以如果你的内部类中不需要访问外部类中的一些非 static 成员,那么请把这个内部类改造成静态内部类;
2、对于一些自定义类的对象,慎用 static 关键字修饰(除非这个类的对象的声明周期确实应该很长),我们已经知道,JVM 在进行垃圾回收时会将 static 关键字修饰的一些静态字段作为 “root” 来进行存活对象的查找,所以程序中 static 修饰的对象越多,对应的 “root” 也就越多,每一次 JVM 能回收的对象就越少。
当然这并不是建议你不使用 static 关键字,只是在使用这个关键字之前可以考虑一下这个对象使用 static 关键字修饰对程序的执行确实更有利吗?
3、为某些组件(大型)提供一个当这个大型组件需要被回收的时候用于合理处理其中的一些小组件的方法(例如上面代码中 MyComponent 的 onDestroy 方法),在这个方法中,确保正确的处理一些需要处理的对象(将某些引用置为 null、释放一些其他(CPU…)资源)。
4.5、 内部类调取外部变量必须是final的问题
因为生命周期的原因。方法中的局部变量,方法结束后这个变量就要释放掉,final保证这个变量始终指向一个对象。如果定义为final,java会将这个变量复制一份作为成员变量内置于内部类中,这样的话,由于final所修饰的值始终无法改变,所以这个变量所指向的内存区域就不会变。 也就是说它是为了解决:局部变量的生命周期与局部内部类的对象的生命周期的不一致性问题。
这也造成类一个问题,当这个被调用的局部变量是简单类型的时候,会直接拷贝值而使得内部类在内部使用的时候保持值不变,而这个局部变量是复杂类型的时候,背拷贝的将会是引用,这个引用保持不变,而引用所指的内容是可变的。这也就是为什么在某些局部内部类中会发生值可变或者不变的问题。
如下图该代码处于Activity里的某一个方法体内,tip是方法所携带的参数,当tip是int、String类型的时候,等你下一次调用这个方法体时,外部的tip的值即使发生改变,图中的tip并不会发生改变。而当tip是复杂类型,比如自己定义的某种类的对象时候,每一次的tip的内容,都会跟外部的tip内容一致。
综上,这就是本文所阐述关于内部类的简单介绍。为了记录与统合,主要参考了网上的一些见解,相当一部分内容参考甚至摘取自参考博客。如果相关作者有意见,请及时联系本人。当然文中如果有不符合甚至错误的地方,也欢迎小伙伴们指正!
参考博文:
---------------------
作者:未知 来源:菜鸟教程 题目:Java 内部类详解
链接:https://www.runoob.com/w3cnote/java-inner-class-intro.html
---------------------
作者:指点 来源:CSDN 题目:详解 Java 内部类
链接:https://blog.csdn.net/hacker_zhidian/article/details/82193100#_227
---------------------
作者:CruiseLoveAshley 来源:CSDN 题目:为什么内部类调用的外部变量必须是final修饰的?
链接:https://blog.csdn.net/u010393325/article/details/80643636