Java内部类

大部分时候,我们把类定义成一个独立的程序单元。在某些情况下,我们把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类(有的地方也叫嵌套类),包含内部类的类也被称为外部类(有的地方也叫宿主类)。Java从JDK 1.1开始引入内部类,内部类主要有如下作用:

  • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。假设需要创建Cow类,Cow类需要组合一个CowLeg对象,CowLeg类只有在Cow类里才有效,离开了Cow类之后没有任何意义。在这种情况下,就可把CowLeg定义成Cow的内部类,不允许其他类访问CowLeg。
  • 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
  • 匿名内部类适合用于创建那些仅需要一次使用的类。对于前面介绍的命令模式,当需要传入一个Command对象时,重新专门定义PrintCommand和AddCommand两个实现类可能没有太大的意义,因为这两个实现类可能仅需要使用一次。在这种情况下,使用匿名内部类将更方便。

1.* 非静态内部类

定义内部类非常简单,只要把一个类放在另一个类内部定义即可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类被称为局部内部类)。内部类定义语法格式如下:

点击查看代码
public class OuterClass {
    //此处可以定义内部类
}

大部分时候,内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与Field、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。

成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。

前面经常看到同一个Java源文件里定义了多个类,那种情况不是内部类,它们依然是两个互相独立的类。例如下面程序:

点击查看代码
//下面A、B两个空类互相独立,没有谁是谁的内部类
class A {}
public class B {}

上面两个类定义虽然写在同一个源文件中,但它们互相独立,没有谁是谁的内部类这种关系。内部类一定是放在另一个类的类体部分(也就是类名后的花括号部分)定义。

因为内部类作为其外部类的成员,所以可以使用任意访问控制符如private、protected和public等修饰。

注意:外部类的上一级程序单元是包,所以它只有2个作用域:同一个包内和任何位置。因此只需2种访问权限:包访问权限和公开访问权限,正好对应省略访问控制符和public访问控制符。省略访问控制符是包访问权限,即同一包中的其他类可以访问省略访问控制符的成员。因此,如果一个外部类不使用任何访问控制符修饰,则只能被同一个包中其他类访问。而内部类的上一级程序单元是外部类,它就具有4个作用域:同一个类、同一个包、父子类和任何位置,因此可以使用4种访问控制权限。

下面程序在Cow类里定义了一个CowLeg非静态内部类,并在CowLeg类的实例方法中直接访问Cow的private访问权限的实例Field:

查看代码

上面程序中CowLeg部分是一个普通的类定义,但因为把这个类定义放在了另一个类的内部,所以它就成了一个内部类,可以使用private修饰符来修饰这个类。

外部类Cow里包含了一个test方法,该方法里创建了一个CowLeg对象,并调用该对象的info方法。读者不难发现,在外部类里使用非静态内部类时,与平时使用普通类并没有太大的区别。

编译上面程序,看到在文件所在路径生成了两个class文件,一个是Cow.class,另一个是Cow$CowLeg.class。

前者是外部类Cow的class文件,后者是内部类CowLeg的class文件,即成员内部类(包括静态内部类、非静态内部类)的class文件总是这种形式:OuterClass$InnerClass.class。

前面提到过,在非静态内部类里可以直接访问外部类的private成员,上面程序中”System.out.println(“本牛腿所在奶牛重:” + weight)”,就是在CowLeg类的方法内直接访问其外部类的private实例变量。这是因为在非静态内部类对象里,保存了一个它寄存的外部类对象的引用(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,而非静态内部类实例必须寄存在外部类实例里)。下图显示了上面程序运行时的内存示意图:

image

当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不存在,则到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果不存在,则到该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果依然不存在,系统将出现编译错误:提示找不到该变量。

因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用this、外部类类名.this作为限定来区分。如下程序所示:

查看代码

上面程序中[1]、[2]代码行分别访问外部类的实例变量、非静态内部类的实例变量。通过OutterClass.this.propName的形式访问外部类的实例Field,通过this.propName的形式访问非静态内部类的实例Field。

非静态内部类的成员可以访问外部类的private成员,但反过来就不成立了。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问非静态内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。下面程序示范了这个规则:

查看代码

程序中System.out.println(“内部类的inProp值:” + inProp)试图在外部类方法里访问非静态内部类的Field,这将引起编译错误。

外部类不允许访问非静态内部类的实例成员还有一个原因,上面程序中main方法的1号代码创建了一个外部类对象,并调用外部类对象的accessInnerProp方法。此时非静态内部类对象根本不存在,如果允许accessInnerProp方法访问非静态内部类对象,将肯定引起错误。

那么非静态内部类对象和外部类对象的关系是怎样的?

