函数式接口(Functional Interface)是JDK 8中新增的特性,其实也是lambda表达式编程模式中的一个很重要的构成。我们先看看什么是函数式接口。
函数式接口:有且只有一个抽象方法的接口,为函数式接口。除此限制之外,函数式接口仍然遵循接口的其他基本设计原则,比如允许声明static属性、static方法,也允许有默认方法等。
@FunctionalInterface public interface Printer { void print(String message); default void print() { System.out.println(System.currentTimeMillis()); } @Override public boolean equals(Object target); static void console(String message) { System.out.println(message); } }
除此之外,函数式接口,还可以覆盖Object类中的public方法,比如上述的“equals”方法。
@FunctionalInterface是一个标记性的注释,只会在编译器检测接口是否符合函数式接口规范,如果其修饰一个非函数式接口,则会在编译时报错。
在JDK 8,重构了少数几个历史接口,将其适用于“函数接口”的设计。你可以通过跟踪@FunctionalInterface来获取这些接口的列表。我们暂且列举几个常用的,以便我们在日后编程中,更多关注它们。
1、java.lang.Runnable
2、java.util.concurrent.Callable
3、java.util.Comparator
函数式接口的使用方式:
Printer printer = (String message) -> System.out.println(message); 或者 Printer printer = (message) -> System.out.println(message); 或者 Printer printer = message -> System.out.println(message);
1、抽象方法无参数时:() -> System.out.println("message");
2、抽象方法如果有多个参数时:(p1,p2) -> System.out.println(p1 + p2);
参数的类型可以忽略,当然如果你习惯了严谨,也可以声明在参数上。从上述的简单使用上,我们会发现这些写法,跟JavaScript中的function几乎一样,这也是我们“热爱”它的原因。对于java而言,函数式接口的使用除了可以基于“行内”写法,也可以声明为实例方法或者静态方法。其实际作用,跟以往的内部类很像,我们也可以在函数式接口中,访问当前类的其他字段、this、super等。
public Printer instancePrinter() { return message -> {System.out.println(message);}; } public static Printer staticPrinter() { return message -> {System.out.println(message);}; }
在使用函数式编程的同时,我们可能还需要细细的思考一下,它会不会有哪些负面的问题?比如GC、内存?既然其跟内部类很像,但是它竟然可以“行内”声明和使用,如果遇到个for循环,会不会创建很多对象或者类?还有函数式接口,在实例化时,其class类型究竟是什么样的?
为了便于理解,我们先大体得一个结论:
1)函数式接口,实例化时,会生成一个对象实例。
2)此实例,也对应一个动态代理产生的代理类。(内部称为:动态调用)
问题来了,如果每次实例化函数式接口,都生成新的代理类和实例对象,在可能潜在引起内存问题,如果这些对象无法被GC,问题会更严重,当然就究竟会不会有什么原因导致其无法回收,也需要我们去探索。
for (int i = 0; i < 2000000; i++) { Printer printer = (String message) -> System.out.println(message); printer.print("I am " + i); }
以此为例,我们很轻巧的使用函数式接口,那么如果每次都创建一个Printer实例以及代理类对象,虽然不会导致严重的GC问题,但是我们肯定会考虑去优化设计。
我们使用如下例子逐步展开思考!
实例一:
public class TestMain { public static void main(String[] args) { for (int i = 0; i < 2; i++) { Printer printer = (String message) -> System.out.println(message); printer.print("I am " + i); System.out.println(printer); } Printer p1 = message -> System.out.println(message); Printer p2 = message -> System.out.println(message); System.out.println("p1,p2:" + (p1 == p2)); System.out.println(p1); System.out.println(p2); Printer p3 = staticPrinter(); Printer p4 = staticPrinter(); System.out.println("p3,p4:" + (p3 == p4)); System.out.println(p3); System.out.println(p4); TestMain tm1 = new TestMain(); Printer p5 = tm1.instancePrinter(); Printer p6 = tm1.instancePrinter(); System.out.println("p5,p6:" + (p5 == p6)); System.out.println(p5); System.out.println(p6); TestMain tm2 = new TestMain(); Printer p7 = tm2.instancePrinter(); System.out.println("p6,p7:" + (p6 == p7)); System.out.println(p7); } public Printer instancePrinter() { return message -> {System.out.println(message);}; } public static Printer staticPrinter() { return message -> {System.out.println(message);}; } }
I am 0 com.vipkid.meteor.TestMain$$Lambda$1/186370029@1b2c6ec2 I am 1 com.vipkid.meteor.TestMain$$Lambda$1/186370029@1b2c6ec2 p1,p2:false com.vipkid.meteor.TestMain$$Lambda$2/1323165413@1e80bfe8 com.vipkid.meteor.TestMain$$Lambda$3/1880587981@66a29884 p3,p4:true com.vipkid.meteor.TestMain$$Lambda$4/1198108795@cc34f4d com.vipkid.meteor.TestMain$$Lambda$4/1198108795@cc34f4d p5,p6:true com.vipkid.meteor.TestMain$$Lambda$5/396873410@65b3120a com.vipkid.meteor.TestMain$$Lambda$5/396873410@65b3120a p6,p7:true com.vipkid.meteor.TestMain$$Lambda$5/396873410@65b3120a
得出结论(我们限定函数式接口的内部逻辑都一样):
1)通常,Block内实例化的函数式接口实例,只有一个,比如for循环中,我们多次创建Printer对象,其实仍然是同一个Printer。
2)通常,方法主体(body)内,多次创建Printer对象(即使内部逻辑完全一致),其为不同的对象代理类不同,实例当然也不同。
3)通常,static方法返回的实例,是同一个。(肯定是同一个)
4)通常,实例方法,多次调用,返回的也是同一个。
当然,这还不是全部,我们继续看实例二:
public class TestMain { private static Object object = new Object(); public static void main(String[] args) { for (int i = 0; i < 2; i++) { Printer printer = (String message) -> System.out.println(message + object); printer.print("I am " + i); System.out.println(printer); } Printer p1 = message -> System.out.println(message + object); Printer p2 = message -> System.out.println(message + object); ... } public Printer instancePrinter() { return message -> {System.out.println(message + object);}; } public static Printer staticPrinter() { return message -> {System.out.println(message + object);}; } }
输出结果与“实例一”完全一样,说明,如果函数式接口中引用了静态实例,并不影响结果。
实例三:
public class TestMain { public static void main(String[] args) { final Object object = new Object(); for (int i = 0; i < 2; i++) { Printer printer = (String message) -> System.out.println(message + object); printer.print("I am " + i); System.out.println(printer); } Printer p1 = message -> System.out.println(message + object); Printer p2 = message -> System.out.println(message + object); ... } public Printer instancePrinter() { return message -> {System.out.println(message + this);}; } public static Printer staticPrinter() { return message -> {System.out.println(message);}; } }
I am 0java.lang.Object@30dae81 com.vipkid.meteor.TestMain$$Lambda$1/186370029@1b2c6ec2 I am 1java.lang.Object@30dae81 com.vipkid.meteor.TestMain$$Lambda$1/186370029@4edde6e5 p1,p2:false com.vipkid.meteor.TestMain$$Lambda$2/1880587981@66a29884 com.vipkid.meteor.TestMain$$Lambda$3/511754216@4769b07b p3,p4:true com.vipkid.meteor.TestMain$$Lambda$4/214126413@6f539caf com.vipkid.meteor.TestMain$$Lambda$4/214126413@6f539caf p5,p6:false com.vipkid.meteor.TestMain$$Lambda$5/1342443276@2dda6444 com.vipkid.meteor.TestMain$$Lambda$5/1342443276@5e9f23b4 p6,p7:false com.vipkid.meteor.TestMain$$Lambda$5/1342443276@4783da3f
这一次,我们在instancePrinter中引用了this,在方法区块中引用了一个Object,此时我们发现结果有了很大变化:
1)静态方法,仍然一致。
2)我们在for循环中,即block中使用的“行内”函数式接口,每次都创建了不同的实例,不过其代理类一致。
3)方法主体中的两个Printer,尽管引用了相同的object而且也是final,但是其实例完全不同,代理类也不同。
4)同一个TestMain对象,其方式实例中返回的两个Printer,对象实例不同,代理类一样。
5)不同的TestMain对象,其返回的Printer,实例肯定不同,但是代理类还是一样。
由此可见,如果函数接口,如果引用了外部的静态对象或者值,这并不影响实例化结果;但是如果引用了普通对象,比如主类的字段、this、super等,均导致每次函数实例化的结果都不同。
此外,如果函数接口中引用了final类型的简单类型,比如int、String等等,则不会影响实例化结果,此处就不再测试。
无状态函数式接口(stateless):如果函数式接口中,只操作传入的“参数列表”、或者引用静态对象、或者引用final类型简单类型(即值不可变,而不是引用不可变),则认为此接口为无状态的,那么在编译期间,则会在字节码层面“脱糖”(desugar)为静态方法函数式接口(其实就是静态的内部类的实例)。
有状态函数式接口(stateful):如果接口中,引用了this、super、主体类的字段对象、或者外部的任何普通对象,都认为是有状态的。我们在设计程序时,应该避免这种情况的出现,可能会导致函数实例无法被GC。
大家可以参考一篇文章:“lambda-translation”。本人摘要几个段落:
Static vs instance methods Lambdas like those in the above section can be translated to static methods, since they do not use the enclosing object instance in any way (do not refer to this, super, or members of the enclosing instance.) Collectively, we will refer to lambdas that use this, super, or capture members of the enclosing instance as instance-capturing lambdas. Non-instance-capturing lambdas are translated to private, static methods. Instance-capturing lambdas are translated to private instance methods. This simplifies the desugaring of instance-capturing lambdas, as names in the lambda body will mean the same as names in the desugared method, and meshes well with available implementation techniques (bound method handles.) When capturing an instance-capturing lambda, the receiver (this) is specified as the first dynamic argument.
All things being equal, private methods are preferable to nonprivate, static methods preferable to instance methods, it is best if lambda bodies are desugared into in the innermost class in which the lambda expression appears, signatures should match the body signature of the lambda, extra arguments should be prepended on the front of the argument list for captured values, and would not desugar method references at all. However, there are exception cases where we may have to deviate from this baseline strategy.
所以,我们在优化设计函数式接口和lambda编程时,静态方法优于实例方法。
有关函数式接口,可能引入性能问题探讨:
1)https://blog.jooq.org/2015/11/10/beware-of-functional-programming-in-java/
JDK中提供了集中“规范式”的Function,用于支撑JDK内部的实现、以及引导开发者适用JAVA API,它们在java.util.function包中:
1、Predicate:断言,主要方法为boolean test(T),传入一个参数、返回boolean判断结果。
2、Consumer:消费,主要方法为void accept(T),传入一个参数、无返回值。
3、Supplier:提供,主要方法为T get(),无传参,返回一个结果。通常Consumer与Supplier可以配合适用。
4、Function:函数,一个比较通用的接口,主要方法R apply(T t),传入、输出。
5、UnaryOperator:一元操作,继承Function,即输入、输出的对象类型需要一致,且输入一个参数。
6、BinaryOperator:二元操作,继承BiFunction,输入、输出的对象类型相同,输入两个同类型的参数。
7、BiFunction:二元操作,以及同类的BiConsumer、BiPredicate等,输入参数为两个。
在我们理解了函数式接口的工作原理之后,大家都可以很方便去使用这些接口完成工作了。我们展示一下,函数式接口,如何与方法引用一起协作的。
public class FunctionModel { public static void staticConsumer(String message) { System.out.println(message); } public void consumer(String message) { System.out.println(message); } public Date supplier() { return new Date(); } public boolean predicate(String message) { return message != null && message.startsWith("function"); } public void biConsumer(String m1,String m2) { System.out.println(m1 + "," + m2);
FunctionModel fm = new FunctionModel(); Consumer<String> staticConsumer = FunctionModel::staticConsumer; staticConsumer.accept("I am static consumer"); Consumer<String> consumer = fm::consumer; consumer.accept("I am consumer"); Predicate<String> predicate = fm::predicate; predicate.test("function test!"); Supplier<Date> supplier = fm::supplier; System.out.println(supplier.get()); BiConsumer<String,String> biConsumer = fm::biConsumer; biConsumer.accept("function","model");
1)任何只有一个入参、且无返回值的方法,都可以转换成Consumer;当然如果两个入参,可以转换为BiConsumer。(此处用“转换”一词不太恰当)
2)任何一个无入参、且有返回值的方法,都可以转换为Supplier。
3)任何只有一个入参、且返回值为boolean的方法,都可以转换为Predicate;当然两个入参,可以转换为BiPredicate。
不过,我们仍然需要兼顾性能方面的考虑,尽量使用静态方法构建function,尽量在function中不引用外部非static对象,这与是否使用“方法引用”并无关系。(假定consumer方法中引用了一个外部对象,那么两次通过“对象引用”生成的Consumer实例,也是不同的)。
相关推荐
本源码资源提供了Java中的函数式接口相关内容,包括接口定义和使用示例。它涵盖了函数式编程在Java中的基本概念、重要性以及如何使用函数式接口来实现函数式编程的方法。 本源码资源适用于具备一定Java编程基础的...
Java函数式编程是一种编程范式,它强调使用函数作为程序的基本构建块,将计算视为函数的组合,并且尽可能避免改变状态和可变数据。在Java 8及更高版本中,函数式编程得到了官方的大力支持,引入了Lambda表达式、...
Java 8 中提供了 several 内置函数式接口,例如: * Consumer 消费型接口: ```java @FunctionalInterface public interface Consumer<T> { void accept(T t); default Consumer<T> andThen(Consumer<? super T> ...
Java自定义函数式接口, 单参数无参使用方法。多参数带返回值使用方法。csdn博文地址:https://blog.csdn.net/yuzhiqiang_1/article/details/100579308
Java8 函数式接口实例详解 Java8 中引入了函数式接口的概念,函数式接口是一个只有一个抽象方法的接口,它可以使用 Lambda 表达式或方法引用来创建实例。函数式接口的出现使得 Java8 的编程模型更加灵活和强大。...
在Java编程中,函数式接口是Java 8引入的一个重要特性,它允许我们将代码块作为参数传递给方法,从而增强了代码的灵活性和可读性。本文将深入探讨函数式接口的概念,以及如何在实际开发中利用它们来提高代码质量。 ...
通过以上内容,我们了解到函数式接口是Java中的一个重要概念,它与Lambda表达式一起,使得Java能够支持函数式编程风格。理解并熟练使用函数式接口可以极大地提升代码的简洁性和效率。同时,Lambda的延迟执行特性在...
JAVA8 函数式接口详解 JAVA8 函数式接口是JAVA8中的一种重要特性,它提供了一些常用的函数式接口,极大的提高了我们的开发效率。在本文中,我们将详细介绍JAVA8 函数式接口的相关知识点,包括四大核心函数式接口和...
Java函数式接口详解
除此之外,`Function`还可以与`Supplier`(无参生产者)和`BiFunction`(双参数函数)等其他函数式接口结合使用,以构建更复杂的逻辑。 在给定的例子中,`actionTest1`和`actionTest2`是`Function, Integer>`的实现...
基于Java8 函数式接口理解及测试 本文主要介绍了 Java 8 中的函数式接口,包括其理解、定义、使用和测试。函数式接口是一种特殊的接口,仅包含一个抽象方法,可以被隐式转换为 lambda 表达式或方法引用。 函数式...
在Java中使用函数式接口反转数组是一种简洁且强大的方法。通过合理选择实现方法和进行性能优化,可以有效地提升代码的性能和系统的响应速度。异常处理和测试验证也是确保代码质量和可靠性的重要步骤。通过使用函数式...
Java 8引入了函数式接口的概念,这是一项重要的更新,旨在支持函数式编程风格。函数式接口是指只有一个抽象方法的接口,这样的设计使得接口能够被用作 Lambda 表达式的类型。Lambda 表达式是一种简洁的匿名函数表示...
Java函数式编程是一种将函数作为一等公民的编程范式,它强调使用函数来构造程序,减少副作用,提高代码的可读性和可维护性。在Java 8及更高版本中,函数式编程得到了显著增强,引入了Lambda表达式、函数接口、Stream...
【函数式接口与方法引用】在Java中,函数式接口是指具有且仅具有一个抽象方法的接口,这一特性使得它们成为实现Lambda表达式的基础。Lambda表达式是Java 8引入的一种新特性,允许以简洁的方式表示匿名函数。函数式...
这是Java 8为了实现函数式接口而引入的一个重要特性。 综上所述,Java 8的函数式编程特性为Java开发者提供了更优雅、高效的编码方式,尤其在处理集合数据时。通过掌握这些特性,开发者能够编写出更简洁、可读且易于...
JDK1.8【函数式接口】【定义与使用】【源码】 文章地址:https://blog.csdn.net/m0_37969197/article/details/124146253 * 函数式接口(类的定义与适应形式,只是一种类的定义形式,属于新增语法) * 包:java....
总的来说,这个压缩包提供的学习资料涵盖了Java函数式接口的理论和实践,以及Java Web开发中的关键概念,对想要深入学习Java和Java Web技术的开发者来说是一份宝贵的资源。通过深入学习和实践,开发者可以掌握使用...
函数式接口`java.util.function.Predicate<T>`代表一个接收T类型参数并返回布尔值的函数,上述代码中`n -> n > 10`就是一个Predicate实例。 此外,`java.util.Comparator<T>`接口用于比较两个对象,Lambda表达式...
Java 8 是一个重要的Java版本,它引入了函数式编程的概念,极大地扩展了语言的功能,使得Java开发者可以采用更加简洁、高效的方式来编写代码。Richard Warburton 的《java 8 函数式编程》是一本深入解析这一主题的...