11

Java内部类

 3 years ago
source link: https://niyanchun.com/java-nested-class.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

基础

Java支持类中嵌套类,称之为nested class。嵌套的层数没有限制,但实际中一般最多用两层。根据内部类是否有static修饰,分为 static nested class non-static nested class 。non-static nested class又被称为 inner class 。inner class里面又有两个特殊一点的类: local class anonymous class 。特殊之处主要在于语法使用上,实质功能是差不多的。 官方 是这样解释的:

Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes.

用图表示分类及关系如下:

Mrymue.png!mobile

上面是按照官方的说法来分的,实际中很多人习惯把所有的嵌套类都称为inner class(内部类),这只是个称谓,不影响沟通就好。后文用nested class来统称static nested class和non-static nested class。各个类的主要特性和限制已经在图中说明了(适用于JDK 8及以后)。

那为什么需要内部类呢?内部类是在Java 1.1中引入的,当时很多人质疑该设计增加了Java的复杂性,但实用性不强,当然这种问题仁者见仁智者见智。官方的解释是这样的:

Why Use Nested Classes?

Compellingreasons for using nested classes include the following:

  • It is a way of logically grouping classes that are only used in one place : If a class is useful to only one other class, then it is logical to embed it in that class and keep the two together. Nesting such "helper classes" makes their package more streamlined.**
  • It increases encapsulation : Consider two top-level classes, A and B, where B needs access to members of A that would otherwise be declared private. By hiding class B within class A, A's members can be declared private and B can access them. In addition, B itself can be hidden from the outside world.
  • It can lead to more readable and maintainable code : Nesting small classes within top-level classes places the code closer to where it is used.

总的来说,嵌套类最大的目的是改善代码的组织,并不是必不可少的功能。嵌套类能实现的功能,通过正常的类都可以实现,只不过可能要多写点代码,且不是很优雅而已。

为什么inner class可以访问外层类的非静态成员

要说明的是: 嵌套类是对于编译器而言的,对虚拟机来说没有什么嵌套类,只有正常的类 。也就是说嵌套类经过编译器编译之后要转化成一个个正常的类,比如A类里面嵌套了B类。那经过编译器之后会变成两个独立的类: AA$B 。这样问题就很简单了,B类要访问A类的非静态成员,要满足两个条件:

  1. 要有A类的实例。
  2. 且要有访问成员的权限或者方式。

而编译器在编译期间就帮我们干了这两件事,下面验证一下。

定义一个嵌套类:

public class OuterClass {
    /**
    * 定义一个公有成员变量
    */
    public Object publicVariable = "public member variable";
    private Object privateVariable = "private member variable";
    /**
    * 定义两个私有成员变量
    */ 
    private Object privateVariable2 = "private member variable2";

    /**
    * 定义一个私有成员方法
    * @param parameter
    */
    private void privateMethod(String parameter) {
        System.out.println(parameter);
    }

    /**
    * 调用inner class
    */
    public void show() {
        InnerClass innerClass =  new InnerClass();
        innerClass.print();
    }

    /**
    * inner class
    */
    class InnerClass {
        void print() {
            // inner class里面直接调用了外部类的私有成员变量和成员方法
            System.out.println(privateVariable);
            privateMethod("invoke outer class private method.");
            // 调用外层类的公有变量
            System.out.println(publicVariable);
        }
    }

    public static void main(String[] args) {
        new OuterClass().show();
    }
}

上面代码定义了外层类是 OuterClass ,内部类是 InnerClass 。外层类定义了1个公有变量、2个私有变量和1个私有方法。然后在InnerClass里面直接使用了OuterClass的所有成员。程序运行结果如下:

private member variable
invoke outer class private method.
public member variable

反编译一下上面的两个类:

# javap -p OuterClass.class       
Compiled from "OuterClass.java"
public class OuterClass {
    public java.lang.Object publicVariable;
    private java.lang.Object privateVariable;
    private java.lang.Object privateVariable2;
    public OuterClass();
    private void privateMethod(java.lang.String);
    public void show();
    public static void main(java.lang.String[]);
    # 注意这两个静态方法
    static java.lang.Object access$000(OuterClass);
    static void access$100(OuterClass, java.lang.String);
}


# javap -p OuterClass\$InnerClass.class
Compiled from "OuterClass.java"
class OuterClass$InnerClass {
    # 注意这个final的成员变量和下面的构造函数
    final OuterClass this$0;
    OuterClass$InnerClass(OuterClass);
    void print();
}

