大部分时候,类被定义成一个独立的程序单元。在某些情况下,也会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类(有的地方也叫嵌套类),包含内部类的类也被称为外部类(有的地方也叫宿主类)。
Java从JDK1.1开始引入内部类,内部类主要有如下作用。
①内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。假设需要创建Cow类,Cow类需要组合一个CowLeg对象,CowLeg类只有在Cow类里才有效,离开了Cow类之后没有任何意义。在这种情况下,就可以把CowLeg定义成Cow内部类,不允许其他类访问CowLeg。
②内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
③匿名内部类适合用于创建那些仅需要一次使用的类。
从语法角度来看,定义内部类与定义外部类的语法大致相同,内部类除了需要定义在其他类里面之外,还存在如下两点区别。
①内部类比外部类可以多三个修饰符:private、protected、static——外部类不可以使用这三个修饰符。
②非静态内部类不能拥有静态成员。
一、非静态内部类成员
定义内部类非常简单,只要把一个类放在另一个类内部定义即可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类被称为局部内部类)。内部类定义语法格式如下:
public class OuterClass
{
//此处可以定义内部类
}
大部分时候,内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。
成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。
前面经常看到同一个Java源文件里定义了多个类,那种情况不是内部类,它们依然是两个互相独立的类。例如下面程序:
//下面A、B两个空类互相独立,没有谁是谁的内部类
class A{}
public class B{}
上面两个类定义虽然写在同一个源文件中,但它们互相独立,没有谁是谁的内部类这种关系。内部类一定是放在另一个类的类体部分(也就是类名后的花括号部分)定义。
因为内部类作为其外部类的成员,所以可以使用任意访问控制符如private、protected和public等修饰。
注意:
外部类的上一级程序单元是包,所以它只有2个作用域:同一个包或任何位置,因此只需2种访问权限:包访问权限,正好对应省略访问控制符和public访问控制符。省略访问控制是包访问权限,即同一个包的其他类可以访问省略访问控制符的成员。因此,如果一个外部类不使用任何访问控制修饰符,则只能被同一个包中其他类访问。而内部类的上一级是外部类,所以它就具有4个作用域:同一个类、同一个包、父子类和任何位置。因此可以使用4种访问权限。
下面程序在Cow类里定义了一个CowLeg非静态内部类,并在CowLeg类的实例方法中直接访问Cow的private访问权限的实例变量。
public class Cow {
private double weight;
public Cow(){}
public Cow(double weight)
{
this.weight = weight;
}
//定义一个非静态内部类
private class CowLeg
{
//非静态内部类有两个实例变量
private double length;
private String color;
//非静态内部类的两个重载的构造器
public CowLeg(){}
public CowLeg(double length , String color)
{
this.length = length;
this.color = color;
}
//非静态内部类的实例方法
public void info()
{
System.out.println("当前牛腿颜色是:" + color + "高:" + length);
//直接访问外部类的private修饰的成员变量
System.out.println("本牛腿所在的奶牛重:" + weight);//①
}
}
public void test()
{
CowLeg c1 = new CowLeg(1.12 ,"黑白相间");
c1.info();
}
public static void main(String[] args) {
Cow cow = new Cow(378.9);
cow.test();
CowLeg c2 = new Cow(383).new CowLeg(1.0,"花色");
c2.info();
}
}
运行结果:
当前牛腿颜色是:黑白相间高:1.12
本牛腿所在的奶牛重:378.9
当前牛腿颜色是:花色高:1.0
本牛腿所在的奶牛重:383.0
上面程序中是一个CowLeg是一个普通的类定义,因为把这个类放在另一个类的内部,所以它就成了一个内部类,可以使用private修饰符来修饰这个类。
外部类Cow里包含了一个test()方法,该方法里创建了一个CowLeg对象,并调用该对象的info()方法。不难发现,在外部类Cow里使用非静态内部类时,与平时使用普通类没有太大区别。
编译上面程序,看到在文件所在路径生成了两个class文件。一个是Cow.class,前者是外部类Cow的class文件,后者是内部类CowLeg的class文件,即成员内部类(包括静态内部类、非静态内部类)的class文件总是这种形式:OuterClass$InnerClass.class。
前面提到过,在非静态内部类里可以直接访问外部类的private成员,上面①号代码就是直接访问其外部类的private实例变量。这是因为在非静态内部类对象里,保存了它所寄生的外部类对象的引用(这这边是Cow.this)(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,非静态内部类实例必须寄生在外部类实例里)。
当在非静态内部类的方法内访问某个变量时,系统首先在该方法里查找是否存在该名字的局部变量,如果存在就使用该变量,如果不存在,见到该方法所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该程序员变量;如果依然不存在,系统将出现编译错误:提示找不到该变量。
因此,如果外部类成员变量、内部类成员变量与内部类方法的局部变量同名,则可通过this.外部类类名.this限定来区别。
如下程序所示。
public class DiscernVariable
{
private String prop = "外部类的实例变量";
private class InClass
{
private String prop = "内部类的实例变量";
public void info()
{
String prop = "局部变量";
//通过外部类类名.this.varName 访问外部类实例变量
System.out.println("外部类的实例变量
值:"+DiscernVariable.this.prop);
//通过this.varName访问内部类实例的变量
System.out.println("内部类的实例变量:"
+ this.prop);
}
}
public void test()
{
InClass in = new InClass();
in.info();
}
public static void main(String[] args)
{
new DiscernVariable().test();
}
}
上面程序中分别访问外部类的实例变量、非静态内部类的实例变量。通过OuterClass.this.propName的形式访问外部类的实例变量,通过this.propName的形式访问非静态内部类的实例变量。
非静态内部类的成员可以访问外部类的private成员,但反过来就不成立了。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问非静态内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。
下面程序示范了这个规则。
public class Outer
{
private int outProp = 9;
class Inner
{
private inProp = 5;
public void acessOuterProp()
{
//非静态内部类可以直接访问外部类的private成员变量
System.out.println("外部类的outProp值:"+outProp);
}
}
public void accessInnerProp()
{
//外部类不能直接访问非静态内部类的实例变量
//下面代码出现编译错误
//System.out.println("内部类的inProp值:" + inProp);
//如访问内部类的实例变量,必须显示创建内部类对象
System.out.println("内部类的inProp值:" +
new Inner().inProp);
}
public static void main(String[] args)
{
//执行下面代码,只创建了外部类对象,还未创建内部类对象
Outer out = new Outer();//①
out.accessInnerProp();
}
}
程序中有行代码试图在外部类方法里访问非静态内部类的实例变量,这将引起编译错误。
外部类不允许访问非静态内部类的实例成员还有一个原因,上面程序中main()方法①号代码创建了一个外部类对象,并调用外部类对象的accessInnerProp()方法。此时非静态内部类对象根本不存在,如果允许accessInnerProp()访问非静态内部类对象,将肯定引起错误。
非静态内部类对象和外部类对象的关系是怎样的?
答:非静态内部类对象必须寄生在外部类对象里,而外部类对象不一定有非静态内部类对象寄生其中。简单来说,如果存在一个非静态内部类对象,则一定存在一个被它寄生的外部类对象。但外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象。因此,外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在!而非静态内部类对象访问外部类成员时,外部类对象一定存在。
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。如下程序所示:
public class StaticTest
{
//定义一个非静态内部类,是一个空类
private class In{}
//外部类的静态方法
public static void main(String[] args)
{
//下面代码引发编译异常,因为静态成员(main()方法)
//无法访问非静态成员(In类)
new In();
}
}
Java不允许在非静态内部类里定义静态成员。下面程序示范了非静态内部类里包含静态成员将引发编译错误。
public class InnerNoStatic
{
public class InnerClass
{
/*
下面三个静态声明都将引发编译错误:
非静态内部类不能有静态声明
*/
static
{
System.out.println("=======");
private static int inProp;
private static void test(){}
}
}
}
非讲台内部类里不能有静态方法、静态成员变量、静态初始化块,所以上面三个静态声明都会引发错误。
注意:非静态内部类里不可以有静态初始化块,但可以包含普通初始化块。非静态内部类普通初始化块的作用与外部类初始化块的作用完全相同。
二、静态内部类
如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也被称为静态内部类。
注意:static关键字的作用就是把类的成员变成类相关,而不是实例相关,即static修饰的成员属于整个类,而不属于但个对象。外部类的上一级程序单元是包,所以不可使用static修饰符;而内部类的上一级程序单元是外部类,使用static修饰符可以将内部类变成外部类相关。
静态内部类可以包含静态成员,也可以包含非静态成员。静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。
public class StaticInnerClassTest {
private int prop1 = 5;
private static int prop2 = 9;
static class StaticInnerClass
{
//静态内部类里可以包含静态成员
private static int age;
public void accessOuterProp()
{
//下面代码出现错误
//静态内部类无法访问外部类的实例变量
System.out.println(prop1);
//下面代码正常
System.out.println(prop2);
}
}
}
为什么静态内部类实例方法不能访问外部类的实例属性呢?
答:因为静态内部类是外部类的类相关的,而不是外部类的对象相关的。也就是说静态内部类是寄生在外部类本身中,而不是外部类的实例中。当静态内部类对象存在时,并不存在一个被它寄生的外部类对象,静态内部类对象只持有一个外部类的类引用,没有外部类对象的引用,如果允许静态内部类的实例方法访问外部类的实例成员,但找不到被外部类对象,这将引起错误。
和非静态内部类一样,外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问内部类的成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。
三、使用内部类
定义内部类的主要作用就是定义变量、创建实例和作为父类被继承。定义内部类的主要作用也如此,但使用内部类定义变量和创建实例和外部类存在一些小小差异。下面分为三种情况来讨论内部类的用法。
1.在外部类中使用内部类
从前面看来,在外部类中使用内部类,与平常使用普通类没有太大的区别。一样可以直接通过内部类类名来定义变量,通过new调用内部构造器来创建实例。
唯一存在的一个区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。
2.在外部类以外使用非静态内部类
如果希望外部类以外的地方使用内部类,则内部类不能使用private访问控制权限。
①省略访问控制符,只能被外部类处于同一个包中的其他类所访问。
②使用protected修饰的内部类,只能被外部类处于统一报中的其他类和外部类的子类访问。
③使用public修饰的内部类,可以在任何地方被访问。
在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:
OuterClass.InnerClass varName
从上面语法格式可以看出,在外部类以外的地方使用内部类时,内部类完整的类名应该是OuterClass.InnerClass。
由于非静态内部类的对象必须寄生在外部类对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类实例的语法格式如下。
OuterInstance.new InnerConstructor()
从上面语法格式可以看出,在外部类以外的地方创建非静态内部类实例必须使用外部类实例和new来调用非静态内部类的构造器。
下面程序示范了如何在外部类以外的地方创建非静态内部类对象,并把他赋给非静态内部类类型的变量。
class Out
{
//定义一个内部类,不使用访问控制符
//即只有同一个包中的其他类可访问该内部类
class In
{
public In(String msg)
{
System.out.println(msg);
}
}
}
public class CreateInnerInstance
{
public static void main(String[] args)
{
Out.In in = new Out().new In("测试信息");
/*
上面代码可改为如下三行代码
使用OutterClass.InnerClass的形式定义内部类变量
Out.In in;
创建外部类实例,非静态内部类实例将寄生在该实例中
Out out = new Out();
通过外部类实例和new来调用内部类构造器创建非静态内部类实例
in = out.new In("测试信息");
*/
}
}
从上面可以看出,非静态内部类的构造器必须使用外部类对象来调用。
当创建一个子类,子类构造器总会调用父类构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类对象。
下面定义了一个子类继承了Out类的非静态内部类In类。
public class SubClass extends Out.In
{
//显式定义SubClass的构造器
public SubClass(Out out)
{
//通过传入的Out对象显式调用In的构造器
out.super("hello");
}
}
上面的代码行看起来有点奇怪,其实很正常:非静态内部类In类的构造器必须使用外部类来调用,代码中super代表调用In类的构造器,而out则代表外部类对象。
从上面代码可以看出,如果需要创建SubClass对象,就必须创建一个Out对象。这是合理的,因为SubClass对象是非静态内部类In的子类,非静态内部类In对象里必须有一个对Out对象的引用,其子类SubClass对象里也应该持有对Out对象的引用。当创建SubClass对象时传给该构造器的Out对象就是SubClass对象里Out对象所指向的对象。
非静态内部类In对象和SubClass对象都必须持有指向Outer对象的引用,区别是创建两种对象时传入Out对象的方式不同:当创建非静态内部类In类的对象时,必须通过Outer对象来调用new关键字;当创建SubClass类的对象时,必须使用Outer对象作为调用者调用In类的构造器。
注意:非静态内部类的子类不一定是内部类,它可以是一个外部类,但非静态内部类子类实例一样需要保留一个引用,该引用指向其父类所在外部类的对象。也就是说,如果有一个内部类子类的对象存在,则一定存在与之对应的外部类对象。
3.在外部类以外使用静态内部类
因为静态内部类是外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。在外部类以外的地方创建静态内部类实例的语法如下:
new OuterClass.InnerConstructor()
下面程序示范了如何在外部类以外的地方创建静态内部类的实例。
class StaticOut
{
//定义一个内部类,不是要访问控制符
//即同一个包中的其他类可访问该内部类
static class StaticIn
{
System.out.println("静态内部类的构造器");
}
}
public class CreateStaticInnerInstance
{
public static void main(String[] args)
{
StaticOut.StaticIn in = new StaticOut.StaticIn();
/*
上面代码可以改为如下两行
StaticOut.StaticIn in ;
in = new StaticOut.StaticIn();
*/
}
}
从上面代码看出,不管是静态内部类还是非静态内部类,它们声明的语法完全一样。区别只是在创建内部类对象时,静态内部类只需使用即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。
因为调用构造器时无须使用外部类对象,所以创建静态内部类的子类也比较简单,下面代码就为静态内部类StaticIn类定义了一个空的子类。
public class StaticSubClass extends StaticOut.StaticIn
{
}
从上面代码,当定义一个静态内部类时,其外部类非常像一个包空间。
注意:
相比之下,使用静态内部类比使用非静态内部类要简单很多,只要把外部类当成静态内部类的包空间即可。因此当程序需要使用内部类时,应该优先考虑使用静态内部类。
问:既然内部类是外部类的成员,那么是否可以为外部类定义子类在子类中再定义一个内部类来重写其父类中的内部类呢?
答:不可以!内部类的类名不再是简单地由内部类的类名组成,它实际上还把外部类的类名作为一个命名空间,作为内部类类名的限制。因此子类中的内部类和父类中的内部类不可能完全同名,即使二者所包含的内部类的类名相同,但因为它们所处的外部空间不同,所以不可能完全同名,也就不可能重写。
4.局部内部类
如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符合static修饰符修饰。
注意:对于局部成员而言,不管是局部变量还是局部内部类,它们的上一级程序都是方法,而不是类,使用static修饰它们没有任何意义。因此,所有局部成员都不能使用static修饰。不仅如此,因为局部成员的作用域是所在方法,其他程序单元永远不可能访问另一个方法中的局部成员,所以所有的局部成员都不能使用访问控制符修饰。
如果要使用局部内部类定义变量、创建实例或派生子类,那么都只能在局部内部类所在的方法内进行
public class LocalInnerClass
{
public static void main(String[] args)
{
//定义局部内部类
class InnerBase
{
int a;
}
//定义局部内部类的子类
class InnerSub extends InnerBase
{
int b;
}
//创建局部内部类的对象
is.a = 5;
is.b = 8;
System.out.println("InnerSub对象的a和b实例变量是:"
+ is.a + "," + is.b);
}
}
编译上面程序,可以看到三个class文件,这表明局部内部类的class**文件总遵循如下的命名格式:OuterClass$NInnerClass**。注意到局部内部类的class文件名比成员内部类的class文件的文件名多一个数字,这是因为同一个类里不可能有两个同名的成员内部类,而同一个类里可能有两个同名的局部内部类(处于不同的方法中),因此Java为局部内部类的class文件增加一个数字,用于区别。
注意:
局部内部类里是一个非常“鸡肋”的语法,在实际开发中很少用,这是因为局部内部类的作用域太小了:只能子啊当前方法中使用。大部分时候,定义一个类之后,当然希望多次复用这个类。
5.Java 8改进的匿名内部类
匿名内部类适合创建那种只需要使用一次的类。匿名内部类的语法有点奇怪,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。
定义匿名内部类的格式如下:
new 实现接口() | 父类构造器(实参列表)
{
//匿名内部类的类体部分
}
从上面定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。
关于匿名内部类还有如下两条规则。
①匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
②匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。
最常用的创建匿名内部类的方式是创建某个接口类型的对象,如下程序所示。
package com.suqian.test;
interface Product
{
public double getPrice();
public String getName();
}
public class AnonymousTest {
public void test(Product p)
{
System.out.println("购买了一个" + p.getName() + ",花费了" + p.getPrice());
}
public static void main(String[] args)
{
AnonymousTest ta = new AnonymousTest();
//调用test()方法时,需要传入一个Product参数
//此处传入其匿名实现类的实例
ta.test(new Product() {
@Override
public double getPrice() {
return 567.8;
}
@Override
public String getName() {
return "显卡";
}
});
}
}
上面程序中的AnonymousTest类定义了一个test()方法,该方法需要一个Product对象作为参数,但Product只是一个接口,无法直接创建对象,因此此处考虑的是创建一个Product接口实现类的对象传入该方法——如果这个Product接口实现类需要重复使用,则应该将该实现类定义成一个独立类。
正如上面所说,定义匿名内部类无须class关键字,而是在定义匿名内部类时直接生产该匿名内部类的对象。上面粗体字代码部分就是匿名内部类的类体部分。
由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或者接口里包含的所有抽象方法。
对于上面创建的Product实现类对象的代码,可以拆分成如下代码:
class AnonymousProduct implements Product
{
public double getPrice()
{
return 567.8;
}
public String getName()
{
return "显卡";
}
}
对比上面两段代码,它们完全一样,但显然匿名内部类的写法更加简洁。
当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口后的括号里不能传入参数值。
但如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,此处的相似指的是拥有相同的行参列表。
abstract class Device
{
private String name;
public abstract double getPrice();
public Device(){}
public Device(String name)
{
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class AnonymousInner {
public void test(Device d)
{
System.out.println("购买了一个" + d.getName() + "花掉了" + d.getPrice());
}
public static void main(String[] args)
{
AnonymousInner ai = new AnonymousInner();
//调用有参数的构造器创建Device匿名内部类实现的对象
ai.test(new Device("电子示波器") {
public double getPrice() {
return 67.8;
}
});
//调用无参数的构造器创建Device匿名实现类的对象
Device d = new Device()
{
{
System.out.println("匿名内部类的初始化块");
}
@Override
public double getPrice() {
// TODO Auto-generated method stub
return 56.2;
}
public String getName()
{
return "键盘";
}
};
ai.test(d);
}
}
上面程序创建了一个抽象父类Device,这个抽象父类包含两个构造器,一个无参数的,一个有参数的。当创建以Device为父类的匿名内部类时,既可以传入参数,代表调用父类带参数的构造器;也可以不传入参数,代表调用父类无参数的构造器。
当创建匿名内部类时,必须实现接口或抽象方法里的所有抽象方法。如果有需要,也可以重写父类中的普通方法。
在Java 8以前,Java要求被局部内部类、匿名内部类访问的局部变量必须使用final修饰,从Java 8开始这个限制被取消了,Java 8 更智能:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final修饰。
例如:
interface A
{
void test();
}
public class ATest
{
public static void main(String args)
{
int age = 8;//①
A a = new A()
{
//在Java8以前下面语句将提示错误:age必须使用final修饰
//从Java8开始,匿名内部类、局部内部类允许
//访问非final的局部变量
System.out.println(age);
}
}
}
如果使用Java 8的JDK来编译、运行上面程序,程序完全正常,但如果使用Java 8以前的JDK版本运行上面程序,将会引起编译错误,编译器提示用户必须使用final修饰age变量。
如果在①号代码后增加如下代码:
//下面代码将会导致编译错误
//由于age局部变量被匿名内部类访问了,因此age相当于被final修饰了
age = 2;
程序①号代码对age指定了初始值,而上面代码再次对age赋值,这会导致Java 8无法自动使用final修饰符修饰age变量,导致Java 8 无法自动使用final修饰符修饰age局部变量,因此编译器会报错:被匿名内部类访问局部变量必须使用final修饰。
提示:
Java 8将这个功能称为“effectively final”,它的意思是对呗匿名内部类访问的局部变量,可以用final修饰,也可以不用final修饰·,但必须按照有final修饰的方法来用。也就是一次赋值后,以后不能重新赋值。