拥抱新年代的Java
Java作为面向目标编程的主力言语,曾经风行一时,在Web范畴是绝对的老迈。随着时间的推移,一些新的编程范式不断的呈现,如函数式编程,呼应式编程,以及对函数的全力支撑(Lambda函数)变成了我们常常谈论的论题。移动互联网的呈现,以及前端的流行,让新一代的编程言语如Scala,Groovy,Swift以及Kotlin都大受欢迎。以函数式编程为中心的新一代编程范式慢慢变成了干流。曾经的王者Java,一度被人垢病,由于对函数支撑不友好,(其实最首要的原因是怎么坚持好向后兼容),但也与时俱进,总算在Java 8版别迈出了重大的一步,彻底支撑了函数式编程。本篇将要点评论Java 8的新特性,以及怎么用Java 8来实践函数式编程。
Lambda表达式
也即匿名函数,称之为lambda。具体数学上的界说比较杂乱就不多说了。为了便于了解,我们先从匿名内部类说起。
Java早就支撑匿名内部类,这是在当年相比较C++一个重要大的提升,它在一些需求提供行为完成的当地仍是十分便利的,典型的比如便是UI中的点击事件的处理:
button.addActionListener(new ActionListener() {
@Override
void actionPerformed(ActionEvent e) {
System.out.println("Button is clicked: " + e);
}
});
这儿的要点便是我们向button传递的是一个行为,也便是说按扭点击了时,要执行什么样的行为。比照其他现代言语,这仍是显得有些粗笨,没有简略明了的说明意图。用Java 8,这就好办多了,能够这样写:
button.addActionListener(e -> System.out.println("Button is clicked: " + e);
括号里边这一坨便是一个Lambda表达式,它是一个行为(严厉来说是一个函数),用以直接向目标目标传递一个行为,比照前面的比如,能够发现,这种场景下运用Lambda愈加的简洁高效。
Lambda表达式的语法
它的通用语法是:
(p1, p2....) -> {
statements;
}
括号里边是参数列表,当只要一个参数时,括号可省掉,但当参数多于1个时,或许显现声明了参数类型时,括号不能省掉,如:
names.sort((a, b) -> b.compareTo(a));
button.addActionListener((ActionEvent e) -> System.out.println("Button is clicked: " + e));
花括号中便是语句块了,这跟惯例语句块(如if, while等)是相同的,假设有回来值就return,把它了解为惯例办法的实体就能够了,像写惯例函数完成那样去写就好了。假设只要一个语句,或许一条表达式,能够省掉花括号。
类型揣度
Lambda表达是匿名函数,首要用以向目标目标传递行为,既然匿名,当然是图简洁和明晰,因而就不要弄的太杂乱。所以,参数的类型,以及表达式的回来值(如有)的类型,都是编译器经过上下文来揣度出来的,因而,不用给参数写类型,假设由于完成的接口不明确,编译器看不懂的话,会有编译报错的。
关于类型揣度能够看《Java 8函数式编程》的第2章第5节有具体的评论。
闭包
也便是closure,严厉的数学界说就不说了,有点杂乱和难于了解,简略来说便是Lambda表达中运用了一个其界说域外的变量的值(称作捕获外部变量),lambda即变成了一个闭包。仍是有点绕,这个其实并不生疏,曾经的匿名内部便是能够运用外部变量的,只不过编译器要强加final润饰,如:
final int numberOfStudents = countStudents();
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Number of students is " + numberOfStudents);
}
}
这儿其实便是一个闭包了,匿名内部类中捕获了外部的变量numberOfStudents,只不过要强加final润饰,这是由于这儿要传值。
Java 8里边呢,外部变量不用用final润饰了,可是,它也有必要实际上是final的:
int numberOfStudents = countStudents();
button.addActionListener(e -> System.out.println("Number of students is " + numberOfStudents));
由于,之前啊,假设捕获了一个外部变量,不是final的,会有编译错误,但假设你用IDE的建议时,它就直接再声明一个final变量,用原变量赋值,然后把新的final变量传给匿名内部类,如:
int numberOfStudents = countStudents();
final int finalNumberOfStudents = numberOfStudents;
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Number of students is " + finalNumberOfStudents);
}
}
到此就明白了,Java 8关于闭包的支撑,其实较之前没有实质的变化,只不过编译器帮你做了这个final变量的界说而已。
这部分能够参考《Java 8函数式编程》中第2章第3节的内容。
接口办法默许完成
Java 8中,能够给接口interface,增加一个办法的默许完成,这样在完成此接口时,子类能够挑选从头完成,或许不完成,直接调用此办法即可,从语法上来说,是比较简略的,用default要害字来润饰办法即可,如:
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
class ComplexFormula implements Formula {
double calculate(int a) {
return super.sqrt(real) + super.sqrt(imaginary);
}
}
这儿面,子类ComplexFormula是能够正常编译和运转的。
留意:接口是支撑多重继承的,比如一个类能够完成多个接口,这就有可能存在接口中有相同的默许办法,最好的处理办法便是子类从头完成一下此办法,然后能够用接口的姓名+super来具体指定父类中的办法。这一具体的规则比较杂乱,能够看《Java 8函数式编程》这本书中的第4章第7节,有比较具体的论说。
别的,需求留意,实际运用中,接口的默许办法并不常用,由于这自身便是比较奇怪的,与开始Java的设计有冲突,接口策重于行为的高档笼统,而笼统类侧重目标的高档笼统(多半触及状况属性)。这东西的呈现首要是为了解决向后兼容,比如说当你一个被广泛运用的接口增加了一个新的办法时,所有完成此接口的类有必要全部要改一遍,要完成此接口,这会影响很多的现存代码,而默许办法便是为了解决这个问题的,给新增加的办法标记为default,就不会影响现存代码了。
这个能够仔细读一下《Java 8函数式编程》中的第4章第6节和第7节的内容。
函数接口
支撑函数式编程范式的言语一般来说呢,会把函数作为语义上的一级类型,比如像Python或许Kotlin都有专门用于声明函数的要害字。别的,需求弄清一下函数的概念,简略来说函数便是给定一些输入,然后给出输出,输出随输入改动而改动,不会发生副作用,也便是不会修改全局变量,不会修改环境变量。且具有幂等性,即针对 同一组输入,多次调用,成果仍是相同的,这便是函数。
关于Java,这事儿就有点难办了,由于前面的版别根本就没有把办法独立成为函数,办法有必要存在于类中。为了支撑函数,函数是函数式编程的基本要素,所以要想支撑函数式编程,有必要以某种方法来支撑函数的界说。Java 8中就提出了函数接口的概念。
函数接口是只要一个笼统办法的接口,这儿有两个要害信息,首要,语义上的类型有必要是一个interface,其次,它只能有一个笼统办法,放在曾经的版别,其实意思便是说只要能一个办法,但还要留意的是前面说到的默许办法。那么这儿的要求便是除了默许办法以外,只能有一个办法。
函数接口有必要用*@FunctionalInterface*注解来标示,编译器会对它做特别的重视,一旦有超越1个笼统接口,就会编译报错。为啥要用注解而不是增加要害字(如function),或许创立一级类型(如function interface),意图仍是向后兼容。注解仅需求在编译阶段做一些额定的事情即可,这即完成了扩展,又坚持了兼容性。
前面说到的Lambda表达式有必要是一个函数接口的实例,这样说太笼统了,慢慢来解说下。Lambda是一个匿名函数,能够把它了解为一个目标,它所完成的有必要是一个函数接口。换句话说,只要声明为函数接口的当地,也便是办法的参数类型或许变量的类型要声明为函数接口,只要这儿才能够传入lambda表达式。
接着前面的Formula比如,假设有如下运用场景:
class Number {
int payload;
public Number transform(Formula formula) {
return formula.calculate(payload);
}
}
现在调用transform办法时假设直接传递lambda,是会报错的:
number.transform(a -> a * a); // won't compile
解决办法,便是要给Formula增加函数接口注解:
@FunctionalInterface
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
关于函数接口,能够参看《Java 8函数式编程》中第2章第4节和第4章第4节。
常用的函数接口
Java 8 界说了一些十分常用的函数接口,这儿做一下简略的介绍。
Predict
断言,给定一个类型为T的输入,给出boolean的输出(true of false)。一般用于过滤操作之中:
Predict<String> isEmpty = String::isEmpty;
students.stream()
.filter(name -> !name.isEmpty());
Function<T, R>
通用的函数操作,给定类型为T的输入,回来类型为R的输出,一般用于map之中:
Function<String, int> length = name -> name.length();
其实,Predict可视为一种特殊的Function,它的回来类型是boolean。
Consumer
消费类型为T的目标,无输出,作为调用链的终点,一般用于生成终值,如前面比如中传给button的lambda就一个Consumer。
Supplier
回来一个类型为T的目标,也即生产者,一般都是用于工厂办法,用来生成新的目标。
UnaryOperator
一元操作符,输入类型是T的目标,回来类型是T的目标:
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator
二元操作符,输入参数是类型同为T的a和b两个参数,输出是一个类型为T的成果:
BinaryOperator<String> join = (first, second) -> first + ", " + second;
这儿比如不是许多,由于独自写这些函数接口的lambda不太好写,且意义不行实用,会在后边结合Stream API,给出更多示例。
Optional
这个类用于封装可能为空的目标,以更好的处理null的状况,它加强了类型检查(null自身是没有类型的),以及运用时的空值检查,所以能够必定程度上避免NullPointerException的呈现。
先来看一下它的简略运用办法:
Optional<String> a = Optional.of("a");
System.out.println(a.get()); // a
Optional empty = Optional.empty(); // 回来一个为空的目标
System.out.println(empty.isPresent()); // false
System.out.println(empty.orElse("b"); // b
System.out.println(empty.orElseGet(() -> "c")); // c
前面2行好了解了,第3行创立一个为空的Optional目标,它的isPresent会回来false,orElse是说假设为空时能够回来一个默许值『b』,而最后一行也能够为默许值提供一个Supplier以在为空的时候发生一个值。
经过这几个小比如能够看出Optional的用处,它能够比较好的封装目标,并提前界说值不存在时的应对状况,能够必定程序上削减NPE。
不过,这个东西关于杂乱项目来说效用不会太大,假设你到处判断isPresent,其实跟检测== null也没有本质区别。实际项目中很多的NPE来自于多线程环境共享成员变量,这种状况下Optional也救不了你。
要想发挥这东西的最大效用,需求从设计角度尽可能的削减变量共享,尽可能的缩小变量作用域,再合作默许值或许默许值的Suppiier,多管齐下,才能有用的避免NPE。
办法引证
函数式编程,函数要是语义层面的一级类型,变量或许参数的类型能够是函数,前面说到了在Java 8中代表函数类型便是函数接口。
那么,当传递具体函数体的时候,我们一向在运用lambda表达式,但这并不是合适所有场景,比如说我已经有了一个类的办法,彻底契合函数接口的办法签名,莫非还非要写一个lambda吗?
button.addActionListener(event -> System.out.println(event));
Predict<String> empty = str -> str.isEmpty();
Function<Artist, String> namer = artist -> artist.getName();
这显然太啰嗦了,这种状况下,能够直接用办法引证,来把已有的办法传递过去,形式是类名::办法名,用办法引证重写上面的几个小比如:
button.addActionListener(System.out::println);
Predict<String> empty = String::isEmpty;
Function<Artist, String> namer = Artist::getName;
简洁了许多吧,不光能够复用已有的办法,简洁明了,而且也省去创立一个lambda目标。必定要留意的便是要用办法的姓名,不能加括号,由于加了括号,在语义上便是对函数的调用了,引证的便是该办法的回来值,除非这个办法的回来值是一个函数接口实例(lambda或许一个办法引证)。
除了惯例办法能够用作引证以外,还能够对结构办法进行引证,格式是类名::new,如
Artist::new; // 适当于 (name, nationality) -> new Artist(name, nationality);
能够参看《Java 8函数式编程》第5章第1节的内容。
Stream API
总算到了最为重要的特性了,为了更进一步的支撑函数式编程,Java 8新增了Stream API,它是针对集合类型(List,Map和Set等)函数式操作的支撑,以更好的把行为与遍历分离。先来看一下小比如:
比如有这样一个列表:
List<String> giants = List.of("Apple", "Google", "Microsoft", "Facebook", "Tesla");
想简略遍历一下,曾经这么写:
for (String item : giants) {
System.out.println(item);
}
但,现在只需求这样写就能够了:
giants.forEach(System.out::println);
是不是很清新,这便是典型的函数式写法,你可能会说就这?客官别急,这仅仅前戏,后边还有更影响的。
留意:这儿必定要与I/O stream区别开来,彻底是两个东西。Stream API是针对 集合操作的函数式支撑。
函数式编程中心元素是函数,它经过对函数的各种组合得到终究的成果,最为典型的便是流式调用,把函数串连起来,或许叫做链式调用,让数据在函数链中流动,终究得到希望的成果。最为经典的函数式『三板斧』便是过滤(filter),转化(map)和折叠(也称化约,英文是reduce),这是所有函数式程序的基本结构单元。能够参看《函数式编程思想》这本书的第2章,有比较具体的评论。
杂乱的实例
为了更好的演示Java 8的Stream API,以及综合运用函数式办法,本文剩余部分,将根据Brooklyn的球员技能核算信息操作为根底的实例。球队中有多名球员,每个球员有一些基本信息和一组比赛技能核算,现在教练需求对信息做一些核算。根底的类型是球员包含其基本信息和技能核算,如下:
public class Player {
private final String firstName;
private final String lastName;
private final String community;
private final int points;
private final int rebounds;
private final float fieldGoal;
@Override
public String toString() {
return "'" + firstName +
", " + lastName + '\'' +
", from '" + community + '\'' +
", scores=" + points +
", rebounds=" + rebounds +
String.format(", fieldGoal=%.2f%%", fieldGoal * 100.f);
}
private Player(String firstName, String lastName, String community, int scores, int rebounds, float fieldGoal) {
this.firstName = firstName;
this.lastName = lastName;
this.community = community;
this.points = scores;
this.rebounds = rebounds;
this.fieldGoal = fieldGoal;
}
public static class Builder {
private String firstName;
private String lastName;
private String community;
private int scores;
private int rebounds;
private float fieldGoal;
public Builder firstName(String firstName) {
this.firstName = firstName;
return this;
}
public Builder lastName(String lastName) {
this.lastName = lastName;
return this;
}
public Builder community(Supplier<String> communitySupplier) {
community = communitySupplier.get();
return this;
}
public Builder points(Supplier<Integer> pointsSupplier) {
scores = pointsSupplier.get();
return this;
}
public Builder rebounds(Supplier<Integer> reboundsSupplier) {
rebounds = reboundsSupplier.get();
return this;
}
public Builder fieldGoal(Supplier<Float> fgSupplier) {
fieldGoal = fgSupplier.get();
return this;
}
public Player build() {
return new Player(firstName, lastName, community, scores, rebounds, fieldGoal);
}
}
}
再有便是球队了,便是针对球员们的操作的当地,首要,需求生成数据:
public class Brooklyn {
public static void main(String[] args) {
List<Player> players = generatePlayers();
}
private static List<Player> generatePlayers() {
List<String> names = List.of(
"James Harden", "Kevin Durant", "Kyrie Irving", "Nic Clyxton",
"Kessler Edwards", "Bruce Brown", "LaMarcus Aldridge", "Blake Griffin"
);
List<String> communities = List.of("The Bronx", "Brooklyn", "Manhattan", "Queens", "Staten Island");
Random random = new Random(System.currentTimeMillis());
return names.stream()
.map(name -> {
String[] parts = name.split(" ");
return new Player.Builder().firstName(parts[0])
.lastName(parts[1])
.community(() -> {
int index = random.nextInt(communities.size());
return communities.get(index);
})
.points(() -> random.nextInt(61))
.rebounds(() -> random.nextInt(31))
.fieldGoal(() -> random.nextFloat())
.build();
})
.collect(Collectors.toList());
}
}
这儿为了便利运用了一个Builder Pattern。别的,创立数据的进程中,其实用到上面说到的很多知识点,如Supplier的运用,以及闭包和办法引证。全体并不难,能够仔细读一读比如中的代码。
Stream根底操作
先来看一下Stream的根底操作,包含filter,map和reduce,以及sort和match,不准备说太多的废话,将以实例操作为主线来解说。
forEach
也即遍历,十分便利:
players.forEach(System.out::println);
输出会是:
‘James, Harden’, from ‘Brooklyn’, scores=11, rebounds=24, fieldGoal=10.62%
‘Kevin, Durant’, from ‘Queens’, scores=48, rebounds=4, fieldGoal=47.16%
‘Kyrie, Irving’, from ‘Staten Island’, scores=17, rebounds=21, fieldGoal=86.05%
‘Nic, Clyxton’, from ‘Queens’, scores=43, rebounds=11, fieldGoal=99.66%
‘Kessler, Edwards’, from ‘The Bronx’, scores=55, rebounds=12, fieldGoal=46.78%
‘Bruce, Brown’, from ‘Queens’, scores=10, rebounds=20, fieldGoal=77.51%
‘LaMarcus, Aldridge’, from ‘Manhattan’, scores=35, rebounds=22, fieldGoal=98.18%
‘Blake, Griffin’, from ‘Brooklyn’, scores=3, rebounds=4, fieldGoal=38.06%
这个运用起来适当简略,forEach接纳一个Consumer,别的需求留意的是forEach不会回来一个Stream目标,所以不能在这以后再持续增加链了。它一般作为整个链路的终端,消费终究成果。
留意:由于数据生成进程中运用了一些随机数,所以运转成果可能会不同。
filter
想看看哪些球员,命中率超越五成:
players.stream()
.filter(player -> player.getFieldGoal() >= 0.5f)
.forEach(System.out::println);
输出:
‘James, Harden’, from ‘Brooklyn’, scores=6, rebounds=22, fieldGoal=95.02%
‘Kevin, Durant’, from ‘The Bronx’, scores=37, rebounds=17, fieldGoal=78.81%
‘Bruce, Brown’, from ‘Queens’, scores=26, rebounds=23, fieldGoal=57.35%
‘LaMarcus, Aldridge’, from ‘The Bronx’, scores=35, rebounds=4, fieldGoal=59.75%
filter仍是很简单了解的,它接纳一个Predict,然后回来Stream中契合条件的元素,也即Predict中是true的。
map
转化,把一种数据类型转化为别的一种类型,其实从创立数据的办法generatePlayers中就能够看到了,是把String转化为Player,根据姓名生成数据目标。
sort
接着前面的比如,把输出按命中率从高到低排个序吧:
players.stream()
.filter(player -> player.getFieldGoal() >= 0.5f)
.sorted((a, b) -> (int) (b.getFieldGoal()*100 - a.getFieldGoal()*100))
.forEach(System.out::println);
输出:
‘Nic, Clyxton’, from ‘Queens’, scores=34, rebounds=17, fieldGoal=93.82%
‘Kyrie, Irving’, from ‘Brooklyn’, scores=37, rebounds=16, fieldGoal=82.95%
‘LaMarcus, Aldridge’, from ‘Staten Island’, scores=43, rebounds=10, fieldGoal=52.94%
flatMap
map是把一种数据类型转化为别的一各类型,然后让其在链式中流动,flatMap是更为杂乱的操作,它是先做map再做flat,想当于把二维的Stream展平成为一维的Stream,传给flatMap的lambda有必要回来一个Stream,来看个比如:
public static Stream<String> queryEmail(String name) {
return Stream.of(name + "@brooklyn.nets");
}
players.stream()
.flatMap(player -> queryEmail(player.getLastName()))
.forEach(System.out::println);
输出:
Harden@brooklyn.nets
Durant@brooklyn.nets
Irving@brooklyn.nets
Clyxton@brooklyn.nets
Edwards@brooklyn.nets
Brown@brooklyn.nets
Aldridge@brooklyn.nets
Griffin@brooklyn.nets
这儿的获取到的Email是别的一个Stream,所以这儿有必要用flatMap,也即当把player转化为Email后,有必要要再flat,变成开始的链中的目标。
及时求值
前面讲的操作都是慵懒求值的,它们都是回来一个Stream,而Stream自身仅是封装操作,其实并没有生成终究值。但有些操作是能够生成终究值的,便是把整个链路的值进行运算,然后生成终究的值,但这个值不再是Stream了,也便是说及时求值操作只能作为链式的终点。
留意:慵懒求值是函数式编程的一个概念,它的首要意图是将行为与成果分离开来,以便利并行化处理。具体能够参看《函数式编程思想》书中的第4章,有具体的论说。
count
核算一下来自『Queens』的球员数量:
long fromQueens = players.stream()
.filter(player -> player.getCommunity().equals("Queens"))
.count();
System.out.println(fromQueens);
// Output is: 2
match
查看Stream的元素中是否有匹配的条件的,有any意即任意元素有了匹配,all所有,none没有(适当于not all):
boolean fromQueens = players.stream()
.anyMatch(player -> player.getCommunity().equals("Queens"));
System.out.println(fromQueens);
// 是否有人来自于Queens,true
boolean fromQueens = players.stream()
.allMatch(player -> player.getCommunity().equals("Queens"));
System.out.println(fromQueens);
// 所有人都来自Queens,false
boolean fromQueens = players.stream()
.noneMatch(player -> player.getCommunity().equals("Queens"));
System.out.println(fromQueens);
// 所有人都来自非Queens,false
reduce
折叠或许叫作化约,有些言语也称之为fold,它接纳两个参数,榜首个是初始值,然后是一个二元操作符BinaryOperator,二元操作的榜首个参数是截止目前的成果,第二个参数是当前的元素,然后针对每个元素进行翻滚执行这个二元操作符。这么说有点难于了解,我们来个,核算球员们的总得分吧:
int totalPoints = players.stream()
.map(Player::getPoints)
.reduce(0, (a, b) -> a + b);
System.out.println(totalPoints);
// output is 247
再来核算均匀命中率:
Optional<Float> averageFieldGoal = players.stream()
.map(Player::getFieldGoal)
.reduce((a, b) -> (a + b) / 2.f);
System.out.println(averageFieldGoal.get());
// Output is 0.40
假设没有初始值,或许初始值是0(针对数值时),为空时(针对目标),那么能够省掉reduce的第1个参数,这时它会用第1个元素用作初始值。
max和min
寻找最少的篮板数的球员:
Optional<Player> minRebound = players.stream()
.min((a, b) -> a.getRebounds() - b.getRebounds());
System.out.println(minRebound.get());
输出:
‘Kessler, Edwards’, from ‘Staten Island’, scores=8, rebounds=2, fieldGoal=97.53%
寻找命中率最高的球员:
Optional<Player> bestShooter = players.stream()
.max((a, b) -> (int) (a.getFieldGoal()*100f - b.getFieldGoal()*100f));
System.out.println(bestShooter.get());
输出:
‘Bruce, Brown’, from ‘The Bronx’, scores=25, rebounds=15, fieldGoal=88.19%
留意:reduce无初始值时,max和min回来的都是Optional,由于可能会取不到具体的值。
This is just the beginning
学无止境,Stream API还有许多高档的东西,以及Java 8的新特性也还有许多,还有待后边持续深入学习。
Android SDK的支撑状况
进入智能手机年代和移动互联网年代,Java曾一度衰败,好在安卓的官方开发言语是Java,这也让Java没有被丢掉,虽然现在谷歌力推Kotlin,不过Java仍是安卓 开发的首选言语,且仍在被广泛运用。不过,安卓的Java,并不是Oracle的Java SE,而是根据Apache开源的OpenJDK,这货自Java 1.6版别以后就没怎么更新,而作为downstream的安卓,更是一向停留在1.6的版别上面,这也导致了安卓开发人猿一向未能跟紧Java的开展,当然 这也是Kotlin自推出以来大受安卓开发人猿欢迎的原因。好在谷歌也在推动,它是以打包插件的方法来支撑Java 7和Java 8的部分子集。现在AGP(Android Gradle Plugin)4.0以上的版别,是能够运用大部分Java 8的特性的,前面叙述的lambda,Optional,函数接口和Stream等都是能够直接运用的,只要把AGP的版别升级到4.0以上,sourceCompatibility挑选VERSION_1_8,就能够了。
能够参看官方文档。而这篇文章适当不错的阐述一些具体的原因,能够仔细读一下。
优质书籍
编程范式的学习曲线都是十分陡峭的,函数式编程注重的是行为的笼统,以行为(函数)为榜首要从来构建解决方案,这需求思想的转变。并不是说你用了一个lambda便是函数式编程了。因而需求系统化的学习。而系统化的学习,最好的方法便是去啃书(没说看,是要啃书)。
下面列出关于函数式编程,特别是用Java 8进行实践函数式编程的几本非优质的书籍:
Functional Thinking
中译名是《函数式编程思想》,是由Neal Ford出品的佳作,专门叙述怎么Thinking in Functional Programming。这本书也不是很厚,十分值得看。由于是要点解说函数式编程思想 的,所以它用了Java/Scala和Groovy,而且Java的版别还不是Java 8的。
这儿也要说一下,编程范式跟言语是否直接支撑没有关系,它更是一种思想笼统办法,比如用C也能写出彻底契合面向目标的代码;用Java 7曾经的版别也能写出函数式程序。
Java 8 Lambdas: Functional Programming For The Masses
中译名是《Java 8函数式编程》,由Richard Warburton写的。里边有丰厚的实例和练习题,也不厚,专注于解说怎么用Java 8来实践函数式编程。
Java 8 in Action
中译名《Java 8实战》,由三位作者Raoul-Gabriel Urma, Mario Fusco, and Alan Mycroft合著。内容其实与前面那个差不多,但略有不同,这本书是要点解说Java 8的新特性的,当然很多篇幅也是讲用Java 8实践函数式编程的(由于Java 8最重要的改善便是对函数式编程的支撑),但还有其他的内容。而且这本书较厚,里边各种知识点解说比较具体。
参考资料
- A Guide to Java Streams in Java 8: In-Depth Tutorial With Examples
- Java8 新特性教程
- [译] 一文带你玩转 Java8 Stream 流,从此操作集合 So Easy
- Java8 Stream彻底运用指南
- The Java 8 Stream API Tutorial