拥抱新时代的Java

news2024/11/17 12:42:13

原文链接 拥抱新时代的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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/570618.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

北邮22信通:实验五 共射放大电路的频率特性与深负反馈的影响

北邮22信通一枚~ 很高兴以一个新身份与大家见面~ 关注作者&#xff0c;解锁更多邮苑模电实验报告~ 获取更多文章&#xff0c;请访问专栏&#xff1a; 北邮22信通——电子电路_青山如墨雨如画的博客-CSDN博客 目录 实验目的&#xff1a; 实验设备及器件&#xff1a; …

区间预测 | MATLAB实现QGPR高斯过程分位数回归多变量时间序列区间预测

区间预测 | MATLAB实现QGPR高斯过程分位数回归多变量时间序列区间预测 目录 区间预测 | MATLAB实现QGPR高斯过程分位数回归多变量时间序列区间预测效果一览基本介绍模型描述程序设计参考资料 效果一览 基本介绍 MATLAB实现QGPR高斯过程分位数回归多变量时间序列区间预测 1.基于…

可用于Stable Diffusion免费的AI绘画标签生成器

这是一个开源项目&#xff0c;借花献佛&#xff0c;基于原作者的基础上进行了微调还有以及修改内容。 支持中英文&#xff0c;权重以及xxxx词汇&#xff0c;老司机都懂。 直接可在右侧根据需求进行点选操作&#xff0c;然后复制到自己的Stable Diffusion中进行AI绘画。 下载…

【产品经理】移动手机区别于PC端的10点特性

众所周知&#xff0c;当今时代已经是移动互联网时代。相比以前大家热衷于在电脑上浏览网页、玩游戏的情形&#xff0c;现在大家都喜欢通过移动手机去满足个人方方面面的需求&#xff0c;无论是购物、吃饭、求职、阅读、聊天、游戏等等。也就是说&#xff0c;在日常的生活中&…

ChatGPT变现赚钱 第二篇

前面我们已经介绍了一些利用ChatGPT做变现的一些玩法&#xff0c;比如做微信问一问的回答&#xff0c;做AI剪映的一键生成视频的(星球里面有详细的指南和视频)。 现在我们会员群在玩百度问一问答题&#xff0c;这个项目蛮好的&#xff0c;成本很低&#xff0c;现在又有苹果手机…

利用视听短片从自然刺激中获得开放的多模式iEEG-fMRI数据集

在认知神经科学领域&#xff0c;数据共享和开放科学变得越来越重要。虽然许多参与认知神经科学实验的志愿者的数据集现在是公开可用的&#xff0c;但颅内脑电图&#xff08;iEEG&#xff09;数据的共享相对较少。iEEG是一种高时间和空间分辨率的记录技术&#xff0c;通过在患者…

组长给组员派活,把组长自己的需求和要改的bug派给组员,合理吗?

组长把自己的工作派给手下&#xff0c;合理吗&#xff1f; 一位程序员问&#xff1a; 组长给他派活&#xff0c;把组长自己的需求或者要改的bug派给他。组长分派完需求之后&#xff0c;他一个人干两个项目&#xff0c;组长却无所事事&#xff0c;这样合理吗&#xff1f; 有人说…

微服务开发系列 第七篇:RocketMQ

总概 A、技术栈 开发语言&#xff1a;Java 1.8数据库&#xff1a;MySQL、Redis、MongoDB、Elasticsearch微服务框架&#xff1a;Spring Cloud Alibaba微服务网关&#xff1a;Spring Cloud Gateway服务注册和配置中心&#xff1a;Nacos分布式事务&#xff1a;Seata链路追踪框架…

华为OD机试真题 Java 实现【打印文件】【2023Q1 100分】

一、题目描述 有 5 台打印机打印文件&#xff0c;每台打印机有自己的待打印队列。 因为打印的文件内容有轻重缓急之分&#xff0c;所以队列中的文件有1~10不同的优先级&#xff0c;其中数字越大优先级越高。 打印机会从自己的待打印队列中选择优先级最高的文件来打印。 如果…

Linux常用命令——help命令

在线Linux命令查询工具 help 显示帮助信息 补充说明 help命令用于显示shell内部命令的帮助信息。help命令只能显示shell内部的命令帮助信息。而对于外部命令的帮助信息只能使用man或者info命令查看。 语法 help(选项)(参数)选项 -s&#xff1a;输出短格式的帮助信息。仅…

