当前位置: 首页 > 科技观察

《lambda表达式》函数式接口、方法引用和构造函数引用

时间:2023-03-16 13:17:36 科技观察

函数式接口Java中已经有很多封装代码块的接口,比如ActionListener或者Comparator。Lambda表达式与这些接口兼容。对于只有一个抽象方法的接口,当需要这个接口的对象时,可以提供lambda表达式。这种接口称为功能接口(functionalinterface)。为什么函数式接口必须有抽象方法。接口中的所有方法不都是抽象的吗?事实上,接口完全可以重新声明Object类的方法,比如toString或clone,这些声明可能使方法不再抽象。(JavaAPI中的一些接口重新声明Object方法以附加javadoc注释。ComparatorAPI就是一个这样的例子。)更重要的是,如第6.1.5节所述,在JavaSE8中,接口可以声明非抽象方法。最好将lambda表达式视为函数而不是对象,并且接受lambda表达式可以传递给函数接口。lambda表达式可以转换为接口这一事实使lambda表达式具有吸引力。具体语法很短。事实上,在Java中,lambda表达式所能做的就是转换为函数式接口。在其他支持函数字面量的编程语言中,可以声明函数类型(如(String,String)->int),声明这些类型的变量,并用变量来保存函数表达式。然而,Java设计者决定保留熟悉的接口概念,并没有在Java语言中添加函数类型。您甚至不能将lambda表达式分配给Object类型的变量,这不是函数式接口。JavaAPI在java.util.function包中定义了许多非常通用的功能接口。其中一个接口BiFunction描述了一个函数,其参数类型为T和U,返回类型为R。我们可以将字符串比较lambda表达式存储在这种类型的变量中:BiFunctioncomp=(first,second)->first.length()-second.length();类似于比较器接口通常有一个特定的目的,而不仅仅是提供一个具有指定参数和返回类型的方法。JavaSE8遵循了这种思路。如果你想用lambda表达式做一些处理,你还是要记住表达式的用途,并为它创建一个特定的功能接口。java.util.function包中有一个特别有用的接口Predicate:publicinterfacePredicate{booleantest(Tt);//Additionaldefaultandstaticmethods}ArrayList类有一个removelf方法,它的参数是一个Predicate.该接口专门用于传递lambda表达式。例如,下面的语句将从一个数组列表中移除所有的空值:list.removelf(e->e==null);行动。例如,假设您希望在定时器事件发生时打印事件对象。当然,也可以调用:Timert=newTimer(1000,event->System.out.println(event)):但如果将println方法直接传递给Timer构造函数会更好。具体方法如下:Timert=newTimer(1000,System.out::println);表达式System.out::println是一个方法引用(methodreference),相当于lambda表达式x->System.out.println(x)。再举一个例子,假设你想对字符串进行排序而不考虑字母大小写。可以传递以下方法表达式:Arrays.sort(strings,String::compareToIgnoreCase)从这些示例中可以看出,::运算符用于将方法名与对象或类名分开。主要有3种情况:object::instanceMethodClass::staticMethodClass::instanceMethod在前2种情况下,方法引用相当于提供方法参数的lambda表达式。如前所述,System.out::println等同于x->System.out.println(x)。同样,Math::pow等同于(x,y)->Math.pow(x,y)。对于情况3,第一个参数成为方法的目标。例如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y)。如果有多个同名的重载方法,编译器将尝试从上下文中找出您指的是哪个方法。例如,Math.max方法有两个版本,一个用于整数,一个用于双精度值。选择哪个版本取决于功能接口Math::max转换为哪个方法参数。与lambda表达式类似,方法引用不能独立存在,总是被转换为函数式接口的实例。您可以在方法引用中使用此参数。例如,this::equals等同于x->this.equals(x)。使用super也是合法的。以下方法表达式:super::instanceMethod使用this作为目标并调用给定方法的超类版本。classGreeter{publicvoidgreet(){System.out.println("Hello,world!");}}classTimedGreeterextendsGreeter{publicvoidgreet(){Timert=newTimer(1000,super::greet);t.开始();}}当TimedGreeter.greet方法开始执行时,会构造一个Timer,它会在每次定时器滴答时执行super::greet方法。此方法调用超类的greet方法。构造函数引用构造函数引用类似于方法引用,只是该方法被命名为new。例如,Person::new是对Person构造函数的引用。哪个构造函数?这取决于上下文。假设您有一个字符串列表。通过对每个字符串调用构造函数,可以将其转换为Person对象数组,如下所示:ArrayListnames=...;流stream=names.stream().map(Person::new);列出人员=stream.collect(Collectors.toList());map方法为每个列表元素调用Person(String)构造函数。如果有多个Person构造函数,编译器会选择带有String参数的构造函数,因为它从上下文推断这是在String上调用构造函数。可以使用数组类型创建构造函数引用。例如,int[]::new是一个构造函数引用,它接受一个参数:数组的长度。这相当于lambda表达式x->newint[x];Java有一个限制,即不能构造泛型类型T的数组。数组构造函数引用可用于克服此限制。表达式newT[n]会产生错误,因为这将改为newObject[n]。这对开发类库的人来说是一个问题。例如,假设我们需要一组Person对象。Stream接口有一个返回对象数组的toArray方法:Object[]people=stream.toArray();然而,这并不令人满意。用户需要一个Person引用数组,而不是一个Object引用数组。流库通过构造函数引用解决了这个问题。您可以将Person[]::new传递给toArray方法:Person[]people=stream.toArray(Person[]::new);toArray方法调用此构造函数以获取正确类型的数组。然后填充这个数组并返回。