likes
comments
collection
share

java内部类

作者站长头像
站长
· 阅读数 29

从种类上说,内部类可以分为四类:普通内部类、静态内部类、匿名内部类、局部内部类。我们来一个个看:

普通内部类

这个是最常见的内部类之一了,其定义也很简单,在一个类里面作为类的一个字段直接定义就可以了,例:

public class InnerClassTest {

    public class InnerClassA {

    }
}

在这里 InnerClassA 类为 InnerClassTest 类的普通内部类,在这种定义方式下,普通内部类对象依赖外部类对象而存在,即在创建一个普通内部类对象时首先需要创建其外部类对象,我们在创建上面代码中的 InnerClassA 对象时先要创建 InnerClassTest 对象,例:

public class InnerClassTest {

    public int field1 = 1;
    protected int field2 = 2;
    int field3 = 3;
    private int field4 = 4;

    public InnerClassTest() {
        // 在外部类对象内部,直接通过 new InnerClass(); 创建内部类对象
        InnerClassA innerObj = new InnerClassA();
        System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
        System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1);
        System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2);
        System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3);
        System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4);
    }

    public class InnerClassA {
        public int field1 = 1;
        protected int field2 = 2;
        int field3 = 3;
        private int field4 = 4;
//        static int field5 = 5; // 编译错误!普通内部类中不能定义 static 属性

        public InnerClassA() {
            System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
            System.out.println("其外部类的 field1 字段的值为: " + field1);
            System.out.println("其外部类的 field2 字段的值为: " + field2);
            System.out.println("其外部类的 field3 字段的值为: " + field3);
            System.out.println("其外部类的 field4 字段的值为: " + field4);
        }
    }

    public static void main(String[] args) {
        InnerClassTest outerObj = new InnerClassTest();
        // 不在外部类内部,使用:外部类对象. new 内部类构造器(); 的方式创建内部类对象
//        InnerClassA innerObj = outerObj.new InnerClassA();
    }
}

这里的内部类就像外部类声明的一个属性字段一样,因此其的对象时依附于外部类对象而存在的,我们来看一下结果:

java内部类

我们注意到,内部类对象可以访问外部类对象中所有访问权限的字段,同时,外部类对象也可以通过内部类的对象引用来访问内部类中定义的所有访问权限的字段,后面我们将从源码里面分析具体的原因。 

我们下面来看一下静态内部类。

静态内部类

我们知道,一个类的静态成员独立于这个类的任何一个对象存在,只要在具有访问权限的地方,我们就可以通过 类名.静态成员名 的形式来访问这个静态成员,同样的,静态内部类也是作为一个外部类的静态成员而存在,创建一个类的静态内部类对象不需要依赖其外部类对象。例:

public class InnerClassTest {
    public int field1 = 1;

    public InnerClassTest() {
        System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
        // 创建静态内部类对象
        StaticClass innerObj = new StaticClass();
        System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1);
        System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2);
        System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3);
        System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4);
    }

    static class StaticClass {

        public int field1 = 1;
        protected int field2 = 2;
        int field3 = 3;
        private int field4 = 4;
        // 静态内部类中可以定义 static 属性
        static int field5 = 5;

        public StaticClass() {
            System.out.println("创建 " + StaticClass.class.getSimpleName() + " 对象");
//            System.out.println("其外部类的 field1 字段的值为: " + field1); // 编译错误!!
        }
    }

    public static void main(String[] args) {
        // 无需依赖外部类对象,直接创建内部类对象
//        InnerClassTest.StaticClass staticClassObj = new InnerClassTest.StaticClass();
        InnerClassTest outerObj = new InnerClassTest();
    }
}

结果:

java内部类

可以看到,静态内部类就像外部类的一个静态成员一样,创建其对象无需依赖外部类对象(访问一个类的静态成员也无需依赖这个类的对象,因为它是独立于所有类的对象的)。但是于此同时,静态内部类中也无法访问外部类的非静态成员,因为外部类的非静态成员是属于每一个外部类对象的,而本身静态内部类就是独立外部类对象存在的,所以静态内部类不能访问外部类的非静态成员,而外部类依然可以访问静态内部类对象的所有访问权限的成员,这一点和普通内部类无异。

匿名内部类

匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了:

public class InnerClassTest {

    public int field1 = 1;
    protected int field2 = 2;
    int field3 = 3;
    private int field4 = 4;

    public InnerClassTest() {
        System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
    }
    // 自定义接口
    interface OnClickListener {
        void onClick(Object obj);
    }

    private void anonymousClassTest() {
        // 在这个过程中会新建一个匿名内部类对象,
        // 这个匿名内部类实现了 OnClickListener 接口并重写 onClick 方法
        OnClickListener clickListener = new OnClickListener() {
            // 可以在内部类中定义属性,但是只能在当前内部类中使用,
            // 无法在外部类中使用,因为外部类无法获取当前匿名内部类的类名,
            // 也就无法创建匿名内部类的对象
            int field = 1;

            @Override
            public void onClick(Object obj) {
                System.out.println("对象 " + obj + " 被点击");
                System.out.println("其外部类的 field1 字段的值为: " + field1);
                System.out.println("其外部类的 field2 字段的值为: " + field2);
                System.out.println("其外部类的 field3 字段的值为: " + field3);
                System.out.println("其外部类的 field4 字段的值为: " + field4);
            }
        };
        // new Object() 过程会新建一个匿名内部类,继承于 Object 类,
        // 并重写了 toString() 方法
        clickListener.onClick(new Object() {
            @Override
            public String toString() {
                return "obj1";
            }
        });
    }

    public static void main(String[] args) {
        InnerClassTest outObj = new InnerClassTest();
        outObj.anonymousClassTest();
    }
}

来看看结果:

java内部类

上面的代码中展示了常见的两种使用匿名内部类的情况: 

  1. 直接 new 一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象并赋值给前面的 OnClickListener 类型的引用; 
  2. new 一个已经存在的类 / 抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。

同样的,在匿名内部类中可以使用外部类的属性,但是外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。

局部内部类

局部内部类使用的比较少,其声明在一个方法体 / 一段代码块的内部,而且不在定义类的定义域之内便无法使用,其提供的功能使用匿名内部类都可以实现,而本身匿名内部类可以写得比它更简洁,因此局部内部类用的比较少。来看一个局部内部类的小例子:

public class InnerClassTest {

    public int field1 = 1;
    protected int field2 = 2;
    int field3 = 3;
    private int field4 = 4;

    public InnerClassTest() {
        System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
    }

    private void localInnerClassTest() {
        // 局部内部类 A,只能在当前方法中使用
        class A {
            // static int field = 1; // 编译错误!局部内部类中不能定义 static 字段
            public A() {
                System.out.println("创建 " + A.class.getSimpleName() + " 对象");
                System.out.println("其外部类的 field1 字段的值为: " + field1);
                System.out.println("其外部类的 field2 字段的值为: " + field2);
                System.out.println("其外部类的 field3 字段的值为: " + field3);
                System.out.println("其外部类的 field4 字段的值为: " + field4);
            }
        }
        A a = new A();
        if (true) {
            // 局部内部类 B,只能在当前代码块中使用
            class B {
                public B() {
                    System.out.println("创建 " + B.class.getSimpleName() + " 对象");
                    System.out.println("其外部类的 field1 字段的值为: " + field1);
                    System.out.println("其外部类的 field2 字段的值为: " + field2);
                    System.out.println("其外部类的 field3 字段的值为: " + field3);
                    System.out.println("其外部类的 field4 字段的值为: " + field4);
                }
            }
            B b = new B();
        }
//        B b1 = new B(); // 编译错误!不在类 B 的定义域内,找不到类 B,
    }

