>  기사  >  Java  >  Java 내부 클래스에 대한 자세한 설명

Java 내부 클래스에 대한 자세한 설명

黄舟
黄舟원래의
2017-03-01 13:08:591992검색


java内部类是指一个类定义在另一个类的内部,其中的方法可以访问包含他们外部类的域。这是一种比较复杂的技术,内部类的主要用于设计那些有协作性关系的类之间。特别是在java处理GUI事件中得到了广泛的应用。除此之外,内部类最常用的原因有以下几点:

  1. 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据

  2. 内部类可以对同一个包中的其他类隐藏起来

  3. 用匿名内部类来定义回调函数会方便很多

  4. 内部类能独立地继承一个(接口的)实现,无论外围类是否已经继承了某个(接口的)实现,对于内部类没有影响。


(一)  简单的内部类例子

内部类是一个非常复杂的功能。千里之行,始于足下,在彻底研究透彻之前,我们不妨从一个最简单的例子开始看起。

package Inner;/**
 * 
 * @author QuinnNorris
 * 外围类ExampleInner类,其中包含一个内部类InnerClass
 */public class ExampleInner {

    private int interval;    
    private boolean beep;//一个在ExampleInner内的变量,在内部类InnerClass中,我们会用到它

    public ExampleInner(int interval, boolean beep) {        
    this.interval=interval;        
    this.beep=beep;
    }//构造器

    public void start() {
        InnerClass ic = new InnerClass();
    }//start方法创建内部类对象

    //内部类
    public class InnerClass {
        public void action() {            if (beep)
                System.out.println("beep is true!");
        }
    }
}

需要注意,这里的InnerClass位于ExampleInner内部,但这并不意味着每个ExampleInner对象都有一个InnerClass实例域。

(二)  内部类与外围类

在这里,我们把包含内部类的类暂且先叫做外围类。

在上面的代码中,内部类InnerClass中的方法中有一个beep变量,但令人惊讶的是,在InnerClass中并没有这个beep变量,之所以这段代码没有问题,是因为内部类自动的引用了外围类中的这个beep变量。内部类是怎样做到这一点的呢?

1.内部类维护隐式引用和外围类相联系

事实上,内部类的对象会自动的维护一个隐式引用,这个引用指向了创建它的外围类对象。这个引用的语法表达式为:

OuterClass.this //内部类的隐式引用,指向外围类,在上述例子中为:ExampleInner.this

也就是说,我们内部类中的action方法可以这样来编写:

public void action() {    
if (ExampleInner.this.beep)//完全写法,隐式引用OuterClass.this可以省略
        System.out.println("beep is true!");
}

通过这个隐式指针,内部类获得了比普通类更大的访问权限——它可以访问外围类中的私有域。

2.编译器自动生成内部类构造器,存放隐式引用

这个隐式引用是在构造器中设置的,编译器修改了所有内部类的构造器,添加了一个外围类的引用参数。比如,我们上面的InnerClass类没有构造器,编译器为其生成了一个默认构造器:

