内部类 ( inner class ) 就是定义在另一个类中的类,下面我们就通过四个问题来全方位认识内部类…
1.内部类与外部类的关系
1)内部类是一个相对独立的实体,与外部类不是is-a关系
一个外部类可以拥有多个内部类对象,而他们之间没有任何关系,是独立的个体。从编译结果来看,内部类被表现为 「外部类$内部类.class 」,所以对于虚拟机来说他个一个单独的类来说没什么区别。但是我们知道他们是有关系的,因为内部类默认持有一个外部类的引用。
PS:内部类的修饰符是可以用 public 的,只不过很多情况下,我们定义内部类仅是为了给当前类服务,所以最常用的是 private。
2)创建一个外部类的时候不一定要创建这个内部类
- 对于非静态内部类:内部类的创建依赖外部类的实例对象,在没有外部类实例之前是无法创建内部类的。正是由于这种依赖关系,所以普通内部类中不允许有 static 成员,包括嵌套类(内部类的静态内部类)
- 对于静态内部类:创建内部类的时刻并不依赖于外部类的创建,因为 static 并不依赖于实例,而依赖与类 Class 本身
对于普通内部类创建方法有两种:
public class ClassOuter {
public void fun(){
System.out.println("外部类方法");
}
public class InnerClass{
}
}
public class TestInnerClass {
public static void main(String[] args) {
// 创建方式1
ClassOuter.InnerClass innerClass = new ClassOuter().new InnerClass();
// 创建方式2
ClassOuter outer = new ClassOuter();
ClassOuter.InnerClass inner = outer.new InnerClass();
}
}
2.内部类的分类
内部类可以根据定义的位置分为三种:成员内部类,局部内部类,匿名内部类。
2.1 类内:成员内部类
成员内部类就是一般我们说的非静态内部类和静态内部类,即在类中定义的。他俩的区别在于一个属于外部类的实例对象,一个属于外部类的Class,再说具体点就体现在以下几点:
- 组成:静态内部类可以有静态成员,而非静态内部类则不能有静态成员。
- 访问:静态内部类可以访问外部类的静态变量,而不可访问外部类的非静态变量;非静态内部类的非静态成员可以访问外部类的非静态变量。
- 创建:静态内部类的创建不依赖于外部类,而非静态内部类必须依赖于外部类的创建而创建。
我们通过一个例子就可以很好的理解这几点区别:
public class ClassOuter {
private int noStaticInt = 1;
private static int STATIC_INT = 2;
public void fun() {
System.out.println("外部类方法");
}
public class InnerClass {
// static int num = 1; 此时编辑器会报错 非静态内部类则不能有静态成员
public void fun(){
// 非静态内部类的非静态成员可以访问外部类的非静态变量。
System.out.println(STATIC_INT);
System.out.println(noStaticInt);
}
}
public static class StaticInnerClass {
static int NUM = 1; // 静态内部类可以有静态成员
public void fun(){
System.out.println(STATIC_INT);
// (noStaticInt); 此时编辑器会报 不可访问外部类的非静态变量错
}
}
}
注:上面说的非静态内部类不能有静态成员,是说的不能定义静态变量,但是它仍可以定义静态常量,比如
static final NUM = 1;
访问方式是ClassOuter,
。
下面我们来看一下怎么访问内部类
- 对于所属的外部类(ClassOuter)创建内部类,无论是静态的还是非静态的,直接 new 就行了
- 对于其余的外部类,静态内部类和非静态内部类创建方式不同(见下例)
public class TestInnerClass {
public static void main(String[] args) {
// 非静态内部类 创建方式1
ClassOuter.InnerClass innerClass = new ClassOuter().new InnerClass();
// 非静态内部类 创建方式2
ClassOuter outer = new ClassOuter();
ClassOuter.InnerClass inner = outer.new InnerClass();
// 静态内部类的创建方式,直接外部类名.静态内部类名
ClassOuter.StaticInnerClass staticInnerClass = new ClassOuter.StaticInnerClass();
}
}
2.2 方法内:局部内部类
如果一个内部类只在一个方法中使用到了,那么我们可以将这个类定义在方法内部,这种内部类被称为局部内部类。其作用域仅限于该方法。局部内部类有两点值得我们注意的地方:
- 局部内类不允许使用访问权限修饰符 public private protected 均不允许
- 局部内部类对外完全隐藏,除了创建这个类的方法可以访问它其他的地方是不允许访问的。
局部内部类与成员内部类不同之处是他可以引用成员变量,但该成员必须声明为 final,并内部不允许修改该变量的值。(这句话并不准确,因为如果不是基本数据类型的时候,只是不允许修改引用指向的对象,而对象本身是可以被就修改的)
public class ClassOuter {
private int noStaticInt = 1;
private static int STATIC_INT = 2;
public void fun() {
System.out.println("外部类方法");
}
public void testFunctionClass(){
class FunctionClass{
private void fun(){
System.out.println("局部内部类的输出");
System.out.println(STATIC_INT);
System.out.println(noStaticInt);
System.out.println(params);
// params ++ ; // params 不可变所以这句话编译错误
}
}
FunctionClass functionClass = new FunctionClass();
functionClass.fun();
}
}
2.3 参数内:匿名内部类
匿名内部类一般用于函数参数,作用是实现某个接口
-
匿名内部类是没有访问修饰符的。
-
匿名内部类必须继承一个抽象类或者实现一个接口
-
匿名内部类中不能存在任何静态成员或方法
-
匿名内部类是没有构造方法的,因为它没有类名
public class Button {
// 匿名内部类必须继承或实现一个已有的接口
public interface ActionListener{
public void onAction();
}
public void click(final int params){
// 匿名内部类,实现的是ActionListener接口
new ActionListener(){
public void onAction(){
System.out.println("click action..." + params);
}
}.onAction();
}
public static void main(String[] args) {
Button button=new Button();
button.click();
}
}
另外,与局部内部类相同,匿名内部类也可以引用局部变量。此变量也必须声明为 final,因为局部变量和匿名内部类的生命周期不同。匿名内部类是创建后是存储在堆中的,而方法中的局部变量是存储在Java栈中,当方法执行完毕后,就进行退栈,同时局部变量也会消失。那么此时匿名内部类还有可能在堆中存储着,那么匿名内部类要到哪里去找这个局部变量呢?
为了解决这个问题编译器为自动地帮我们在匿名内部类中创建了一个局部变量的备份,也就是说即使方法执结束,匿名内部类中还有一个备份,自然就不怕找不到了。
但是问题又来了。如果局部变量中的a不停的在变化。那么岂不是也要让备份的a变量无时无刻的变化。为了保持局部变量与匿名内部类中备份域保持一致。编译器不得不规定死这些局部域必须是常量,一旦赋值不能再发生变化了。所以为什么匿名内部类应用外部方法的域必须是常量域的原因所在了。
for(int thrIdx = 0; thrIdx < threadCount; thrIdx++){
int finalThrIdx = thrIdx;
new Thread(() -> this.doQuery(finalThrIdx, urlArray)).start();
}
比如上面的代码,我们在 for 循环里面通过 lamada 表达式不断创建线程,并且直接使用循环次数作为 threadID。那么问题来了,如果在 doQuery 的入参中直接写 thrIdx 就会报错,因为它会不断变化,所以这里要定义一个赋值后就不会再变的局部变量作为参数。
注:在Java8中已经去掉要对final的修饰限制,但其实只要在匿名内部类使用了,该变量还是会自动变为final类型(只能使用,不能赋值)
3.内部类有什么用
我们为什么需要内部类?或者说内部类为啥要存在?其主要原因有如下几点:
- 内部类方法可以访问该类定义所在作用域中的数据,包括被 private 修饰的私有数据
- 内部类可以对同一包中的其他类隐藏起来
- 内部类可以实现 java 单继承的缺陷
- 当我们想要定义一个回调函数却不想写大量代码的时候我们可以选择使用匿名内部类来实现
下面我们就逐一来看这四点…
3.1 访问类作用域中的数据
- 当外部类的对象创建了一个内部类的对象时,内部类对象必定会秘密捕获一个指向外部类对象的引用,然后访问外部类的成员时,就是用那个引用来选择外围类的成员的。当然这些编辑器已经帮我们处理了。
- 另外注意内部类只是一种编译器现象,与虚拟机无关。编译器会将内部类编译成外部类名$内部类名的常规文件,虚拟机对此一无所知。
class TestInnerClass{
// private 私有变量
private String name;
private int age;
public class Output {
// 打印当前对象的 name和age
public void out() {
System.out.println(name + ":" + age);
}
}
...
}
上文中 Output 便是 TestInnerClass的一个内部类。也可以看出 Output 可以直接访问 TestInnerClass 中定义的 name和age 私有变量。如果将 Output 不定义为内部类访问 name和age 私有变量 没有 getXXX 方法是做不到的。 这就是内部类的第一点好处。
3.2 实现 java 单继承的缺陷
我们知道 java 是不允许使用 extends 去继承多个类的。内部类的引入可以很好的解决这个事情。以下引用 《Thinking In Java》中的一段话:
每个内部类都可以队里的继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类没有影响如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就难以解决。接口解决了部分问题,一个类可以实现多个接口,内部类允许继承多个非接口类型(类或抽象类)。
- Java只能继承一个类这个学过基本语法的人都知道,而在有内部类之前它的多重继承方式是用接口来实现的
- 但使用接口有时候有很多不方便的地方。比如我们实现一个接口就必须实现它里面的所有方法
- 而有了内部类就不一样了。它可以使我们的类继承多个具体类或抽象类。如下面这个例子:
// 类一
public class ClassA {
public String name(){
return "zhangsan";
}
public String doSomeThing(){
// doSomeThing
}
}
// 类二
public class ClassB {
public int age(){
return 18;
}
}
// 想要复用ClassA和ClassB中的方法
public class InnerClassExample{
// 内部类继承 ClassA
private class Test1 extends ClassA{
public String name(){
return super.name();
}
}
// 内部类继承 ClassB
private class Test2 extends ClassB{
public int age(){
return super.age();
}
}
// 可以通过创建出内部类,然后调用内部类父类的方法
public String name(){
return new Test1().name();
}
public int age(){
return new Test2().age();
}
public static void main(String args[]){
InnerClassExample example = new InnerClassExample();
System.out.println("姓名:" + example .name());
System.out.println("年龄:" + example .age());
}
}
上边这个例子可以看出来,MainExample 类通过内部类拥有了 ClassA 和 ClassB 的两个类的继承关系。 而无需关注 ClassA 中的 doSomeThing 方法的实现。这就是比接口实现更有戏的地方。
3.3 简单的接口实现
关于匿名内部类相信大家都不陌生,我们常见的点击事件的写法就是这样的:
...
view.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(){
// ... do XXX...
}
})
...
–> 函数式接口
view.setOnClickListener(v -> gotoVipOpenWeb());
3.4 隐藏同一包中的其他类
- 我们都知道外部类即普通的类不能使用 private protected 访问权限符来修饰的,而内部类则可以使用 private 和 protected 来修饰。
- 当我们使用 private 来修饰内部类的时候这个类就对外隐藏了。这看起来没什么作用,但是当内部类实现某个接口的时候,在进行向上转型,对外部来说,就完全隐藏了接口的实现了。
public interface Incrementable{
void increment();
}
//具体类
public class Example {
private class InsideClass implements InterfaceTest{
public void test(){
System.out.println("这是一个测试");
}
}
public InterfaceTest getIn(){
return new InsideClass();
}
}
public class TestExample {
public static void main(String args[]){
Example a=new Example();
// 获取到实现了接口的内部类
InterfaceTest a1=a.getIn();
a1.test();
}
}
从这段代码里面我只知道Example的getIn()方法能返回一个InterfaceTest实例但我并不知道这个实例是这么实现的。而且由于InsideClass是private的,所以我们如果不看代码的话根本看不到这个具体类的名字,所以说它可以很好的实现隐藏。
4.内部类可能引起的问题
内部类会造成程序的内存泄漏。
JVM可达性分析下,内部类在什么情况下会造成内存泄漏
- 如果一个匿名内部类没有被任何引用持有,那么匿名内部类对象用完就有机会被回收。
- 如果内部类仅仅只是在外部类中被引用,当外部类的不再被引用时,外部类和内部类就可以都被GC回收。
- 如果当内部类的引用被外部类以外的其他类引用时,就会造成内部类和外部类无法被GC回收的情况,即使外部类没有被引用,因为内部类持有指向外部类的引用)。
public class ClassOuter {
Object object = new Object() {
public void finalize() {
System.out.println("inner Free the occupied memory...");
}
};
public void finalize() {
System.out.println("Outer Free the occupied memory...");
}
}
public class TestInnerClass {
public static void main(String[] args) {
try {
Test();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void Test() throws InterruptedException {
System.out.println("Start of program.");
ClassOuter outer = new ClassOuter();
Object object = outer.object;
outer = null;
System.out.println("Execute GC");
System.gc();
Thread.sleep(3000);
System.out.println("End of program.");
}
}
运行程序发现 执行内存回收并没回收 object 对象,这是因为即使外部类没有被任何变量引用,只要其内部类被外部类以外的变量持有,外部类就不会被GC回收。我们要尤其注意内部类被外面其他类引用的情况,这点导致外部类无法被释放,极容易导致内存泄漏。