    public static void main(String[] args) {
        InnerClassTest outObj = new InnerClassTest();
        outObj.localInnerClassTest();
    }
}

同样的,在局部内部类里面可以访问外部类对象的所有访问权限的字段,而外部类却不能访问局部内部类中定义的字段,因为局部内部类的定义只在其特定的方法体 / 代码块中有效,一旦出了这个定义域,那么其定义就失效了,就像代码注释中描述的那样,即外部类不能获取局部内部类的对象,因而无法访问局部内部类的字段。最后看看运行结果:

java内部类

内部类的嵌套

  • 内部类的嵌套,即为内部类中再定义内部类,这个问题从内部类的分类角度去考虑比较合适: 
  • 普通内部类:在这里我们可以把它看成一个外部类的普通成员方法,在其内部可以定义普通内部类(嵌套的普通内部类),但是无法定义 static 修饰的内部类,就像你无法在成员方法中定义 static 类型的变量一样,当然也可以定义匿名内部类和局部内部类;
  • 静态内部类:因为这个类独立于外部类对象而存在,我们完全可以将其拿出来,去掉修饰它的 static 关键字,他就是一个完整的类,因此在静态内部类内部可以定义普通内部类,也可以定义静态内部类,同时也可以定义 static 成员;
  • 匿名内部类:和普通内部类一样,定义的普通内部类只能在这个匿名内部类中使用,定义的局部内部类只能在对应定义域内使用;
  • 局部内部类:和匿名内部类一样,但是嵌套定义的内部类只能在对应定义域内使用。

深入理解内部类

不知道小伙伴们对上面的代码有没有产生疑惑:非静态内部类可以访问外部类所有访问权限修饰的字段(即包括了 private 权限的),同时,外部类也可以访问内部类的所有访问权限修饰的字段。而我们知道,private 权限的字段只能被当前类本身访问。然而在上面我们确实在代码中直接访问了对应外部类 / 内部类的 private 权限的字段,要解除这个疑惑,只能从编译出来的类下手了,为了简便,这里采用下面的代码进行测试:


/**
 * @author lz
 */
public class InnerClassTest3 {
    int field1; // 默认访问权限  
    private int field2;

    class InnerClassA {
        int field1; // 默认访问权限  
        private int field2;

        void accessOuterFields() {
            // 访问外部类的字段  
            System.out.println(InnerClassTest3.this.field1);
            System.out.println(InnerClassTest3.this.getField2()); // 
            // 假设有一个getter方法  

            // 注意:我们不能直接访问外部类的private字段 field2  
            // System.out.println(InnerClassTest.this.field2); // 这会编译错误  

            // 访问内部类的字段  
            System.out.println(this.field1);
            // System.out.println(this.field2); // 这可以在内部类的方法中访问,但仅限于内部类的方法  
        }

        // 假设有一个getter方法来访问private字段field2  
        public int getField2() {
            return field2;
        }
    }

    // 假设的getter方法  
    public int getField2() {
        return field2;
    }

    public InnerClassTest3() {
        InnerClassA inner = new InnerClassA();
        // 注意:这里不能直接访问内部类的private字段field2  
        // int value = inner.field2; // 这会编译错误  

        // 但如果我们有getter方法,可以这样做  
        int value = inner.getField2(); // 假设InternalClassA有一个public的getField2方法  

        // ... 其他代码  
    }

    public static void main(String[] args) {
        InnerClassTest3 test = new InnerClassTest3();
        // ... 其他代码  
    }
}

我在外部类中定义了一个默认访问权限(同一个包内的类可以访问)的字段 field1, 和一个 private 权限的字段 field2 ,并且定义了一个内部类 InnerClassA ,并且在这个内部类中也同样定义了两个和外部类中定义的相同修饰权限的字段,并且访问了外部类对应的字段。最后在外部类的构造方法中我定义了一个方法内变量赋值为内部类中 private 权限的字段。我们用 javac 命令(javac InnerClassTest.java)编译这个 .java 文件,会得到两个 .classs 文件。InnerClassTest.class 和 InnerClassTestInnerClassA.class,我们再用javap−c命令(javap−cInnerClassTest和javap−cInnerClassTestInnerClassA.class,我们再用 javap -c 命令(javap -c InnerClassTest 和 javap -c InnerClassTestInnerClassA.class,我们再用javapc命令(javapcInnerClassTestjavapcInnerClassTestInnerClassA)分别反编译这两个 .class 文件,InnerClassTest.class 的字节码如下:

java内部类

在这段代码中,内部类InnerClassA可以访问外部类com.lz.springbootconfig.InnerClassTest3的私有字段field1

在构造方法com.lz.springbootconfig.InnerClassTest3中,通过new一个com/lz/springbootconfig/InnerClassTest3$InnerClassA 创建了内部类InnerClassA的实例。在创建内部类实例时,会将外部类对象作为参数传递给内部类的构造方法。

因此,内部类InnerClassA的构造方法com/lz/springbootconfig/InnerClassTest3$InnerClassA."<init>":(Lcom/lz/springbootconfig/InnerClassTest3;)V可以访问外部类对象的所有成员,包括私有字段field1。内部类可以通过外部类对象来访问外部类的私有字段,就像访问自己的字段一样。

通过这种方式,内部类对象可以获取并操作外部类的私有字段。这是Java语言中内部类的一个特性,可以实现更紧密的类之间的交互。 

上面我们只是对普通内部类进行了分析,但其实匿名内部类和局部内部类的原理和普通内部类是类似的,只是在访问上有些不同:外部类无法访问匿名内部类和局部内部类对象的字段,因为外部类根本就不知道匿名内部类 / 局部内部类的类型信息(匿名内部类的类名被隐匿,局部内部类只能在定义域内使用)。但是匿名内部类和局部内部类却可以访问外部类的私有成员,原理也是通过外部类提供的静态方法来得到对应外部类对象的私有成员的值。而对于静态内部类来说,因为其实独立于外部类对象而存在,因此编译器不会为静态内部类对象提供外部类对象的引用,因为静态内部类对象的创建根本不需要外部类对象支持。但是外部类对象还是可以访问静态内部类对象的私有成员,因为外部类可以知道静态内部类的类型信息,即可以得到静态内部类的对象,那么就可以通过静态内部类提供的静态方法来获得对应的私有成员值。来看一个简单的代码证明:

public class InnerClassTest {

    int field1 = 1;
    private int field2 = 2;

    public InnerClassTest() {
        InnerClassA inner = new InnerClassA();
        int v = inner.x2;
    }

    // 这里改成了静态内部类,因而不能访问外部类的非静态成员
    public static class InnerClassA {
        private int x2 = 0;
    }
}

同样的编译步骤,得到了两个 .class 文件,这里看一下内部类的 .class 文件反编译的字节码 InnerClassTest$InnerClassA:

java内部类

java内部类 这段代码是内部类com.lz.springbootconfig.InnerClassTest3$InnerClassA的定义,它是外部类com.lz.springbootconfig.InnerClassTest3的一个内部类。下面对代码进行解析:

  1. 内部类com.lz.springbootconfig.InnerClassTest3$InnerClassA

    • 有一个成员变量field1,类型为int。
    • 有一个final字段this$0,类型为外部类com.lz.springbootconfig.InnerClassTest3的引用。
    • 有一个构造方法com.lz.springbootconfig.InnerClassTest3$InnerClassA(com.lz.springbootconfig.InnerClassTest3),接收一个外部类对象作为参数,并将其存储在this$0字段中。
    • 有一个方法accessOuterFields,用于访问外部类的字段。
    • 有一个公共方法getField2,返回类型为int。该方法获取并返回内部类自己的字段field2的值。
  2. 构造方法com.lz.springbootconfig.InnerClassTest3$InnerClassA(com.lz.springbootconfig.InnerClassTest3)

    • 在构造方法中,首先将外部类对象存储在this$0字段中。
    • 然后调用了父类java/lang/Object的构造方法。
  3. 方法accessOuterFields

    • 首先通过this$0字段获取外部类对象。
    • 使用外部类对象的引用,使用.操作符访问外部类对象的私有字段field1和公共方法getField2
    • 使用java/io/PrintStream.println方法将获取的字段值打印至控制台。
  4. 公共方法getField2

    • 直接返回内部类自己的字段field2的值。

这段代码展示了内部类可以访问外部类的私有字段和方法。通过内部类的特殊语法和生成的this$0字段,内部类可以直接访问外部类的成员。这是Java内部类的一个特性,有助于实现更紧密的类之间的交互。

OK,到这里问题都得到了解释:在非静态内部类访问外部类私有成员 / 外部类访问内部类私有成员 的时候,对应的外部类 / 外部类会生成一个静态方法,用来返回对应私有成员的值,而对应外部类对象 / 内部类对象通过调用其内部类 / 外部类提供的静态方法来获取对应的私有成员的值。

内部类和多重继承

我们已经知道,Java 中的类不允许多重继承,也就是说 Java 中的类只能有一个直接父类,而 Java 本身提供了内部类的机制,这是否可以在一定程度上弥补 Java 不允许多重继承的缺陷呢?我们这样来思考这个问题:假设我们有三个基类分别为 A、B、C,我们希望有一个类 D 达成这样的功能:通过这个 D 类的对象,可以同时产生 A 、B 、C 类的对象,通过刚刚的内部类的介绍,我们也应该想到了怎么完成这个需求了,创建一个类 D.java:

class A {}

class B {}

class C {}

public class D extends A {

    // 内部类,继承 B 类
    class InnerClassB extends B {
    }

    // 内部类,继承 C 类
    class InnerClassC extends C {
    }

    // 生成一个 B 类对象
    public B makeB() {
        return new InnerClassB();
    }

    // 生成一个 C 类对象
    public C makeC() {
        return new InnerClassC();
    }

    public static void testA(A a) {
        // ...
    }

    public static void testB(B b) {
        // ...
    }

    public static void testC(C c) {
        // ...
    }

    public static void main(String[] args) {
        D d = new D();
        testA(d);
        testB(d.makeB());
        testC(d.makeC());
    }
}

内部类在Java中提供了一种多重继承的模拟方式,因为内部类可以访问外部类的所有成员(包括私有成员),而外部类也可以访问内部类的所有成员(如果它们是公共的或受保护的)。然而,这种机制并不是没有代价的,它确实可能带来一些设计和维护上的挑战。

关于内部类破坏类结构的问题,确实是一个需要注意的点。内部类通常用于封装那些与外部类紧密相关的逻辑,这些逻辑可能不适合作为外部类的一部分,但也不是完全独立的。如果过度使用内部类,可能会使得类的结构变得复杂和难以理解。

一般来说,建议将类和它们的依赖项清晰地分开,除非它们之间有非常明确的依赖关系。例如,如果一个类只是为了辅助另一个类而存在,并且这种辅助关系是紧密的、私有的,那么使用内部类可能是合适的。但是,如果两个类之间的关系是松散的,或者它们可以被独立地使用和测试,那么将它们分开写可能会更好。

此外,将类分开写还可以提高代码的可读性和可维护性。当类被分开时,每个类都可以专注于自己的职责,这使得代码更容易被理解和修改。同时,这也使得代码更容易被测试和重用。

最后,需要注意的是,内部类并不是Java中唯一一种实现多重继承的机制。接口和组合(即一个类使用另一个类的对象作为其成员)也可以用来模拟多重继承。这些机制各有优缺点,具体使用哪种机制取决于具体的需求和场景。

转载自:https://juejin.cn/post/7378512100186177588
评论
请登录