public InnerClass(ExampleInner ei){
    ExampleClass.this = ei; //将外围类的一个对象赋给隐式引用}

当我们要实例化一个内部类时我们默认的省略了外围类的引用,这个引用通常是this引用。

public void start() {
    InnerClass ic = this.new InnerClass();
    //this引用指示了创建的内部类的外围类,通常省略}
    //start方法创建内部类对象

3.外围类作用域外,通过外围类对象构造内部类实例

通常情况下,this引用是多余的。正如我们上面说过,InnerClass位于ExampleInner内部,但这并不意味着每个ExampleClass对象都有一个InnerClass实例域。在内部类是公有的情况下,我们可以通过显示的命名将外围类引用设置为某个ExampleInner对象,这样一来我们可以在外围类的外部随意的实例化这两个类对象来使用。
在外围类作用域之外,引用内部类语法:

OuterClass.InnerClass object = object.new InnerClass();

我们在外围类之外用OuterClass.InnerClass的方法来表示内部类的类名。与此同时,我们需要用一个已经实例化好的外围类对象object来进行 .new InnerClass( )的操作,此时就不能用this来表示外围类了,而且这个外围类对象也不能省略,否则报错。


下面我们来看一下在测试类中我们创建这两个类实例的情况。

package Inner;/**
 * 
 * @author QuinnNorris
 * 在ExampleInner类的外部,我们通过如下方式将外围类和内部类实例化。
 */public class Test {  

    public static void main(String[] args) {

    ExampleInner ei = new ExampleInner(10,true);//实例化外围类

    //OuterClass.InnerClass object = object.new InnerClass

    ExampleInner.InnerClass eiic = ei.new InnerClass();    //ok 通过上述的特殊语法,我们在外围类的外部,成功的实例化了内部类

    ExampleInner.InnerClass eiic = new InnerClass();    //error 此时如果不用外围类对象去.new InnerClass()会报错,即使直接引内部类的包也不行 

    eiic.action();//ok 调用内部类的方法,没问题
    }  

}

(三)  虚拟机中的内部类(语法糖)

当在java1.1的java语言中增加内部类时,很多程序员都认为这将是一项很主要的新特性,但内部类的语法如此麻烦,以至于违背了java要比c++更加简单的设计理念。尽管如此,虽然内部类很复杂,但它其实只是一种语法糖,虚拟机对内部类一无所知,那么虚拟机中是如何处理内部类的呢?

1.内部类反编译

为了能够得到虚拟机中内部类实际的情况,我们使用了javap将内部类的源代码进行反编译,结果如下:

public ExampleInner$InnerClass{    
public ExampleInner$InnerClass(ExampleInner);//构造器

    public void action();    
    final ExampleInner this$0;

}

可见,虚拟机中将内部类解释为:外围类名+$+内部类名。(这里有个基本知识,java是可以使用$来作为变量名的一部分的,甚至是变量名的开始,但是并不推荐)前两个函数还是很好理解的,第一个构造器正好印证了我们上面说的自动生成的内容,第二个是内部类中的函数。第三个this$0是一种合成方法,编译器为了引用外围类,生成了一个附加的实例域this$0,(这个名字是编译器合成的,在自己的编写代码中不能引用它,而且这个名字是随机的)。

2.创建获取外围类的私有域的静态方法

不仅仅将内部类反编译,我们将外围类也反编译之后会发现,除了其他一样的代码外,外围类会多出一个静态方法:

static boolean access$0(ExampleInner);

这个静态方法的功能是,将传入的外围类对象中的beep变量值返回给内部类。我们上面知道,内部类中需要用到的私有的beep变量,这个方法正是编译器生成的一个合成方法。如果内部类中需要用到其他的私有域变量,编译器会继续的生成这样的合成方法来返回那些私有域的值。这也正是,为什么内部类可以访问外围类的私有域的原因。

(四)  局部内部类

如果内部类只在一个方法中被使用,其他的地方无需这个内部类,我们可以把它定义在这个方法中,我们把这种定义在一个方法中(或定义在一个作用域中)的内部类叫做局部内部类。

1.局部内部类作用域

局部内部类不能用public或private来声明,它的作用域被限定在声明这个局部类的块中,也正因此,局部类对于除了这个方法之外的外部世界可以完全的隐藏起来,除了这个方法,没人知道有这样一个类的存在。

package Inner;/**
 * 
 * @author QuinnNorris
 * 局部内部类,只对饱含着他的方法或作用域可见
 */public class LocalInner {

    private int interval;    
    private boolean beep;    
    public LocalInner(int interval, boolean beep) {        
    this.interval=interval;        
    this.beep=beep;
    }//构造器

    public void start() {        //局部内部类,只对start方法可见
        class InnerClass {            
        public void action() {                
        if (beep)
                    System.out.println("beep is true!");
            }
        }
        InnerClass ic = new InnerClass();
    }

}

2.局部内部类访问final变量

相比较其他类型的内部类,局部内部类还有一个显著的优点。它不仅可以访问包含着它的外围类,还可以访问在方法、作用域中的局部变量,但是前提是这些变量要用final修饰。这个特点看似天经地义,毕竟内部类在方法中,为什么不能访问方法的局部变量呢?不妨看一下下面的例子:

在有些情况下,在方法执行过程中,方法中的一条语句将局部内部类作为一个参数传递给其他的方法,而后方法结束。在其他的方法调用内部类时,内部类中原方法的局部变量已经随着方法的结束被释放,这个时候无法找到局部变量。

正是这个原因,才让我们明白一个局部内部类访问作用域中的局部变量是需要处理的,不是天经地义的。
编译器会为那些在局部内部类中要用到的局部变量做备份,也正是做备份的原因,导致局部变量需要用final修饰,如果不是这样,很有可能局部变量后来被修改,导致和备份的内容不一样从而出错。

(五)  匿名内部类

在局部内部类的基础上再深入一步,如果我们只需要用到这个类一次,那么我们只需要创建一个对象就好,不用命名了。这种不命名的内部类被称作匿名内部类。

1.表示匿名内部类

那么问题来了,如果我们不去命名一个内部类,我们怎么才能知道我们创建的是什么东西呢?解决的方法是,我们用实现一个接口或者扩展一个超类来表示我们正在编写的内部类。

package Inner;/**
 * 
 * @author QuinnNorris
 * 匿名内部类,实现了CallBack接口
 */public class AnonymousInner {

    private int interval;    
    private boolean beep;    
    public AnonymousInner(int interval, boolean beep) {        
    this.interval=interval;        
    this.beep=beep;
    }//构造器

    public void start() {        
    //匿名内部类,我们只知道它是一个实现了CallBack的子类,因为匿名,他没有自己的名字。
        //CallBack接口要存在,我们不可空穴来风的创建匿名类。
        CallBack cb = new CallBack(){            
        public void solve(String result){
                result="result";
            }
        };
    }
}

2.匿名内部类的构造器

需要注意的是,匿名内部类没有构造器。这是理所当然的,因为它本身连名字都没有,它只能使用父类的构造方法,如果匿名内部类实现的是接口那么更简单,只需要在后面跟上一对空的圆括号即可,就像我们刚才例子中做的那样。如果是继承父类,则需要使用父类构造器。

3.匿名内部类与局部内部类

因为匿名内部类也存在于方法之中,所以,局部内部类的final理论也适用于匿名内部类。

匿名类在处理一些代码较短、事件较为简单的内容时具有很大的优势,更切实际,也更易于理解。现在很多很火的技术中,这种匿名机制也已经屡见不鲜了。

(六)  静态内部类(嵌套内部类)

有的时候,其实我们用内部类只是为了把一个类隐藏在另外一个类内部,甚至不需要这两个类之间的联系。那么这个时候可以把内部类声明为static,取消两个类产生的引用,这就是静态内部类(也叫嵌套内部类)。当我们把什么东西和静态的联系到一起时,还是那套理论,这个东西和对象无关,纯粹变成了类的产物。那么静态内部类的特点也很好推断:

  1. 静态内部类不再有对外围类的引用特权

  2. 静态内部类能够使用外围类的静态变量,能使用同时也能被外围类的静态方法使用

  3. 只有内部类可以被声明为静态类

  4. 如果需要在外围类之外使用静态内部类,可以用OuterClass.InnerClass的方法

  5. 静态内部类一般设置为public而不是private,便于调用

(七)  总结

如果你喜欢,内部类一共可以被分为四种:成员内部类(我们一开始举例的最普通的内部类),局部内部类,匿名内部类,静态内部类。这四种各有各的特点,在运用的时候要综合考虑。除此之外,我们还一起研究了在虚拟机中内部类的实现情况,这是很有必要的,懂得它的原理才能在使用时判断的更准,在出错时直击要害。

java内部类是指一个类定义在另一个类的内部,其中的方法可以访问包含他们外部类的域。这是一种比较复杂的技术,内部类的主要用于设计那些有协作性关系的类之间。特别是在java处理GUI事件中得到了广泛的应用。除此之外,内部类最常用的原因有以下几点:

  1. 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据

  2. 内部类可以对同一个包中的其他类隐藏起来

  3. 用匿名内部类来定义回调函数会方便很多

  4. 内部类能独立地继承一个(接口的)实现,无论外围类是否已经继承了某个(接口的)实现,对于内部类没有影响。


(一)  简单的内部类例子

内部类是一个非常复杂的功能。千里之行,始于足下,在彻底研究透彻之前,我们不妨从一个最简单的例子开始看起。

package Inner;/**
 * 
 * @author QuinnNorris
 * 外围类ExampleInner类,其中包含一个内部类InnerClass
 */public class ExampleInner {

    private int interval;    private boolean beep;//一个在ExampleInner内的变量,在内部类InnerClass中,我们会用到它

    public ExampleInner(int interval, boolean beep) {        this.interval=interval;        this.beep=beep;
    }//构造器

    public void start() {
        InnerClass ic = new InnerClass();
    }//start方法创建内部类对象

    //内部类
    public class InnerClass {
        public void action() {            if (beep)
                System.out.println("beep is true!");
        }
    }
}

需要注意,这里的InnerClass位于ExampleInner内部,但这并不意味着每个ExampleInner对象都有一个InnerClass实例域。

(二)  内部类与外围类

在这里,我们把包含内部类的类暂且先叫做外围类。

在上面的代码中,内部类InnerClass中的方法中有一个beep变量,但令人惊讶的是,在InnerClass中并没有这个beep变量,之所以这段代码没有问题,是因为内部类自动的引用了外围类中的这个beep变量。内部类是怎样做到这一点的呢?

1.内部类维护隐式引用和外围类相联系

事实上,内部类的对象会自动的维护一个隐式引用,这个引用指向了创建它的外围类对象。这个引用的语法表达式为:

OuterClass.this //内部类的隐式引用,指向外围类,在上述例子中为:ExampleInner.this

也就是说,我们内部类中的action方法可以这样来编写:

public void action() {    if (ExampleInner.this.beep)//完全写法,隐式引用OuterClass.this可以省略
        System.out.println("beep is true!");
}

通过这个隐式指针,内部类获得了比普通类更大的访问权限——它可以访问外围类中的私有域。

2.编译器自动生成内部类构造器,存放隐式引用

这个隐式引用是在构造器中设置的,编译器修改了所有内部类的构造器,添加了一个外围类的引用参数。比如,我们上面的InnerClass类没有构造器,编译器为其生成了一个默认构造器:

public InnerClass(ExampleInner ei){
    ExampleClass.this = ei; //将外围类的一个对象赋给隐式引用}

当我们要实例化一个内部类时我们默认的省略了外围类的引用,这个引用通常是this引用。

public void start() {
    InnerClass ic = this.new InnerClass();//this引用指示了创建的内部类的外围类,通常省略}//start方法创建内部类对象

3.外围类作用域外,通过外围类对象构造内部类实例

通常情况下,this引用是多余的。正如我们上面说过,InnerClass位于ExampleInner内部,但这并不意味着每个ExampleClass对象都有一个InnerClass实例域。在内部类是公有的情况下,我们可以通过显示的命名将外围类引用设置为某个ExampleInner对象,这样一来我们可以在外围类的外部随意的实例化这两个类对象来使用。
在外围类作用域之外,引用内部类语法:

OuterClass.InnerClass object = object.new InnerClass();

我们在外围类之外用OuterClass.InnerClass的方法来表示内部类的类名。与此同时,我们需要用一个已经实例化好的外围类对象object来进行 .new InnerClass( )的操作,此时就不能用this来表示外围类了,而且这个外围类对象也不能省略,否则报错。


下面我们来看一下在测试类中我们创建这两个类实例的情况。

package Inner;/**
 * 
 * @author QuinnNorris
 * 在ExampleInner类的外部,我们通过如下方式将外围类和内部类实例化。
 */public class Test {  

    public static void main(String[] args) {

    ExampleInner ei = new ExampleInner(10,true);//实例化外围类

    //OuterClass.InnerClass object = object.new InnerClass

    ExampleInner.InnerClass eiic = ei.new InnerClass();    //ok 通过上述的特殊语法,我们在外围类的外部,成功的实例化了内部类

    ExampleInner.InnerClass eiic = new InnerClass();    //error 此时如果不用外围类对象去.new InnerClass()会报错,即使直接引内部类的包也不行 

    eiic.action();//ok 调用内部类的方法,没问题
    }  

}

(三)  虚拟机中的内部类(语法糖)

当在java1.1的java语言中增加内部类时,很多程序员都认为这将是一项很主要的新特性,但内部类的语法如此麻烦,以至于违背了java要比c++更加简单的设计理念。尽管如此,虽然内部类很复杂,但它其实只是一种语法糖,虚拟机对内部类一无所知,那么虚拟机中是如何处理内部类的呢?

糖衣语法(语法糖)传送门:http://www.php.cn/

1.内部类反编译

为了能够得到虚拟机中内部类实际的情况,我们使用了javap将内部类的源代码进行反编译,结果如下:

public ExampleInner$InnerClass{    public ExampleInner$InnerClass(ExampleInner);//构造器

    public void action();    final ExampleInner this$0;

}

可见,虚拟机中将内部类解释为:外围类名+$+内部类名。(这里有个基本知识,java是可以使用$来作为变量名的一部分的,甚至是变量名的开始,但是并不推荐)前两个函数还是很好理解的,第一个构造器正好印证了我们上面说的自动生成的内容,第二个是内部类中的函数。第三个this$0是一种合成方法,编译器为了引用外围类,生成了一个附加的实例域this$0,(这个名字是编译器合成的,在自己的编写代码中不能引用它,而且这个名字是随机的)。

2.创建获取外围类的私有域的静态方法

不仅仅将内部类反编译,我们将外围类也反编译之后会发现,除了其他一样的代码外,外围类会多出一个静态方法:

static boolean access$0(ExampleInner);

这个静态方法的功能是,将传入的外围类对象中的beep变量值返回给内部类。我们上面知道,内部类中需要用到的私有的beep变量,这个方法正是编译器生成的一个合成方法。如果内部类中需要用到其他的私有域变量,编译器会继续的生成这样的合成方法来返回那些私有域的值。这也正是,为什么内部类可以访问外围类的私有域的原因。

(四)  局部内部类

如果内部类只在一个方法中被使用,其他的地方无需这个内部类,我们可以把它定义在这个方法中,我们把这种定义在一个方法中(或定义在一个作用域中)的内部类叫做局部内部类。

1.局部内部类作用域

局部内部类不能用public或private来声明,它的作用域被限定在声明这个局部类的块中,也正因此,局部类对于除了这个方法之外的外部世界可以完全的隐藏起来,除了这个方法,没人知道有这样一个类的存在。

package Inner;/**
 * 
 * @author QuinnNorris
 * 局部内部类,只对饱含着他的方法或作用域可见
 */public class LocalInner {

    private int interval;    private boolean beep;    public LocalInner(int interval, boolean beep) {        this.interval=interval;        this.beep=beep;
    }//构造器

    public void start() {        //局部内部类,只对start方法可见
        class InnerClass {            public void action() {                if (beep)
                    System.out.println("beep is true!");
            }
        }
        InnerClass ic = new InnerClass();
    }

}

2.局部内部类访问final变量

相比较其他类型的内部类,局部内部类还有一个显著的优点。它不仅可以访问包含着它的外围类,还可以访问在方法、作用域中的局部变量,但是前提是这些变量要用final修饰。这个特点看似天经地义,毕竟内部类在方法中,为什么不能访问方法的局部变量呢?不妨看一下下面的例子:

在有些情况下,在方法执行过程中,方法中的一条语句将局部内部类作为一个参数传递给其他的方法,而后方法结束。在其他的方法调用内部类时,内部类中原方法的局部变量已经随着方法的结束被释放,这个时候无法找到局部变量。

正是这个原因,才让我们明白一个局部内部类访问作用域中的局部变量是需要处理的,不是天经地义的。
编译器会为那些在局部内部类中要用到的局部变量做备份,也正是做备份的原因,导致局部变量需要用final修饰,如果不是这样,很有可能局部变量后来被修改,导致和备份的内容不一样从而出错。

(五)  匿名内部类

在局部内部类的基础上再深入一步,如果我们只需要用到这个类一次,那么我们只需要创建一个对象就好,不用命名了。这种不命名的内部类被称作匿名内部类。

1.表示匿名内部类

那么问题来了,如果我们不去命名一个内部类,我们怎么才能知道我们创建的是什么东西呢?解决的方法是,我们用实现一个接口或者扩展一个超类来表示我们正在编写的内部类。

package Inner;/**
 * 
 * @author QuinnNorris
 * 匿名内部类,实现了CallBack接口
 */public class AnonymousInner {

    private int interval;    private boolean beep;    public AnonymousInner(int interval, boolean beep) {        this.interval=interval;        this.beep=beep;
    }//构造器

    public void start() {        //匿名内部类,我们只知道它是一个实现了CallBack的子类,因为匿名,他没有自己的名字。
        //CallBack接口要存在,我们不可空穴来风的创建匿名类。
        CallBack cb = new CallBack(){            public void solve(String result){
                result="result";
            }
        };
    }
}

2.匿名内部类的构造器

需要注意的是,匿名内部类没有构造器。这是理所当然的,因为它本身连名字都没有,它只能使用父类的构造方法,如果匿名内部类实现的是接口那么更简单,只需要在后面跟上一对空的圆括号即可,就像我们刚才例子中做的那样。如果是继承父类,则需要使用父类构造器。

3.匿名内部类与局部内部类

因为匿名内部类也存在于方法之中,所以,局部内部类的final理论也适用于匿名内部类。

匿名类在处理一些代码较短、事件较为简单的内容时具有很大的优势,更切实际,也更易于理解。现在很多很火的技术中,这种匿名机制也已经屡见不鲜了。

(6) 정적 내부 클래스(중첩된 내부 클래스)

사실 때때로 우리는 내부 클래스를 사용하여 한 클래스를 다른 클래스 안에 숨기는데, 이들 간의 연결조차 되지 않습니다. 두 개의 수업이 필요합니다. 그런 다음 이때 내부 클래스를 정적으로 선언하고 두 클래스에서 생성된 참조를 취소할 수 있습니다. 이는 정적 내부 클래스(중첩 내부 클래스라고도 함)입니다. 우리가 무언가를 정적인 것과 연관시킨다면 그것은 여전히 ​​이론일 뿐입니다. 이것은 객체와 아무 관련이 없으며 순전히 클래스의 산물이 됩니다. 그러면 정적 내부 클래스의 특성도 쉽게 추론할 수 있습니다.

  1. 정적 내부 클래스는 더 이상 외부 클래스에 대한 참조 권한이 없습니다

  2. 정적 내부 클래스는 외부 클래스의 정적 변수를 사용할 수 있고 외부 클래스의 정적 메서드에서도 사용할 수 있습니다.

  3. 내부 클래스만 정적 클래스로 선언할 수 있습니다

  4. 외부 클래스 외부에서 정적 내부 클래스를 사용해야 하는 경우 OuterClass.InnerClass의 메서드를 사용할 수 있습니다.

  5. 정적 내부 클래스는 일반적으로 공개 대신 공개로 설정됩니다. private for easy call

(7) 요약

원하시면 내부 클래스를 네 가지 유형으로 나눌 수 있습니다. 클래스(가장 일반적인 내부 클래스(처음에 예시), 로컬 내부 클래스, 익명 내부 클래스 및 정적 내부 클래스입니다. 이 네 가지 유형은 각각 고유한 특성을 갖고 있으므로 이를 사용할 때 종합적으로 고려해야 합니다. 또한 가상 머신에서 내부 클래스를 구현하는 방법도 연구했습니다. 이는 원리를 이해해야만 사용할 때 더 정확한 판단을 내리고 오류가 발생할 때 핵심 사항을 맞출 수 있습니다.

위 내용은 Java 내부 클래스에 대한 자세한 설명입니다. 더 많은 관련 내용은 PHP 중국어 홈페이지(www.php.cn)를 참고해주세요!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.