谈谈对内部类的理解?

1,450 阅读5分钟

分析&回答

java中的内部类可以分为普通内部类(成员内部类),静态内部类,局部内部类和匿名内部类。

成员内部类

成员内部类就是像普通的成员函数一样声明的内部类,下面我们先给出一个简单的示例,InnerClass是OutClass的成员内部类,可以访问OutClass的成员变量。

public class OutClass {
    private int i;

    public OutClass(int i) {
        this.i = i;
    }

    public class InnerClass {
        private int j;

        public InnerClass(int j) {
            this.j = j;
        }

        public void print() {
            System.out.println("OutClass.i = " + i);
            System.out.println("InnerClass.j = " + j);
        }
    }
}

//测试
public class OutClassTest {

    public static void main(String [] args) {
        OutClass outClass = new OutClass(1);
        OutClass.InnerClass innerClass = outClass.new InnerClass(2);
        innerClass.print();
    }
}

我认为主要有以下几点用法:
(1)可以使用public,private,protected或者默认的访问权限控制符声明,表示该内部类的访问权限;
(2)可以访问外部类的成员变量和方法;
(3)成员内部类不能声明静态成员;
​ 对于(2),我们可以认为在内部类持有外部类的引用,这样内部类就可以通过这个引用访问外部类的所有的成员的方法和变量。同时,这也解释为什么要通过外部类对象来创建内部类。为了验证这个问题,我们对OutClass.java编译得到OutClass.class和OutClass$InnerClass.class,然后我们通过idea直接打开内部类class文件(我们之所以用idea自带的反编译器,是因为相比于javap,idea反编译的文件可读性更高),具体如下:

image.png

java在编译内部类时会在其构造函数中默认添加外部类引用的参数,从而持有外部类的引用。
对于(3),成员内部类就相当于外部类的一个普通的成员变量,当jvm加载外部类的时候并不会加载非静态变量,因此也就不会加载内部类。如果内部类可以声明静态变量,那么就会出现类还没有加载却要初始化静态变量的现象,因此java不会允许这种情况通过编译。

静态内部类

静态内部类就是有static修饰的内部类,类似静态变量或者静态函数。相比于成员内部类对外部类的依赖,静态内部类基本不依赖外部类。通过其官方名称"static nested classes"(静态嵌套类),更能说明其与外部类没有关系,只是自己的类声明嵌套在外部类的java文件中。静态内部类只能访问外部类的静态成员和方法,这点非常好理解,因为静态内部类和外部类没关系。静态内部类和静态变量或方法一样可以使用public,private,protected或者默认的访问权限控制符声明,这点也很好理解。下面我们给出个静态内部类的示例:

public class OutClass {
    private static int k=3;
    private int i;

    public OutClass(int i) {
        this.i = i;
    }

    static class InnerClass {
        private int j;

        public InnerClass(int j) {
            this.j = j;
        }

        public void print() {
            System.out.println("OutClass.k = " + k);
            System.out.println("InnerClass.j = " + j);
        }
    }
}

//测试类
public class OutClassTest {

    public static void main(String [] args) {
        OutClass.InnerClass innerClass = new OutClass.InnerClass(2);
        innerClass.print();
    }
}

示例很简单,从测试类可以看出内部类的声明不再依赖外部类,可以完全独立声明对象,其实静态内部类不再持有外部类的引用,下面我们看下反编译的class文件:

image.png

匿名内部类

对于匿名内部类,大家肯定在日常开发中都有使用但却不知道叫法,比如函数回调,线程声明等都会使用匿名内部类。如下面这段常用的代码:

