本篇文章的标题是,Java内部类。其实这样称呼是不对的。因为我们想要表达的其实是Java nested class。而Java nested class 包含有一个分支叫做 non-static nested class 或者 叫 Inner class。所以说,只是我觉得在Thinking in Java翻译中我觉得可能产生puzzle的地方。
正如我们之前提到的,nested class(应该翻译为嵌套类,但是我们的Java 中又会把嵌套类对应为 static nested class,所以我觉得这里不妥)一共分为四种类型:
1. static nested class
2. non-static nested class / inner class includes following three types:
2.1) member class
2.2) local class
2.3)anonymous class
在本篇文章中,我只会对四个nested class 做最基本的解释,non-nested class 中的.this 和 .new 语法使用,讲解我们使用nested class 来实现Java 中的闭包和回调的示例,同时会涉及到nested class 的继承和覆盖的一部分内容。
1.基本概念:
non-static nested class / inner class:非静态内部类(在Thinking in Java 中译作 内部类)。其包含上述三种类型:
a. member class:成员类,也就是我们最直观的理解的内部类。在外围类的内部直接声明。在我们声明一个内部类对象的时候,此对象与制造他的外部对象之间就存在这一种特殊关系。内部类可以无条件的使用外部类的所有元素的访问权,包括private等。我们可以通过.this来生成对外部类对象的引用,也可以通过.new来生成内部类对象。
public class A {
void sayA(){
System.out.println("A");
}
public class B{
public A getA(){
return A.this;
}
}
public B getB(){
return new B();
}
public static void main(String args []){
A a = new A();
A.B b = a.getB();
b.getA().sayA();
}
}
我们还可以使用.new对上面的代码进行小小改动:
public class A {
void sayA(){
System.out.println("A");
}
public class B{
public A getA(){
return A.this;
}
}
public B getB(){
return new B();
}
public static void main(String args []){
A a = new A();
A.B b = a.new B();
b.getA().sayA();
}
}
注意:我们在任何时候构造一个内部类(这里指的是我们这种成员类)的时候,都要确定首先创建外围类对象,然后才能创建内部类。下面的代码是错误的:
b.local class:定义在方法内、代码块的内部类。区别于匿名类。
c.匿名类:匿名类指的是我们不像之前的成员类那样给内部类命名,而是以匿名的形式来创建类:如下所示:
public interface Content{ void say();}
public Content c = new Content() {
@Override
public String toString() {
return super.toString();
}
public void say(){System.out.print("hello");}
private int content;
};
上面的生成content对象的方式就是匿名类,我们直接在后面的大括号内完成类的声明。注意,如果在匿名内部类中使用到外部对象,要保证外部对象是final的。
d.static nested class:静态嵌套类(在Thinking in Java中直接译作嵌套类),意为我们在我们的nested class 前添加static标识符。所以我们的静态嵌套类不能使用外围类的非静态对象、变量。并且不得使用.this 和.new 语法。因为静态嵌套类和外围类之间的相互联系的关系是不存在的。
2.实现Java闭包和回调:
首先我们来解释一下什么叫闭包和回调:
闭包:一种可以被调用的对象,该对象保留了创建其的作用域中的信息。对于非静态内部类而言,它保留了一个对其 外围类的引用,通过它可以访问外部类的私有成员,因此可以把非静态内部类当成面向对象领域的闭包。
回调:我们给出维基百科中最准确的解释。回调就类似于,我们不去主动调用一些函数,而是将参数传递出去并且等待其他程序在某个时刻可以来调用我们。我们给出Thinking in Java中的例子:
import java.util.Objects;
/**
* Created by YanMing on 2017/3/6.
*/
// innerclasses/Callbacks.java
// Using inner classes for callbacks
interface Incrementable {
void increment();
}
// Very simple to just implement the interface
class Callee1 implements Incrementable {
private int i = 0;
public void increment() {
i++;
System.out.println(i);
}
}
class MyIncrement {
public void increment() {System.out.println("Other operation");}
static void f(MyIncrement mi) {mi.increment();}
}
// If your class must implement increment() in
// some other way, you must use an inner class:
class Callee2 extends MyIncrement {
private int i=0;
public void increment() {
super.increment();
i++;
System.out.println(i);
}
private class Closure implements Incrementable {
public void increment() {
// Specify outer-class method, otherwise
// you'd get an infinite recursion
Callee2.this.increment();
}
}
Incrementable getCallbackReference() {
return new Closure();
}
}
class Caller {
private Incrementable callbackReference;
Caller(Incrementable cbh) {callbackReference = cbh;}
void go() {callbackReference.increment();}
}
public class Callbacks {
public void print(Object o){
System.out.println(o.toString());
}
public static void main(String[] args) {
Callee1 c1 = new Callee1();
Callee2 c2 = new Callee2();
MyIncrement.f(c2);
Caller caller1 = new Caller(c1);
Caller caller2 = new Caller(c2.getCallbackReference());
caller1.go();
caller1.go();
caller2.go();
caller2.go();
}
}
执行结果:
下面我们来解释一下这个代码。Callee1是最简单的方法,直接实现了Increment接口中的increment()函数。Callee2继承了MyIncrement,并且在MyIncrement()中含有increment()方法,于是我们创建了一个内部类closure来实现Increment中的increment函数。我们可以从结果看出,2,3均来自我们的caller2,也就是利用c2创建了一个闭包。虽然我们没有再使用c2中的i,但是由于caller2利用了c2的闭包,像是一个hook一样。回调体现在哪里呢?我们通过传递了一个Increment类型的参数,可以使得caller对象不断地回调callee类。
回调更多的体现在很多的动态语言中,我们在javascript中也可以经常使用到回调。
3.内部类的继承和覆盖:
a.继承:
首先我们来看一个例子:
class Withinner{
class Inner{}
}
public class InheritInner extends Withinner.Inner {
InheritInner(Withinner wi){
wi.super();
}
}
在可以看出,我们的Inherit类继承了Withinner中的Inner。注意我们的构造函数,没有想往常一样是某人构造函数或者能够正常的含参构造函数,而是我们传入了我们的基类的外部类对象,并且调用super函数。
b.覆盖:
/**
* Created by YanMing on 2017/3/6.
*/
class Egg2{
protected class Yolk{
public Yolk(){
System.out.println("1: Egg2.Yolk");
}
public void f(){
System.out.println("2: Egg2.Yolk.f");
}
}
private Yolk y = new Yolk();
public Egg2(){System.out.println("3: New Egg2");}
public void insertYolk(Yolk yy){y = yy;}
public void g(){y.f();}
}
public class BigEgg2 extends Egg2{
public class Yolk extends Egg2.Yolk{
public Yolk(){System.out.println("4: BigEgg2.Yolk");}
public void f(){System.out.println("5: BigEgg2.Yolk.f");}
}
public BigEgg2(){insertYolk(new Yolk());}
public static void main(String args[]){
Egg2 e = new BigEgg2();
e.g();
}
}
我们来分析一下,上述代码的运行过程:
首先我们在实例化一个BigEgg2的对象。我们给出Java实例化的规则:
a.首先实例化内部对象。
b.含有继承关系,要首先实例化基类。
1.所以,在这里我们在调用BigEgg2的构造函数之前,首先实例化Egg2。在调用Egg2的构造函数之前,我们先实例化了Egg.Yolk内部变量。打印Egg2.Yolk。
2.调用Egg2的构造函数,打印New Egg2
3.调用BigEgg2构造函数,返回一个Yolk对象(这里是BigEgg中的Yolk)。该Yolk继承自Egg2.Yolk,所以首先实例化基类,打印Egg2.Yolk。
4.调用BigEgg2的Yolk构造函数,打印BigEgg2.Yolk。
5.调用g()函数,然后调用e对象中的y对象的f方法。在这里,我们的BigEgg2.Yolk被向上转型为Egg2.Yolk,同时由于f()在BigEgg2.Yolk中被覆盖,所以打印的是Bigegg2.Yolk.f。
P.S. 文章不妥之处还望指正