Java高级编程:闭包

2022-09-23 12:28:46 浏览数 (1)

闭包

前提摘要:Java基础知识:Lambda表达式

1 什么是闭包

闭包本身定义比较抽象,MDN官方上解释是:A closure is the combination of a function and the lexical environment within which that function was declared。中文解释是:闭包是一个函数和该函数被定义时的词法环境的组合

  • 闭包的价值在于可以作为函数对象或者匿名函数,持有上下文数据,作为第一级对象进行传递和保存;
  • 闭包被广泛应用于回调函数、函数式编程中;

2 Java中的闭包

在Java中,闭包一般是通过“接口 内部类”实现的,其中内部类也可以有匿名内部类

2.1 内部类

在JAVA中,内部类可以访问到外围类的变量、方法或者其它内部类等所有成员(即使它被定义成private了)但是外部类不能访问内部类中的变量。这样通过内部类就可以提供一种代码隐藏和代码组织的机制,并且这些被组织的代码段还可以自由地访 问到包含该内部类的外围上下文环境

代码语言:javascript复制
public class OuterClass {
    private final int length =0;

    //隐式内部类
    private class InnerClass implements Runnable {
        //内部类调用外部类变量
        private final int _length = length   1;

        @Override
        public void run() {
            //使用this指向OuterClass来引用非static的外部类变量
            System.out.println("线程运行,outer length = "   OuterClass.this.length);
        }
    }

    //隐式内部类中的方法无法在外部类中被调用也无法被实例化
    //通过方法暴露内部类
    public InnerClass runThread() {
        return new InnerClass();
    }

    public static void main(String[] args) {
        //实例化外部类
        OuterClass md = new OuterClass();
        //闭包构造内部类
        InnerClass ic = md.runThread();
        //获得内部类的变量
        System.out.println(ic._length);
        //线程启动
        ic.run();
    }
}

2.2 局部内部类

在Java中,被定义在类方法体中的类称之为局部内部类,局部内部类在外围方法中不可见,被局限在了方法中使用呈现封闭性。如下代码中,PartClass 类被定义在了 runThread() 方法中,因而在 runThread() 方法以外是无法访问PartClass 类的。局部内部类属于内部类的一种。

代码语言:javascript复制
public class OuterClass {
    private final int length =0;

    public Runnable runThread() {
        //在方法作用域中构造局部内部类使得外围不可见
        class PartClass implements Runnable {
            @Override
            public void run() {
                System.out.println("线程运行,outer length = "   OuterClass.this.length);
            }
        }
        return new PartClass();
    }

    public static void main(String[] args) {
        OuterClass md = new OuterClass();
        //调用方法中的局部内部类来开启线程
        md.runThread().run();
    }
}

2.3 匿名内部类

在Java中,方法返回一个被直接实例化的对象则称为匿名内部类。匿名内部类没有类名,但是拥有更加简介的代码块、并且同样需要重写接口类中的方法。匿名内部类同样属于内部类的一种。

代码语言:javascript复制
public class OuterClass {
    private final int length =0;

    public Runnable runThread() {
        //使用匿名内部类来返回一个被实例化的Runnable接口
        return new Runnable() {
            @Override
            public void run() {
                System.out.println("线程运行,outer length = "   OuterClass.this.length);
            }
        };
    }

    public static void main(String[] args) {
        OuterClass md = new OuterClass();
        md.runThread().run();
    }
}

更方便地,使用 new 关键字创建的匿名内部类可以使用 Lambda 表达式来快速实例化:

代码语言:javascript复制
public class OuterClass {
    private final int length =0;

    public Runnable runThread() {
        //使用 Lambda 表达式的箭头函数更简洁地实例化 return 的对象
        return () -> System.out.println("线程运行,outer length = "   OuterClass.this.length);
    }

    public static void main(String[] args) {
        //匿名调用方法来执行线程
        new OuterClass().runThread().run();
    }
}

2.4 内部不可变

在Java中,被内部类直接调用的变量一般都加以 final 进行修饰,以表示为一个恒定不变的值,即创建后便不能被更改。先看如下代码,再做详细解释。

代码语言:javascript复制
public class OuterClass {
    //priority表示线程优先级需要用final关键字来修饰表示不可变类型
    public Runnable runThread(final int priority) {
        //如若不加以final修饰则不能对priority进行任何新的赋值操作以防止污染闭包内变量
        return switch (priority) {
            case 1 -> () -> System.out.println("线程优先级为最低级");
            case 2 -> () -> System.out.println("线程优先级为非最低");
            default -> () -> System.out.println("错误的线程优先级");
        };
    }

    public static void main(String[] args) {
        new OuterClass().runThread(1).run();
    }
}

为什么需要 final 来修饰被闭包调用的变量?

在Java中,我们都知道方法参数传递是引用传递而非值传递,用一个简单的例子来说明:我们将方法 People.get("老王") 得到的 People 对象传递给方法 managePeople(People p),managePeople 方法得到的是这个 老王 People 对象的一个地址值。如果我们在闭包内修改了这个对象的某个属性的值,那么就会造成这个对象的值被全局污染使得其他方法在调用该 王五 对象时发现参数被修改了,同样的如果在多线程中,不论是外部方法还是闭包本身造成数据污染都会导致数据的不一致性,这就违背了缓存一致性协议

通过 final 来修饰变量就使得闭包内部调用时不受外部影响也防止了闭包内部修改导致外部不一致,但值得注意的是在多线程下如果外部进行了值修改则仍然会导致与闭包内的对象数据不一致,这就需要对对象的修改进行适当的控制(可参考Vue3的变量监听多级缓存来设计一类注解防止并行过程中修改导致的数据不一致)。

2.5 类的初始化

在Java中,类内允许使用 static 块initializer 块 来对类进行数据初始化,在类被加载的时候会自动执行其内部的代码。同样的,在闭包中也同样可以使用这两个初始化代码块来对闭包内部类进行初始化,其初始化的顺序也会按照代码编写顺序来执行。

代码语言:javascript复制
public class OuterClass {
    static class Person {
        public int age;

        public void grow(final int year) {
            this.age  = year;
            System.out.println("长大了 "   year   " 岁");
        }
    }

    public Person pastYears(final int year) {
        return new Person() {
            final int year = 10;
            //匿名代码块
            {
                this.age = 18;
            }
            //重写grow方法
            @Override
            public void grow(int year) {
                super.grow(year   1);
                System.out.println("现在年龄是 "   this.age   " 岁");
            }
            //在匿名代码块内调用重写后的grow方法
            {
                this.grow(this.year);
            }
        };
    }

    public static void main(String[] args) {
        //在实例化并调用的同时匿名代码块内的代码也会执行
        new OuterClass().pastYears(18);
    }
}

值得注意的是,这些属于匿名的方法块(Anonymous Method)是无法被重载的,一般地,对于静态类或属性的操作会使用 static 块来初始化。

3 Consumer、匿名函数式接口、闭包

代码语言:javascript复制
import java.util.function.Consumer;

public class OuterClass {
    @FunctionalInterface
    interface InnerInter {
        void awake(String name);
    }

    public InnerInter method(InnerInter lambda) {
        return lambda;
    }

    public static void show(String name, Consumer<String> cs1, Consumer<String> cs2){
        cs1.accept(name);
        cs2.accept(name);
    }

    public static void nothing() {
        return;
    }

    public static void main(String[] args) {
        show("蔡徐坤",
                name -> System.out.println(name   "_唱_跳_rap_篮球"),
                name -> new OuterClass().method(System.out::println).awake(name   "_ctrl")
        );
    }
}

使用 Consumer<E> 、匿名函数式接口、闭包 相结合可以实现更复杂的功能与数据处理方法,在更复杂一点的 Lambda 表达式上,开发者可以选择 Kotlin 进行进一步的开发。

0 人点赞