public class ThreadTest {
    public static void main(String []args) {
        final User user= new User("chenxiaosuo");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("user = " + user);
            }
        }, "threadtest");
        thread.start();
    }

    static class User {
        private String name;

        public User(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + ''' +
                    '}';
        }
    }
}

Runable是一个接口,我们通过new来实现了一个匿名内部类,这个类没有类名(其实真实编译后会以"外部类$数字"的形式作为类名)。
​不知道大家有没有这个困惑,在匿名内部类中使用的变量必须声明为final类型,表示变量不可变,否则编译报错,如上的变量user。我们先分析下上述代码,变量user是个局部变量,当函数testAnonymousInnerClass结束后会回收,而此时threadtest可能还在继续执行,如果访问变量user应该会报错,而实际却不会出现这种情况。我们看下编译后的类文件发生了什么,匿名内部会复制一份访问的变量到内部,这样就解决上述的声明周期问题。因为这种拷贝,如果java允许这个变量发生改变,那么肯定就造成了数据不一致的问题,这也就解释为什么匿名内部类的访问的变量必须为final类型了。

image.png

局部内部类

可以把局部内部类与我们函数里的局部变量作为类比,局部内部类就是声明在一个函数或者某个作用域中的内部类。可以看出局部内部类只作用于其所声明的函数或者局部作用域中,因此在局部内部类中不能使用public,private和protected。

方法中声明的内部类进行反射,得到的代码如下:

class Outer$Inner {
    Outer$Inner(Outer,int);
    final int val$x;
    final Outer this$0;
}
注意构造器的int参数和val$x的实例变量。当创建一个对象的时候,x会被传入构造函数,
并且最终对于val$x进行初始化,这也是局部内部类中的变量必须为final的原因,
所以final关键字的目的就是为了保证内部类和外部函数对变量“认识”的一致性。

由于局部内部类比较简单,这里不再举例说明。

内部类作用

以上介绍了内部的使用及相关分析,最后我们再思考一个问题,为什么要使用内部类?在java编程思想中讲到,使用内部类可以使得java类继承多个类,使得java多继承的方案更加的完善。是不是很抽象? 下面我这边总结两个我感觉使用内部类的理由:
(1)使用成员内部类可以使得内部类访问外部类的成员变量;
(2)使用匿名内部类使得我们代码变得更简洁,不需要定义一些只是用一次的类;

匿名内部类使用场景

第一种使用方法是在模板回调模式当中,使用匿名内部类来作为回调接口的实现。在Spring的JDBC模块和Hibernate模块当中,都提供了模板类。以JDBC为例,Spring提供了一个JDBCTemplate作为模板,还提供了一系列的回调接口。在模板当中,主要提供的是JDBC访问数据库的基本流程,将SQL语句和对结果集的处理设置的回调接口当中。在使用该模板时,就可以使用匿名内部类来实现。

第二种使用方法是使用匿名内部类来实现多线程。如果不希望在一个类的外部来产生一个该类的线程,就可以再该类当中定义一个匿名内部类,实现Runnablle接口,或者是继承Thread类。因为匿名内部类没有名字,并且可以将其权限设置为private,所以说不可能在别的地方生成匿名内部类的对象。这样就可以很好的隐藏调用线程的run()函数。

总体来说最常用的内部类是匿名内部类。这种用法是专门供特定问题用的,一次性的类。好处是它能把解决某个问题的代码全都集中到一个地方。

反思&扩展

静态内部类和非静态内部类有什么区别?

  1. 静态内部类可以有静态成员,而非静态内部类则不能有静态成员。
  2. 静态内部类的非静态成员可以访问外部类的静态变量,而不可访问外部类的非静态变量;
  3. 非静态内部类的非静态成员可以访问外部类的非静态变量。

为什么用静态内部类?

例如,Java的LinkedList实现包含private static class Node<E>

LinkedList是一个很好的例子,说明了为什么会有一个static嵌套类:LinkedList需要多个LinkedList.Node实例,并且不需要那些实例来引用列表本身。那将是毫无意义的开销。因此,Node类为static,以便不使这些列表反向引用。它也是private,因为它仅供LinkedList内部使用。


喵呜面试助手: 一站式解决面试问题,你可以搜索微信小程序 [喵呜面试助手] 或关注 [喵呜刷题] -> 面试助手 免费刷题。如有好的面试知识或技巧期待您的共享!