ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

第44项:坚持使用标准的函数接口

2019-07-09 21:00:24  阅读:251  来源: 互联网

标签:map 功能性 函数 44 接口 使用 类型


  既然Java有lambda,那么编写API的最佳实践已经发生了很大变化。例如,模板方法模式[Gamma95],其中子类重写基本方法以具体化其超类的行为,远没那么有吸引力。现在的替代方案是提供一个静态工厂或构造函数,它接受一个函数对象来实现相同的效果。更一般地说,你将编写更多以函数对象作为参数的构造函数和方法。需要谨慎地选择正确的功能参数类型。

  考虑LinkedHashMap。你可以通过重写其受保护的removeEldestEntry方法将此类用作缓存,该方法每次将新key添加到map时都会调用。当此方法返回true时,map将删除其最旧的entry,该entry将传递给该方法。 以下覆盖允许地图增长到一百个entry,然后在每次添加新key时删除最旧的entry,保留最近的一百个entry:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return size() > 100;
}

  这种技术【实现方式】很好,但你可以用lambdas做得更好。如果今天编写LinkedHashMap,它将有一个带有函数对象的静态工厂或构造函数。查看removeEldestEntry的声明,你可能会认为函数对象应该采用Map.Entry <K,V>并返回一个布尔值,但是不会这样做:removeEldestEntry方法调用size()来获取map中entry的数目,因为removeEldestEntry是map的实例方法。传递给构造函数的函数对象不是map上的实例方法,并且无法捕获它,因为在调用其工厂或构造函数时map尚不存在。因此,map必须将自身传递给函数对象,因此函数对象必须在输入的地方获得map,就像获取最老的entry【方式】一样【函数的形参需要传入map本身以及最老的entry】。如果你要声明这样一个功能性接口,它看起来像这样:

// Unnecessary functional interface; use a standard one instead.
@FunctionalInterface
interface EldestEntryRemovalFunction<K,V>{
    boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

  此接口可以正常工作,但您不应该使用它,因为你不需要为了这个目的声明新接口。java.util.function包提供了大量标准功能性接口供您使用。如果其中一个标准功能接口完成了这项工作,您通常应该优先使用它,而不是专门构建的功能接口。 这将使您的API学习起来更容易,通过减少其概念表面积(by reducing its conceptual surface area),并将提供重要的互操作性优势(and will provide significant interoperability benefits),因为许多标准功能性接口提供有用的默认方法。例如,Predicate接口提供了结合断言(combine predicates)的方法。对于LinkedHashMap示例,应优先使用标准BiPredicate <Map <K,V>,Map.Entry <K,V >>接口,而不是自定义EldestEntryRemovalFunction接口。java.util.Function中有43个接口。不指望你记住它们,但如果你记得6个基本接口,你可以在需要时得到其余的接口。基本接口对对象引用类型进行操作。Operator接口表示结果和参数类型相同的函数。Predicate接口表示一个接收一个参数并返回布尔值的函数。Function接口表示其参数和返回类型不同的函数。Supplier接口表示不带参数并返回(或“提供”)值的函数。最后,Consumer表示一个函数,它接受一个参数并且什么都不返回,本质上消费它的参数(essentially consuming its argument)。6个基本功能接口总结如下:

Interface Function Signature Example
UnaryOperator T apply(T t) String::toLowerCase
BinaryOperator T apply(T t1, T t2) BigInteger::add
Predicate boolean test(T t) Collection::isEmpty
Function<T,R> R apply(T t) Arrays::asList
Supplier T get() Instant::now
Consumer void accept(T t) System.out::println

  Function接口有九个附加变体,供结果类型为基本类型时使用。源(source)类型和结果类型总是不同,因为从类型到自身的函数是UnaryOperator。如果源类型和结果类型都是基本类型,则使用SrcToResult作为前缀Function,例如LongToIntFunction(六个变体)。如果源是基本类型并且结果是对象引用,则使用ToObj作为前缀Function,例如DoubleToObjFunction(三个变体)。