结论就是:

  1. 编译器修改了内部类:

    • 增加了一个final的外层类实例作为内部类的成员变量;
    • 修改了内部类的构造函数,将外部类实例通过内部类的构造函数传递给内部类。这样内部类就有了外部类的实例,上面的第1个条件就达成了。
  2. 编译器在外部类中增加了几个非private的静态方法。对于内部类访问外部类的每一个私有成员,都会有这么一个方法。这样内部类就可以通过这些静态方法去访问外部类的私有成员了。非私有的成员直接通过1中的外层类实例即可访问,所以就无需生成这些静态方法了。

再进一步验证一下上面的结论1,当执行 InnerClass innerClass = new InnerClass(); 语句创建一个内部类实例之后,可以观测到下面的结果:

RZBRNfF.png!mobile

可以看到,内部类实例( OuterClass$InnerClass@470 )自动引用了外层类实例( OuterClass@464 )。所以, inner class之所以能访问外层类的成员是因为它在实例化的时候就已经和一个外层类的实例关联了 ,实际是通过这个外层类实例去访问外层类的成员。对于私有成员,还生成了一些辅助的静态方法。这也说明,要实例化inner class,必须先实例化它的外层类。

另外有个限制就是inner class里面不能定义静态变量和静态方法,一个例外是可以定义基础类型和String类型的静态常量。比如:

static final String s = “s”;        // OK
static final int i = 5;    // OK
static final Integer ii = 5;    // 错误

local class和anonymous class都属于特殊的inner class,所以上面讲述的所有东西对他们也适用。

为什么static nested class不能访问外层类的非静态成员

原因很简单,static nested class除了被定义到某个类里面以外,几乎和普通的类没有什么区别。 它不会和外层类的某个实例关联 ,比如我们在上面的OuterClass里面再定义一个 StaticNestedClass

static class StaticNestedClass {
    private int a;
    void foo() {}
}

反编译以后:

# javap -p OuterClass\$StaticNestedClass.class
Compiled from "OuterClass.java"
class OuterClass$StaticNestedClass {
    private int a;
    OuterClass$StaticNestedClass();
    void foo();
}

除了类名被改写了以外,和原来定义的类没有任何区别。所以如果没有被定义为private的话,static nested class完全可以独立于外层类使用。

所有nested class都可以访问外层类的静态成员

上面讨论的都是nested class能不能访问外层类的非静态成员,那如果是静态成员呢?结论就是所有nested class都可以访问的静态成员,不管是不是私有。原理的话和inner class访问外层类非static成员是一样的,如果是private的,编译器会在外层中生成一个辅助访问的static方法,如果是非私有的,那通过类就可以直接访问。

## 如果nested class是private的?

我们知道正常的类是不能使用private和protected的(只能是public或者不加访问修饰符),但nested class却可以,因为nested class其实就是外层类的一个特殊成员,就像成员变量、成员方法一样。比如,如果我们不想让外部的其它类看到nested class的类,就可以将它设置成private的,但对于外层类是没有影响的,照样可以操作这个类。这个怎么做到的呢?

我们将上面的StaticNestedClass改为private的:

private static class StaticNestedClass {
   void foo() {
       System.out.println(a);
   }
}

反编译看下:

# javap -p OuterClass\$StaticNestedClass.class
Compiled from "OuterClass.java"
class OuterClass$StaticNestedClass {
    private OuterClass$StaticNestedClass();
    void foo();
    OuterClass$StaticNestedClass(OuterClass$1);
}

可以看到,如果nested class被设置成private,它原来的构造函数就会被设置为private的,同时编译器又新增了一个外部可见的构造函数 OuterClass$StaticNestedClass(OuterClass$1) ,这个构造函数的一个入参就是外部类的实例。这样,外部类实例化nested class的时候会先调用这个构造函数,这个构造函数内部又调用了原始的private的构造函数。inner class也是这样的。

## 总结

嵌套类的实质就是外层类的成员,就像成员变量、成员方法一样,初衷是提高代码结构的紧凑和可维护性。 使用嵌套类的几乎唯一的场景就是这个内部类仅供外层类(或者包含它的代码块)使用,其它场景都应该使用正常的一级类 。按这个思路使用即可,不要滥用,更不要搞骚操作。

Reference


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK