定义内部类非常简单, 只要把一个类放在另一个类内部定义即 可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定 义内部类(方法里定义的内部类被称为局部内部类)。内部类定义语 法格式如下:
public class OutClass {
// 此处定义类部类
}
大部分时候, 内部类都被作为成员内部类定义, 而不是作为局部 内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相 似的类成员;局部内部类和匿名内部类则不是类成员。
成员内部类分为两种:静态内部类和非静态内部类, 使用static 修饰的成员内部类是静态内部类, 没有使用static修饰的成员内部类是非静态内部类。
因为内部类作为其外部类的成员, 所以可以使用任意访问控制符如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;
}
// 下面省略length、color的setter和getter方法
public void setLength(double length) {
this.length = length;
}
public double getLength() {
return this.length;
}
public void setColor(String color) {
this.color = color;
}
public String getColor() {
return this.color;
}
// 非静态内部类的实例方法
public void info() {
System.out.println("当前牛腿颜色是:"
+ color + ", 高:" + length);
// 直接访问外部类的private修饰的成员变量
System.out.println("本牛腿所在奶牛重:" + weight); // ①
}
}
public void test() {
CowLeg cl = new CowLeg(1.12, "黑白相间");
cl.info();
}
public static void main(String[] args) {
Cow cow = new Cow(378.9);
cow.test();
}
}
上面程序中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实例变量。这是因为在非静态内部类对象里,保存 了一个它所寄生的外部类对象的引用(当调用非静态内部类的实例方 法时,必须有一个非静态内部类实例,非静态内部类实例必须寄生在 外部类实例里)。下图显示了上面程序运行时的内存示意图。
当在非静态内部类的方法内访问某个变量时, 系统优先在该方法 内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不 存在,则到该方法所在的内部类中查找是否存在该名字的成员变量, 如果存在则使用该成员变量;如果不存在,则到该内部类所在的外部 类中查找是否存在该名字的成员变量,如果存在则使用该成员变量; 如果 依然不存在,系统将出现编译错误:提示找不到该变量。
因此, 如果外部类成员变量、内部类成员变量与内部类里方法的 局部变量同名, 则可通过使用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);
// 直接访问局部变量
System.out.println("局部变量的值:" + prop);
}
}
public void test() {
InClass in = new InClass();
in.info();
}
public static void main(String[] args) {
new DiscernVariable().test();
}
}
上面程序中System.out.println("外部类的实例变量值:" + DiscernVariable.this.prop);代码和System.out.println("内部类的实例变量值:" + this.prop);分别访问外部类的实例变量、非静态内 部类的实例变量。 通过OuterClass.this.propName的形式访问外部类 的实例变量, 通过this.propName的形式访问非静态内部类的实例变 量。
非静态内部类的成员可以访问外部类的实例成员, 但反过来就不成立了。如果外部类需要访问非静态内部类的实例成员,则必须显式创建非静态内部类对象来调用访问其实例成员。下面程序示范了这个规则。
public class Outer {
private int outProp = 9;
class Inner {
private int inProp = 5;
public void accessOuterProp() {
// 非静态内部类可以直接访问外部类的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()方法的Outer out = new Outer();代码创建了一个外部类对象, 并调用外部类对象accessInnerProp()方法。 此时非静态内部类对象根本不存在,如果允许accessInnerProp()方法访问非静态内部类的实例成员, 将肯定引起错误。
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义 变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非 静态内部类。如下程序所示。
public class StaticTest {
// 定义一个非静态的内部类,是一个空类
private class In {
}
// 外部类的静态方法
public static void main(String[] args) {
// 下面代码引发编译异常,因为静态成员(main()方法)
// 无法访问非静态成员(In类)
new In();
}
}
Java不允许在非静态内部类里定义静态成员。 下面程序示范了非 静态内部类里包含静态成员将引发编译错误。
public class InnerNoStatic {
private class InnerClass {
static {
System.out.println("==========");
}
private static int inProp;
private static void test() {
}
}
}
ps:非静态内部类里不可以有静态初始化块,但可以包含普通初始化块。非静态内部类普通初始化块的作用与外部类初始化块的作用完全相同。
1.2 静态内部类 如果使用static来修饰一个内部类, 则这个内部类就属于外部类 本身, 而不属于外部类的某个对象。 因此使用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);
}
}
}
为什么静态内部类的实例方法也不能访问外部类的实例属性?—>因为静态内部类是外部类的类相关的,而不是外部类的对象相关 的。也就是说,静态内部类对象不是寄生在外部类的实例中,而是寄 生在外部类的类本身中。当静态内部类对象存在时,并不存在一个被 它寄生的外部类对象,静态内部类对象只持有外部类的类引用,没有 持有外部类对象的引用。如果允许静态内部类的实例方法访问外部类 的实例成员,但找不到被寄生的外部类对象,这将引起错误。
静态内部类是外部类的一个静态成员, 因此外部类的所有方法、 所有初始化块中可以使用静态内部类来定义变量、创建对象等。
外部类依然不能直接访问静态内部类的成员, 但可以使用静态内 部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态 内部类对象作为调用者来访问静态内部类的实例成员。下面程序示范 了这条规则。
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(prop2);
// 上面代码出现错误,应改为如下形式:
// 通过实例访问静态内部类的实例成员
System.out.println(new StaticInnerClass().prop2);
}
}
除此之外, Java还允许在接口里定义内部类, 接口里定义的内部 类默认使用public static修饰,也就是说,接口内部类只能是静态内 部类。 如果为接口内部类指定访问控制符, 则只能指定public访问控制 符;如果定义接口内部类时省略访问控制符, 则该内部类默认是 public访问控制权限。
1.3 局部内部类 如果把一个内部类放在方法里定义, 则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部 类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和 static修饰符修饰。
对于局部成员而言,不管是局部变量还是局部内部类,它们的 上一级程序单元都是方法,而不是类,使用static修饰它们没有任 何意义。 因此, 所有的局部成员都不能使用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;
System.out.println("InnerSub对象的a和b实例变量是:" + is.a + "," + is.b);
}
}
编 译 上 面 程 序 ,看 到 生 成 了 三 个 class 文 件 : LocalInnerClass.class 、 LocalInnerClass$1Innerbase.class 和 LocalInnerClass$1InnerSub.class, 这表明局部内部类的class文件 总是遵循如下命名格式:OuterClass$NInnerClass.class。 注意到局 部内部类的class文件的文件名比成员内部类的class文件的文件名多 了一个数字,这是因为同一个类里不可能有两个同名的成员内部类, 而同一个类里则可能有两个以上同名的局部内部类(处于不同方法 中),所以Java为局部内部类的class文件名中增加了一个数字,用于区分。
局部内部类是一个非常“鸡肋”的语法,在实际开发中很少定 义局部内部类,这是因为局部内部类的作用域太小了:只能在当前 方法中使用。大部分时候,定义一个类之后,当然希望多次复用这 个类,但局部内部类无法离开它所在的方法,因此在实际开发中很 少使用局部内部类。
匿名内部类适合创建那种只需要一次使用的类, 例如前面介绍命令模式时所需要的Command对象。匿名内部类的语法有点奇怪,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名 内部类不能重复使用。
定义匿名内部类的格式如下:
new实现接口() | 父类构造器(实参列表)
{
匿名内部类的类体部分
}
从上面定义可以看出,匿名内部类必须继承一个父类, 或实现一个接口,但最多只能继承一个父类,或实现一个接口。
关于匿名内部类还有如下两条规则。
➢ 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会 立即创建匿名内部类的对象。 因此不允许将匿名内部类定义成 抽象类。
➢ 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以 无法定义构造器, 但匿名内部类可以定义初始化块, 可以通过 实例初始化块来完成构造器需要完成的事情。
最常用的创建匿名内部类的方式是需要创建某个接口类型的对象,如下程序所示。
interface Product {
double getPrice();
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() {
public double getPrice() {
return 567.8;
}
public String getName() {
return "AGP显卡";
}
});
}
}
上面程序中的AnonymousTest类定义了一个test()方法,该方法需 要一个Product对象作为参数, 但Product只是一个接口, 无法直接创 建对象,因此此处考虑创建一个Product接口实现类的对象传入该方法 —如果这个Product接口实现类需要重复使用,则应该将该实现类定义 成一个独立类;如果这个Product接口实现类只需一次使用,则可采用 上面程序中的方式,定义一个匿名内部类。
正如上面程序中看到的,定义匿名内部类无须class关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象。上面
public double getPrice() {
return 567.8;
}
public String getName() {
return "AGP显卡";
}
代码部分就是匿名内部类的类体部分。
由于匿名内部类不能是抽象类, 所以匿名内部类必须实现它的抽 象父类或者接口里包含的所有抽象方法。
当创建匿名内部类时, 必须实现接口或抽象父类里的所有抽象方法。如果有需要,也可以重写父类中的普通方法。
在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; // ①
// 下面代码将会导致编译错误
// 由于age局部变量被匿名内部类访问了,因此age相当于被final修饰了
age = 2;
A a = new A() {
public void test() {
// 在Java 8以前下面语句将提示错误:age必须使用final修饰
// 从Java 8开始,匿名内部类、局部内部类允许访问非final的局部变量
System.out.println(age);
}
};
a.test();
}
}
Java 8 以 后 版 本 的 JDK 将 这 个 功 能 称 为 “effectively final”, 它的意思是对于被匿名内部类访问的局部变量, 可以用 final修饰,也可以不用final修饰,但必须按照有final修饰的方式 来用—也就是一次赋值后,以后不能重新赋值。
1.5使用内部类定义类的主要作用就是定义变量、创建实例和作为父类被继承。 定义内部类的主要作用也如此,但使用内部类定义变量和创建实例则 与外部类存在一些小小的差异。下面分三种情况讨论内部类的用法。
1.在外部类内部使用内部类 从前面程序中可以看出, 在外部类内部使用内部类时, 与平常使 用普通类没有太大的区别。 一样可以直接通过内部类类名来定义变 量,通过new调用内部类构造器来创建实例。
唯一存在的一个区别是:不要在外部类的静态成员(包括静态方 法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非 静态成员。
在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。
如果希望在外部类以外的地方访问内部类(包括静态和非静态两 种), 则内部类不能使用private访问控制权限, private修饰的内部 类只能在外部类内部使用。对于使用其他访问控制符修饰的内部类, 则能在访问控制符对应的访问权限内使用。
➢ 省略访问控制符的内部类,只能被与外部类处于同一个包中的 其他类所访问。
➢ 使用protected修饰的内部类, 可被与外部类处于同一个包中 的其他类和外部类的子类所访问。
➢ 使用public修饰的内部类,可以在任何地方被访问。
在外部类以外的地方定义内部类(包括静态和非静态两种)变量 的语法格式如下:
Outerclass.Innerclass varname
由于非静态内部类的对象必须寄生在外部类的对象里, 因此创建 非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的 地方创建非静态内部类实例的语法如下:
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("测试信息");
}
}
上面程序中粗体代码行创建了一个非静态内部类的对象。 从上面代码可以看出,非静态内部类的构造器必须使用外部类对象来调用。
如果需要在外部类以外的地方创建非静态内部类的子类, 则尤其 要注意上面的规则:非静态内部类的构造器必须通过其外部类对象来 调用。
当创建一个子类时, 子类构造器总会调用父类的构造器, 因此在 创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态 内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部 类对象。下面程序定义了一个子类继承了Out类的非静态内部类In类。
public class SubClass extends Out.In {
//显示定义SubClass的构造器
public SubClass(Out out) {
//通过传入的Out对象显式调用In的构造器
out.super("hello");
}
}
从上面代码中可以看出, 如果需要创建SubClass对象时, 必须先创建一个Out对象。这是合理的,因为SubClass是非静态内部类In类的 子类, 非静态内部类In对象里必须有一个对Out对象的引用, 其子类 SubClass对象里也应该持有对Out对象的引用。当创建SubClass对象时 传给该构造器的Out对象, 就是SubClass对象里Out对象引用所指向的对象。
非静态内部类In对象和SubClass对象都必须持有指向Outer对象的引用,区别是创建两种对象时传入Out对象的方式不同:当创建非静态 内部类In类的对象时, 必须通过Outer对象来调用new关键字;当创建 SubClass类的对象时,必须使用Outer对象作为调用者来调用In类的构造器。
非静态内部类的子类实例一样需要保留一个引用,该引用指向其 父类所在外部类的对象。也就是说,如果有一个内部类子类的对象 存在,则一定存在与之对应的外部类对象。
因为静态内部类是外部类类相关的, 因此创建静态内部类对象时无须创建外部类对象。在外部类以外的地方创建静态内部类实例的语法如下:
new Outerclass.Innerconstructor()
下面程序示范了如何在外部类以外的地方创建静态内部类的实例。
class StaticOut {
// 定义一个静态内部类,不使用访问控制符,
// 即同一个包中其他类可访问该内部类
static class StaticIn {
public StaticIn() {
System.out.println("静态内部类的构造器");
}
}
}
public class CreateStaticInnerInstance {
public static void main(String[] args) {
StaticOut.StaticIn in = new StaticOut.StaticIn();
}
}
从上面代码中可以看出, 不管是静态内部类还是非静态内部类, 它们声明变量的语法完全一样。区别只是在创建内部类对象时,静态 内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外 部类对象来调用构造器。
因为调用静态内部类的构造器时无须使用外部类对象, 所以创建 静态内部类的子类也比较简单, 下面代码就为静态内部类StaticIn类 定义了一个空的子类。
public class Staticsubclass extends Staticout.Staticin {}
从上面代码中可以看出, 当定义一个静态内部类时, 其外部类非常像一个包空间。