  有三个基本功能性接口的两个参数版本,使用它们是有意义的:BiPredicate <T,U>,BiFunction <T,U,R>和BiConsumer <T,U>。还有BiFunction变体返回三种相关的基本类型:ToIntBiFunction <T,U>,ToLongBiFunction <T,U>和ToDoubleBiFunction <T,U>。Consumer的两个参数变体采用一个对象引用和一个基本类型:ObjDoubleConsumer ,ObjIntConsumer 和ObjLongConsumer 。总共有九个基本接口的双参数版本。

  最后,还有BooleanSupplier接口,这是Supplier的一个变量,它返回布尔值。这是任何标准功能接口名称中唯一明确提到的布尔类型,但是通过Predicate及其四种变体形式支持返回布尔值。BooleanSupplier接口和前面段落中描述的四十二个接口占所有四十三个标准功能接口。 不可否认,这是一个很大的合并,而不是非常正交(Admittedly, this is a lot to swallow, and not terribly orthogonal)。另一方面,你需要的大部分功能接口都是为你编写的,并且它们的名称足够常规,以便你在需要时不会遇到太多麻烦。

  大多数标准功能接口仅提供对基本类型的支持。不要试图用基本类型的包装类来使用基本的功能性接口,而不是用基本类型的功能性接口(Don’t be tempted to use basic functional interfaces with boxed primitives instead of primitive functional interfaces)。 虽然它有效,但是它违了第61项的建议,“基本类型优先于装箱基本类型”。使用装箱基本类型进行批量操作可能会导致致命的性能后果。

  现在你知道,通常【情况下】应该使用标准功能性接口而不是编写自己的接口。但你应该什么时候写自己的【功能性接口】?当然,如果那些标准【接口】没有符合您的需要,您需要自己编写,例如,如果您需要一个带有三个参数的谓词(predicate),或者一个抛出已检查异常的谓词(predicate)。但有时你应该编写自己的功能性接口,即使其中一个标准结构完全相同。

  考虑我们的老朋友Comparator,它在结构上与ToIntBiFunction <T,T>接口相同。即使后者接口已经存在,当前者被添加到库中时,使用它也是错误的。Comparator有几个原因值得拥有自己的接口。首先,它的名称在每次在API中使用时都提供了优秀的文档,并且它被大量使用。其次,Comparator接口对构成有效实例的内容有很强的要求,有效实例包含其通用约定( general contract)。通过接口的实现,你承诺遵守其约定。第三,接口配备了大量有用的默认方法来转换和组合比较器(comparators)。

  如果你需要一个与Comparator共享以下一个或多个特性的功能接口,您应该认真考虑编写专用的功能接口而不是使用标准接口:

  • 它将被普遍使用,并可从描述性名称中受益。
  • 它与之相关的约定很强(It has a strong contract associated with it)。
  • 它将受益于自定义默认方法。

  如果您选择编写自己的功能性接口,请记住它是一个界面,因此应该非常谨慎地设计(第21项)。

  请注意,EldestEntryRemovalFunction接口(原书第199页)标有@FunctionalInterface注释。此注释类型在灵魂(spirit)上与@Override类似。它是程序员意图的声明,有三个目的:它告诉读者该类及其文档,该接口旨在启用lambdas;它保持诚实,因为除非它只有一个抽象方法,否则接口不会编译;并且它可以防止维护者在接口升级时意外地将抽象方法添加到接口。始终使用@FunctionalInterface注释来注释您的功能接口。

  最后应该关心的点是关于API中功能性接口的使用。如果在客户端中有可能产生歧义,则不要提供具有多个重载的方法,这些方法在相同的参数位置采用不同的功能接口。这不仅仅是一个理论问题。ExecutorService的submit方法可以采用Callable 或Runnable,并且可以编写一个需要强制转换的客户端程序来表示正确的重载(第52项)。避免此问题的最简单方法是不要编写在同一参数位置使用不同功能接口的重载。 这是第52项建议中的一个特例,“慎用重载”。

  总而言之,既然Java已经有了lambdas,那么在设计API时必须考虑到lambdas。 接受输入上的功能接口类型并在输出上返回它们。通常最好使用java.util.function.Function中提供的标准接口,但请注意那些相对少见的情况,那就最好编写自己的功能接口。

第45项:谨慎使用Stream

标签:map,功能性,函数,44,接口,使用,类型
来源: https://blog.csdn.net/Coloured_Glaze/article/details/95236634

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有