Java中Lambda表达式并行与组合行为
从串行到并行
串行指一个步骤一个步骤地处理,也就是通常情况下,代码一行一行地执行。
如果将我们常用的迭代器式的循环展开的话,就是串行执行了循环体内所定义的操作:
sum += arr.get(0); sum += arr.get(1); sum += arr.get(2); //...
在书的一开始,就提到java需要支持集合的并行计算(而lambda为这个需求提供了可能)。
这些功能将全部被实现于库代码中,对于我们使用者,实现并行的复杂性被大大降低(最低程度上只需要调用相关方法)。
另外,关于并发与并行这两个概念,其实是不同的,如果不明白的话请自行了解,在此只引用一句非常流行的话:
一个是关于代码结构,一个是关于代码执行。
如果我们想将一个计算任务均匀地分配给cpu的四个内核,我们会给每个核分配一个用于计算的线程,每个线程上进行整个任务的子任务。
书上有一段非常形象的伪代码:
if the task list contains more than n/4 elements { lefttask = task.getlefthalf() righttask = task.getrighthalf() doinparallel { leftresult = lefttask.solve() rightresult = righttask.solve() } result = combine(leftresult, rightresult) } else { result = task.solvesequentially() }
代码中,将每四个任务元素分为一组,用四个内核对其进行并行处理,然后每两组进行一次结果的合并,最终得到整个任务队列的最终结果。
从整体处理流程上看,先将任务队列递归地进行分组,并行处理每一组,然后将结果递归地进行合并(合并通过管道终止操作实现)。
java8之前,开发者们使用一种针对集合的fork/join框架来实现该模式。
然而现在,想对代码进行性能优化,就是一件非常容易的事了。
还记得我们上一节中所得出的最终代码:
long validcontactcounter = contactlist.stream() .map(s -> new contact().setname(s)) .filter(contact::call) .count();
稍加改动:
long validcontactcounter = contactlist.parallelstream() .map(s -> new contact().setname(s)) .filter(contact::call) .count();
注意stream()变为parallelstream()
同时下图将展示如何根据四个核对上述任务进行分解处理,最终合并结果并终止管道。
注意递归分解的目的是使子任务们足够小来串行执行。
组合行为
java写手应该知道,java中并不存在纯粹的“函数”,只存在“方法”。也就是说,java中的函数必须依赖于某一个类,或者作为类的某种行为存在。
而在其他语言中,存在纯函数,以coffeescript的语法,声明一个函数:
eat = (x) -> alert("#{x} has been eatten!")
这种写法与lambda表达式的语法非常相近,也就是说,相比于匿名内部类,lambda表达式看上去更像是一种函数表达式。
对于函数,一个核心操作便是组合。如果要求一元二次函数的其中一个解sqrt(sqr(b) - 4 * a * c),便是对多个子函数进行了组合。
对于面向对象,我们通过解耦的方式来分解它,同样,我们也希望以此种方式分解一个函数行为。
首先,沿用上两节中使用的例子,对contact类稍作修改,将name属性分拆为名和姓:
private string firstname; private string lastname;
假设我们现在想要对联系人们进行排序,创建自定义排序的java标准方式是创建一个comparator:
public interface comparator<t> { int compare(t o1, t o2); //... }
我们想通过比较名的首字母来为联系人排序:
comparator<contact> byfirstname = new comparator<contact>() { @override public int compare(contact o1, contact o2) { return character.compare(o1.getfirstname().charat(0), o2.getfirstname().charat(0)); } };
lambda写法:
comparator<contact> byfirstnamelambdaform = (o1, o2) -> character.compare(o1.getfirstname().charat(0), o2.getfirstname().charat(0));
写完这段代码后,idea立即提醒我代码可以替换为comparator.comparingint(...),不过这是后话,暂且不表。
在上面的代码中,我们发现了组合行为,即comparator<contact>
的compare(...)方法里面还套用了o.getfirstname()与character.compare(...)这两个方法(为了简洁,这里暂不考虑charat(...)),在java.util.function中,我们找到了这种函数的原型:
public interface function<t, r> { r apply(t t); //... }
接收一个t类型的参数,返回一个r类型的结果。
现在我们将“比较名的首字母”这个比较键的提取行为抽成一个函数对象的实例:
function<contact, character> keyextractor = o -> o.getfirstname().charat(0);
再将“比较首字母”这个具体的比较行为抽出来:
comparator<character> keycomparator = (c1, c2) -> character.compare(c1, c2);
有了keyextractor和keycomparator,我们再来重新装配一下comparator:
comparator<contact> byfirstnameadvanced = (o1, o2) -> keycomparator.compare(keyextractor.apply(o1), keyextractor.apply(o2));
到了这一步,我们牺牲了简洁性,但获得了相应的灵活性,也就是说,如果我们改变比较键为姓而非名,只需改动keyextractor为:
function<contact, character> keyextractor = o -> o.getlastname().charat(0);
值得庆幸的是,库的设计者考虑到了这一自然比较的需求的普遍性,因此为comparator接口提供了静态方法comparing(...),只需传入比较键的提取规则,就能针对该键生成相应的comparator,是不是非常神奇:
comparator<contact> comparebyfirstname = comparator.comparing(keyextractor);
即使我们想改变比较的规则,比如比较联系人姓与名的长度,也只需做些许改动:
comparator<contact> comparebynamelength = comparator.comparing(p -> (p.getfirstname() + p.getlastname()).length());
这是一个重大的改进,它将我们所关注的焦点真正集中在了比较的规则上面,而不是大量地构建所必须的胶水代码。
comparing(...)通过接收一个简单的行为,进而基于这个行为构造出更加复杂的行为。
赞!
然而更赞的是,对于流和管道,我们所需要的改动甚至更少:
contacts.stream() .sorted(comparebynamelength) .foreach(c -> system.out.println(c.getfirstname() + " " + c.getlastname()));
小结
本章的代码:
import java.util.arraylist; import java.util.comparator; import java.util.list; import java.util.function.function; public class bar { public static void main(string[] args) { // long validcontactcounter = contactlist.parallelstream() // .map(s -> new contact().setfirstname(s)) // .filter(contact::call) // .count(); list<contact> contacts = new arraylist<contact>() {{ add(new contact().setfirstname("foo").setlastname("jack")); add(new contact().setfirstname("bar").setlastname("ma")); add(new contact().setfirstname("olala").setlastname("awesome")); }}; comparator<contact> byfirstname = new comparator<contact>() { @override public int compare(contact o1, contact o2) { return character.compare(o1.getfirstname().charat(0), o2.getfirstname().charat(0)); } }; //--- using lambda form ---// comparator<contact> byfirstnamelambdaform = (o1, o2) -> character.compare(o1.getfirstname().charat(0), o2.getfirstname().charat(0)); function<contact, character> keyextractor = o -> o.getfirstname().charat(0); comparator<character> keycomparator = (c1, c2) -> character.compare(c1, c2); comparator<contact> byfirstnameadvanced = (o1, o2) -> keycomparator.compare(keyextractor.apply(o1), keyextractor.apply(o2)); comparator<contact> comparebyfirstname = comparator.comparing(keyextractor); comparator<contact> comparebynamelength = comparator.comparing(p -> (p.getfirstname() + p.getlastname()).length()); contacts.stream() .sorted(comparebynamelength) .foreach(c -> system.out.println(c.getfirstname() + " " + c.getlastname())); } }
以及运行结果:
bar ma foo jack olala awesome
以上所述是小编给大家介绍的java中lambda表达式并行与组合行为,希望对大家有所帮助