Java 内部类与闭包

6,815 阅读10分钟

作者:不洗碗工作室 - Marklux

出处:Marklux's Pub

版权归作者所有,转载请注明出处

Java内部类

基本定义

很简单,无非是在类的内部再定义一个类,这被称为成员内部类:

public class OuterClass {
    private String name;
    private int age;
    class InnerClass {
        public InnerClass(){
            name = "mark";
            age = 20;
        }
        public void echo() {
            System.out.println(name + " " + age);
        }
    }
}

问题思考

上面这个很简单的例子中,也包含了很多应该思考的问题:

  • 内部类如何被实例化?
  • 内部类能否改变外围类的属性,两者之间又是什么一种关系?
  • 内部类存在的意义是什么?

在回答这三个问题之前,必须要明确一个点,那就是内部类是依附于外围类而存在的,其实也就是内部类存在着指向外围类的引用。明白了这个之后,上面的问题就好解答了。

实例化与数据访问

内部类与外围类之间形成了一种联系,使得内部类可以无限制地访问外围类中的任意属性。 正如上面的例子中,InnerClass内部可以随意访问OuterClass中的private属性。

同样的,因为内部类依赖与外围类的存在,所以无法在外部直接将其实例化,而是必须先实例化外围类,才能够实例化内部类(注意,在外围类的成员方法里仍然是可以直接实例化内部类的):

public static void main(String[] args) {
        InnerClass inner = new OuterClass().new InnerClass();
        inner.echo();
}

使用外围类的.new来创建外部类。

我们也知道,内部类和外围类的联系是通过内部类所持有的外部类的引用来实现的,想要获取这个引用,可以使用外围类的.this来实现,可以参考下面这个测试用例

public class OuterClass {
    private String name;
    private int age;
    class InnerClass {
        public InnerClass(){
            name = "mark";
            age = 20;
        }
        public void echo() {
            System.out.println(name + " " + age);
        }
        public OuterClass getOuter() {
            return OuterClass.this;
        }
    }

    @Test
    public void test() {
        OuterClass outer = new OuterClass();
        InnerClass inner = outer.new InnerClass();
        Assert.assertEquals(outer, inner.getOuter());
    }
}

内部类的作用

内部类创建起来很麻烦,使用起来也令人困扰,那么内部类存在的意义是什么呢?

实现多重继承

这可能是内部类存在的最重要的意义,参考《Thinking in Java》中的解释:

使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

我们都知道,Java中取消了C++中类的多重继承(但是允许接口的多重实现),但是在实际编程中,又不免会遇到同一个类需要同时继承自两个类的情况,这时候就可以使用内部类来实现了。

比如有两个抽象类

public abstract class AbstractFather {
    protected int number;
    protected String fatherName;

    public abstract String sayHello();
}

public abstract class AbstractMother {
    protected int number;
    protected String motherName;

    public abstract String sayHello();
}

如果想要同时继承这两个类,势必会引起number变量的冲突,以及sayHello方法的冲突,这些问题在C++中是以一种复杂的方案来实现的,如果使用内部类,就可以使用两个不同的类来继承不同的基类,并且可以根据自己的需要来组织数据的访问:

public class TestClass extends AbstractFather {
    @Override
    public String sayHello() {
        return fatherName;
    }

    class TestInnerClass extends AbstractMother {
        @Override
        public String sayHello() {
            return motherName;
        }
    }
}

其他

(摘自《Think in Java》)

  1. 内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。

  2. 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。

  3. 创建内部类对象的时刻并不依赖于外围类对象的创建。

  4. 内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。

  5. 内部类提供了更好的封装,除了该外围类,其他类都不能访问。

内部类的分类

