内部类之前我用的不多,但在java类库里却经常看到,是java的常用技术。这章主要围绕内部类和匿名内部类,同时也涉及到“闭包”,“回调”,“lambda表达式”这些常见概念。最后,讨论了内部类常见的使用场景,以及优缺点。
这是一个最简单的套嵌结构,Outer是一个外部类,里面有一个Inner内部类。外部类有个方法,创建并返回一个内部类。
class Outer {
/**
* INNER CLASS
*/
class Inner {
private Inner() {System.out.println("Hello I am Inner Class!");}
}
/**
* METHODS
*/
Inner getInner(){return new Inner();}
/**
* CONSTRUCTOR
*/
Outer(){System.out.println("Hello I am Outer Class!");}
/**
* FIELDS
*/
}
简单创建一个外部类,然后通过外部类创建内部类。观察初始化顺序。
public static void main(String[] args) {
Outer testOuter=new Outer();
Inner myInner=testOuter.getInner(); //实例化内部类,方法一
Inner newInner=testOuter.new Inner(); //实例化内部类,方法二
}
}
//output:
Hello I am Outer Class!
Hello I am Inner Class!
Hello I am Inner Class!
因此得出两个重要事实:
由第一条可以得出,内部类的性质就像外部类的一个“组件”。很类似compositon。但不同在于这个组件不是必须的,而是备选的。平时不需要的时候它不会被初始化,只有必要的时候,才实例化出来使用。
需要当心第二条,想当然的new Outer.Inner()是不能实例化内部类的。因为内部类并不是静态的。因此必须通过外部类的一个实例来实例化,无论是否用new。
另外,在编译层面,Outer类编译完会产生3个class文件。我们以后再探讨这三个文件的具体作用。
Outer$1.class Outer.class
Outer$Inner.class Outer.java
第二个实验前,需要指出:内部类和普通类不同,可以用private, protected, public修饰。
第二个实验:主角是外部Outer类的一个字段:字符串info,以及一个方法outerMethod()。
class Outer {
/**
* PRIVATE INNER CLASS
*/
class Inner {
//直接访问并修改Outer的info字段,能行吗?
public String toString(){
info="苍天还没有死";
}
//constructor of inner
Inner() {System.out.println("Hello I am Inner Class!");}
}
/**
* PUBLIC METHODS
*/
public Inner getInner(){return new Inner();}
//显示Outer的info
public void outerMethod(){System.out.println(info);}
/**
* CONSTRUCTOR
*/
Outer(String inStr){
info=inStr;
System.out.println("Hello I am Outer Class!");
System.out.println(info);
}
/**
* PRIVATE FIELDS
*/
private String info;
}
结果显示,内部Inner类能够访问外部Outer类的所有字段和方法,哪怕是private私有的。不是继承,而是完全指向同一个对象。也就是说内部类完全共享外部类的所有信息,而且能直接对外部类的字段做修改,哪怕是private的字段。 这是因为,在内部类中,会自动被加上了一个指向外部类的引用。所以内部类能够直接对外部类成员字段操作。
public static void main(String[] args) {
Outer testOuter=new Outer("苍天已死,黄天当立!");
Inner myInner=testOuter.getInner();
myInner.toString();
myInner.outerMethod();
}
//output:
Hello I am Outer Class!
苍天已死,黄天当立
Hello I am Inner Class!
苍天还没有死
讲到内部类自动包含指向外部类的引用,就引出了“闭包(Closure)”这个概念。
其实闭包的概念非常简单,不严肃地,一句话定义闭包就是,
定义中含有自由变量的函数叫“闭包”。
什么叫“自由变量”? 如上图所示,
f(x)=x+y
函数中,x是约束变量,y就是自由变量。也就是函数的返回值不仅取决于约束变量x,还取决于环境变量y。代码写出来就是下面这样:
int y=50;
add(int x){
return x+y;
}
所以如果我们单独调用add()函数,是计算不出最后的结果的,因为缺了自由变量y。所以add()函数为了能完成计算,必须随身携带自由变量y,这时候add()函数就可以称为一个“闭包”。这里“闭”的意思其实是封闭,封装了外部自由变量,所以叫“闭包”。
和闭包相对的一个概念叫“组合子(Combinator)”,定义为,
不含有自由变量的函数叫“组合子”。
很简单,如果add()函数有x和y两个参数,不依赖于外部自由变量的话,就是一个组合子。
add(int x, int y){
return x+y;
}
讲到这里,很容易就看出来,内部类就是一个典型的“闭包”!因为它自带创建它的环境(外部类)的全部变量。
根据内部类这两条特性,就引出了每个类库里都会有的迭代器(Iterator)。其实它一点也不神秘, 就是一个简单的内部类,唯一的字段就是一个整型计数器index。就像挂在主序列上的一个指针,表示现在遍历到哪儿了。原理就是利用了内部类自动继承外部类字段的特性,通过访问这个指针,可以直接对外部主序列进行操作。
class Sequence {
/**
* PRIVATE INNER CLASS
*/
private class Pointer {
//methods
public boolean hasNext(){return index!=charSeq.length;}
public char next(){
if(index<charSeq.length){index++;}
return charSeq[index-1];
}
//constructor of inner
private Pointer() {index=0;}
//fields
private int index; //迭代器的主体“指针”
}
/**
* PUBLIC METHODS
*/
public Pointer getPointer(){return new Pointer();}
/**
* PRIVATE CONSTRUCTOR
*/
private Sequence(String inStr){
charSeq=inStr.toCharArray();
}
/**
* PRIVATE FIELDS
*/
private char[] charSeq; //主体字符串
}
测试一下,遍历并打印字符串的每一个字符,并用”-“间隔。
/**
* MAIN
*/
public static void main(String[] args) {
Sequence testSeq=new Sequence("So an inner class has automatic access to the members of the enclosing class.");
Pointer testPointer=testSeq.getPointer();
while(testPointer.hasNext()){
System.out.print(testPointer.next()+"-");
}
}
//output:
S-o- -a-n- -i-n-n-e-r- -c-l-a-s-s- -h-a-s- -a-u-t-o-m-a-t-i-c- -a-c-c-e-s-s- -t-o- -t-h-e- -m-e-m-b-e-r-s- -o-f- -t-h-e- -e-n-c-l-o-s-i-n-g- -c-l-a-s-s-.-
但问题来了,同样的事情,我不用内部类的形式,而是把指针直接作为一个字段放进字符串类照样能实现功能:
//不用内部类,直接用“组合”
class SequenceComp {
/**
* PUBLIC METHODS
*/
public boolean hasNext(){return index!=charSeq.length;}
public char next(){
if(index<charSeq.length){index++;}
return charSeq[index-1];
}
/**
* PRIVATE CONSTRUCTOR
*/
private SequenceComp(String inStr){
charSeq=inStr.toCharArray();
index=0;
}
/**
* PRIVATE FIELDS
*/
private char[] charSeq;
private int index; //迭代器直接作为组件
}
实验三在实验二的基础上更进一步,外部类访问内部类私有构造器和私有方法。反过来内部类私有方法再访问外部类的私有字段和方法。
public class Outer {
/**
* PRIVATE INNER CLASS
*/
private class Inner {
//methods
private void privateInnerMethod(){
privateOuterMethod();
info="privatedInnerMethod visit info!";
}
//constructor of inner
private Inner() {System.out.println("Hello I am Inner Class!");}
}
/**
* PUBLIC METHODS
*/
public Inner getInner(){return new Inner();}
public void callPrivateInner(){
Inner theInner=getInner();
theInner.privateInnerMethod();
System.out.println(info);
}
private void privateOuterMethod(){System.out.println("Private Outer Method visited!");}
/**
* PRIVATE CONSTRUCTOR
*/
public Outer(String inStr){
info=inStr;
System.out.println("Hello I am Outer Class!");
System.out.println(info);
}
/**
* PRIVATE FIELDS
*/
private String info;
从外部另一个包运行这个实验。结果显示,外部类对内部类私有构造器和私有方法都无障碍访问。
/**
* MAIN
*/
public static void main(String[] args) {
Outer testOuter=new Outer("Inner class haven't visited me!");
testOuter.callPrivateInner();
}
}
//output:
Hello I am Outer Class!
Inner class haven't visited me!
Hello I am Inner Class!
Private Outer Method visited!
privatedInnerMethod visit info!
最后的结果显示,
所以内部类和外部类之间的访问一切畅行无阻。如果外部类是一个姑娘,那内部类就像姑娘颈上的项链,手上的戒指。是附属品也是整体的一部分。可以带,也可以不带。
外部有个公开接口Destination,private内部类PDestination实现了这个接口。然后只要外部类在实例化内部类的时候向上转型成为公开接口类型。
//外部公开接口
public interface Destination {
String readLabel();
}
class Parcel {
private class PContents implements Contents {
private int i = 11;
public int value() { return i; }
}
//内部类实现外部接口
protected class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
//关键:外部类实例化内部类的时候向上转型
public Destination destination(String s) {
return new PDestination(s);
}
public Contents contents() {
return new PContents();
}
}
这样做的好处是,在公开类的内部对内部类的访问非常自由,但是从外部类的外面访问内部类的时候,就只能看到公开接口定义的方法。这样的结构非常适合对外隐藏内部实现。
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Contents c = p.contents();
Destination d = p.destination("Tasmania");
}
例子很简单,我是一个学生,我的work()函数是写作业。但有一天我接到一个助教的活,可是老师接口也有work()函数。显然作为老师我的work()函数应该是上课。但如果我又想同时保持我写作业的work()函数呢?能不能同时有两个行为不同的work()函数呢?这时候内部类就是个很好的选择。
//老师的work()接口
public interface Teacher{
public void work();
}
public abstract class Students {
public void work(){System.out.println("do home work");}
}
class Me extends Students{
//作为老师的teach()函数已经写好,放在外部类,等待内部类回调
public void teach(){System.out.println("give lessons");}
//内部类实现老师接口,work()函数回调外部类teach()函数
public class TeachAssistant implements Teacher{
public work(){teach();}
}
//返回内部类的引用,向上转型到老师接口
public Teacher becomeTA{return new TeachAssistant();}
}
class Test {
public static void main(String[] args){
Me shen = new Me();
//作为学生工作
shen.work();
//作为助教上课
Teacher ta = shen.becomeTA();
ta.work();
}
//output:
do homework
give lessons
}
我的基本身份是个学生,所以我继承了学生基类,自带的work()函数是做作业。但我同时也具备教课teach()的能力,平时不施展。只有当我的隐藏身份(内部类)“助教”被调用的时候,我的work()函数,回调我的teach()技能。这就是“回调”在java内部类上的应用。
从例子可以看出,利用内部类“回调”,可以让外部类在基本身份之外,有很多不同的新身份,以不同的接口来调用。而且最大的好处就像之前讲的,每个接口只对外暴露接口约定的几个方法,隐藏了内部的其他实现。
“回调”是很常用的一种技术,回调的方式有很多种,上面讲的内部类只是其中比较常用的一种方式罢了。回调说起来其实很简单,如下图所示,
应用层的程序调用函数库的某一函数,但这个函数说某个具体步骤我不会做,需要应用层告诉我怎么做。所以最好就是应用层写好这个方法,等库函数回调。关于回调其他的形式,以及常见的使用场景可以参考知乎上futeng的一个用心的回答《回调函数(callback)是什么?》。其中提到,匿名内部类的示例才是开源工具中常见到的使用方式。而且回调方法最大的优势在于,异步回调,这样是其最被广为使用的原因。
像我这样的初学者可能会觉得内部类语法很怪,但其实到现在为止的都还属于小清新,Java语法允许的口味要重得多。
首先,内部类可以在外部类的某个成员方法里,简单结构如下:
//外部类
class Outer {
public void outerMethod(){
//内部类
private class Inner{
public void innerMethod(){}
}
}
}
甚至,内部类可以在外部类某成员方法的某个域(花括号)里,
//外部类
class Outer {
public void outerMethod(boolean needInner){
if(needInner){
//内部类
private class Inner{
public void innerMethod(){}
}
}
}
}
比较常用的是更鬼畜的一种:匿名内部类。
//外部接口
public interface Info {
public printInfo();
}
//外部类
class Outer {
public Info getInfo(){
//现场制作并返回匿名内部类
return new Info(){
private String info="Hello World";
public printInfo(){System.out.println(info)};
}; //冒号不能省
}
}
最常见的匿名类使用场景,当参数。有的时候参数的类型很不常用,比如只用1,2次,专门写个类文件太麻烦。就可以现场制作这样一个匿名类。只不过,看上去对用户相当不友好呢:>
//外部接口
public interface Info {
public printInfo();
}
//外部类
class Outer {
//函数需要一个Info类型的参数
public Info showNews(Info inputInfo){
System.out.println("Today's news: ");
inputInfo.printInfo();
}
public static void main(String[] args){
Outer myOuter=new Outer();
//要用到Info型的参数的时候,才现场制作
myOuter.showNews(new Info(){
private String info="Hello World";
public printInfo(){System.out.println(info)};
});
}
}
Java 8之前为保护外部类的变量不被破坏,java硬性规定匿名内部类只能访问final类型的外部对象。作者写书的时候Java 8还没有发布。但从Java 8开始,访问非final对象编译器也不会报错了。但匿名内部类内部仍然不能改变这些对象,因为匿名内部类内部会对这些对象做一份拷贝。所有操作都在这个镜像上进行。
以匿名内部类作为函数的参数,其实是Lambda表达式的一种形式。Java8中已经正式引入了Lambda表达式。
//之前的匿名内部类的写法
myOuter.showNews(new Info(){
private String info="Hello World";
public printInfo(){System.out.println(info)};
});
//Lambda表达式替代内部匿名类
myOuter.showNews(()->{
System.out.println("Hello World!");
});
}
上面的例子里,看上去使用Lambda表达式的条件还是挺苛刻的。首先Info接口就规定了一个printInfo()方法,还没有成员字段。
“Lambda 表达式”(lambda expression)是一个匿名函数,基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。
那λ演算到底是个什么鬼?这就要涉及到编程语言最核心的话题了:其实编程语言的本质是关于{逻辑学,语言学,数学}。λ演算是一套用于研究函数定义、函数应用和递归的形式系统。
首先Lambda表达式的基本形式如下:
λ[变量].[表达式]
任何函数都只能带一个参数,如果想表达一个加2函数,
其中的λ就是lambda抽象,代表一个抽象的匿名函数。x是这个函数的唯一参数。x+2代表对函数参数x所做的操作,也就是函数的返回值。
λ演算的函数都只能带一个参数。那想表示带两个或两个以上参数的函数呢?可以用Currying技术:
变量x的函数的返回值本身是另一个函数y。然后调用的环境变量x也被传入了函数y。形成了闭包。
其实说到这里,也就理解了为什么匿名内部类被称为Lambda表达式了。除了有“闭包”这样的共同形式之外,其实λ演算还透露了另外一种重要的“函数式编程”思想或者说范式:“一切都是函数”。
实际上λ演算的强大之处在于,它是可以用来表达所有函数的一套形式系统。不但是函数,甚至还有一切我们编程用到的变量,数据结构。具体细节可以看《维基百科-λ演算》的介绍。以及另一篇科普文《编程语言的基石——Lambda calculus》。
如果想在外部类的外面,继承内部类,需要给继承类的构造器像这样加一个外部类的引用作为参数。注意继承类内部实际包含的是外部类的基类构造器super()。
class Outer {
class Inner {}
}
public class InheritInner extends Outer.Inner {
//! InheritInner() {} // Won’t compile
InheritInner(Outer o) {
o.super();
}
}
继承外部类,如果直接再定义一遍内部类,并不会覆盖原有内部类。若想覆盖原有内部类,必须连同外部类一起,同时显式地继承内部类,然后再重写内部类子类的方法。
class Outer {
class Inner {
public innerMethod(){System.out.println("Outer.Inner.innerMethod()")}
}
}
public class InheritOuter extends Outer {
public class InheritInner extends Outer.Inner {
public innerMethod(){System.out.println("InheritOuter.InheritInner.innerMethod()")}
}
}
内部类最直观的一个替代者,就是平时最常用的“组合”。只是需要一个内部组件的时候,用组合就好了,干嘛还非要用内部类呢?
作者给出的答案是:“多重继承”。
Each inner class can independently inherit from an implementation. Thus, the inner class is not limited by whether the outer class is already inheriting from an implementation.
在外部类已经继承了某个基类之后,内部类仍然可以继承别的类。不是简单多实现几个接口,而是实实在在地继承基类。
相较于组合,内部类的另一个优势在于,内部类不会像组合这样被强制和外部类一起被初始化。只有在我们需要用到的时候才去初始化内部类。这就是内部类的“可选组件”的地位,这点我们从上面“迭代器Iterator”的例子就能看出来。从设计的角度,内部类没有被逻辑绑定成外部类的一部分”IS-A”的关系。因此设计上更加灵活。
The point of creation of the inner-class object is not tied to the creation of the outer-class object.
另外,对于内部匿名类,它的好处在于简便,在一些类的使用次数非常有限,几乎是一次性的情况下,匿名类可以减轻工作量。
(4) Create an interface U with three methods. Create a class A with a method that produces a reference to a U by building an anonymous inner class. Create a second class B that contains an array of U. B should have one method that accepts and stores a reference to a U in the array, a second method that sets a reference in the array (specified by the method argument) to null, and a third method that moves through the array and calls the methods in U. In main( ), create a group of A objects and a single B. Fill the B with U references produced by the A objects. Use the B to call back into all the A objects. Remove some of the U references from the B.
结构很简单:
每个U实例都被标记上了生产它的A实例的ID和它自己的ID。这里体现出了匿名内部类作为闭包的性质:自带一个指向创建环境的指针,能访问创建者的全部信息。因此,每个U实例的三个方法根据创建环境ID参数的变化,都是全新的实现。这样的结构,很适合做工厂方法,批量生产特殊类。
练习的过程中,我做了个实验,想要验证关于匿名内部类只能访问final参数的问题。结果没想到Java 8已经取消了final参数的规定。但是看起来java编译器还是给每个传入匿名内部类的参数做了一份拷贝,因此匿名内部类对参数的操作,并不能改变外部参数。虽然保证了外部参数的安全性,但怎么觉得反而是个坑呢,不报错的隐蔽bug才最麻烦啊。
我实验的时候把容器B当成参数传给getU()函数,然后在匿名内部类中每次都重新初始化容器B。因此,按理最后容器B永远只能保存最后一个写入的U实例。但实际上最后容器B里的列表活得好好的。
public U getU(){
//Anonymous inner class
//传入非Final容器B为参数
return new U(B inputB){
inputB=B.getB(); //每次都把B重新指向一个新的空容器
public void uMethod1(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method1");}
public void uMethod2(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method2");}
public void uMethod3(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method3");}
private int uId=uCount++;
};
interface U {
public void uMethod1();
public void uMethod2();
public void uMethod3();
}
//facotry of the object of type U
class A {
//maintain a counter to generate the ID for each A
private static int aCount=0;
public static A getA(){return new A(aCount++);}
public U getU(){
//Anonymous inner class
//闭包:匿名内部类自带创建者A的信息。
return new U(){
public void uMethod1(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method1");}
public void uMethod2(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method2");}
public void uMethod3(){System.out.println("{A-"+aId+"}.{U-"+uId+"}.method3");}
private int uId=uCount++;
};
}
private A(int inputAId){aId=inputAId;}
private int aId;
//maintain a counter to generate the ID for each U
private int uCount=0;
}
//containor of created objects of type U
class B {
public static B getB(){return new B();}
//insert an U at the end of the uArray
public void addU(U inputU){
uArray.add(inputU);
}
//remove a U from uArray by their id
public void removeU(int index){
if(index<uArray.size() && uArray.get(index)!=null){
uArray.remove(index);
}
}
//pass through the U in uArray and call all three methods
public void uIteration(){
for(U u : uArray){
u.uMethod1();
u.uMethod2();
u.uMethod3();
}
}
//containor: List of U object
private ArrayList<U> uArray=new ArrayList<U>();
public static void main(String[] args){
//list of A
A[] aArray=new A[3];
for(int i=0;i<aArray.length;i++){
aArray[i]=A.getA();
}
//B is list of U
B b=B.getB();
//A create U, insert into B
for(int j=0;j<aArray.length;j++){
for(int i=1;i<=j+1;i++){
b.addU(aArray[j].getU());
}
}
//show content in B
b.uIteration();
b.removeU(3);
System.out.println("================================");
b.uIteration();
}
//每个A类实例都有ID,他们生产的U也有各自ID。每个U都是一个独一无二的类。
//output:
{A-0}.{U-0}.method1
{A-0}.{U-0}.method2
{A-0}.{U-0}.method3
{A-1}.{U-0}.method1
{A-1}.{U-0}.method2
{A-1}.{U-0}.method3
{A-1}.{U-1}.method1
{A-1}.{U-1}.method2
{A-1}.{U-1}.method3
{A-2}.{U-0}.method1
{A-2}.{U-0}.method2
{A-2}.{U-0}.method3
{A-2}.{U-1}.method1
{A-2}.{U-1}.method2
{A-2}.{U-1}.method3
{A-2}.{U-2}.method1
{A-2}.{U-2}.method2
{A-2}.{U-2}.method3
================================
{A-0}.{U-0}.method1
{A-0}.{U-0}.method2
{A-0}.{U-0}.method3
{A-1}.{U-0}.method1
{A-1}.{U-0}.method2
{A-1}.{U-0}.method3
{A-1}.{U-1}.method1
{A-1}.{U-1}.method2
{A-1}.{U-1}.method3
{A-2}.{U-1}.method1
{A-2}.{U-1}.method2
{A-2}.{U-1}.method3
{A-2}.{U-2}.method1
{A-2}.{U-2}.method2
{A-2}.{U-2}.method3