冯斌:突破认知临界点,打造自驱型团队 | 开发者说

熟悉冯斌的人&#xff0c;大都直接称呼其网名 Kid&#xff0c;包括他在 ONES 的同事。人如其名&#xff0c;Kid 的寓意就是「用孩子的眼光看世界」&#xff0c;返璞归真的思维方式才能发现新大陆。正如毕加索说的&#xff1a;「我一生都在向孩子学习。」 在 ONES 联合创始人兼 …

设计师解放双手之作!3秒生成风景园林效果图,AIGC赋能景观设计

‍ 项目简介 在过去几十年&#xff0c;风景园林经历了从“刀耕火种”的完全手绘设计时代到当下比较流行的参数化设计时代&#xff0c;过去的每一轮技术革新都让风景园林作品的表现形式产生了巨大的改变。随着计算机图像技术的发展&#xff0c;我们有更多的建模和渲染软件辅助提…

Python中深拷贝与浅拷贝介绍

用赋值的时候的一些注意事项 a "< meta name“Keywords” content“小博测试成长之路” />" b a c bprint(id("< meta name“Keywords” content“小博测试成长之路” />")) print(id(a)) print(id(b)) print(id(c)) print(a is b) # a和b…

面试题百日百刷-java基础篇(九)

锁屏面试题百日百刷&#xff0c;每个工作日坚持更新面试题。请看到最后就能获取你想要的,接下来的是今日的面试题&#xff1a; 1.Java 中IO 流分为几种? 按照流的流向分&#xff0c;可以分为输入流和输出流&#xff1b; 按照操作单元划分&#xff0c;可以划分为字节流和字符…

BGP 基础知识学习笔记

今天海翎光电的小编为大家介绍一下BGP的相关基础知识&#xff0c;文章浅显易懂&#xff0c;适合对BGP完全没有了解的同学。 BGP&#xff08;边界网关协议&#xff09;是将互联网联合在一起的路由协议&#xff0c;海翎光电的小编将解释在哪些情况下我们需要 BGP 以及它是如何工作…

为什么你工作很努力却没有晋升?分析晋升的关键点!

见字如面&#xff0c;我是军哥&#xff01; 昨天有一位二线大厂的程序员读者和我吐槽&#xff0c;说马上公司一波人晋升&#xff0c;可是晋升名额并么有他&#xff0c;但是他在这家公司已经干了三年了&#xff0c;工作也很努力&#xff0c;996 更是家常便饭&#xff0c;难道是大…

C语言小游戏的实现——扫雷(使用C语言基础语法)

前言 结合前边我们所学的C语言知识&#xff0c;本期我们将使用C语言实现一个简单的小游戏——扫雷 目录 前言 总体框架设计 多文件分装程序 各功能模块化实现 初始化棋盘 棋盘打印 埋雷 判赢与排雷 游戏逻辑安排 总结 总体框架设计 和三子棋相同&#xff0c;游戏开始时…

109.(cesium篇)cesium椎体上下跳动+旋转

地图之家总目录(订阅之前请先查看该博客) 地图之家:cesium+leaflet+echart+地图数据+地图工具等相关内容的介绍 文章末尾处提供保证可运行完整代码包,运行如有问题,可“私信”博主。 效果如下所示: 下面献上完整代码,代码重要位置会做相应解释 <html lang="en…

python+django高校疫情防控管理系统vue

随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;高校疫情防控管理系统也不例外&#xff0c;但目前国内的有些学校仍都使用人工管理&#xff0c;学校规模越来越大&#xff0c;同时信息量也越来越庞大&#xff0c;人工管理显然已无法应对时代的变化…

屏幕挂灯是不是智商税?明基ScreenBar Halo屏幕挂灯初体验

目录 一、屏幕挂灯是不是智商税&#xff1f;二、文心一言眼里的屏幕挂灯1、明基ScreenBar Halo屏幕挂灯2、屏幕挂灯和普通台灯哪个好&#xff1f; 三、屏幕挂灯初体验四、使用体验五、无线控制器六、专业角度分析1、屏幕工作照明&#xff0c;不是随便一盏灯就可以2、引导光线照…