上面的例子中创建的内部类,都属于成员内部类,实际上Java中还有三种其他的内部类:

  1. 局部内部类

    嵌套在方法里或者是某个作用域内,通常情况下不希望这个类是公共可用的,相比于成员内部类,局部内部类的作用域更加狭小了,出了方法或者作用域就无法被访问。一般用于在内部实现一些私有的辅助功能。

    定义在方法里:

    public class Parcel5 {
    public Destionation destionation(String str){
        class PDestionation implements Destionation{
            private String label;
            private PDestionation(String whereTo){
                label = whereTo;
            }
            public String readLabel(){
                return label;
            }
        }
        return new PDestionation(str);
    }
    
    public static void main(String[] args) {
        Parcel5 parcel5 = new Parcel5();
        Destionation d = parcel5.destionation("chenssy");
    }
    }
    

    定义在作用域内:

    public class Parcel6 {
    private void internalTracking(boolean b){
        if(b){
            class TrackingSlip{
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip(){
                    return id;
                }
            }
            TrackingSlip ts = new TrackingSlip("chenssy");
            String string = ts.getSlip();
        }
    }
    
    public void track(){
        internalTracking(true);
    }
    
    public static void main(String[] args) {
        Parcel6 parcel6 = new Parcel6();
        parcel6.track();
    }
    

} ```

  1. 静态内部类

    使用了static修饰的内部类即为静态内部类,和普通的成员内部类最大的不同是,静态内部类没有了指向外围类的引用。 因此,它的创建不需要依赖于外围类,但也不能够使用任何外围类的非static成员变量和方法。

    静态内部类一个很好的用途是,用来创建线程安全的单例模式:

    public class Singleton {  
        private static class SingletonHolder {  
            private static final Singleton INSTANCE = new Singleton();  
        }  
        private Singleton (){}  
        public static final Singleton getInstance() {  
            return SingletonHolder.INSTANCE; 
        }  
    }
    

    这是利用了JVM的特性:静态内部类时在类加载时实现的,因此不会受到多线程的影响,自然也就不会出现多个实例。

  2. 匿名内部类

    匿名内部类就是没有被命名的内部类,当我们需要快速创建多个Thread的时候,经常会使用到它:

    new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }).start();
    

    当然现在也可以用λ表达式来实现了,我们暂且不提这两者之间的关系,先来看一下使用匿名内部类时要注意哪些事情:

    • 匿名内部类没有访问修饰符,也没有构造方法
    • 匿名内部类依附于接口而存在,如果它要继承的接口并不存在,那这个类就无法被创建
    • 如果匿名内部类要访问局部变量,那这个数据必须是final的

    熟悉函数式编程的人会发现匿名内部类和函数闭包有些类似(这也是为什么能够用λ表达式来代替它),但实际上两者还是有着一些区别的,下面的部分中我们就来对比闭包和内部类。

Java内部类与闭包

何为闭包?

在之前的浅谈闭包一文中已经阐述过了,用一句话来形容闭包就是:

闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)

仍然用Go语言写一个非常简单的闭包以便后面和Java匿名内部类进行对比:

func Add(y int) {
	return func(x int) int {
		return x + y
	}
}

a := Add(10)
a(5) // return 15

我们知道这个返回的闭包中包含了Add函数中提供的变量y(也就是闭包产生的环境),现在我们来思考一下闭包究竟是如何实现的。

根据定义,闭包 = 函数 + 引用环境, 在上述的例子中,引用环境就是局部变量y,那么可不可以用一个结构体来定义这个闭包呢(注:Golang中的结构体和Java中的类地位相同),答案是可以的(实际上Golang底层也是这样定义闭包的):

type Closure struct {
	F func(x int) int
	y *int
}

也就是说这个闭包包含了函数本身,以及一个对局部变量y的引用。

这里特别需要注意的一点是,如果y是定义在函数Add的调用栈里的一个变量,那么当Add()函数被调用完毕后,y就销毁了,这时候再用原来的指针去访问y就会出问题,因此这里就出现了一个原则:

闭包中引用的外部变量必须是在堆上分配的

实际上Go的编译器在处理到这个闭包时,会使用escape analyze来识别变量y的作用域,当发现变量y被一个闭包所引用时,就会把y转移到堆中(这一过程称为变量逃逸)。

总结一下,闭包从底层理解,就是函数本身和其所需外部变量的引用,用R大的话来形容闭包的创建过程就是:

capture by reference

内部类与闭包

看一下最后我们对闭包的定义,“所需外部变量的引用”是否像极了Java中内部类对外围类的引用?

所以大家都认为Java中不存在闭包,其实Java里处处都是闭包(对象就是闭包),以致于我们感觉不到自己在使用闭包,成员内部类就是一个最典型的例子,因为它持有一个指向外围类的引用,看下面这个例子:

public class OuterClass {
    private int y = 10;
    private class Inner {
        public int innerAdd(int x) {
            return x + y;
        }
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        Inner inner = outer.new Inner();
        System.out.println(inner.innerAdd(5)); //result: 15
    }
}

这样的用法,和之前在Go语言里演示的那个闭包用起来其实别无二致,只是在Go语言里写起来要简洁许多(毕竟天生支持函数式编程)。

匿名内部类是闭包吗?

刚才在讲到匿名内部类时,也提到了闭包,那么匿名内部类是闭包吗?

先来尝试用匿名内部类创建一个类似闭包的结构吧:

interface AnnoInner {
    int innerAdd(int x);
}

public class OuterClass {
    private int y = 100;
    public AnnoInner getAnnoInner() {
        int z = 10;
        return new AnnoInner() {
            @Override
            public int innerAdd(int x) {
                // z = 20; 报错
                return x + y + z;
            }
        };
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        AnnoInner inner = outer.getAnnoInner();
        System.out.println(inner.innerAdd(5)); //result: 115
    }
}

我们可以看到,匿名内部类也能正常的引用外围类的成员变量y,但是对于局部变量z,如果尝试在匿名内部类里改变它的值(读取没问题),编译器就会报错。

这也对应了上面介绍匿名内部类的时候引入的规则:

如果匿名内部类要访问局部变量,那个变量必须是final的

之所以会形成这样的结果,是由于Java在实现匿名内部类对外部局部变量访问时,并不是像Go的编译器那样将局部变量提升到堆中,然后传递引用来实现;而是对局部变量创建值拷贝,然后供匿名内部类来使用。

所以上面的例子在Java编译器的实现可以简单的理解成这样:

return new AnnoInner() {
            @Override
            public int innerAdd(int x) {
                int copyZ = z; // 创建z的值拷贝
                return x + y + copyZ;
            }
        };

所以说,Java中的匿名内部类是一个不完整的闭包,用R大的话说就是:

capture by value, 而不是 capture by reference

参考阅读: