scala集合转java
杰西卡·克尔 ( Jessica Kerr)在12月的《 JAX杂志》上解释了Scala概念如何在使用普通的旧Java时对我们有帮助。
Java开发人员为什么要学习一种新语言? 当然要编写更好的Java!
Scala是一种可编译为Java字节码的语言,可实现更强大的面向对象设计,并在此基础上扩展功能支持。 谁会想到与Scala一起玩耍会教给我新的使用Collections这样的核心Java的新方法?
Scala的技术和习惯用法开辟了全新的推理思路,这些思路又直接适用于Java代码。 本文详细介绍了几行推理:在Scala中如何举例说明它们,如何在Java中使用它们以及如何很快在Java中使用它们。
您认为这很简单吗?
我将展示一段Java代码,将其与Scala进行对比,然后指出在学习Scala之前我从未想过的Java风格的三个缺点。 然后,我将解释Scala使得其卓越的风格成为可能的是什么,最后说明如何在Java上花更多的力气才能完成同样的事情。
这是 清单1中 常见的Java代码 。 假设我们有一个附近生物的清单,我们想要一个所有敌对生物的清单:
public List<Creature> getHostileCreatures(List<Creature> allCreatures) {
List<Creature> output = new LinkedList<Creature>();
for(creature : allCreatures) {
if (isHostile(creature )) {
output.add(creature);
}
}
return output;
}
任何Java程序员都熟悉该代码。 遍历列表,拉出所有符合条件的元素。 这是我多年编写和维护的Enterprise Java代码的全部内容。 唯一变化的部分是条件表达式( 清单1中以粗体突出显示)。
这个常见的配方不是设计模式-不要相信它! 这是一个缝模式。 在很多地方重复出现的一段代码,只有特定的部分有所不同。 请记住:仅仅因为您的祖母以这种方式编写Java并不意味着您必须这样做。 它是如此的熟悉,我们不必阅读它。 我们可以直接跳到条件代码,跳到这9行中的一小段意思。
我给你简单
减少缝模式的样板是Scala的优势。 我们在getHostileCreatures中到底想做什么? 我们正在从一个列表到另一个元素更少的列表。 像咖啡研磨一样,不需要的元素也不会通过过滤器。 这是在Scala中的外观:
瓦尔敌人= allCreatures.filter(isHostile)
哦,那更短。 它是声明性的:它说明了我们正在尝试做的事情–从列表中过滤掉某些元素–而不显示如何完成。 这种样式的直接好处是可读性。 与清单1不同,这对于那些没有十年盯着惯用的Java代码的人来说是可以理解的。 一个警告:有人期望过滤器消除满足条件的元素; 似乎倒退了。 但是,此语法描述了返回列表中的元素,例如“具有敌意的生物”。 我们对想要的元素做出了积极的评价。
除了可读性之外,Scala的声明式样式还有另外两个重大胜利。 跟着。
简单意味着硬东西很容易
当我们将“我们在做什么”与“我们如何做”分开时,我们在“方法”上有了更大的灵活性。 如果这是最后的大型战斗场景,那么生物名单可能很大。 我们可以将其分解并并行过滤! 在Java中,这太多了。 但是在Scala中:
Val敌人= allCreatures。 par .filter(isHostile)
Scala的默认List的par属性返回List的另一种实现,该方法的filter方法将列表分为多个部分,并在不同线程上过滤每个部分。 我们正在执行的操作本质上并不是“循环遍历每个元素并将它们添加到新列表中”; 这是一个硬编码到Java版本中的实现细节。 将“什么”与“如何”分开意味着可以独立地定制“如何”。 这是关注点分离。 它符合发展的主要指令,即单一责任原则。
Scala的filter方法的第三个优势在于Java代码的这一行:
List <Creature>输出=新的LinkedList <Creature>();
List的特定实现与此方法紧密结合。 如果将生物列表更有效地表示为ArrayList怎么办? 还是根本不像列表,而是集合或流? 在这个缝模式被使用的每个地方,List的具体实现都被缝合到我们的应用程序中。
考虑过滤一个非常长的列表,这是从250M文件中读取的内容。 清单1中的样式需要一次将整个列表存储在内存中; 方法返回之前,必须处理每一行。 对于严重的数据处理,这是不可接受的。 另一方面,Scala的filter方法是在工作方式不同的集合类型(包括流和迭代器)上定义的。 为了处理一个很长的文件,我将坚持使用Iterator(请参见清单2 )。
清单2
val source = io.Source.fromFile("creatures.scala")
val linesIterator = source.getLines()
linesIterator.filter(isRelevant).foreach { s =>
// process and output
}
source.close()
在此Scala代码中,整个文件都不必存储在内存中( 清单2 )。 由于从过滤后的迭代器请求元素,因此延迟应用过滤器,一次仅一行。 过滤掉或已处理过的元素可以被垃圾回收。 我们可以在有限的内存中处理任意大小的文件。
这里发生了什么?
我们已经看到,Scala的集合处理习惯用法比传统的Java风格更具可读性和灵活性。 还有许多工作方式相同的集合处理习惯用法。 在查看其中的一些内容之前,让我们探讨一下使这种样式既可行又受欢迎的Scala。
瓦尔敌人= allCreatures.filter( isHostile )
在这里,isHostile是一个接受Creature并返回布尔值的函数。
def isHostile(creature:Creature)=生物.alignment ==邪恶
在Scala中,函数是一等公民,就在那儿,上面有Java中的基元和对象。 这意味着功能可以作为其他功能的参数。 在Scala中,这样方便地传递函数和方法很方便,而且很惯用。 或者在参数列表中声明我们自己的小匿名函数:
瓦尔敌人= allCreatures.filter((creature)=> isHostile(creature))
就像能够将清单1中的重要片段isHostile(creature)传递给方法一样。 这就是我们将正在执行的“做什么”与正在执行的“如何”分开的方式。 传递代码位可以使关注点更细化。
那不是魔术
现在我们已经定义了这里发生的事情,我们可以使用Java来做到这一点。 传递代码的一种方法是策略设计模式,该模式将“如何”包装在自定义对象中。 在列表过滤中,该策略是“如何知道某个元素是否属于输出?” 我们称此函数为一个返回布尔值的谓词。 这适用于Java:
清单3
import com.google.common.base.Predicate;
import static com.google.common.collect.Iterables.filter;
…
private static final Predicate<Creature> isHostile = new Predicate<Creature>() {
@Override
public boolean apply(Creature input) {
return input.alignment == EVIL;
}
};
…
Iterable<Creature> enemies = filter(allCreatures, isHostile);
Google的Guava库提供了Scala中常见的一些有用的集合处理方法。 谓词的声明有些痛苦,因为我们必须将代码包装在一个匿名类中。 但是,将这些混乱的东西运送到某个地方之后,我们获得了所有三个好处:我们的应用程序逻辑是声明性和可读性的; 过滤器的原理封装在其他地方; 并且filter方法可以即时地在任何Iterable或Iterator上工作。 以这种方式使用Guava,我们并没有将自己绑定到List的特定实现上。
警告
在Iterables和Iterators之间切换时要小心。 迭代器可以被查看,循环,过滤和转换很多次。 这些操作不会更改列表或集合的状态。 使用Iterator,您只会得到一发子弹–在Iterator上调用一项操作,并认为该变量已过期。
坏消息是:这不会使我们习惯于缝样式for循环。 我以前的一些同事已宣布使用Guava Iterables不可读,因为他们不熟悉。
好消息:在Java 8中,它看起来更像Scala:
可迭代的<Creature>敌人= allCreatures.filter(creature-> isHostile(creature));
Java 8的两个功能组合在一起使之成为可能:扩展方法和lambda表达式。 虚拟扩展方法会添加到现有接口(例如Iterable)中,而不会破坏任何旧代码。 他们通过根据现有接口方法编写新方法来做到这一点。 缝模式for循环将封装在List的filter方法中。 欢呼,我们再也不必写了! Java 8中的第二个杀手级功能称为lambda表达式。 这些消除了Guava示例中的样板,在该示例中花了7行来声明谓词。
在Java 8中,我们将内联编写代码段。 编译器将它们形成为需要单方法接口的任何匿名实现。 生物-> isHostile(creature)在编译时成为谓词。 该代码非常整洁,不需要导入。 因此,如果您的同事抱怨Guava的收藏品伤害了他们疲倦的双眼,请告诉他们吸吮它,因为从现在开始一年后,这将是惯用的本地Java。
备份步骤
那是对一个非常简单的任务的大量分析。 这是值得的,因为相同的思考过程可对集合的其他操作起作用。 举个简单的例子,而不是“提取某些元素”,而是“从元素中提取一些数据”。 让我们将敌对生物带入其剩余力量(生命值)列表。 相同的逻辑适用。
清单4显示了Java示例:
List<Integer> getHitPoints(List<Creature> input) {
List<Integer> output = new LinkedList<Integer>();
for(creature : input) {
output.add(creature.getHitPoints());
}
return output;
}
List<Integer> enemyStrengths = getHitPoints(enemies);
那是a缝模式。 我们正在做的是将每个列表元素转换为其他元素。 一段有意义的代码是生物.getHitPoints()。
这是Scala。 出于数学上的历史原因,将“将每个元素转换为其他元素”的方法称为“地图”。
val敌人的力量=敌人。地图(_.hitPoints)
Scala程序员不喜欢键入,因此我们使用下划线字符来引用list元素。 这等效于:
val敌人的生命=敌人。地图((生物)=>生物。命中点)
在Java 7 + Guava中,该方法称为“转换” (清单5) :
import com.google.common.base.Function;
import static com.google.common.collect.Iterables.transform;
…
private static final Function<Creature, Integer> getHitPoints = new Function<Creature, Integer>() {
@Override
public Integer apply(Creature creature) {
return creature.hitPoints;
}
};
…
Iterable<Integer> enemyLife = transform(enemies, getHitPoints);
它具有与替换其他缝模式完全相同的特权:可读的应用程序逻辑,具体实现的灵活性以及即时操作。 直到以后的代码从敌人生命中请求元素时,转换功能才会应用。
做一次,做两次,第三次推广
在这里,我们采用了一段非常常见的代码,我可以闭着眼睛编写代码,而不再一遍又一遍地编写。 在使用Scala的collections库之后,每次键入代码变得如此简单时,我都会停下来问自己:“等等。 有人为我做过这件事吗? 我如何才能更普遍地做到这一点?”
例如,也许我想按角色类别对敌人进行分类,一组中的法师,另一组中的远程战斗机,第三组中的近战战斗机。 我想要一个角色类别映射到敌人列表。 在标准Java中,我将手动创建地图,并遍历整个列表。 在Scala中:
val friendsByClass =敌人.groupBy(_。characterClass)
现在敌人清单类是一个地图,其键是字符类,其值是生物序列。 什么样的顺序? 不管是什么样的敌人 。 它可以是列表,集合或流式迭代器。 将列表分成这样的类别是一项足够普遍的操作,有人为我实现了它。 我不必考虑如何实例化可变映射并遍历列表并实例化新列表以进入映射值-我所要考虑的就是我想做的事情。 我的代码清楚明了,我的代码对此更胜一筹。
Guava在Java 7中为我们提供了相同的功能。它的Multimap是对每个键包含多个值的映射的概括。 要创建我们想要的一个:
Multimap <CharacterClass,Creature> nobodyByClass = Multimap.index(敌人,getCharacterClass)
当代码作为参数传递时,没有重复太小而无法消除。 这并不意味着我们应该对看到的每个for循环进行概括。 有一个平衡。 但是,每当我考虑编写一个for循环时,我都会问自己:“是否有一个函数可以一行完成?” 在Scala中,答案通常是“是”。
想要将列表中的元素合并为一个吗? 减少了。 它可以是求和或连接或任何其他将两个元素转换为一个元素的操作。 是否想要找到特定元素? 找。 将列表列表变成一个合并列表? 展平。 检查所有元素是否都满足条件? 你明白了。
目的 | Scala的GenIterable | 番石榴可迭代 |
获取第一个元素 | 头 | getFirst |
获取第一个元素 | 头 | getFirst |
形成一个逗号分隔的字符串 | mkString(“,”) | Joiner.on(“,”).join |
向后 | 逆转 | 逆转 |
搜索列表 | ||
得到第一个满足条件的元素 | 找 | tryFind或firstMatch |
获取符合条件的第一个元素的索引 | indexWhere(lastIndexWhere) | 指数 |
问一个问题 | ||
是否有任何元素满足此条件? | 存在 | 任何或任何匹配 |
是否所有元素都满足此条件? | 对所有人 | 全部或全部匹配 |
选择一个子集 | ||
忘记头那么多元素 | 放下,放下 | 跳跃 |
只保留头那么多元素 | 采取 | 限制 |
保留不符合条件的元素 | filterNot | removeIf |
分成较小的集合 | ||
分成特定大小的集合 | 分组 | 划分 |
分为符合谓词的和不符合谓词的 | 划分 | |
删除重复项 | 不同 | toImmutableSet |
建立一个新的收藏 | ||
集合在一起 | 康卡特 | 康卡特 |
将每个元素转换为0个或多个元素 | flatMap | 转换然后展平 |
表1:Guava和Scala的有用收集功能
表1列出了Guava和Scala中其他有用的收集函数。 唯一的一致性就是不一致,因此请使用完全不同的名称在其他语言(例如Groovy)中查找这些相同的功能。
有效载荷
这些工具中的每一个都是有用的,但是将它们组合在一起的好处会更加复杂。 让我们获得生命值最低的法师。 在Scala中:
val target =敌人ByClass.get(法师).map(_。minBy(_。hitPoints))
Guava用Java提供了这种流畅的界面:
生物目标= FluentIterable.from(生物(ByClass.get(CharacterClass.MAGE)).toImmutableSortedSet(compareByHp).first();
组合是Scala和其他功能语言的精妙之处。 功能越小,关注点越分离,我们组装它们的方式就越多。 从Java到Scala的感觉就像是从Mega Bloks到Lego的毕业。 还有许多种方法可以将各个部分组合在一起!
您不必切换到Scala就能以更小巧,更精确的方式获得思考和编码的好处。 使用Guava,2013年Java 8带有lambda表达式时,用手指指着,然后在将其他缝模式嵌入代码中之前考虑一下您的选择。 相信我,没有他们,它会更加美丽。
放大代码(1)
流利的 从 (creaturesByClass。 得到 (CharacterClass.MAGE))。 toImmutableSortedSet (compareByHp)。 第一 ();
来自 :Guava的FluentIterable用流利的接口包装任何Iterable。
get :Multimap的get方法永远不会返回null –只有一个空集合。
toImmutableSortedSet :将Comparator传递给sort方法是正常的Java –看,这些东西并不奇怪!
- first :这是标准的Java Set方法。 如果集合为空,它将引发异常,这将在杀死所有敌人的法师之后发生。
放大代码(2)
val target = nobodyByClass。 得到 (法师)。 地图 ( _。minBy (_。 hitPoints ))
get : Scala的map方法从不返回null; 相反,您将获得0或1个元素的集合,称为Option。 如果有任何法师,则选项将包含它们的列表。
map :传递给map的操作适用于Option内的法师列表。
minBy :这将选择最小法师,其中 。 hitPoints 定义最小值的含义。 我们不必将Comparator传递给 minBy ; 任何将Creature转换为Comparable的功能都可以。
目标 的 类型为Option [Creature]。 如果敌人有法师,则 目标 包含最弱的一个。 如果没有要战斗的法师, 目标 是空的。
进一步阅读:
作者简介 : Jessica Kerr是密苏里州圣路易斯的一名由Java转型为Scala的开发人员。 她对学习的热情只能由对威士忌的品味来抵消。 她参加了JetBrains Academy,帮助组织了本地Java用户组,招待了她的两个女儿,并以@jessitron的身份发了推文。
本文先前在《 JAX杂志》的TomEE中发表:您喜欢的Tomcat等。 对于该问题和其他问题, 请在此处检查 。
翻译自: https://jaxenter.com/everything-i-know-about-java-collections-i-learned-from-scala-105792.html
scala集合转java