参考:《疯狂的java讲义》
我们把一个类放在另一个类的内部定义,这个定义在其他类内部的类成为内部类,包含内部类的类也被称为外部类。(或者叫宿主类)
内部类的主要作用:
• 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一包中的其他类访问该类。假设需要创建Cow类,cow类需要组合一个cowleg对象,cowleg类只有在cow类才有效,离开了cow类之后就没有任何意义。这种情况下,就可以把cowleg定义成cow的内部类,不允许其他类访问cowleg.
• 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
• 匿名内部类适合用于创建那些仅需要一次使用的类。
内部类作为其外部类的成员,所以可以使用任意访问控制符如private、protected和public等修饰。
外部类的上一级程序单元是包,所以它只有2个作用域:同一个包内和任意位置。因此只需要2种访问权限:包访问权限和公开访问权限,正好对应省略访问控制符和public访问控制符。省略访问控制符是包访问权限,即同一包中的其他类可以访问省略访问控制符的成员。因此,如果一个外部类不使用任何访问控制符修饰,则只能被同一个包中其他类访问,而内部类的上一级程序单元是外部类,它就具有4个作用域:同一个类,同一个包,父子类和任何位置,因此可以使用4种访问控制权限。
1.非静态内部类
大部分时候,内部类都被当做成员内部类定义,而不是局部内部类。
成员内部类是一种与field、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。
成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有static修饰的成员内部类是非静态内部类。
经常看到同一个java源文件里定义了多个类,那种情况不是内部类。它们仍然是两个相互独立的类。例如下面的程序:
Class A{}
Public class B{}
上面两个类定义虽然写在同一个源文件里面,但是它们相互独立。内部类一定是放在另一个类的类体部分。
成员内部类(包括静态内部类、非静态内部类)的编译生成的class文件总是下面的形式:
OuterClass$Innerclass.class。
非静态内部类里可以直接访问外部类的private成员,但是反过来就不成立了。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问静态内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。
这是因为在非静态内部类对象里,保存了一个它所寄生的外部类对象的引用。非静态内部类对象必须寄生在外部类对象里,而外部类对象则不一定有非静态内部类对象寄生其中。简单地说,如果存在一个非静态内部类对象,则一定存在一个被它寄生的外部类对象。但是外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象。因此外部类对象访问非静态内部类成员时,可能非静态内部类对象根本不存在,而非静态内部类对象访问外部类成员时,外部类对象一定存在。
当在非静态内部类的方法访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不存在,就在内部类中查找;不存在,就在外部类中查找;还不存在就出现编译错误:提示找不到该变量。
因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则通过使用this、外部类名.this作为限定区分。
不允许在外部类的静态成员中直接使用非静态内部类。
不允许在非静态内部类里定义静态成员。非静态内部类了不能有静态方法、静态field、静态初始化块,上面三个静态声明都会引发错误。
2.静态内部类
如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。
静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。
静态内部类是外部类的一个静态成员因此外部类的所有方法、所有初始化块可以使用静态内部类类定义变量、创建对象等。
外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。
public class AccessStaticInnerClass{
static class StaticInnerClass{
private static int prop1 = 5;
private int prop2 = 9;
}
public void accessInnerProp{
System.out.println(prop1);//这种调用错误
System.out.println(StaticInnerClass.prop1);//通过类名访问静态内部类的类成员
System.out.println(new StaticInnerClass().prop2);//通过实例访问静态内部类的实例成员
}
}
除此之外,java还允许在接口里定义内部类,接口里定义的内部类默认使用public static修饰,也就是说,接口内部类只能是静态内部类。
3.使用内部类
(1)在外部类内部使用内部类
外部类内部使用内部类时,与普通使用普通类没有太大的区别。一样可以通过内部类类名来定义变量,通过new调用内部类构造器来创建实例。唯一存在的一个区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。
(2)在外部类以外使用非静态内部类
如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用。
省略访问控制符的内部类,只能被外部类处于同一个包中的其他类所访问。
使用protected修饰的内部类,可被与外部类处于同一包中的其他类和外部类的子类所访问。
使用public修饰的内部类,可以在任何地方被访问。
在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:
OuterClass.InnerClass.varName
由于非静态内部类的对象必须寄生在外部类的对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类
实例
的语法如下:
OuterInstance.new InnerConstructor()
从上面的语法格式可以看出,在外部类以外的地方创建非静态内部类实例必须使用外部类实例和
new
来调用非静态内部类的构造器。下面程序示范了如何在外部类以外的地方创建非静态内部类的对象,并把它赋给非静态内部类类型的变量。
如果需要在外部类以外的地方创建非静态内部类的子类,则尤其要注意上面的规则:非静态内部类的构造器必须通过其外部类对象来调用。
当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类对象。
(3)在外部类以外使用静态内部类
因为静态内部类是外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。在外部类以外的地方创建内部类实例的语法如下:
new OuterClass.InnerConstructor()
创建内部类对象时,静态内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。
4.局部内部类
如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰。
如果需要用局部内部类定义变量,创建实例或者派生子类,那么都只能在局部内部类所在的方法内进行。
public class LocalInnerClass{
public static void main(String[] args){
//定义局部内部类
class InnerBase{
int a;
}
//定义局部内部类的子类
class InnerSub extends InnerBase{
int b;
}
//创建局部内部类的对象
InnerSub is = new InnerSub();
is.a = 5;
is.b = 8;
}
局部内部类的编译生成的class文件遵循如下命名格式:
OuteClass$NInnerClass.class
注意到局部内部类的class文件的文件名比成员内部类的class文件的文件名多了一个数字,这是因为同一个类里不可能有两个同名的成员内部类,而同一个类里则可能有两个以上同名的局部内部类(处于不同的方法中),所以java为局部内部类的class文件名增加了一个数字,用于区分。
5.匿名内部类
匿名内部类适合创建那种只需要一次使用的类。
匿名内部类不能重复使用。
匿名内部类不能是抽象类。
匿名内部类不能定义构造器。
最常用的创建匿名内部类的方式是需要创建某个接口类型的对象。如下程序所示:
public class AnonynousTest{
public void test(Product p){
System.out.println("购买了一个"+p.getName()+"花掉了"+p.getPrice());
}
public static void main(String[] args){
AnonynousTest ta = new AnonynousTest();
//调用test方法时,需要传入一个Product参数
//此处传入其匿名实现类实例
ta.test(new Product() {
@Override
public double getPrice() {
return 567.8;
}
@Override
public String getName() {
return "N卡";
}
});
}
}
正如上面程序中看到的,定义匿名内部类无须class关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象。上面红色代码部分就是匿名内部类的类体部分。
由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父亲或者接口里包含的所有抽象方法。
当通过实现接口来创建匿名内部类时,匿名内部类也不能显示创建构造器,因此匿名内部类只有一个隐式的无参构造器,故new接口名后的括号里不能传入参数值。
但是如果通过继承父类来创建匿名内部类,匿名内部类将拥有和父亲相似的构造器,此外的相似指的是拥有相同的形参列表。
当创建匿名内部类时,必须实现接口或者抽象父类里的所有抽象方法。如果有需要,也可以重写父类中的普通方法。
如果匿名内部类需要访问外部类的局部变量,则必须使用final修饰符来修饰外部类的局部变量,否则系统将报错。
6.闭包(Closure)和回调
闭包(Closure)是一种能被调用的对象,它保存了创建它的作用域信息。
对于非静态内部类而言,它不仅记录了其外部类的详细信息,还保存了一个创建非静态内部类对象的引用,而且可以直接调用外部类的private成员,因此可以把非静态内部类当成面向对象领域的闭包。
通过这种仿闭包的非静态内部类,可以很方便地实现回调功能,回调就是某个方法一旦获得了内部对象的引用后,就可以在合适的时候反过来调用外部类实例的方法。换句话说,回调就是允许客户类通过内部类引用来调用其外部类的方法。
假设有下面的Teachable接口和Programmer基类,它们都提供了一个work方法,这两个方法的方法签名完全相同,但是方法功能不一样。
nterface Teachable{
void work();
}
public class Programmer{
private String name;
//Programmer 类的两个方法
public Programmer(){}
public Programmer(String name){
this. name=name;
}
//此处省略name的 setter 和 getter 方法
public void work(){
System.out.println(name+"在灯下认真敲键盘...");
}
}
假设现在有一个人,既是程序员,又是老师。也就是说,需要定义一个特殊的类,既需要实现Teachable接口,也需要继承Programmer 父亲。但是如果采用下面的代码:
public class TeachableProgrammer extends Programmer implements Teachable{
public void work(){
System.out.println(getname()+"教师在讲台上讲解...");
}
}
这时候TeachableProgrammer类只有一个work方法,这个work方法只能进行“教学”,不可以进行“编程”。但是实际需要TeachableProgrammer类里既包含“教学”的work方法,也包含“编程”的work方法。
这时候,我们可以通过一个仿闭包的内部类来实现这个功能。
public class TeachableProgrammer extends Programmer {
public TeachableProgrammer(){}
public TeachableProgrammer(String name){
super(name);
}
//教学工作依然由TeachableProgrammer类定义
private void teach(){
System.out.println(getName+"教师在讲台上讲解");
}
/*
非静态内部类回调外部类实现work方法,非静态内部类引用的作用仅仅是向客户类提供一个回调外部类的方法
*/
private class Closure implements Teachable{
public void work(){
teach();
}
}
//返回一个非静态内部类的引用,允许外部类通过该非静态内部类引用来回调外部类的方法
public Teachable getCallbackReference(){
return new Closure();
}
}
下面程序示范了如何让TeachableProgrammer对象既执行“教学”的work方法,也执行“编程”的work法。
public class TeachableProgrammerTest{
public static void main(String[] args){
TeachableProgrammer tp = new TeachableProgrammer("李刚");
//直接调用TeachableProgrammer类从programmer类继承的work方法
tp.work();
//表面上调用的是Closure的work方法
//实际上是回调TeachableProgrammer 的teach方法
tp.getCallbackReference().work();
}
}