【Java】反射, 枚举, Lambda

反射

反射的概念

Java中的反射(reflection)机制, 是指在运行状态中, 对于类来说, 我们可以获取一个类的所有属性和方法, 比如它的成员属性或者成员方法, 甚至是构造方法. 对于对象来说, 我们可以调用它的任意方法或者修改它的任意属性.

但是这里可能有人会提出一个问题: 为什么反射机制是指在运行状态中才获取的呢?

为了探究这个问题我们先往下了解一下反射相关的类

反射相关的类

我们上面说, 我们可以运用反射来获取类的信息, 那么我们要如何存储这些信息呢? 实际上 Java 就提供了相关的类来帮我们存储这些信息, 如下所示

类名用途
Class类代表类本身, 用于表示类和接口
Field类代表类的字段/属性
Method类代表类的方法
Constructor类代表类的构造方法

其实这些属性, 分别就是 Java 对于一个类本身以及其属性的抽象, Class 类用于去代表一个类, 例如我有一个 Student 类, 那么当我们运行 Java 程序的时候, 就会去自动的生成一个代表着 Student 类本身的一个 Java 对象, 而这个对象就是 Class 类型的. 同时 Field, Method则是代表这个学生类里面的属性和方法的.

当然, 这里听起来非常的绕, 并且有一些抽象, 因此我们这里简单了解, 随后在后面的例子里面理解到底是什么意思即可.

初识Class类

上面展示的 4 个类中的 Class 类, 是反射机制的起源. 假如我们到时候想要获取类的信息, 就要运用 Class 类的方法, 那么我们首先先接下来了解一下 Class 类里面的一些方法, 后续会演示其中的部分方法

  • 一些常用于获得类相关信息的方法
方法用途
forName(String className)根据类名返回类的对象
newInstance()创建类的实例
getName()获得类的完整路径名字
getDeclaredClasses()返回一个数组, 数组中包含该类中所有类和接口类的对象(包括私有的)
getClassLoader()获得类的加载器
  • 一些常用于获得类中属性相关信息的方法(其返回值与 Field 相关)
方法用途
getField(String name)获得某个公有的属性对象
getField()获得某个公有的属性对象
getDeclaredField(String name)获得某个属性对象(包括私有的)
getDeclaredField()获得某个属性对象(包括私有的)
  • 一些常用于获得类中方法相关信息的方法(其返回值与Method相关)
方法用途
getMethod(String name, Class…<?> parameterTypes)获得该类中与参数类型匹配的公有的方法
getMethods()获得该类所有公有的方法
getDeclaredMethod(String name, Class…<?> parameterTypes)获得该类中与参数类型匹配的方法(包括私有的)
getDeclaredMethods()获得该类某个方法(包括私有的)
  • 一些常用于获得类中构造方法相关信息的方法(其返回值与Constructor相关)
方法用途
getConstructor(Class…<?> parameterTypes)获得该类中与参数类型匹配的公有构造方法
getConstructor()获得该类的所有公有构造方法
getDeclaredConstructor(Class…<?> parameterTypes)获得该类中与参数类型匹配的构造方法
getDeclaredConstructors()获得该类所有构造方法

类对象的获取

上面说了一大堆, 但是此时可能有人还是一头雾水, 因此我们这里直接通过一个例子去演示一下反射是什么, 它涉及到的各种类究竟代表了什么.

我们先写一个用于反射的类, 后续的反射操作将会以这个类来演示

class Student {
    //私有属性name
    private String name = "zhangsan";

    //公有属性age
    public int age = 18;

    //不带参数的构造方法
    public Student() {
        System.out.println("无参构造方法");
    }

    private Student(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("含参构造方法");
    }

    private void eat() {
        System.out.println("吃饭");
    }

    public void sleep() {
        System.out.println("睡觉");
    }

    private void function(String str) {
        System.out.println(str);
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

我们上面说过, 反射的起源是 Class 类, 那么具体是什么意思呢? 实际上是因为我们假如要实现反射, 那么第一步就是先拿到一个 Class 对象, 那么接下来我们就来演示一下如何获取 Class 对象, 我们主要演示三种方法

  • 使用Class内部的静态方法forName()
public class Demo01 {
    public static void main(String[] args) {
        try {
            Class<?> class1 = Class.forName("reflect.Student");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

forName()方法的参数要求是一个类的全路径名, 什么是全路径名, 简单的说就是包括包名在内的类名. 这里可以看到, 我们就是去获得了一个 Class 对象, 这个 Class 对象, 代表的就是 Student 的这个类

下面我们继续看一下其他获取 Class 对象的方法

  • 使用 class 关键字
public class Demo01 {
    public static void main(String[] args) {
        Class<?> class2 = Student.class;
    }
}

这里就是直接通过类名本身, 然后去调用这个关键字就可以获取对应的 Class 对象了

  • 使用对象的 getClass() 方法
public class Demo01 {
    public static void main(String[] args) {
        Student student = new Student();
        Class<?> class3 = student.getClass();
    }
}

这个方法主要就是需要我们事先有一个对应类的对象, 然后通过这个对象去获取它的 Class 对象.


上面可以看到我们创建了三个 Class 对象, 如果是根据我们之前的知识, 创建出来的对象, 它们的引用应该是不同的, 那么如果我们使用==对它们进行比较, 那么这个结果是肯定为 false 的.

但是假如我们尝试运行下面的代码, 则会发现情况似乎有所变化

public class Demo01 {
    public static void main(String[] args) {
        Class<?> class1;
        try {
            class1 = Class.forName("reflect.Student");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        Class<?> class2 = reflect.Student.class;

        Student student = new Student();
        Class<?> class3 = student.getClass();

        System.out.println(class1 == class2);
        System.out.println(class2 == class3);
        System.out.println(class1  == class3);
    }
}

运行一下, 会发现这三个equals()的结果都为true, 那么是为什么呢?

在这里插入图片描述

这个时候就不得不提到这个 Class 对象是怎么创建出来的了, 我们上面说过反射是运行状态下的机制, 因为这个 Class 对象实际上是由我们的 JVM 在程序运行时创建的.

也就是说, 我们的这些看似创建 Class 对象的操作, 实际上并不是真正意义的去创建了一个对象, 而是相当于去向 JVM 要它创建好了的对象, 那么自然指向的都是同一个对象了.

反射使用

那么接下来我们将利用Class类里面的方法, 来展示一些反射的效果

首先是一个通过类对象去创建对应类的对象的代码

public class Demo01 {
    public static void reflectNewInstance() throws InstantiationException, IllegalAccessException {
        // 获取 Student 类的类对象
        Class<?> class1;
        try {
            class1 = Class.forName("reflect.Student");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        // 使用 Student类对象 里面的方法创建 Student对象
        Student student = (Student)class1.newInstance();
        System.out.println(student);
    }
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        reflectNewInstance();
    }
}

在这里插入图片描述


下面是一个获取私有构造方法, 然后再构造对象的例子

public static void reflectPrivateConstructor() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    // 获取 Student 类的类对象
    Class<?> class1;
    try {
        class1 = Class.forName("reflect.Student");
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }

    // 获取一个空参数的构造方法
    Constructor<?> constructor = class1.getConstructor();
    // 向下转型
    Student student1 = (Student) constructor.newInstance();
    System.out.println(student1);


    // 获取一个有 String 参数和 int 参数的构造方法
    Constructor<?> declaredConstructor
            = class1.getDeclaredConstructor(String.class, int.class);
    // 由于这个构造方法是私有的, 需要手动确认
    // 允许访问私有构造方法
    declaredConstructor.setAccessible(true);
    // 传递参数, 构造新对象
    Student student2 = (Student) declaredConstructor.newInstance("李四", 25);
    System.out.println(student2);
}

可以发现一个恐怖的事实就是, 在反射面前, 我们的封装似乎变得一文不值, 即便是 private 修饰的属性, 我们依旧还是能够轻松获取. 因此如果要使用反射, 那么势必需要去考虑到这一点.

反射的优点和缺点

优点:

  1. 对于任意一个类, 都能够直到这个类的所有属性和方法, 对于任意个对象都能调用其方法
  2. 增加代码灵活性, 可以针对不同的类采取相同的处理方式

缺点:

  1. 使用不当容易使得封装无效化
  2. 过多使用反射会降低运行效率
  3. 反射使用了绕过源代码的技术, 因此会带来维护问题, 并且运用了反射的代码可读性比起不用反射达成相同目的的代码会更弱

枚举

初识枚举

枚举是 Java 中一个非常特殊的类型, 它通常用于表示一些常量集合. 例如我们可以定义一个颜色枚举, 它的里面就是一些颜色, 例如红色,蓝色,绿色这样的. 下面我们就简单演示一下如何创建一个枚举

我们可以在创建类的时候直接选择枚举类

在这里插入图片描述

枚举成员我们是没有办法去实例化的, 因为枚举就相当于是我们可以预先去定义一些成员, 后续就直接把这些成员取出来. 因此我们需要在创建枚举的时候, 就把它的成员写好.

书写方式就是直接给这些枚举对象取名字, 然后以逗号分割, 分号结尾, 如下所示. 实际上在这里, 如果不写分号也是可以的, 不过如果后面需要补充一些代码, 那么还是要把分号加上, 因此这里就直接推荐加上

另外这里对于枚举类型来说, 推荐的命名方式和常量相同, 采取全大写的写法, 单词和单词之间使用下划线隔开

public enum ColorEnum {
    RED,BLUE,GREEN,WHITE;
}

随后, 如果我们想要去调用这些对象, 那么就可以直接使用枚举类的名字取出来

public class Main {
    public static void main(String[] args) {
        System.out.println(ColorEnum.RED);
    }
}

在这里插入图片描述

枚举对象的使用

实际上, 这些枚举对象, 他还有一个值用于表示这个枚举对象的创建顺序, 我们可以通过original()方法去获取这个值, 如下所示

public class Main {
    public static void main(String[] args) {
        System.out.println(ColorEnum.RED.ordinal());
        System.out.println(ColorEnum.BLUE.ordinal());
        System.out.println(ColorEnum.GREEN.ordinal());
        System.out.println(ColorEnum.WHITE.ordinal());
    }
}

在这里插入图片描述

同时, 我们也可以通过创建枚举变量的方式, 把这些枚举取出来.

public class Main {
    public static void main(String[] args) {
        ColorEnum color1 = ColorEnum.RED;
        ColorEnum color2 = ColorEnum.RED;
        ColorEnum color3 = ColorEnum.RED;
    }
}

此时通过调试, 我们也可以看出, 它们确实取出的是同一个对象, 并且有两个属性

在这里插入图片描述

其中的 name 实际上就是我们写枚举对象时候提供的名字.


那么这些枚举类是否能够自定义一些属性呢? 当然也是可以的, 如下所示

public enum ColorEnum {
    RED,BLUE,GREEN,WHITE;
    
    int value;
    
    String name;

}

那我们又需要如何给这些属性赋值呢? 我们又没有办法去调用构造方法.

此时我们就需要在创建枚举的时候, 直接去调用构造方法赋值, 如下所示

public enum ColorEnum {
    RED(12, "red"),
    BLUE(13, "blue"),
    GREEN(14, "green"),
    WHITE(15, "white");

    int value;

    String name;

    ColorEnum(int value, String name) {
        this.value = value;
        this.name = name;
    }

}

这里需要注意的是, 枚举的构造方法是默认为 private 的, 如果我们给构造方法加其他的修饰就会直接报错.

在这里插入图片描述

在这里插入图片描述

同时当我们书写含参构造方法后, 那么此时创建枚举的时候必须要赋值, 除非再提供一个无参构造方法. 换句话说, 上面创建枚举对象的时候, 本质上就是在调用构造方法.

Enum类

枚举类的本质实际上是继承了 Enum 类的子类, 那么也就是说, 我们的枚举类是可以用它带有的一些方法的. 例如下面就是我们枚举类可以用的一些方法

方法名称描述
values()以数组形式返回枚举类型的所有成员
ordinal()获取枚举成员的索引位置
valueOf()将普通字符串转换为枚举实例
compareTo()比较两个枚举成员在定义时的顺序

我们这里就简单演示一下第一个方法和第三个方法

public class Main {
    public static void main(String[] args) {
        // 遍历枚举
        ColorEnum[] values = ColorEnum.values();
        for (ColorEnum value : values) {
            System.out.println(value);
        }

        // 根据字符串获取枚举
        System.out.println(ColorEnum.valueOf("RED"));
    }
}

此时可能他有人翻阅了源码, 发现 Enum 类中根本就没有values()这个方法, 那么我们调用的方法究竟是从何而来的?

下面我们就借助一下我们刚学习的反射, 来研究一下这个问题

public class EnumReflection {
    public static Set<String> analyze(Class<?> enumClass) {
        Set<String> methods = new TreeSet<>();
        for (Method m : enumClass.getMethods()) {
            methods.add(m.getName());
        }
        return methods;
    }
    public static void main(String[] args) {
        // 获取颜色枚举类的方法
        Set<String> colorEnumMethods = analyze(ColorEnum.class);
        // 获取 Enum 的方法
        Set<String> enumMethods = analyze(Enum.class);

        System.out.println("ColorEnum 中的方法: " + colorEnumMethods);
        System.out.println("Enum 中的方法: " + enumMethods);
        System.out.print("ColorEnum 中的方法减去 Enum 中的方法: ");
        colorEnumMethods.removeAll(enumMethods);
        System.out.println(colorEnumMethods);
    }
}

在这里插入图片描述

此时可以发现, 这个values()方法, 似乎并不是通过继承拿过来的, 但是确实也存在这个方法. 因此我们再去字节码文件看一眼

在这里插入图片描述

发现确实存在, 因此我们可以大致确定, 这个方法应该就是编译器在编译的时候, 会自动添加上去, 而不是继承自 Enum 类.

同时, 如果你尝试对枚举类进行向上转型, 也可以发现没有办法调用values()方法

在这里插入图片描述

枚举与反射

枚举实际上还有一个特殊的点, 就是它有关于反射的一些设定. 在上面我们介绍反射的时候提到, 即使一个属性是 private 的, 反射也可以轻易的获取. 而枚举它的构造方法, 又是一个自带 private 的构造方法, 因为枚举不允许在外部去进行初始化

那么如果此时我尝试使用反射去获取枚举的构造方法, 是否可以成功呢?

我们先尝试运行下面的代码

public class EnumConstructorReflection {
    public static void main(String[] args) throws NoSuchMethodException {
        // 获取 ColorEnum 的 Class 对象
        Class<ColorEnum> colorEnumClass = ColorEnum.class;
        // 获取私有构造方法
        Constructor<ColorEnum> constructor
                = colorEnumClass.getDeclaredConstructor(int.class, String.class);
    }
}

此时会发现, 直接报错, 显示找不到对应的方法

在这里插入图片描述

实际上由于枚举类比较特殊, 默认继承了 Enum 类, 因此我们这里不仅仅要管我们自己枚举里面的值, 还要管父类里面的值, 可以看到 Enum 也是需要两个值的.

在这里插入图片描述

换句话说, 我们要找的应该是一个四个参数的构造方法.

那么将代码进行修改, 如下所示. 注意这里参数的排列顺序是先父类, 再子类

public class EnumConstructorReflection {
    public static void main(String[] args) throws NoSuchMethodException {
        // 获取 ColorEnum 的 Class 对象
        Class<ColorEnum> colorEnumClass = ColorEnum.class;
        // 获取私有构造方法
        Constructor<ColorEnum> constructor
                = colorEnumClass.getDeclaredConstructor(String.class, int.class, int.class, String.class);
        // 设置访问权限
        constructor.setAccessible(true);
        
        try {
            // 调用私有构造方法
            ColorEnum colorEnum = constructor.newInstance("BLACK", 16, 16, "black");
            System.out.println(colorEnum);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

此时会发现, 爆了一个独特的错误, 直接显示不能反射创建枚举对象

在这里插入图片描述

此时为了查询原因, 我们直接点到源码里面, 看看为什么newInstance()方法不能创建一个枚举类型的对象.

可以看到, 它这里直接就对枚举进行了一个特殊的检查, 如果发现是枚举类型, 直接抛出异常.

在这里插入图片描述

那么这里在最强的矛和盾的战斗中, 最终还是盾获取了胜利. 实际上, 这个也是一个相关问题的部分答案, 即为什么使用枚举去实现单例模式是安全的?, 不过这个问题需要对单例模式有一定了解, 这里不过多介绍了.

枚举的优缺点

优点:

  1. 枚举常量更加安全, 只能取特定值, 可以防止出现赋值不合法值得情况
  2. 便于理解维护, 枚举的名称可以直接反映出其含义

缺点:

  1. 由于继承了 Enum类, 因此不能再继承其他类了, 难以拓展
  2. 占用更多内存, 由于枚举也是对象, 比起一般的常量来说更加占据内存

Lambda表达式

概念介绍

Lambda 表达式是一个用于简化函数式接口的语法而设置的一个语法糖, 语法糖顾名思义, 语法上吃起来甜甜的东西, 实际指的就是一系例简化后的语法. 例如我们之前使用过的 for-each 循环, 就是简化了遍历数组或者集合这样的语法而设置的一种语法糖.

那此时可能有人就要问了: 既然 Lambda 表达式是为了简化函数式接口设置的语法糖, 那函数式接口又是什么呢?

实际上, 当我们的接口中, 有且仅有一个抽象方法的时候, 那么这个接口就可以被称作是函数式接口. 例如下面这个接口就是一个函数式接口

@FunctionalInterface
interface NoParameterNoReturn {
    void test();
}

并且我们还在上面加上了一个注解@FunctionalInterface, 这个注解和之前那个重载的注解@Override作用一样, 都是用来帮助我们进行检查的, 这个注解就是来帮我们检查这个接口是否是函数式接口的

初步使用

从上面的介绍中, 我们知道了 Lambda 表达式是用来简化使用函数式接口的语法而设定的语法糖, 那么在了解如何使用 Lambda 表达式前, 我们首先需要看看如果没有简化的话, 我们如何去使用函数式接口.

我们以前如果要使用一个接口, 那么就务必需要通过一个类去实现一个接口, 那么此时就会用两种方案

  1. 创建一个类实现接口
  2. 使用匿名内部类实现接口

那么我们首先就来演示一下, 如果按照原始的方法, 我们如何使用这个函数式接口

假设我们现在有如下函数式接口

@FunctionalInterface
interface NoParameterNoReturn {
    void test();
}

那么我们这里首先是创建一个类实现这个借口, 以及使用匿名内部类去实现接口, 他们做的事情都是去打印一个Hello World

class Test implements NoParameterNoReturn{
    @Override
    public void test(){
        System.out.println("Hello World");
    }
}

public class Demo {
    public static void main(String[] args) {
        // 通过类实现接口的方式
        NoParameterNoReturn test1 = new Test();
        test1.test();

        // 通过匿名内部类的方式
        NoParameterNoReturn test2 = new NoParameterNoReturn() {
            @Override
            public void test() {
                System.out.println("Hello World");
            }
        };
        test2.test();
    }
}

可以看到, 这个代码还是比较繁琐的, 那么此时我们引入Lambda表达式, 就会形成如下的代码

public class Demo {
    public static void main(String[] args) {
        // 通过 Lambda 表达式
        NoParameterNoReturn test1 = () -> System.out.println("Hello world");
        test1.test();
    }
}

可以看到, 这个代码就被简化为了只有一串核心的打印语句. 那么它是如何去调用的呢?

实际上, 这个括号就是去重写参数的, 不过由于这里没有参数, 就是一个空的括号. 然后就是一个箭头, 指向要执行的代码, 如果是有多行代码那么就需要使用大括号.

public class Demo {
    public static void main(String[] args) {
        // 通过 Lambda 表达式
        NoParameterNoReturn test1 = () -> {
            System.out.println("Hello world");
            System.out.println("Hello world");
            System.out.println("Hello world");
        };

        test1.test();
    }
}

如果需要返回值, 那么也可以在代码中进行返回

其他情况使用

上面我们初步了解了一下 Lambda 表达式的使用, 接下来我们就通过一些不同的情况来熟悉一下 Lambda 表达式的使用

首先是一个没有返回值有一个参数的情况

//无返回值一个参数
@FunctionalInterface
interface OneParameterNoReturn {
    void test(int a);
}

public class Demo {
    public static void main(String[] args) {
        OneParameterNoReturn test = (a) -> System.out.println("hello " + a);

        test.test(123456);
    }
}

可以看到, 我们并不需要去传参数的类型, 直接取一个名字就行, 那么如果有多个参数也是同理的

//无返回值一个参数
@FunctionalInterface
interface MoreParameterNoReturn {
    void test(int a, int b);
}

public class Demo {
    public static void main(String[] args) {
        MoreParameterNoReturn test = (a, b) -> {
            System.out.println("hello " + a);
            System.out.println("hello " + b);
        };

        test.test(123456, 654321);
    }
}

接下来看一个有返回值的情况, 其实也很好处理, 直接在代码块里面写 return 即可

//有返回值无参数
@FunctionalInterface
interface NoParameterReturn {
    int test();
}

public class Demo {
    public static void main(String[] args) {
        NoParameterReturn test = () -> {
            return 123456;
        };

        int ret = test.test();
        System.out.println(ret);
    }
}

但是这个其实也可以简化, 假如说代码块里面只有一个返回值语句, 此时我们可以直接使用箭头指向返回值, 如下所示

//有返回值无参数
@FunctionalInterface
interface NoParameterReturn {
    int test();
}

public class Demo {
    public static void main(String[] args) {
        NoParameterReturn test = () -> 123456;

        int ret = test.test();
        System.out.println(ret);
    }
}

变量捕获机制

下面我们看一段代码, 看看这段代码中有没有什么问题.

public class Demo {
    public static void main(String[] args) {
        int num = 123456;
        NoParameterNoReturn test = new NoParameterNoReturn() {
            @Override
            public void test() {
                System.out.println(num);
            }
        };

        test.test();
    }
}

此时可能乍一看, 似乎没有什么问题, 那此时我就要提出一个问题: 为什么这个 num 明明是在 main() 方法创建的一个局部变量, 他却可以在一个匿名内部类中其他方法里面使用呢?

换句话说, 你的这个局部变量明明是应该属于 main() 的, 凭什么可以去跨方法域使用?

实际上匿名内部类中的一个语法规则, 叫做变量捕获机制. 它可以直接捕获到上一层作用域中的变量, 同时, 既然能在匿名内部类里面用, 那在Lambda表达式里面也可以使用. 如下所示

public class Demo {
    public static void main(String[] args) {
        int num = 123456;
        
        NoParameterNoReturn test = () -> System.out.println(num);
        
        test.test();
    }
}

但是这个机制有一个前提条件, 就是这个变量一定要是一个常量.

那此时可能有人就要问了: 你上面的这个例子, 捕获的也不是一个常量啊?为什么没有报错呢?

实际上这里的常量, 既可以是一个代码上的常量, 也可以是一个事实上的常量. 什么是事实上的常量?就是只要我们没有修改它, 那它不就没有被改变吗?没有改变过的变量, 自然就算作是一个事实上常量了

为了验证这一点, 我们在上面那一个例子的基础上, 添加一个修改的操作.

public class Demo {
    public static void main(String[] args) {
        int num = 123456;
        num = 1;
        NoParameterNoReturn test = () -> System.out.println(num);

        test.test();
    }
}

在这里插入图片描述

最后发现, 果然报错了.

为什么需要Lambda表达式

上面我们也说过, Lambda 表达式是一种语法糖, 能够去简化语法, 这当然是 Lambda 表达式的作用之一, 但是实际上它还有其他的一些用途

要理解 Lambda 表达式的作用, 那么我们就需要去思考一下 Lambda 表达的本质.

Lambda 表达式, 看起来的效果实际上非常类似于, 我直接写了一个方法传给一个变量, 也就是将这个方法看作是了一种参数去进行了传递(虽然实际上并不是这样, 但是至少看起来是的). 而不是传统的如果要如果要实现一个方法, 那么必须要去基于一个类来实现.

实际上, 它本质上就是去作为匿名函数, 实现回调函数的效果. 这句话听起来有一点点抽象, 因此我们拆解一下这句话.

匿名函数很好理解, 就是没有名字的函数. 那什么是回调函数呢?

回调函数指的就是, 它允许你把函数作为一种参数的方式去进行传递, 从而实现把这个函数去交给别人使用的效果.

下面我们通过一个 C 语言的代码来简单的演示一下回调函数的效果

#include "stdio.h"

int cmp (int a, int b){
    return a - b;
}

// 这个函数, 主要就是使用 cmp 函数来比较 a 和 b
// 第三个参数就是用于接受函数的
void compare(int a, int b, int (*cmp)(int, int)) {
    
    // 有了函数参数后, 就可以直接调用对应的函数进行比较, 并且接收返回结果
    int ret = cmp(a, b);
    
    if(ret > 0){
        printf("a 比 b 大");
    }else if(ret < 0){
        printf("a 比 b 小");
    }else{
        printf("a 和 b 一样大");
    }
}

int main(){
    int a = 10;
    int b = 20;
    // 这里把 a 和 b 传过去, 同时把 cmp 函数也传过去
    compare(a, b, cmp);
}

可以看到, C语言中支持一种指针, 它去指向一个函数, 从而可以把函数直接传递过去进行调用, 实现回调函数.

但是如果 Java 想要做到这种直接传递函数的方式, 是做不到的, 因为 Java 严格意义上来说, 根本就没有函数, 只有方法, 而方法又必须基于类存在. (严格意义上来说, 方法是必须属于类的, 而函数可以独立存在, 不过由于两者非常相似, 因此一般来说我们不区分这两者, 因此日常中可能会出现混用的情况, 非常正常)

因此 Java 就提供了 Lambda 表达式, 来实现类似效果, 例如这是上面代码修改过后的 Java 版本.

@FunctionalInterface
interface MyCompare{
    int cmp(int a, int b);
}

public class Demo {
    public static void compare(int a, int b, MyCompare myCompare){
        int ret = myCompare.cmp(a, b);
        if(ret > 0){
            System.out.println("a 比 b 大");
        }else if(ret < 0){
            System.out.println("a 比 b 小");
        }else{
            System.out.println("a 和 b 一样大");
        }
    }
    
    public static void main(String[] args) {
        // 通过 Lambda 表达式, 实现类似于回调函数的效果
        // 看着就好像直接把一个函数直接传过去了一样
        compare(10, 20, (a, b) -> a - b);
    }
}

此时大概我们就能够理解, 什么叫做作为匿名函数, 实现回调函数的效果了.


Java 虽然无法真正的实现函数式编程, 但是它通过了一系列方法去使得我们可以去模拟函数式编程的编程风格. 而 Lambda 表达式, 就是 Java 为了实现这种风格中的一部分, 其他还有一些例如方法引用, 高阶函数这样的东西, 我们这里就不详细介绍了, 感兴趣可以自行了解.

Lambda表达式的优缺点

优点

  1. 作为一种语法糖, 简化了语法
  2. 支持了函数式编程

缺点

  1. 对于函数式编程以及 Lambda表达式不熟悉的人, 可能会导致可读性降低
  2. Lambda 表达式的变量捕获机制, 只能捕获到常量, 导致灵活性降低

Lambda表达式的应用

Lambda 表达式在 Java 自带的很多地方都是可以使用的, 例如我们之前学习过的 PriorityQueue, 它允许我们去提供一个 Comparator 对象, 然后去自定义比较逻辑.

而实际上, 这个 Comparator 接口, 他就是只有一个抽象方法的, 也就是一个函数式接口

在这里插入图片描述

此时可能有人往下一翻, 发现有一大堆方法, 此时就要问了: 你这还是函数式接口?

在这里插入图片描述

实际上这些方法, 要么都是继承过来的, 要么就是 default 方法, 要么就是一些 default static 方法, 这些并不是 Comparator 的抽象方法, 因此是不纳入计算的

那么既然 Comparator 是一个函数式接口, 我们自然就可以采用 Lambda 表达式去简化它

如下所示

class Student {
     String name;
     int age;
}

public class Demo {
    public static void main(String[] args) {
        // 通过 Lambda 表达式, 简化 Comparator 接口使用
        PriorityQueue<Student> objects = new PriorityQueue<>(
                (o1, o2) -> o2.age - o1.age
        );
    }
}

同时, Lambda 表达式还可以在很多地方, 例如 TreeMap 的比较逻辑逻辑自定义, 多线程编程等等, 但是实际上本质相同, 就是去简化函数式接口的使用. 这里就不细致介绍了, 后续在学习过程中想起来的时候适当使用即可.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值