非静态内部类对象必须寄存在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄存其中。简单地说,如果存在一个非静态内部类对象,则一定存在一个被它寄存的外部类对象。但外部类对象存在时,外部类对象里不一定寄存了非静态内部类对象。因此外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在!而非静态内部类对象访问外部类成员时,外部类对象一定存在。

根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。如下程序所示:

查看代码

Java不允许在非静态内部类里定义静态成员。下面程序示范了非静态内部类里包含静态成员将引发编译错误:

查看代码

非静态内部类里不能有静态方法、静态Field、静态初始化块,所以上面三个静态声明都会引发错误。

注意:非静态内部类里不可以有静态初始化块,但可以包含普通初始化块。非静态内部类普通初始化块的作用与外部类初始化块的作用完全相同。

1.* 静态内部类

如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。

注意:static关键字的作用是把类的成员变成类相关,而不是实例相关,即static修饰的成员属于整个类,而不属于单个对象。外部类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关。因此static关键字不可修饰外部类,但可修饰内部类。

静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。下面程序就演示了这条规则:

查看代码

上面程序中[1]代码行定义了一个静态Field,因为这个静态Field处于类内部类中,所以完全没有问题。StaticInnerClass类里定义了一个accessOuterProp方法,这是一个实例方法,但依然不能访问外部类的prop1成员变量,因为这是实例变量;但可以访问prop2,因为它是静态Field。

为什么静态内部类的实例方法也不能访问外部类的实例属性呢?

因为静态内部类是外部类的类相关,而不是外部类的对象相关的。也就是说,静态内部类对象不是寄存在外部类对象里的,而是寄存在外部类的类本身中。当静态内部类对象存在时,并不存在一个被它寄存的外部类对象,静态内部类对象里只有外部类的类引用,没有持有外部类对象的引用。如果允许静态内部类的实例方法访问外部类的实例成员,但找不到被寄存的外部类对象,这将引起错误。

静态内部类是外部类的一个静态成员,因此外部类的静态方法、静态初始化块中可以使用静态内部类来定义变量、创建对象等。

外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。

下面程序示范了这条规则:

查看代码

除此之外,Java还允许在接口里定义内部类,接口里定义的内部类默认使用public static修饰,也就是说,接口内部类只能是静态内部类。

如果为接口内部类指定访问控制符,则只能指定public访问控制符;如果定义接口内部类时省略访问控制符,则该内部类默认是public访问控制权限。

那么,接口里是否能定义内部接口?

可以的。接口里的内部接口是接口的成员,因此系统默认添加public static两个修饰符。如果定义接口里的内部接口时指定访问控制符,则只能使用public修饰符。当然,定义接口里的内部接口的意义不大,因为接口的作用是定义一个公共规范(暴露出来供大家使用),如果把这个接口定义成一个内部接口,那么意义何在呢?至少在开发过程中很少见到这种应用场景。

1.* 使用内部类

定义类的主要作用就是定义变量、创建实例和作为父类被继承。定义内部类的主要作用也如此,但使用内部类定义变量和创建实例则与外部类存在一些小小的差异。下面分三种情况讨论内部类的用法。

1.*.& 在外部类内部使用内部类

从前面程序中可以看出,在外部类内部使用内部类时,与平常使用普通类没有太大的区别。一样可以直接通过内部类类名来定义变量,通过new调用内部类构造器来创建实例。

唯一存在的一个区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。

在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。

1.*.& 在外部类以外使用非静态内部类

如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用。

  • 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类所访问。
  • 使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问。
  • 使用public修饰的内部类,可以在任何地方被访问。

在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:

点击查看代码
OuterClass.InnerClass varName

从上面语法格式可以看出,在外部类以外的地方使用内部类时,内部类完整的类名应该是OuterClass.InnerClass。如果外部类有包名,则还应该增加包名前缀。

因为非静态内部类的对象必须寄存在外部类的对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类实例的语法如下:

点击查看代码
OuterInstance.new InnerConstructor()

从上面语法格式可以看出,在外部类以外的地方创建非静态内部类实例必须使用外部类实例和new来调用非静态内部类的构造器。

下面程序示范了如何在外部类以外的地方创建非静态内部类的对象,并把它赋给非静态内部类类型的变量:

查看代码

上面程序中Out.In in = new Out().new In("测试信息")创建了一个非静态内部类的对象。从上面代码可以看出,非静态内部类的构造器必须使用外部类对象来调用。

如果需要在外部类以外的地方创建非静态内部类的子类,则尤其要注意上面的规则:非静态内部类的构造器必须通过其外部类对象来调用。

我们知道:当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类对象。

下面程序定义了一个子类继承了Out类的非静态内部类In类:

查看代码

