为了庆祝几分钟前发布的Java 8的发布,我正在发布Java 8 Lambdas教程的草稿版本。 这是学习Streams API的一种很好的可视化方法,从第一天开始,它将帮助您开始在自己的应用程序中利用lambda。本文计划在下一期Java Magazine发行中发表,因此,请期盼最终的。版本,如果它们在出版截止日期之前,我会尽力纳入评论和反馈。
玛丽有个小拉姆达
自从Java 5中的泛型发布以来,Java Lambda是进入Java语言最有影响力的功能。它从根本上改变了编程模型,允许开发一种功能样式,并支持有效的代码并行化以利用多核系统。 尽管作为Java开发人员,您首先会注意到使用Java 8中启用了lambda的新API所带来的生产率提高。
在本文中,我们将通过使用JavaFX编写的复古游戏向您介绍新的Streams API,以使用集合和数据。 该游戏既是一个从头开始编写的简单Java 8应用程序,用于展示lambda的最佳实践,又是使用Streams API进行编程的直观指南。 但是,我们将首先通过介绍lambdas语言更改来奠定基础。
Lambdas简介
要使用lambda,必须使用最新的Java SDK(8或更高版本),并在编译时将语言级别设置为Java 8。 您可以从以下位置下载最新的Java SDK版本:
- http://www.oracle.com/technetwork/java/javase/downloads/index.html
使用支持新语法的IDE时,开发lambda变得容易得多。 大多数Java IDE已更新为具有lambdas支持,并将帮助您进行lambdas的实时错误报告和代码完成。 NetBeans和IntelliJ值得一提,因为它们在Java 8发行时具有开箱即用的最佳lambda支持,并且两者在我们在此演示的示例中均能很好地工作。
为了演示新的lambdas功能是如何工作的,下面是一小段代码,它循环访问形状列表并将蓝色的形状更改为红色:
for (Shape s : shapes) {
if (s.getColor() == BLUE)
s.setColor(RED);
}
在Java 8中,您可以通过使用如下的forEach和lambda表达式来重写相同的代码:
shapes.forEach(s -> {
if (s.getColor() == BLUE)
s.setColor(RED);
});
lambda表单在Collection接口上使用了一个称为forEach的新方法,该方法采用lambda表达式并对所有包含的元素求值。 为了简化lambda表达式的使用,整个Java核心类都进行了类似的API增强。
您可能遇到的一个相关问题是Java团队如何在不破坏向后兼容性的情况下向接口添加新方法。 例如,如果您具有实现Collection接口的代码,但未定义forEach方法,那么升级到Java 8不会破坏您的实现吗? 幸运的是,另一个称为扩展方法的功能解决了Java 8中的此问题。下面的代码清单显示了Collection接口上的forEach实现:
interface Collection<T> {
default void forEach(Block<T> action) {
Objects.requireNonNull(action);
for (T t : this)
action.apply(t);
}
// Rest of Collection methods…
}
注意新的默认关键字,它指示该方法后将带有默认实现。 子类可以自由创建自己的方法实现,但是如果未定义子类,则它们将获得与接口中定义的相同的标准行为。 这允许将新方法添加到核心Java类以及您自己的库和项目中的现有接口。
实际的lambda语法非常简单……以完整的形式在左侧提供类型和参数,在中间添加一个破折号,大于号[->],并在其后加上花括号的方法主体:
(int a, int b) -> { return a + b; }
在函数返回值的情况下,可以通过删除花括号,return关键字和分号来简化此操作:
(a, b) -> a + b
此外,在只有一个参数的情况下,您可以省略括号:
a -> a * a
最后,如果没有参数,则只需将括号留空,这对于替换Runnable实现或其他无参数方法很常见:
() -> { System.out.println("done"); }
除了基本语法外,还有一种特殊的快捷方式语法称为“方法引用”,它使您可以快速创建将单个方法引用为实现的lambda表达式。 下表总结了不同类型的方法引用以及等效的长格式lambda语法。
方法参考 | 等效λ | |
对象:: toString | obj-> Objects.toString(obj) | 静态方法参考 |
对象:: toString | obj-> obj.toString() | 会员方法参考 |
obj :: toString | ()-> obj.toString() | 对象方法参考 |
对象::新 | ()->新的Object() | 构造方法参考 |
使用新的lambdas方法时最重要的最后一个概念是创建允许您接受lambda表达式的接口。 为此,具有一个显式声明的抽象方法的任何接口都可以用于接受lambda表达式,因此被称为功能接口。
为了方便起见,他们引入了新的FunctionalInterface批注,可以选择使用该批注来标记接口,以便在检查以确保您的接口满足单个显式声明的抽象方法要求时从编译器获取帮助:
@FunctionalInterface
interface Sum {
int add(int a, int b);
}
这是推荐的最佳实践,因为它会捕获功能接口定义中的一些极端情况,例如包含默认方法,这些默认方法使您可以在功能接口上定义多个方法,因为它们不是抽象的,并且不计入单一抽象方法要求。
现在您已经对lambda语法有了基本的了解,是时候探索流API并在一个可视示例的上下文中展示lambda的功能了。
Lambdas复古游戏
玛丽有点lambda
谁的羊毛洁白如雪
玛丽去过的任何地方
Lambda一定会去!
如今,视频游戏都是关于高分辨率3D图形,电影品质的剪切场景以及从新手到和平主义者的难度级别。 但是,在游戏的美好时光中,我们只有精灵……可爱,像素化的小人物跳舞和RPG穿越精心设计的疯狂难度关卡。
基于Sprite的图形也很容易编程,使我们能够用不到400行代码构建完整的动画系统。 完整的应用程序代码在GitHub的以下位置:
- https://github.com/steveonjava/ MaryHadALittleLambda
对于游戏中使用的所有图形,图像以标准的3×4平铺格式进行布局,如Mary的相邻Sprite表中所示。 (当然)使用Lambda完成了动画精灵的代码,只需在平铺的图像周围移动视口即可产生3帧的行走动画[水平]并更改角色朝向的方向[垂直]。
ChangeListener<Object> updateImage =
(ov, o, o2) -> imageView.setViewport(
new Rectangle2D(frame.get() * spriteWidth,
direction.get().getOffset() * spriteHeight,
spriteWidth, spriteHeight));
direction.addListener(updateImage);
frame.addListener(updateImage);
为背景添加静态图像,并添加一些关键事件侦听器以在输入时移动角色,您便拥有了经典RPG游戏的基础知识!
产生流
有几种创建新Java 8 Stream的方法。 最简单的方法是从您选择的集合开始,然后简单地调用stream()或parallelStream()方法来获取Stream对象,如以下代码片段所示:
anyCollection.stream();
您还可以使用Stream类上的静态帮助器方法从一组已知的对象返回流。 例如,要获取包含一组字符串的流,可以使用以下代码:
Stream.of("bananas", "oranges", "apples");
同样,您可以使用Stream数字子类(例如IntStream)取回生成的一系列数字:
IntStream.range(0, 50)
但是,生成新系列最有趣的方法是在Stream类上使用generate和iterate方法。 这些使您可以使用lambda创建新的对象流,该lambda被调用以返回新对象。 迭代方法特别有趣,因为它将先前创建的对象传递给lambda。 这使您可以为每个调用返回一个不同的对象,例如迭代地返回彩虹中的所有颜色:
Stream.iterate(Color.RED,
c -> Color.hsb(c.getHue() + .1, c.getSaturation(),
c.getBrightness()));
为了演示它在视觉上是如何工作的,我们将在踩到绵羊的应用程序中添加一个新元素。
新的Barn类的代码如下:
public static class Barn extends MapObject {
static final Image BARN = loadImage("images/barn.png");
public Barn(Main.Location loc) {
super(BARN, loc);
}
@Override
public void visit(Shepherd s) {
SpriteView tail = s.getAnimals().isEmpty() ?
s : s.getAnimals().get(s.getAnimals().size() - 1);
Stream.iterate(tail, SpriteView.Lamb::new)
.skip(1).limit(7)
.forEach(s.getAnimals()::add);
}
}
这段代码指定了用于基于Sprite的图形的图像,该图像被传递给超级构造函数,并实现了一个visit方法,该方法具有当Mary踏上谷仓时将被执行的逻辑。
visit方法中的第一条语句只是从Mary后面的动物列表中获取最后一个元素,如果还没有动物,则返回她。 然后将其用作iterate方法的种子,该方法将被传递给Lamb构造函数以进行lambda的首次调用。 然后,由此生成的羔羊将被传递给Lamb构造函数以进行第二次调用,并且此过程将连续重复。
结果流包括种子,因此我们可以使用skip函数从流中删除该种子,并且从理论上讲它是无限的。 由于流是惰性的,因此在添加终端操作之前,我们不必担心会创建对象,但是固定流长度的一种简单方法是使用limit函数,我们将参数7设置为跟随玛丽生出七只羊。 最后一步是添加将使用该流的终端操作。 在这种情况下,我们将使用forEach函数,并将lambda表达式设置为对动物列表中add方法的方法引用。 执行此lambda的结果是接连跟随Mary的七个羔羊:
我们要添加到游戏中的下一个元素是彩虹,它将演示Streams API中的过滤。 过滤器函数的工作方式是采用谓词lambda,该谓词对流中的每个元素求值为true或false。 结果流包含谓词lambda评估为true的所有元素。
对于彩虹的逻辑,我们将执行一个过滤器,该过滤器返回流中每第 4 个动物,并应用JavaFX ColorAdjust函数来改变色相以匹配传入的颜色。 对于白色,我们使用null(无颜色偏移)。 以下代码是Rainbow MapObject的visit方法的实现:
s.getAnimals().stream()
.filter(a -> a.getNumber() % 4 == 1)
.forEach(a -> a.setColor(null));
s.getAnimals().stream()
.filter(a -> a.getNumber() % 4 == 2)
.forEach(a -> a.setColor(Color.YELLOW));
s.getAnimals().stream()
.filter(a -> a.getNumber() % 4 == 3)
.forEach(a -> a.setColor(Color.CYAN));
s.getAnimals().stream()
.filter(a -> a.getNumber() % 4 == 0)
.forEach(a -> a.setColor(Color.GREEN));
当玛丽踩到彩虹时,所有的羔羊都会根据您指定的颜色值着色:
“ Lamb” da问题1:如果您在参观彩虹后踏上谷仓会怎样?
使用过滤的另一种方法是利用添加到Collection API的新方法来接受谓词lambda。 其中包括removeIf,它过滤掉与给定谓词不匹配的所有元素,并进行过滤(位于ObservableList上)并返回只包含与谓词匹配的项的FilteredList。
我们将使用它们来实现Church对象,该对象将过滤“纯”动物。 教会职员会煮熟任何白色的动物来喂养有需要的人。 这包括增加标牌上“已送餐”的计数器,并从列表中删除“纯”动物。 教会拜访方法的代码如下所示。
Predicate<SpriteView> pure =
a -> a.getColor() == null;
mealsServed.set(mealsServed.get() +
s.getAnimals().filtered(pure).size()
);
s.getAnimals().removeIf(pure);
在以下屏幕截图中,您可以看到连续踩彩虹和教堂的结果。
问题2:是否可以在所有动物上色后使用教堂清理它们?
Streams API中最强大的操作可能是地图功能。 这使您可以将流中的所有元素从一种对象类型转换为另一种对象,并在此过程中执行强大的转换。 我们将用它来实现鸡舍,所有跟随玛丽的动物都将被转换成卵。
对于鸡舍,我有两种访问方法的实现。 第一个使用带有lambda表达式的单个map操作用鸡蛋替换流元素,如下所示:
// single map:
s.getAnimals().setAll(s.getAnimals()
.stream()
.map(sv -> new Eggs(sv.getFollowing())
).collect(Collectors.toList()));
第二种实现使用方法引用和一组映射操作链来首先将流转换为动物所跟随的流,然后调用构造函数方法引用来创建卵,并将以下信息传递给构造函数参数:
// or a double map:
s.getAnimals().setAll(s.getAnimals()
.stream().parallel()
.map(SpriteView::getFollowing)
.map(Eggs::new)
.collect(Collectors.toList())
);
这两个代码片段的行为和执行相似,因为流API被设计为惰性的,并且仅在调用终端操作(例如collect)时评估流。 因此,它主要是您更喜欢使用的样式问题。 使用新的鸡舍MapObject运行该程序,可以让您从羔羊生成鸡蛋,如下图所示:
问题3:如果您将彩色的羔羊送到鸡舍,鸡蛋是什么颜色?
请注意,每个鸡蛋精灵包含三个小弹跳鸡蛋。 如果我们能把这些家伙孵化成鸡,那不是很好吗?
要孵化这些鸡蛋,我们将为巢添加一个新的MapObject,其中使用以下孵化方法将这些鸡蛋孵化成三只鸡的组:
public static Stream<SpriteView> hatch(SpriteView sv) {
if (!(sv instanceof Eggs)) {
return Stream.of(sv);
}
return Stream.iterate(sv, Chicken::new).skip(1).limit(3);
}
请注意,此方法返回一个对象流,这意味着,如果我们使用常规映射操作,则将返回一个流流。 为了将Stream扁平化为单个鸡列表,我们可以改用flatMap,它既可以使用lambda函数映射流,也可以将嵌套的Stream折叠为单个对象列表。 使用flatMap的嵌套访问功能的实现如下所示:
s.getAnimals().setAll(s.getAnimals()
.stream().parallel()
.flatMap(SpriteView.Eggs::hatch)
.collect(Collectors.toList())
);
现在,将鸡蛋放到巢中后,您将得到爆炸的鸡,如以下屏幕截图所示:
问题4:在游戏内存不足之前,您可以添加大约几只动物?
我们将添加的最后一个元素是一只狐狸,以演示如何减少流。 为此,我们将首先根据动物的规模将流映射到整数列表,然后使用sum方法引用将其减少为单个值。 reduce函数需要一个种子值(我们将使用0作为其值),以及一个可以将两个元素简化为单个结果的函数。 该lambda将递归应用于流中的所有元素,直到得到单个值,该值将是所有动物比例的总和。
Double mealSize = shepherd.getAnimals()
.stream()
.map(SpriteView::getScaleX)
.reduce(0.0, Double::sum);
setScaleX(getScaleX() + mealSize * .2);
setScaleY(getScaleY() + mealSize * .2);
shepherd.getAnimals().clear();
然后,我们取总和(存储到名为mealSize的变量中),并使用它来按比例拉伸狐狸。 您可以在下图中看到为狐狸吃一顿美味的食物的结果:
问题5:您如何更改Fox的代码以使其在进食时变胖?
在本文中,我们介绍了基本的lambda语法,包括方法引用,扩展方法和功能接口。 然后,我们在Streams API中进行了详细介绍,展示了一些常见的操作,例如迭代,过滤,映射,flatMap和reduce。 如您所见,Java 8 lambda极大地改变了编程模型,使您可以编写更简单,更精美的代码,并为诸如Streams之类的新功能强大的API开辟了可能性。 现在是时候在您自己的开发中开始利用这些功能了。
翻译自: https://www.javacodegeeks.com/2014/03/java-8-released-lambdas-tutorial.html