文章目录
在一个类的内部定义的类称为内部类。内部类允许把一些逻辑相关的类组织在一起,并且控制内部类代码的可视性。
12.1 内部类的基本语法
变量按照作用域可进行如下分类:
|变量
|-成员变量
|--实例变量
|--静态变量
|-局部变量
内部类按照作用域可进行如下分类:
|内部类
|-成员内部类
|--实例内部类
|--静态内部类
|-局部内部类
顶层类只能处于 public 和默认访问级别,而成员内部类可以处于 public、protected、private 和默认这 4 种访问级别。
//在Outer类中定义了一个public的内部类InnerTool
package visitcontrol;
class Outer{
public class InnerTool { //如果不希望客户程序访问成员内部类可以定义为private类型
public int add(int a, int b) {return a + b;}
}
private InnerTool tool = new InnerTool(); //在Outer类中可以直接使用InnerTool类
public void add(int a, int b, int c) {
tool.add(tool.add(a, b), c);
}
}
public class Tester {
public static void main(String[] args) {
Outer o = new Outer();
o.add(1, 2, 3);
Outer.InnerTool tool = new Outer().new InnerTool(); //在Tester类中使用需要引用完整类名
}
}
成员内部类还可分为两种:实例内部类和静态内部类,后者用 static 修饰。不管是何种类型的内部类,都应该保证内部类与外部类不重名。
class A {
class A{ } //编译错误,不语序内部类与外部类重名
public void method() {
class A{ } //编译错误,不语序内部类与外部类重名
}
}
实例内部类
实例内部类是成员内部类的一种,没有 static 修饰。实例内部类具有以下特点:
- 在创建实例内部类的实例时,外部类的实例必须已经存在。例如要创建 InnerTool 类的实例,必须先创建 Outer 外部类的实例:
Outer.InnerTool tool = new Outer().new InnerTool(); //等价于 Outer outer = new Outer(); Outer.InnerTool tool = outer.new InnterTool(); //以下代码编译出错 Outer.InnerTool tool = new Outer.InnerTool();
- 实例内部类的实例自动持有外部类的实例的引用。在内部类中,可以直接访问外部类的所有成员,包括成员变量和成员方法。
类 B 之所以能访问类 A 的成员,是因为当内部类 B 的实例存在时,外部类 A 的实例肯定已经存在,实例 B 自动持有当前实例 A 的引用。在多重嵌套中,内部类可以访问所有外部类的成员。package outerref; public A { private int a1; public int a2; static int a3; public A(int a1, int a2) {this.a1 = a1; this.a2 = a2;} protected int methodA() {return a1 * a2;} class B { int b1 = a1; //直接访问private的a1 int b2 = a2; //直接访问public的a2 int b3 = a3; //直接访问static的a3 int b4 = new A(3, 4).a1; //访问一个新建的实例A的a1 int b5 = methodA(); //访问methodA()方法 } public static void main(String[] args) { A.B b = new A(1,2).new B(); System.out.println(b.b1); //打印1 System.out.println(b.b2); //打印2 System.out.println(b.b3); //打印0 System.out.println(b.b4); //打印3 System.out.println(b.b5); //打印2 } }
- 外部类实例与内部类实例之间是一对多的关系,一个内部类实例只会引用一个外部类实例,而一个外部类实例对应零个或多个内部类实例。在外部类中不能直接访间内部类的成员,必须通过内部类的实例去访问:
class A { class B{ private int b1 = 1; public int b2 = 2; class C {} } public void test() { int v1 = b1; //编译错误,不能直接访问内部类的成员变量 int v2 = b2; //编译错误,不能直接访问内部类的成员变量 B.C c1 = new C(); //编译错误,不能直接访问内部类B的内部类C B b = new B(); //合法,相当于 B b = this.new B() 语句 int v3 = b.b1; //合法,通过内部类B的实例去访问变量b1 int v4 = b.b2; //合法,通过内部类B的实例去访问变量b2 B.C c2 = b.new C(); //合法,通过内部类B的实例访问内部类C B.C c3 = new B().new C(); //合法,通过内部类B的实例访问内部类C } }
- 在实例内部类中不能定义静态成员,实例内部类中只能定义实例成员。
class A { class B { static int b1; //编译错误 int b2; //合法 static class C { } //编译错误 class D{ } //合法 } }
- 如果实例内部类 B 与外部类 A 包含同名的成员(比如成员变量 v),那么在类 B 中,this.v 表示类 B 的成员,A.this.v 表示类 A 的成员。
package diffnames; public class A { int v = 1; class B { int v = 2; public void test() { System.out.println(v); //打印2 System.out.println(this.v); //打印2 System.out.println(A.this.v); //打印1 } } public static void main(String[] args) { new A().new B().test(); } }
静态内部类
静态内部类是成员内部类的一种,用 static 修饰。静态内部类具有以下特点:
- 静态内部类的实例 不会 自动持有外部类的特定实例的引用,在创建内部类的实例时,不必创建外部类的实例。
class A { public static class B { //静态内部类 int v; } } class Tester{ public void test() { A.B b = new A.B(); //创建类B的实例时不必创建类A的实例 b.v = 1; //即无需 A.B b = new A().new B(); } }
- 静态内部类可以直接访问外部类的静态成员,如果访问外部类的实例成员,必须通过外部类的实例去访问。
class A { private int a1; private static int a2; public static class B { int b1 = a1; //编译错误,不能直接访问外部类A的实例变量a1 int b2 = a2; //合法,可以直接访问外部类A的静态变量a2 int b3 = new A().a1; //合法,通过类A的实例访问变量a1 } }
- 在静态内部类中可以定义静态成员和实例成员。
class A{ public static class B{ int vl; //实例变量 static int v2; //静态变量 public static class C{ //静态内部类 static int v3; } } }
- 客户类可以通过完整的类名直接访问静态内部类的静态成员。
package visitstatic; class A{ public static class B { int v1; static int v2; public static class C{ static int v3; int v4; } } } public class Tester { public void test() { A.B b = new A.B(); A.B.C c = new A.B.C(); b.v1 = 1; b.v2 = 1; A.B.v1 = 1; //编译错误 A.B.v2 = 1; //合法 A.B.C.v3 = 1; //合法 } }
局部内部类
局部内部类是在一个方法中定义的内部类,它的可见范围是当前方法。和局部变量一样, 局部内部类 不能 用访问控制修饰符(public、private 和 protected),以及 static修饰符来修饰。局部内部类具有以下特点:
- 局部内部类只能在当前方法中使用。
class A { B b = new B(); //编译错误 public void method() { class B { int v1; int v2; class C { int v3; } } B b = new B(); //合法 B.C c = b.new C(); //合法 } }
- 局部内部类和实例内部类一样,不能包含静态成员。
class A { public void method() { class B { static int v1; //编译错误 int v2; static class C { //编译错误 int v3; } } } }
- 在局部内部类中定义的内部类也不能被 public、protected 和 private 这些访问控制修饰符修饰。
- 局部内部类和实例内部类一样,可以访问外部类的所有成员,此外,局部内部类还可以访问所在方法中的符合以下条件之一的参数和变量:
- 最终变量或参数:用 final 修饰。
- 实际上的最终变量或参数:虽然没有用 final 修饰,但是程序不会修改变量的值。
class A { int a; public void method(final int p1, int p2) { int localV1 = 1; final int localV2 = 2; int localV3 = 0; localV3 = 3; class B { int b1 = a; //合法,访问外部类的实例变量 int b2 = p1; //合法,访问final类型的参数 int b3 = p2; //合法,访问实际上的最终参数p2 int b4 = localV1; //合法,访问实际上的最终变量 int b5 = localV2; //合法,访问最终变量 int b6 = localV3; //编译出错,localV3发生了修改 } } }
12.2 内部类的继承
//外部类 Sample 继承了另一个外部类 Outer 的内部类 Inner
//每个 Sample 实例必须自动引用 一个 Outer 实例
//当调用 一个 Sample 实例的 print() 方法时, print() 方法会访问当前 Outer 实例的成员变量 a
package inherit;
class Outer {
private int a;
public Outer(int a) { this.a = a; }
class Inner{
public Inner() {}
public void print() {
System.out.println(a); //访问外部类的实例变量a
}
}
}
public class Sample extends Outer.Inner {
public Sample() {} //编译出错
public Sample(Outer o) {
o.super();
}
public static void main(String[] args) {
Outer outer1 = new Outer(1);
Outer outer2 = new Outer(2);
Outer.Inner in = outer1.new Inner();
in.print(); //打印1
Sample s1 = new Sample(outer1);
Sample s2 = new Sample(outer2);
s1.print(); //打印1
s2.print(); //打印2
}
}
在直接构造实例内部类的实例的时候,Java 虚拟机会自动使内部类实例引用它的外部类实例。
//inner变量引用的Inner实例会自动引用outer1变量引用的实例
Outer outer1 = new Outer();
Outer.Inner inner = outer1.new Inner();
但是如果通过以下形式构造 Sample 实例, Java 虚拟机无法决定让 Sample 实例引用哪个 Outer 实例:
Sample s = new Sample();
为了避免这种错误,在编译阶段, Java 编译器会要求 Sample 类的构造方法必须通过参数传递一个 Outer 实例的引用,然后在构造方法中调用 super
语句来建立 Sample 实例与 Outer 实例的关联关系:
public Sample(Outer o) {
o.super();
}
通过以上构造方法创建 Sample 实例时,Java 虚拟机会使它引用参数指定的 Outer 实例。在以上代码中,s1 与 outerl 关联,s2 与 outer2 关联,执行 s1.print() 方法时, 会打印 outer1 实例的变量 a, 执行 s2.print() 方法时,会打印 outer2 实例的变量 a。
12.3 子类与父类的内部类同名
内部类并不存在覆盖的概念,如果子类与父类中存在同名的内部类,那么这两个内部类分别在不同的命名空间中,不会发生冲突。
package nooverride;
class Outer {
Inner in;
Outer() { //构造 Outer.Inner类的实例
in = new Inner();
}
public class Inner {
public Inner() {
System.out.println("inner of Outer");
}
}
public class SubOuter extends Outer{
class Inner{
public Inner() {
System.out.println("inner of SubOuter");
}
}
public static void main(String[] args) {
SubOuter.Inner in1 = new SubOuter().new Inner();
Outer.Inner in2 = new Outer().new Inner();
}
}
}
/*打印结果
inner of Outer
inner of SubOter
inner of Outer
inner of Outer
*/
执行 new SubOuter() 语句时,Java 虚拟机会调用 Outer 父类的构造方法;在执行该构造方法中的 new Inner() 语句时,Java 虚拟机会构造 Outer.Inner 类的实例,而不是SubOuter.Inner 类的实例。
12.4 匿名类
匿名类是一种特殊的内部类,这种类没有名字。
package noname;
public class A {
A () {
System.println("default constructor");
}
A (int v) {
System.println("another constructor");
}
void method() {
System.println("from A");
}
public static void main(String[] args) {
new A().method(); //打印 from A
A a = new A() { //匿名类
void method() {
System.println("from anonymous");
}
};
a.method(); //打印 from anonymous
}
}
/*打印结果
default constructor
from A
default constructor
from anonymous
*/
以上 new A(){..}
定义了一个继承类 A 的匿名类,大括号内是类 A 的类体,newA(){..}
返回匿名类的一个实例的引用。此处的匿名类相当于在 main()
方法内定义了一个局部内部类,并且创建了它的实例。
// A a = new A(){..} 相当于以下代码,假定局部内部类命名为SubA
class SubA extends A {
void method() {
System.println("from anonymous");
}
}
A a = new SubA();
匿名类具有以下特点:
- 匿名类本身没有构造方法,但是会调用父类的构造力法。
在以上public static void main(String[] args) { int v= 1; //v = 2; 修改变量会导致不能访问变量v致使编译出错 A a = new A(v) { //匿名类 void method() {System.out.println("from anonymous");} }; a.method(); //打印 from anonymous } // 打印结果: another constructor from anonymous
new A(v){..}
中,如果参数 v 是局部变量,并且在匿名类的类体中会使用它,那么 v 必须是满足以下条件之一, 否则会导致编译错误:- 最终变量:用 final 修饰。
- 实际上的最终变量:虽然没有用 final 修饰,但是程序不会修改变量的值。
- 匿名类尽管没有构造方法,但是可以在匿名类中提供一段实例初始化代码,Java 虚拟机会在调用了父类的构造方法后,执行这段代码:
由此可见,实例初始化代码具有和构造方法同样的效果,不过,前者不允许被重载,匿名类的实例只能有一种初始化方式。public static void main(String[] args) { int v = 1; A a = new A(v){ //匿名类 { System.out.println("initialize instance");} //实例初始化代码 void method() {System.out.println("from anonymous");} }; a.method(); } /*打印结果 another constructor initialize instance from anonymous */
- 除了可以在外部类的方法内定义匿名类,还可以在声明一个成员变量时定义匿名类。
//以下类 A 有一个实例变量 a, 它引用一个继承类 A 的匿名类的实例 abstract class A { A a = new A() { void method() {System.out.println("inner");} }; abstract void method(); }
- 匿名类除了可以继承类,还可以实现接口。
以上匿名类实现了class Sample{ public static void main(String[] args) { Thread t = new Thread(new Runnable() { public void run() { for (int i = 0; i < 100; i++) System.out.println(i); } }); t.start(); } }
Java.lang.Runnable
接口,这个匿名类的实例的引用作为参数,传给 Java.lang.Thread 类的构造方法。 main() 方法的第一条语句相当于以下代码 :Runnable r = new Runnable() { //创建匿名类的实例 public void run() { for (int i = 0; i < 100; i++) System.out.println(i); } }; Thread t = new Thread(r); //把匿名类的实例传给Thread类的构造方法
- 匿名类和局部内部类一样,可以访问外部类的所有成员,如果匿名类位于一个方法中,还能访问所在方法的最终变量和参数,或者实际上的最终变量和参数。
- 局部内部类的名字在方法外是不可见的,因此与匿名类一样,能够起到封装类型名字的作用。局部内部类与匿名类有以下区别:
- 匿名类的程序代码比较简短 。
- 一个局部内部类可以有多个重载构造方法,并且客户类可以多次创建局部内部类的实例。而匿名类没有重载构造方法,并且只能创建一次实例。
因此,如果只需创建内部类的一个实例,那么可以用匿名类,它能使程序代码比较简洁,如果需要多次创建内部类的实例,那么用局部内部类。
12.5 内部接口以及接口中的内部类
在一个类中也可以定义内部接口。
// Outer 类中有一个静态内部接口 Tool
// Outer 类的一个匿名内部类实现了这一接口
// 此外一个顶层类 MyTool 也实现了这个接口
package innerinterface;
public class Outer {
public static interface Tool { //静态内部接口
public int add(int a, int b);
}
private Tool tool = new Tool(){ //匿名类
public int add(int a, int b) {
return a + b;
}
};
public void add(int a, int b, int c) {
tool.add(tool.add(a, b), c);
}
public void setTool(Tool tool) {
this.tool = tool;
}
class MyTool implements Outer.Tool {
public int add(int a, int b) {
int res = a + b;
System.out.println(res);
return res;
}
}
}
在接口中可以定义静态内部类,此时静态内部类位于接口的命名空间中。
// 以下接口 A 中定义了静态内部类 B, 类 C 实现了接口 A, 类 B 的名字对类 C 是可见的,
// 但在类 D 中,必须通过 A.B 的形式使用类 B
public interface A {
static class B {}
public void method(B b);
}
class C implements A {
B b = new B();
public void method(B b) {}
}
class D {
A.B b1 = new A.B(); //合法
B b2 = new B(); //编译出错
}
12.6 内部类的用途
封装类型
面向对象的核心思祀之一是封装:把所有不希望对外公开的实现细节封装起来。顶层类只能处于 public 和默认访问级别,而成员内部类可以处于 public、protected、默认和 private 4 个访问级别。此外,如果一个内部类仅仅为特定的方法提供服务,那么可以把这个内部类定义在方法之内。可见,内部类是一种封装类型的有效手段。
// 以下 private 类型的 InnerTool 内部类实现了公共的 Tool 接口
public interface Tool {
public int add(int a,int b);
}
public class Outer{
private class lnnerTool implements Tool{
public int add(int a,int b) {
return a + b;
}
}
public Tool getTool(){
return new InnerTool();
}
}
在客户类中不能访问 Outer.InnerTool 类,但是可以通过 Outer 类的 getTool() 方法获得 InnerTool 的实例:
Tool tool = new Outer().getTool(); //InnerTool 实例向上转型为 Tool 类型
由此可见,私有内部类可以用来封装类型。
直接访问外部类的成员
内部类的一个特点是能够访问外部类的各种访问级别的成员。
假设有类 A 和类 B,类B的 reset() 方法负责重新设置类 A 的实例变量 count 的值。
// 第一种实现方式:把类A和类B都定义为外部类
// 为了使类B能够访问类A的count属性,类A必须提供 getCount() 和 setCount()方法。
// 此外, 还建立了类B到类A的关联关系。
class A{
private int count;
public int add() {
return ++count;
}
public getCount() {return count;}
public void setCount(int count) {this.count = count;}
}
class B {
A a; //类B 与 类A 关联
B(A a) {this.a = a;}
public void reset() {
if (a.getCount() > 0) a.setCount(1);
else a.setCount(-1);
}
}
假如应用需求要求类 A 的 count 属性不允许被除类 B 以外的其他类读取或设置,那么以上实现方式就不能满足这一要求。
//第二种实现方式:把B定义为内部类
class A {
private int count;
public int add() {
return ++count;
}
class B {
public void reset() {
if (count > 0) count = 1;
else count = -1;
}
}
}
当类 B 作为类 A 的 实例内部类时,可以直按访问类 A 的 count 属性。Java 虚拟机会保证类 B 的实例持有类 A 的实例的引用,因此无须显式地建立类 B 与类 A 的关联。
回调
在以下 Adjustable 接口和 Base 类中都定义了 adjust() 方法。这两个方法的参数签名相同,但是有着不同的功能
public interface Adjustable {
public void adjust(int temperature); //调节温度
}
public class Base {
private int speed;
public void adjust(int speed) {this.speed = speed;} //调节速度
}
如果有一个 Sub 类同时具有调节温度和调节速度的功能,那么 Sub 类需要继承 Base类,并且实现 Adjustable 接口,但是以下代码并不能满足这一需求:
public class Sub extends Base implements Adjustable {
private int temperature;
public void adjust(int temperature) {this.temperature = temperature;}
}
以上 Sub 类实现了 Adjustable 接口中的 adjust() 方法,并且把 Base 类中的 adjust() 方法覆盖了,这意味着 Sub 类仅仅有调节温度的功能,但失去了调节速度的功能。可以用内部类来解决这一问题:
public class Sub extends Base {
private int temperature;
private void adjustTemperature(int temperature) {
this.temperature = temperature;
}
private class Closure implements Adjustable {
public void adjust(int temperature) {
adjustTemperature(temperature);
}
}
public Adjustable getCallBackReference() {
return new Closure();
}
}
为了使 Sub 类既不覆盖 Base 类的 adjust() 方法,又能实现 Adjustable 接口的 adjust()方法,Sub 类采取了以下措施:
- 定义了 一个 private 类型的 adjustTemperature() 方法,该方法实现了调节温度的功能。为了避免覆盖 Base 类的 adjust() 方法,故意取了不同的名字。
- 定义了 一个 private 类型的实例内部类 Closure, 这个类实现了 Adjustable 接口,在这个类的 adjust() 方法中,调用 Sub 外部类的 adjustTemperature()方法。
- 定义了 getCallBackReference() 方法,该方法返回 Closure 类的实例。
以下代码演示客户类使用 Sub 类的调节温度的功能:
Sub sub = new Sub();
Adjustable ad = sub.getCallBackReference();
ad.adjust(15);
客户类先调用 Sub 实例的 getCallBackReference() 方法,获得内部类 Closure
的实例,然后再调用 Closure 实例的 adjust()
方法,该方法又调用 Sub 实例的 adjustTemperamre()
方法。这种调用过程称为 回调(CallBack)。
回调实质上是指一个类尽管实际上实现了某种功能,但是没有直接提供相应的接口,客户类可以通过这个类的内部类的接口来获得这种功能。而这个内部类本身并没有提供真正的实现,仅仅调用外部类的实现。可见,回调充分发挥了内部类具有访问外部类的实现细节的优势。
12.7 内部类的类文件
对于每个内部类,Java 编译器会生成独立的 .class
文件。这些类文件的命名规则如下:
- 成员内部类:外部类的名字$内部类的名字。
- 局部内部类:外部类的名字$数字和内部类的名字。
- 匿名类:外部类的名字$数字。
在以下程序中定义了各种类型的内部类:
class A { //外部类 A.class
static class B {} //成员内部类 A$B.class
class C { //成员内部类 A$C.class
class D {} //成员内部类 A$C$D.class
}
public void method1() {
class E {} //局部内部类1 A$1E.class
B b = new B() {} //匿名类1 A$1.class
C c = new C() {} //匿名类2 A$2.class
}
public void method2() {
class E {} //局部内部类2 A$2E.class
}
}