上面代码中[1]代码行看起来有点奇怪,其实很正常:非静态内部类In类的构造器必须使用外部类对象来调用,代码中super代表调用In类的构造器,而out则代表外部类对象(上面的Out、In两个类直接来自于前一个CreateInnerInstance.java)。

从上面代码中可以看出,如果需要创建SubClass对象时,必须先创建一个Out对象。这是合理的,因为SubClass是非静态内部类In类的子类,非静态内部类In对象里必须有一个对Out对象的引用,其子类SubClass对象里也应该存在一个Out对象的引用。当创建SubClass对象时传给该构造器的Out对象,就是SubClass对象里Out对象引用所指向的对象。

非静态内部类In对象和SubClass对象都必须保留有指向Outer对象的引用,区别是创建两种对象时传入Out对象的方式不同:当创建非静态内部类In类的对象时,必须通过Outer对象来调用new关键字;当创建SubClass类的对象时,必须使用Outer对象作为调用者来调用In类的构造器。

注意:非静态内部类的子类不一定是内部类,它可以是一个外部类。但非静态内部类的子类实例一样需要保留一个引用,该引用指向其父类所在外部类的对象。也就是说,如果有一个内部类子类的对象存在,则一定存在与之对应的外部类对象。

1.*.& 在外部类以外使用静态内部类

因为静态内部类是外部类类相关的,因此创建内部类对象时无须创建外部类对象。在外部类以外的地方创建静态内部类实例的语法如下:

点击查看代码
new OuterClass.InnerConstructor()

下面程序示范了如何在外部类以外的地方创建静态内部类的实例:

查看代码

从上面代码中可以看出,不管是静态内部类还是非静态内部类,它们声明变量的语法完全一样。区别只是在创建内部类对象时,静态内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。

因为调用静态内部类的构造器时无须使用外部类对象,所以创建静态内部类的子类也比较简单,下面代码就为静态内部类StaticIn类定义了一个空的子类:

点击查看代码
public class StaticSubClass extends StaticOut.StaticIn {}

从上面代码中可以看出,当定义一个静态内部类时,其外部类非常像一个包空间。

注意:相比之下,使用静态内部类比使用非静态内部类要简单很多,只要把外部类当成静态内部类的包空间即可。因此当程序需要使用内部类时,应该优先考虑使用静态内部类。

既然内部类是外部类的成员,那么是否可以为外部类定义子类,在子类中再定义一个内部类来重写其父类中的内部类?

不可以!从上面知识可以看出,内部类的类名不再是简单地由内部类的类名组成,它实际上还把外部类的类名作为一个命名空间,作为内部类类名的限制。因此子类中的内部类和父类中的内部类不可能完全同名,即使二者所包含的内部类的类名相同,但因为它们所处的外部类空间不同,所以它们不可能完全同名,也就不可能重写。

1.* 局部内部类

如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰。

注意:对于局部成员而言,不管是局部变量还是局部内部类,它们的上一级程序单元都是方法,而不是类,使用static修饰它们没有任何意义。因此,所有的局部成员都不能使用static修饰。不仅如此,因为局部成员的作用域是所在方法,其他程序单元永远也不可能访问另一个方法中的局部成员,所以所有的局部成员都不能使用访问控制符修饰。

如果需要用局部内部类定义变量、创建实例或派生子类,那么都只能在局部内部类所在的方法内进行:

查看代码

编译上面程序,看到生成了三个class文件:

  • LocalInnerClass.class
  • LocalInnerClass$1InnerBase.class
  • LocalInnerClass$1InnerSub.class

这表明局部内部类的class文件总是遵循如下命名格式:OuterClass$NInnerClass.class。注意到局部内部类的class文件的文件名比成员内部类的class文件的文件名多了一个数字,这是因为同一个类里不可能有两个同名的成员内部类,而同一个类里则可能有两个以上同名的局部内部类(处于不同方法中),所以Java为局部内部类的class文件名中增加了一个数字,用于区分。

1.* 匿名内部类

匿名内部类适合创建那种只需要一次使用的类,例如前面介绍命令模式时所需要的Command对象。匿名内部类的语法有点奇怪,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。

定义匿名内部类的格式如下:

点击查看代码
new 父类构造器(实参列表)|实现接口() {
    //匿名内部类的类体部分
}

从上面定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。

关于匿名内部类还有如下两条规则:

  • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
  • 匿名内部类不能定义构造器,因为匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义实例初始化块,通过实例初始化块来完成构造器需要完成的事情。

最常用的创建匿名内部类的方式是需要创建某个接口类型的对象,如下程序所示:

查看代码

