内部类基础
什么是内部类
在 Java 中,可以将一个类的定义放在另一个类的定义内部,这就是内部类。
内部类的创建
内部类的创建就像它的定义一样,将类置于其他类的内部即可,如下:
public class OutClass {
/**
* 内部类
*/
class InnerClass {
private String info = "a inner class";
String getInfo() {
return info;
}
}
/**
* 调用内部类的方法
*/
public void apply() {
InnerClass innerClass = new InnerClass();
System.out.println(innerClass.getInfo());
}
}
class Test{
public static void main(String[] args) {
OutClass outClass = new OutClass();
outClass.apply();
}
}
我们可以看到,在 apply() 方法中调用内部类的时候,和调用普通的类没有什么不同,现在看看下面这段代码:
public class OuterClass {
/**
* 内部类
*/
class InnerClass {
private String info = "a inner class";
String getInfo() {
return info;
}
}
/**
* 返回内部类的对象
* @return
*/
public InnerClass getInnerClass() {
return new InnerClass();
}
}
class Test{
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.getInnerClass();
System.out.println(innerClass.getInfo());
}
}
在这段代码中,加入了一个 getInnerClass() 方法,该方法返回一个 InnerClass 对象,在 Test 类的 main 方法中通过 getInnerClass() 这样的非静态方法去获取内部类的对象的时候,就必须显示地指明这个内部类对象的类型为:OuterClass.InnerClass。
与外部类的联系
当在一个类中创建一个内部类的时候,这个内部类就与这个外部类之间存在一种联系,至于这个联系是什么,看下面这段代码:
public class OuterClass {
//成员变量 num
private int num = 0;
/**
* 加法操作
* @return
*/
public int add(){
for (int i = 0; i < 10; i++) {
num ++;
}
return num;
}
/**
* 内部类
*/
class InnerClass {
/**
* 获取初始值
* @return
*/
int getInitialNum(){
return num;
}
/**
* 获取最终值
* @return
*/
int getFinalNum(){
return add();
}
}
/**
/**
* 返回内部类的对象
*
* @return
*/
public InnerClass getInnerClass() {
return new InnerClass();
}
}
class Test{
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.getInnerClass();
System.out.println(innerClass.getInitialNum());
System.out.println(innerClass.getFinalNum());
}
}
我们看看内部类 InnerClass 中的两个方法,分别调用变量 num 和方法 add(),但是变量 num 和方法 add() 都不属于内部类 InnerClass 而是属于外部类 OuterClass,但是内部类却可以直接调用,所以我们得到一个结论,就是内部类自动拥有外部类所有成员的访问权。当使用外部类对象创建内部类对象的时候,内部类对象就会捕获一个指向外部类对象的引用,当访问外部类的成员的时候,就是用这个引用来操作的。
.this 语法
当需要在内部类中生成外部类对象的时候,就可以直接使用这种格式:OuterClass.this,这样就能产生外部类的引用,并且没有任何运行时开销。而且如果在内部类中访问与外部类同名的成员变量和方法的时候,也需要使用 .this 语法,格式如下:
OuterClass.this.field;
OuterClass.this.f();
具体可以参考如下代码:
public class OuterClass {
//外部类成员变量
public String info = "ourter class";
/**
* 内部类
*/
class InnerClass {
//内部类成员变量
public String info = "inner class";
/**
* 创建外部类对象
* @return
*/
public OuterClass getOuterClass() {
return OuterClass.this;
}
/**
* 获取内部类成员
* @return
*/
public String getInfo(){
return info;
}
/**
* 获取外部类成员
* @return
*/
public String getOutInfo(){
return OuterClass.this.info;
}
}
/**
* /**
* 返回内部类的对象
*
* @return
*/
public InnerClass getInnerClass() {
return new InnerClass();
}
}
class Test {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.getInnerClass();
System.out.println(innerClass.getInfo());
System.out.println(innerClass.getOutInfo());
System.out.println(innerClass.getOuterClass().info);
}
}
运行 Test 类中的 main 方法,打印结果如下:
inner class
ourter class
ourter class
当在内部类 InnerClass 的 getNum() 方法中直接访问 num 成员的时候,默认的是访问内部类中的成员,可参考打印结果,如果要访问外部类中的成员变量的时候,就需要使用 OuterClass.this.info 来访问。当然也可以先创建外部类对象,然后通过外部类的对象来访问。
.new 语法
要想直接创建一个内部类对象的时候,就需要使用 .new 语法在 new 表达式中提供对其他外部类对象的引用,格式:outerClass.new InnerClass();代码如下:
public class OuterClass {
/**
* 内部类
*/
class InnerClass {
}
}
class Test {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
//通过外部类对象来创建内部类对象
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
}
}
要直接创建内部类的对象,就必须使用外部类的对象来创建该内部类的对象,内部类对象和外部类对象是相互联系的,在创建一个外部类对象之前是不可能创建内部类对象的。
内部类的向上转型
内部类和普通类一样,也可以进行向上转型。代码如下:
public class OuterClass {
/**
* 内部类实现 Contents 接口
*/
public class InnerClass implements Contents {
private int i = 10;
@Override
public int value() {
return i;
}
}
/**
* 返回 Contents 对象
* @return
*/
public Contents getContents() {
//向上转型
return new InnerClass();
}
}
class Test {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
Contents contents = outerClass.getContents();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
//通过 contents 对象访问 value 方法
System.out.println(contents.value());
//通过 innerClass 对象访问 value 方法
System.out.println(innerClass.value());
}
}
执行 Test 中的 main 方法,打印结果如下:
10
10
代码中,InnerClass 类实现了接口 Contents 中的 value() 方法,然后通过 getContents() 方法向上转型。可以通过 contents 对象和 innerClass 对象来访问 value() 方法,结果是一样的。但是如果想隐藏具体实现,该怎么操作呢,结着往下看。
内部类的访问权限
当我们想控制属性或方法的访问权限的时候,可以通过访问修饰符来解决这一问题,对于内部类,也可以采用这种办法,就像这段代码:
public class OuterClass {
/**
* 内部类实现 Contents 接口
*/
private class InnerClass implements Contents {
private int i = 10;
@Override
public int value() {
return i;
}
}
/**
* 返回 Contents 对象
* @return
*/
public Contents getContents() {
//向上转型
return new InnerClass();
}
}
class Test {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
Contents contents = outerClass.getContents();
//OuterClass.InnerClass innerClass = outerClass.new InnerClass();
//通过 contents 对象访问 value 方法
System.out.println(contents.value());
//通过 innerClass 对象访问 value 方法
//System.out.println(innerClass.value());
}
}
代码中,我们将内部类 InnerClass 的访问修饰符改为 private,这样除了 OuterClass 类,没有人能访问 InnerClass 了,也无法通过 new 关键字来直接获取 InnerClass 的对象了,而且因为 private 关键字的存在,Contents 无法向下转型为 InnerClass 类。所以 private 内部类给程序猿们提供了一种全新的方式,这种方式可以完全阻止任何依赖于类型的代码,并且可以完全隐藏实现的细节。
方法中的内部类
我们总结下,上面的代码做了两件事情:
-
实现了一个接口,然后创建并返回这个接口的对象。
-
创建一个类来解决这个问题,而且不希望这个类是公共可用的。
要实现以上功能,除了 private 内部类,还有其他的方式,比如方法中的内部类,我们修改一下上面的代码:
public class OuterClass {
/**
* 获取 Contents 对象
* @return
*/
public Contents getContents() {
/**
* 创建一个方法中的内部类
*/
class InnerClass implements Contents {
private int i = 10;
@Override
public int value() {
return i;
}
}
//向上转型
return new InnerClass();
}
}
class Test {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
//创建 contents 对象
Contents contents = outerClass.getContents();
System.out.println(contents.value());
}
}
我们在方法 getContents() 中创建了一个实现 Contents 接口的内部类 InnerClass,然后通过 return 语句的向上转型,返回一个 Contents 的对象。由于 内部类 InnerClass 是 getContents() 的一部分,而不是外部类 OuterClass 的一部分,所以除了 getContents() 方法,其余的地方都不能访问 InnerClass, 有效隐藏了实现细节。
作用域中的内部类
除了可以在方法中创建内部类以外,还可以在任意的作用域内创建内部类。代码如下:
public class OuterClass {
/**
* 获取 Contents 对象
* @return
*/
public Contents getContents(boolean flag) {
if(flag){
/**
* 创建 if 语句作用域内的内部类
*/
class InnerClass implements Contents {
private int i = 10;
@Override
public int value() {
return i;
}
}
return new InnerClass();
}
return null;
}
}
class Test {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
//创建 contents 对象
Contents contents = outerClass.getContents(true);
}
}
在这段代码中,我们创建了一个 if 作用域内的内部类 InnerClass,但是这并不是说这个内部类的创建是有条件的,它其实已经和其他的类一起编译过了。但是,在定义这个内部类的作用域之外,它是不可用的。
什么是嵌套类
我们在 内部类基础 中提到过,内部类和外部类是紧密联系的,并且内部类可以自动获得外部类所有成员的访问权。但是如果不需要内部类对象与外部类对象之间有联系的话,那么就可以将内部类声明为 static,这就是嵌套类。
普通的内部类对象都隐式的保存了一个指向外部类的引用,但是嵌套类就不是这样了,具体体现在两个方面:
- 创建嵌套类的对象,不需要外部类对象;
- 不能从嵌套类访问非静态的外部类对象;
除去以上两点,普通内部类和嵌套类还有一个区别,那就是普通的内部类不能包含静态的属性、方法和嵌套类,但是嵌套类可以包含这些东西。
public class OuterClass {
//外部类非静态成员
private int i = 30;
//外部类静态成员
private static int j = 30;
/**
* 嵌套类
*/
public static class InnerClass1 implements Contents {
//静态属性
private static int m = j;
//该操作不允许,不能访问非静态成员
//private static int k = OuterClass.this.i;
@Override
public int value() {
return m;
}
/**
* 静态方法
*/
public static void f(){}
/**
* 嵌套类中的嵌套类
*/
static class InnerClass2 {
//静态属性
static int n = 20;
/**
* 静态方法
*/
public static void f(){}
}
}
}
其中的接口 Contents:
public interface Contents {
int value();
}
所以,通过代码可以发现,代码中试图通过 .this 语法访问外部类的非静态成员 i,但是编译器报错了。所以在普通的内部类中通过 .this 语法访问外部类非静态成员的方式,在嵌套类中已经不管用了,嵌套类没有这种特殊的 this 引用,有点像一个 static 方法。
接口中的嵌套类
接口中除了可以放置成员和方法,也可以放置类,由于任何位于接口中的类都自动地是 public 和 static 的,所以接口中的类都是嵌套类,嵌套类可以直接访问接口中的成员(因为接口中的成员都被隐式地指定为 static 和 final 的),并且可以用来实现外部接口。代码如下:
public interface OuterInterface {
//成员(默认是 static 和 final)
int N = 20;
//抽象方法
void f();
/**
* 嵌套类
*/
class InnerClass implements OuterInterface {
@Override
public void f() {
System.out.println("N = " + N);
}
public static void main(String[] args) {
new InnerClass().f();
}
}
}
嵌套类 InnerClass 可以直接在接口中实现外部接口 OuterInterface,并且可以直接访问接口中的成员变量 N,耳目一新,有木有!!
多重继承
内部类最吸引人的原因就是:每个内部类都能独立地继承一个类或实现一个接口,无论外部类是否已经继承了某个类或实现了某个接口,对于内部类都没有影响。如果没有内部类提供的,可以继承多个具体的或抽象的类的能力,有些编程问题就变得难以解决,因此,内部类使得多重继承的方案变得完整,接口解决了部分问题(一个类可以实现多个接口),而内部类有效地解决了“多重继承”,换句话说,内部类允许继承多个类(普通类或者抽象类)。代码如下:
public class MutipleImplement {
public static void main(String[] args) {
C c = new C();
B b = c.getB();
A a = c;
}
}
/**
* 普通类
*/
class A {
}
/**
* 抽象类
*/
abstract class B{
/**
* 抽象方法
*/
public abstract void f();
}
class C extends A{
/**
* 以匿名内部类的方式返回 B 的对象
* @return
*/
B getB(){
return new B() {
@Override
public void f() {
}
};
}
}
我们创建了普通类 A 和抽象类 B,如果要同时继承这两个类,怎么办?于是创建了类 C 去继承类 A,然后在类 C 中通过方法 getB() 以匿名内部类的方式返回类 B 的对象,这就解决了“多重继承”的问题。
闭包
闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。从这个概念来看,内部类就是面向对象的闭包,因为内部类自动拥有外部类所有成员的访问权,包括 private 的。
public class OuterClass extends MyImplements{
/**
* 重写 value 方法
* @return
*/
@Override
public int value() {
int value = super.value();
return ++value;
}
/**
* 实现 Contents 的内部类
*/
private class InnerClass implements Contents {
@Override
public int value() {
//直接调用外部类的 value 方法
return OuterClass.this.value();
}
}
/**
* 获取 Contents 对象
* @return
*/
public Contents getContents(){
//向上转型
return new InnerClass();
}
}
class ContentImplement implements Contents{
private int i = 0;
@Override
public int value() {
return i;
}
}
class MyImplements{
private int i = 5;
public int value(){
return i;
}
}
class Test {
public static void main(String[] args) {
ContentImplement contentImplement = new ContentImplement();
OuterClass outerClass = new OuterClass();
Contents c1 = contentImplement;
Contents c2 = outerClass.getContents();
System.out.println(c1.value());
System.out.println(c2.value());
}
}
上述代码进一步展示了外部类实现接口和内部类实现接口的区别。普通类 ContentImplement 实现接口 Contents 是简单的解决方式,外部类 OuterClass 继承 MyImplement 然后重写其中的 value() 方法,其内部类 InnerClass 实现 Contents 接口,并且只提供 getContents() 方法去获取 Contents 对象,这样无论谁获得此 Contents 对象,都只能调用 value() 方法,除此之外没有其他功能。
命令模式与内部类
在单个的外部类中,可以让每个内部类以不同的方式去实现一个接口,或者继承一个类,就像下面这段代码:
public class OuterClass extends Controller{
private int i = 1;
public class Increase extends Event {
public Increase(long delayTime) {
super(delayTime);
}
@Override
public void action() {
System.out.println(++i);
}
}
public class Decrease extends Event {
public Decrease(long delayTime) {
super(delayTime);
}
@Override
public void action() {
System.out.println(--i);
}
}
}
/**
* 事件类
*/
abstract class Event {
protected long delayTime;
public Event(long delayTime) {
this.delayTime = delayTime;
}
//抽象方法
public abstract void action();
}
/**
* 控制类
*/
class Controller {
//event 集合
private List<Event> list = new ArrayList<>();
/**
* 添加 event
* @param event
*/
public void addEvent(Event event){
list.add(event);
}
/**
* 执行 action
*/
public void run() {
for(Event event : list){
event.action();
}
}
}
class Test {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
outerClass.addEvent(outerClass.new Increase(100));
outerClass.addEvent(outerClass.new Decrease(200));
outerClass.run();
}
}
上述代码使用内部类,在一个单一的外部类中对同一个父类 Event 导出了多个版本(Increase 和 Decrease),这样可以更方便的将一个请求封装成一个对象,从而根据不同的请求对客户进行参数化,这样客户就可以寻找可以处理该命令的合适的对象,并执行命令,这就是命令模式。命令模式是一种特殊的策略模式,策略模式体现的是从多个策略中选择一个而且策略模式是接口的最基本使用方法,而命令模式体现的是多个策略执行的问题。
内部类的继承
我们知道在使用外部类对象创建内部类对象的时候,内部类对象就会捕获一个指向外部类对象的引用,因为内部类的构造器必须连接到指向其外部类对象的引用,所以在继承内部类的时候,那个指向外部类对象的引用必须被初始化,但是在子类中不再存在可连接的默认对象。所以需要使用特殊的语法来说明它们之间的关联,上代码:
public class OuterClass {
/**
* 内部类
*/
class InnerClass {}
}
/**
* 继承内部类
*/
class InheritInner extends OuterClass.InnerClass {
/**
* 构造器需要传入外部类对象
* @param outerClass
*/
public InheritInner(OuterClass outerClass) {
outerClass.super();
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
InheritInner inheritInner = new InheritInner(outerClass);
}
}
子类 InheritInner 只继承自内部类 InnerClass,在子类的构造器中必须要传入一个外部类的对象,而且必须在构造器中使用 outerClass.super(),才能够成功的构造出一个继承自内部类的对象。
还有一种继承内部类的方式:
public class OuterClass {
/**
* 内部类
*/
protected class InnerClass {
public InnerClass() {
}
}
}
/**
* 继承外部类
*/
class InheritOuter extends OuterClass {
/**
* 继承内部类
*/
public class InheritInner extends OuterClass.InnerClass {
public InheritInner() {
}
}
}
这种方式就比较简单粗暴了,直接通过内部类 InheritOuter.InheritInner 去继承父类中的内部类 OuterClass.InnerClass,由于 InheritInner 持有指向 InheritOuter 的引用,同时 InheritOuter 继承了 OuterClass,所以 InheritInner 也持有了指向 OuterClass 的引用了,不再需要 super 关键字了。
内部类能被覆盖吗
我们知道创建一个类去继承一个父类,然后重写父类中的方法,这个是可以做到的,但是同样的事情会不会发生在内部类中呢?也就是说,创建一个类去继承一个外部类,然后重新定义这个外部类中的内部类,那么会覆盖这个内部类吗?用代码说明一切:
public class OuterClass {
/**
* 内部类
*/
protected class InnerClass {
public InnerClass() {
System.out.println("InnerClass constructor");
}
}
public OuterClass() {
System.out.println("OuterClass construtor");
new InnerClass();
}
}
/**
-
继承内部类
*/
class InheritOuter extends OuterClass {/**
- 内部类
*/
public class InnerClass {
public InnerClass() {
System.out.println(“InheritOuter constructor”);
}
}
public static void main(String[] args) {
new InheritOuter();
}
} - 内部类
我们用 InheritClass 去继承外部类 OuterClass,并且重新定义了它的内部类 InnerClass,那么当我通过调用子类构造器去调用内部类 InnerClass 的构造器的时候,它会调用哪一个呢,是父类的还是子类的?看打印结果:
OuterClass construtor
InnerClass constructor
从打印结果可以看出,调用的是父类 OuterClass 的内部类构造器,这说明当继承了某个外部类的时候,内部类并没有发生什么特别的变化,这两个内部类是完全独立的两个实体,各自在自己的命名空间内。
内部类标识符
每个类编译后都会产生一个 .class 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个"meta-class",叫做 Class 对象),那么如果是内部类呢,会产生怎样的 .class 文件?且看代码:
public class OuterClass {
/**
* 内部类
*/
public class InnerClass {
//内部类中的内部类
class InnerClass2 {
}
}
public Contents getContents() {
//匿名内部类
return new Contents() {
@Override
public int value() {
return 0;
}
};
}
}
我们在代码中定义了一个内部类 InnerClass,然后在其内部又定义了一个内部类 InnerClass2,还定义了一个匿名内部类,我们看看编译后的文件名:
OuterClass$InnerClass.class
OuterClass$InnerClass$InnerClass2.class
OuterClass$1.class
OuterClass.class
可以看到,内部类也必须生成一个 .class 文件已包含它们的 Class 对象信息。并且有这严格的命名规则:外部类的名字,加上 “$”,再加上内部类的名字。如果内部类中还有内部类,那么直接加上 “$” 然后将名字加在后面;如果内部类是匿名的,那么编译器会产生一个数字作为其标识符,如上例中的 OuterClass$1.class。