上面程序中的AnonymousTest类定义了一个test方法,该方法需要一个Product对象作为参数,但Product只是一个接口,无法直接创建对象,因此此处考虑创建一个Product接口实现类的对象传入该方法——如果这个Product接口实现类需要重复使用,则应该将该实现类定义成一个独立类;如果这个Product接口实现类只需一次使用,则可采用上面程序中的方式,定义一个匿名内部类。

正如上面程序中看到的,定义匿名内部类无须class关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象。上面1代码部分就是匿名内部类的类体部分。

由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或者接口里包含的所有抽象方法。

对于上面创建Product实现类对象的代码,可以拆分成如下代码:

查看代码

对比两段代码,它们完全一样,但显然采用匿名内部类的写法更加简洁。

当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口名后的括号里不能传入参数值。

但如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,此处的相似指的是拥有相同的形参列表。

查看代码

上面程序创建了一个抽象父类Device类,这个抽象父类里包含两个构造器:一个无参数的,一个有参数的。当创建以Device为父类的匿名内部类时,既可以传入参数(如上面程序中第一段粗体字部分),也可以不传入参数(如上面程序中第二段粗体字部分)。

当创建匿名内部类时,必须实现接口或抽象父类里的所有抽象方法。如果有需要,也可以重写父类中的普通方法,如上面程序的第二段粗体字代码部分,匿名内部类重写了抽象父类Device类的getName方法,其中getName方法并不是抽象方法。

如果匿名内部类需要访问外部类的局部变量,则必须使用final修饰符来修饰外部类的局部变量,否则系统将报错。看下面程序:

查看代码

上面程序中代码是匿名内部类访问了外部类的局部变量,由于age变量没有使用final修饰符修饰,所以代码将引起编译异常。

1.* 闭包Closure和回调

闭包(Closure)是一种能被调用的对象,它保存了创建它的作用域信息。Java7虽然没有显式地支持闭包,但对于非静态内部类而言,它不仅记录了其外部类的详细信息,还保留了一个创建非静态内部类对象的引用,并且可以直接调用外部类的private成员,因此可以把非静态内部类当成面向对象领域的闭包。

通过这种仿闭包的非静态内部类,可以很方便地实现回调功能,回调就是某个方法一旦获得了内部类对象的引用后,就可以在合适的时候反过来去调用外部类实例的方法。所谓回调,就是允许客户类通过内部类引用来调用其外部类的方法,这是一种非常灵活的功能。

假设有下面的Teachable接口和Programmer基类,它们都提供了一个work方法,这两个方法的方法签名完全相同,但方法功能可能不一样:

查看代码

假设现在有一个人(如笔者一样),既是一个程序员,也是一个教师。也就是说,需要定义一个特殊的类,既需要实现Teachable接口,也需要继承Programmer父类。表面上看起来这没有任何问题,问题是Teachable接口和Programmer父类里包含了相同的work方法,如果采用如下代码来定义一个特殊的TeachableProgrammer类:

查看代码

显然上面的TeachableProgrammer类只有一个work方法,这个work方法只能进行“教学”,不可以进行“编程”。但实际需要TeachableProgrammer类里既包含“教学”的work方法,也包含“编程”的work方法。

这个时候,我们可以通过一个仿闭包的内部类来实现这个功能:

查看代码

上面的TeachableProgrammer类只是Programmer类的子类,它可以直接调用Programmer基类的work方法,该类也包含教学的teach方法,但这个方法与Teachable接口没有任何关系,TeachableProgrammer也不能当成Teachable使用。此时创建了一个Closure内部类,它实现了Teachable接口,并实现了教学的work方法(粗体字部分)——但这种实现是通过回调TeachableProgrammer类的teach方法实现的。如果需要让TeachableProgrammer对象进行教学,只需调用Closure内部类(它是Teachable接口的实现类)对象的work方法即可。

TeachableProgrammer类提供了一个获取内部类对象的方法,该方法无须返回Closure类型,只需返回所实现接口:Teachable类型即可,接下来它就可当成一个Teachable对象使用了。

下图是上面三个类和一个接口的类图:

image

从图中清楚地看出Closure内部类的作用,它可以实现Teachable接口,也可以当成Teachable使用,而且它是TeachableProgrammer的内部类,是回调TeachableProgrammer对象方法的入口,它的work方法实际上回调了TeachableProgrammer类的teach方法。

下面程序示范了如何让TeachableProgrammer对象既执行“教学”的work方法,也执行“编程”的work方法:

查看代码

上面程序中tp.work()和tp.getCallbackReference().work(),表面上调用了Teachable对象(实际上是Closure对象)的work方法,实际上回调了TeachableProgrammer对象的teach方法。内部类对象可以很方便地回调其外部类的Field、方法,这样就可以让编程更加灵活。

原文地址:http://www.cnblogs.com/hzhiping/p/16875121.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性