Java8函数式编程

2023-11-11

文章目录

Java8函数式编程

首先推荐两篇博客作为参考:其一其二

简介

为什么需要再次修改Java

  1. 为了让代码有效在多核CPU上高效运行,提高并行度,增加了Lambda表达式
  2. 写回调函数或者事件处理程序时,程序员不必要再纠缠于内部类的冗繁

什么是函数式编程

核心是:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值

Lambda表达式

在Swing中,对按钮添加监听器时,我们需要编写如下代码:

button.addActionListener(new ActionListener() { 
	public void actionPerformed(ActionEvent event) {
 		System.out.println("button clicked");
	}
});

Java8中可以直接使用Lambda表达式写成一行:

//event是参数,->是表达式主体
button.addActionListener(event -> System.out.println("button clicked"));

表达式中的参数类型可以通过编译器类型推断得到(也可能推断不出来),也可以显式声明其类型。几种L表达式的变体:

Runnable noArguments = () -> System.out.println("Hello World");
ActionListener oneArgument = event -> System.out.println("button clicked");
Runnable multiStatement = () -> { 
	System.out.print("Hello");
	System.out.println(" World");
};
BinaryOperator<Long> add = (x, y) -> x + y;
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

L表达式有一个目标类型的概念,它是依赖于上下文的,由编译器推断出来的(不是显式new)

引用值,而不是变量

这是L表达式的一个重要概念,L表达式使用的是值,因此为了保证值不被修改,传递的参数隐式是final的(不用显式声明为final),如果隐式为final的事实不成立,则无法通过编译,如下面的代码所示:

String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));

这种行为也解释了为什么 Lambda 表达式也被称为闭包

函数接口

函数接口是只有一个抽象方法的接口(比如前面的EventListener接口),可以用作 Lambda 表达式的类型

类型推断

Java7中已经支持泛型的类型推断,如下

//根据变量类型
Map<String, Integer> diamondWordCounts = new HashMap<>();

Java8更近一步,可以对方法参数进行类型推断:

//根据参数类型
useHashmap(new HashMap<>());

但是如果提供的信息不足推断类型,则会报错:

BinaryOperator<Long> addLongs = (x, y) -> x + y;
//改成:
BinaryOperator add = (x, y) -> x + y;

流使程序员得以站在更高的抽象层次上对集合进行操作。下面我们将以音乐来举例,有一个专辑Album类,它里面包含了Artist的数组信息(因为乐团的专辑肯定是多人,所以类型为数组),然后还有一个Track类表示专辑中的歌曲。

通常我们会使用foreach来遍历集合:

int count = 0;
for (Artist artist : allArtists) {
	if (artist.isFrom("London")) { count++;
}}

这里for 循环其实是一个封装了迭代的语法糖,也就是实际是通过外部迭代实现的:

int count = 0;
Iterator<Artist> iterator = allArtists.iterator(); 
while(iterator.hasNext()) {
	Artist artist = iterator.next(); 
	if (artist.isFrom("London")) {
		count++; 
	}
}

另一种循环方法是内部迭代,也就是通过流来实现:

long count = allArtists.stream().filter(artist -> artist.isFrom("London")).count();

stream()方法和iterator()的作用一样,只不过它返回的是内部迭代的接口:Stream

Stream 是用函数式编程方式在集合类上进行复杂操作的工具

上面遍历的操作实际上只需要做两件事:判断艺术家是否来自London;计算它们的个数,每个操作都对应Stream接口的一个方法。filter操作不会产生数据,只是描述数据,所以它的返回值是Stream,而count会产生值,一般这种产生值的方法会放在最后。

常用的流操作

生成流

Stream.of可以返回流,同时调用集合的stream方法也能生成流;还有通过Stream.iteratoe方法生成流,JDK1.8中该方法只能生成无限流,JDK1.9支持生成有限的;另外通过generate方法也可以生成:

Stream.generate(Math::random).limit(10).forEach(System.out::println);

collect(toList())返回集合

toList是Collectors的方法

List<String> collected = Stream.of("a", "b", "c").collect(Collectors.toList());

map将一个流中的值转为另一个新的流

比如下面的操作:

List<String> collected = new ArrayList<>();
for (String string : asList("a", "b", "hello")) {
String uppercaseString = string.toUpperCase();
	collected.add(uppercaseString);
}
assertEquals(asList("A", "B", "HELLO"), collected);

可以写成:

其中map方法参数是Function接口的实例

List<String> collected = Stream.of("a", "b", "hello").map(string -> string.toUpperCase()).collect(toList());

filter方法过滤元素

对集合元素进行判断,并过滤除符合条件的

List<String> beginningWithNumbers = new ArrayList<>();
for (String value : asList("a", "1abc", "abc1")) {
    if (isDigit(value.charAt(0))) {
        beginningWithNumbers.add(value);
    }
}
assertEquals(asList("1abc"), beginningWithNumbers);

函数式写法:

List<String> beginningWithNumbers= Stream.of("a", "1abc", "abc1").filter(value->isDigit(value.charAt(0))).collect(toList());

flatMap

flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream,如下代码所示:

List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
    .flatMap(numbers -> numbers.stream())
    .collect(toList());
assertEquals(asList(1, 2, 3, 4), together);

max和min

求最大值和最小值

List<Track> tracks = asList(new Track("Bakai", 524),
                new Track("Violets for Your Furs", 378),
                new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
    .min(Comparator.comparing(track -> track.getLength()))
    .get();

min和max传入的是Comparator参数,这里面使用的是Comparator的静态方法comparing,它返回的也是Comparator,min和max方法返回的是Optional类型,调用get用来获取里面的值

reduce操作

reduce 操作可以实现从一组值中生成一个值。在上述例子中用到的 count、min 和 max 方法,因为常用而被纳入标准库中。事实上,这些方法都是 reduce 操作。下面是使用reduce操作实现累加:

int count = Stream.of(1, 2, 3).reduce(0, (acc, element) -> acc + element);

reduce第一个元素是初始元素,第二个元素时累加器,它是BinaryOperator类型,这里面我们传递的就是该类中apply方法的实现。我们也可以直接写成这样:

BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
int count = accumulator.apply(accumulator.apply(
    accumulator.apply(0, 1),
    2), 3);

等价于传统写法:

T result = identity;
for (T element : this stream)
    result = accumulator.apply(result, element);
return result;

整合操作

假设针对专辑这两个类要解决一个问题:找出某张专辑上所有乐队的国籍,首先这个问题可以拆分为:

  1. 找出专辑上的所有表演者。
  2. 分辨出哪些表演者是乐队。
  3. 找出每个乐队的国籍。
  4. 将找出的国籍放入一个集合。

代码如下:

getMusicians返回的是Stream对象

Set<String> origins = album.getMusicians()
    //找出乐队
    .filter(artist -> artist.getName().startsWith("The"))
    .map(artist -> artist.getNationality())
    .collect(toSet());

再看另一个问题:假定选定一组专辑,找出其中所有长度大于 1 分钟的曲目名称,如果使用传统的写法,需要遍历两次,一次是Album,一次是Track(歌曲),代码如下:

public Set<String> findLongTracks(List<Album> albums) {
return albums.stream().flatMap(album -> album.getTracks()) //把多个stream合并成一个
        .filter(track -> track.getLength() > 60) //过滤元素
        .map(track -> track.getName()) /t换为名称
        .collect(toSet());	//返回set

高阶函数

高阶函数是指接受另外一个函数作为参数,或返回一个函数的函数,Stream 接口中几乎所有的函数都是高阶函数。我们可以大致总结下Stream中方法设计到的参数类型:

Predicate

filter方法使用的参数类型,其主要执行的方法是:

boolean test(T t);

Function

map、flatMap使用的参数,其主要执行的方法:

R apply(T t);

BinaryOperator

reduce使用的参数,继承了BiFunction,其主要执行方法也是apply

练习

  1. 编写一个求和函数,计算流中所有数之和:

    public static int addUp(Stream<Integer> numbers) {
        return numbers.reduce(0, (acc, x) -> acc + x);
    }
    
  2. 编写一个函数,接受艺术家列表作为参数,返回一个字符串列表,其中包含艺术家的 姓名和国籍

    public static List<String> getNamesAndOrigins(List<Artist> artists) {
        return artists.stream()
            .flatMap(artist -> Stream.of(artist.getName(), artist.getNationality()))
            .collect(toList());
    }
    
  3. 编写一个函数,接受专辑列表作为参数,返回一个由最多包含 3 首歌曲的专辑组成的 列表

    public static List<Album> getAlbumsWithAtMostThreeTracks(List<Album> input) {
        return input.stream()
            .filter(album -> album.getTrackList().size() <= 3)
            .collect(toList());
    }
    
  4. 将下面的外部迭代转为内部迭代:

    int totalMembers = 0;
    for (Artist artist : artists) {
        Stream<Artist> members = artist.getMembers();
        totalMembers += members.count();
    }
    

    改写后:

    public static int countBandMembersInternal(List<Artist> artists) {
        return artists.stream()
            .map(artist -> artist.getMembers().count())
            .reduce(0L, Long::sum)
            .intValue();
    }
    
  5. 计算一个字符串中小写字母的个数

    public static int countLowercaseLetters(String string) {
        return (int) string.chars()
            .filter(Character::isLowerCase)
            .count();
    }
    
  6. 在一个字符串列表中,找出包含最多小写字母的字符串

    public static Optional<String> mostLowercaseString(List<String> strings) {
        return strings.stream()
            .max(Comparator.comparing(StringExercises::countLowercaseLetters));
    }
    
  7. 只用 reduce 和 Lambda 表达式写出实现 Stream 上的 map 操作的代码,如果不想返回 Stream,可以返回一个 List ,代码如下:

    public class MapUsingReduce {
    
        public static <I, O> List<O> map(Stream<I> stream, Function<I, O> mapper) {
            return stream.reduce(new ArrayList<O>(), (acc, x) -> {
            	// We are copying data from acc to new list instance. It is very inefficient,
            	// but contract of Stream.reduce method requires that accumulator function does
            	// not mutate its arguments.
            	// Stream.collect method could be used to implement more efficient mutable reduction,
            	// but this exercise asks to use reduce method.
            	List<O> newAcc = new ArrayList<>(acc);
            	newAcc.add(mapper.apply(x));
                return newAcc;
            }, (List<O> left, List<O> right) -> {
            	// We are copying left to new list to avoid mutating it. 
            	List<O> newLeft = new ArrayList<>(left);
            	newLeft.addAll(right);
                return newLeft;
            });
        }
    
    }
    

    这里使用的是reduce的方法为:

    <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
    

类库

基本类型

Java中集合存储的不是基本类型,而是其装箱类型,但是存储装箱类型需要更多的内存空间,这样是不高效的.为了减小这些性能开销, Stream 类的某些方法对基本类型和装箱类型做了区分。 在 Java 8 中, 仅对整型、
长整型和双浮点型做了特殊处理, 因为它们在数值计算中用得最多, 特殊处理后的系统性能提升效果最明显。

ToLongFunction 能够转为基本类型

LongFunction 表示参数是基本类型

这些基本类型都有与之对应的 Stream, 以基本类型名为前缀, 如 LongStream,其map 方法的实现方式也不同,它接受一个 LongUnaryOperator 函数将一个长整型值映射成另一个长整型值。如有可能, 应尽可能多地使用对基本类型做过特殊处理的方法, 进而改善性能。

案例:统计曲目长度

mapToInt转为基本类型,返回的是一个统计,这些统计值在所有特殊处理的 Stream, 如 DoubleStream、 LongStream 中都可以得出。 如无需全部的统计值, 也可分别调用 min、 max、 average 或 sum 方法获得单个的统计值

public class Primitives {
	public static void printTrackLengthStatistics(Album album) {
	    IntSummaryStatistics trackLengthStats
	            = album.getTracks()
	                   .mapToInt(track -> track.getLength())
	                   .summaryStatistics();

	    System.out.printf("Max: %d, Min: %d, Ave: %f, Sum: %d",
	                      trackLengthStats.getMax(),
	                      trackLengthStats.getMin(),
	                      trackLengthStats.getAverage(),
	                      trackLengthStats.getSum());
	}
}

重载解析

Java加入了默认方法,当存在多个重载方法是,Lambda表达式的类型推断可能会有问题,总体的原则如下:

如果只有一个可能的目标类型, 由相应函数接口里的参数类型推导得出;
如果有多个可能的目标类型, 由最具体的类型推导得出(子类优先);
如果有多个可能的目标类型且最具体的类型不明确, 则需人为指定类型。

@FunctionalInterface

Java 中有一些接口, 虽然只含一个方法, 但并不是为了使用Lambda 表达式来实现的,因此加入该注解说明,该注释会强制 javac 检查一个接口是否符合函数接口的标准。 如果该注释添加给一个枚举类型、 类或另一个注释, 或者接口包含不止一个抽象方法, javac 就会报错。

二进制接口的兼容性

如果Java8加入Stream后,集合等接口也加入了新方法,这是所有使用第三方集合类库的梦魇, 要避免这个糟糕情况, 则需要在 Java 8 中添加新的语言特性: 默认方法

默认方法

默认方法的调用总体判断规则:

  1. 类胜于接口。 如果在继承链中有方法体或抽象的方法声明, 那么就可以忽略接口中定义
    的方法。
  2. 子类胜于父类。 如果一个接口继承了另一个接口, 且两个接口都定义了一个默认方法,
    那么子类中定义的方法胜出。
  3. 没有规则三。 如果上面两条规则不适用, 子类要么需要实现该方法, 要么将该方法声明
    为抽象方法。

针对继承多个接口,且多个接口具有相同名称的默认方法时,需要显式重写调用方法:

public class MusicalCarriage
    implements Carriage, Jukebox {
    @Override
    public String rock() {
        return Carriage.super.rock();
    }
}

接口的静态方法

Stream 是个接口,Stream.of 是接口的静态方法。 这也是 Java 8 中添加的一个新的语言特性, 旨在帮助编写类库的开发人员, 但对于日常应用程序的开发人员也同样适用。

Optional

reduce 方法的一个重点尚未提及: reduce 方法有两种形式, 一种如前面出现的需要有一个初始值, 另一种变式则不需要有初始值。 没有初始值的情况下, reduce 的第一步使用Stream 中的前两个元素。 有时, reduce 操作不存在有意义的初始值, 这样做就是有 意义的, 此时, reduce 方法返回一个 Optional 对象。

创建某个值的Optional对象:

Optional<String> a = Optional.of("a");
assertEquals("a", a.get());

Optional 对象也可能为空, 因此还有一个对应的工厂方法 empty,另外一个工厂方法ofNullable 则可将一个空值转换成 Optional 对象

Optional emptyOptional = Optional.empty();
Optional alsoEmpty = Optional.ofNullable(null);
assertFalse(emptyOptional.isPresent());
// 例 4-22 中定义了变量 a
assertTrue(a.isPresent());

使用 Optional 对象的方式之一是在调用 get() 方法前, 先使用 isPresent 检查 Optional对象是否有值。 使用 orElse 方法则更简洁, 当 Optional 对象为空时, 该方法提供了一个备选值。 如果计算备选值在计算上太过繁琐, 即可使用 orElseGet 方法。 该方法接受一个Supplier 对象, 只有在 Optional 对象真正为空时才会调用

assertEquals("b", emptyOptional.orElse("b"));
assertEquals("c", emptyOptional.orElseGet(() -> "c"));

相关博客:

API解释

常见的使用方法

正确使用姿势

练习

返回艺术家或者乐队的名字:

这里使用了contact来连接流

public interface PerformanceFixed {
    public Stream<Artist> getMusicians();
    public default Stream<Artist> getAllMusicians() {
        return getMusicians()
              .flatMap(artist -> concat(Stream.of(artist), artist.getMembers()));
    }

}

让接口中getArtist 方法返回一个 Optional<Artist> 对象。 如果索引在有效范围内, 返回对应的元素, 否则返回一个空Optional 对象。 此外, 还需重构 getArtistName 方法, 保持相同的行为

public class ArtistsFixed {

    private List<Artist> artists;

    public ArtistsFixed(List<Artist> artists) {
        this.artists = artists;
    }

    public Optional<Artist> getArtist(int index) {
        if (index < 0 || index >= artists.size()) {
            return Optional.empty();
        }
        return Optional.of(artists.get(index));
    }

    public String getArtistName(int index) {
        Optional<Artist> artist = getArtist(index);
        return artist.map(Artist::getName)
                     .orElse("unknown");
    }

}

高级集合类和收集器

方法引用

Lambda 表达式经常调用参数,如下所示

artist -> artist.getName()

因此 Java 8 为其提供了一个简写语法, 叫作方法引用,帮助程序员重用已有方法:

Artist::getName

标准语法为:

Classname::methodName

同时方法引用自动支持多个参数:

(name, nationality) -> new Artist(name, nationality)
//写成
Artist::new

元素顺序

流是有序的, 因为流中的元素都是按顺序处理的。 这种顺序称为出现顺序,在一个有序集合中创建一个流时, 流中的元素就按出现顺序排列:

List<Integer> numbers = asList(1, 2, 3, 4);
List<Integer> sameOrder = numbers.stream().collect(toList());
assertEquals(numbers, sameOrder);

如果集合本身就是无序的(set), 由此生成的流也是无序的:

Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
List<Integer> sameOrder = numbers.stream().collect(toList());
// 该断言有时会失败
assertEquals(asList(4, 3, 2, 1), sameOrder);

一些中间操作会产生顺序,这种顺序就会保留下来;如果进来的流是无序的, 出去的流也是无序的

Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
List<Integer> sameOrder = numbers.stream().sorted().collect(toList());
assertEquals(asList(1, 2, 3, 4), sameOrder);

使用收集器

收集器

前面我们使用过 collect(toList()), 在流中生成列表,toList()就是收集器, 一种通用的、 从流生成复杂值的结构。只要将它传给 collect 方法, 所有的流就都可以使用它了。除了toList(),还有toSet 和 toCollection,而且你可以指定该集合的类:

stream.collect(toCollection(TreeSet::new));

这里使用的api如下所示,它接收一个Collector参数,

<R, A> R collect(Collector<? super T, A, R> collector);

Collectors类中有很多现成的Collector,这些Collector实际通过返回一个内部类CollectorImpl实现的,该类实现了Collector接口,该类的构造函数:

CollectorImpl(Supplier<A> supplier,
                      BiConsumer<A, T> accumulator,
                      BinaryOperator<A> combiner,
                      Function<A,R> finisher,
                      Set<Characteristics> characteristics) {
            this.supplier = supplier;
            this.accumulator = accumulator;
            this.combiner = combiner;
            this.finisher = finisher;
            this.characteristics = characteristics;
        }
CollectorImpl(Supplier<A> supplier,
              BiConsumer<A, T> accumulator,
              BinaryOperator<A> combiner,
              Set<Characteristics> characteristics) {
    this(supplier, accumulator, combiner, castingIdentity(), characteristics);
}

前几个参数和collect带有三个参数的方法类似,区别在于第三个参数变成了BinaryOperator,即带了返回值

<R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);

这里需要注意Characteristics的set参数,该类是一个枚举,它的定义如下,表明该收集器的特征:

enum Characteristics {
        /**
         * Indicates that this collector is <em>concurrent</em>, meaning that
         * the result container can support the accumulator function being
         * called concurrently with the same result container from multiple
         * threads.
         *
         * <p>If a {@code CONCURRENT} collector is not also {@code UNORDERED},
         * then it should only be evaluated concurrently if applied to an
         * unordered data source.
         */
        CONCURRENT,

        /**
         * Indicates that the collection operation does not commit to preserving
         * the encounter order of input elements.  (This might be true if the
         * result container has no intrinsic order, such as a {@link Set}.)
         */
        UNORDERED,

        /**
         * Indicates that the finisher function is the identity function and
         * can be elided.  If set, it must be the case that an unchecked cast
         * from A to R will succeed.
         */
        IDENTITY_FINISH
    }

常见收集器

maxBy 和 minBy 允许用户按某种特定的顺序生成一个值。 下面展示了如何找出成员最多的乐队:

public Optional<Artist> biggestGroup(Stream<Artist> artists) {
Function<Artist,Long> getCount = artist -> artist.getMembers().count();
	return artists.collect(maxBy(comparing(getCount)));
}

还有些收集器实现了一些常用的数值运算:

public double averageNumberOfTracks(List<Album> albums) {
	return albums.stream().collect(averagingInt(album -> album.getTrackList().size()));
}

另外一个常用的流操作是将其分解成两个集合:

public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) {
	return artists.collect(partitioningBy(artist -> artist.isSolo()));
}
//或者写成:
public Map<Boolean, List<Artist>> bandsAndSoloRef(Stream<Artist> artists) {
	return artists.collect(partitioningBy(Artist::isSolo));
}

数据分组是一种更自然的分割数据操作, 与将数据分成 ture 和 false 两部分不同, 可以使用任意值对数据分组:

public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) {
	return albums.collect(groupingBy(album -> album.getMainMusician()));
}

很多时候, 收集流中的数据都是为了在最后生成一个字符串。 假设我们想将参与制作一张专辑的所有艺术家的名字输出为一个格式化好的列表(joining传的三个参数为分隔符,前缀和后缀):

String result = artists.stream().map(Artist::getName).collect(Collectors.joining(", ", "[", "]"));

这些收集器也可以整合起来:现在来考虑如何计算一个艺术家的专辑数量,代码如下

public Map<Artist, Long> numberOfAlbums(Stream<Album> albums) {
return albums.collect(groupingBy(album -> album.getMainMusician(),
	counting()));
}

可以看到这里groupingBy传了两个参数,前面的案例传了一个参数,实际上都是调用三个参数的方法:

public static <T,K,D,A,M extends Map<K,D>> Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier,Supplier<M> mapFactory,Collector<? super T,A,D> downstream)

第一个是分类操作,也是groupingBy的核心,第三个是一个reduction操作,也就是将流转为一个值,可以看到最后的返回结果是个Map,这个能力是第二个参数提供的,在上面的第一个案例中,实际调用的是:

return groupingByConcurrent(classifier, ConcurrentHashMap::new, toList());

第二个案例实际调用的是:

return groupingByConcurrent(classifier, ConcurrentHashMap::new, downstream);

有时候我们希望最的结果是经过映射的,此时可以通过downstream传递一个mapping方法,比如使用收集器求每个艺术家的专辑名,可以写成:

public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums) {
	return albums.collect(groupingBy(Album::getMainMusician,mapping(Album::getName, toList())));
}

Collectors中很多返回Collector类型的方法很多都可以作为downstream。

定制收集器(collect)

Java 内置的收集器已经相当好用,但是我们完全可以定制自己的收集器,针对前面使用joining的案例,如果采用普通的代码可以写成:

StringBuilder builder = new StringBuilder("[");
for (Artist artist : artists) {
if (builder.length() > 1)
    builder.append(", ");
    String name = artist.getName();
    builder.append(name);
} 
builder.append("]");
String result = builder.toString();

我们可以使用reduce改写成:

StringBuilder reduced =
artists.stream()
.map(Artist::getName)
.reduce(new StringBuilder(), (builder, name) -> {
    if (builder.length() > 0)
        builder.append(", ");
        builder.append(name);
        return builder;
    }, (left, right) -> left.append(right));
reduced.insert(0, "[");
reduced.append("]");
String result = reduced.toString();

继续可以优化为(这里调用的toString是StringCombiner的):

String result =
artists.stream().map(Artist::getName).reduce(new StringCombiner(", ", "[", "]"),
    StringCombiner::add,
    StringCombiner::merge).toString();

这里用到了我们自定义的StringCombiner,它的源码:

public class StringCombiner {

    private final String prefix;
    private final String suffix;
    private final String delim;
    private final StringBuilder buIlder;

    public StringCombiner(String delim, String prefix, String suffix) {
        this.prefix = prefix;
        this.suffix = suffix;
        this.delim = delim;
        this.buIlder = new StringBuilder();
    }
    public StringCombiner add (String word) {
        if(!this.areAtStart()) {
            this.buIlder.append(delim);
        }
        this.buIlder.append(word);

        return this;
    }

    public StringCombiner merge (StringCombiner other) {
        if(!other.equals(this)) {
            if(!other.areAtStart() && !this.areAtStart()){
                other.buIlder.insert(0, this.delim);
            }
            this.buIlder.append(other.buIlder);
        }
        return this;
    }

    @Override
    public String toString() {
        return prefix + buIlder.toString() + suffix;
    }
    private boolean areAtStart() {
        return buIlder.length() == 0;
    }
}

现在的代码看起来已经差不多完美了, 但是在程序中还是不能重用。 因此, 我们想将reduce 操作重构为一个收集器, 在程序中的任何地方都能使用:

String result = artists.stream()
                        .map(Artist::getName)
                        .collect(new StringCollector(", ", "[", "]"));

StringCollector的代码:

StringCollector的泛型确定过程:

待收集元素的类型, 这里是 String

累加器的类型 StringCombiner

最终结果的类型, 这里依然是 String

public class StringCollector implements Collector<String, StringCombiner, String> {

    private static final Set<Characteristics> characteristics = Collections.emptySet();

    private final String delim;
    private final String prefix;
    private final String suffix;

    public StringCollector(String delim, String prefix, String suffix) {
        this.delim = delim;
        this.prefix = prefix;
        this.suffix = suffix;
    }

    @Override
    public Supplier<StringCombiner> supplier() {
        return () -> new StringCombiner(delim, prefix, suffix);
    }

    @Override
    public BiConsumer<StringCombiner, String> accumulator() {
        return StringCombiner::add;
    }

    @Override
    public BinaryOperator<StringCombiner> combiner() {
        return StringCombiner::merge;
    }

    @Override
    public Function<StringCombiner, String> finisher() {
        return StringCombiner::toString;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return characteristics;
    }

}

我们再回过头看一下之前Joining的实现:

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                             CharSequence prefix,
                                                             CharSequence suffix) {
        return new CollectorImpl<>(
                () -> new StringJoiner(delimiter, prefix, suffix),
                StringJoiner::add, StringJoiner::merge,
                StringJoiner::toString, CH_NOID);
    }

和我们自定义的StringCollector类似,只不过这里使用的是StringJoiner,它的实现和StringCombiner差不多

computeIfAbsent以及Map的遍历

computeIfAbsent

Java 8 引入了一个新方法 computeIfAbsent, 该方法接受一个 Lambda 表达式, 值不存在时使用该 Lambda 表达式计算新值:

class Java8ArtistService extends ArtistService {    
    public Artist getArtist(String name) {
        return artistCache.computeIfAbsent(name, this::readArtistFromDB);
    }
}

类似的方法还有computeIfPresent和compute,compute的是用来替换原先的值

练习

找出名称最长艺术家,分别通过reduce和collect来实现

public class LongestName {

    private static Comparator<Artist> byNameLength = comparing(artist -> artist.getName().length());

    public static Artist byReduce(List<Artist> artists) {
        return artists.stream()
                      .reduce((acc, artist) -> (byNameLength.compare(acc, artist) >= 0) ? acc : artist)
                      .orElseThrow(RuntimeException::new);
    }

    public static Artist byCollecting(List<Artist> artists) {
        return artists.stream()
                      .collect(Collectors.maxBy(byNameLength))
                      .orElseThrow(RuntimeException::new);
    }
    //最简单的写法
    public static Artist byCollecting(List<Artist> artists) {
        return artists.stream().max(byNameLength)
                      .orElseThrow(RuntimeException::new);
    }

}

假设一个元素为单词的流, 计算每个单词出现的次数,按名称分类输出:

public class WordCount {

    public static Map<String, Long> countWords(Stream<String> names) {
        return names.collect(groupingBy(name -> name, counting()));
    }

}

用一个定制的收集器实现 Collectors.groupingBy 方法:

思路:首先确定返回类型为Map,一次呢

public class GroupingBy<T, K> implements Collector<T, Map<K, List<T>>, Map<K, List<T>>> {

    private final static Set<Characteristics> characteristics = new HashSet<>();
    static {
        //恒等的结束操作
        characteristics.add(Characteristics.IDENTITY_FINISH);
    }

    private final Function<? super T, ? extends K> classifier;

    //传入一个函数,用来对映射key
    public GroupingBy(Function<? super T, ? extends K> classifier) {
        this.classifier = classifier;
    }

    @Override
    public Supplier<Map<K, List<T>>> supplier() {
        return HashMap::new;
    }

    @Override
    public BiConsumer<Map<K, List<T>>, T> accumulator() {
        return (map, element) -> {
            K key = classifier.apply(element);
            List<T> elements = map.computeIfAbsent(key, k -> new ArrayList<>());
            //添加key
            elements.add(element);
        };
    }

    //组合操作
    @Override
    public BinaryOperator<Map<K, List<T>>> combiner() {
        return (left, right) -> {
            right.forEach((key, value) -> {
                left.merge(key, value, (leftValue, rightValue) -> {
                    leftValue.addAll(rightValue);
                    return leftValue;
                });
            });
            return left;
        };
    }

    //结束操作,直接返回参数
    @Override
    public Function<Map<K, List<T>>, Map<K, List<T>>> finisher() {
        return map -> map;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return characteristics;
    }

}

使用 Map 的 computeIfAbsent 方法高效计算斐波那契数列:

public class Fibonacci {

    private final Map<Integer,Long> cache;

    public Fibonacci() {
        cache = new HashMap<>();
        cache.put(0, 0L);
        cache.put(1, 1L);
    }

    public long fibonacci(int x) {
        return cache.computeIfAbsent(x, n -> fibonacci(n-1) + fibonacci(n-2));
    }

}

数据并行化

这里举一个案例,抛两个筛子,求和的不同结果的案例,由于测试次数很多,使用并行流能够优化效率,实现代码:

代码的解释,使用groupingBy生成不同结果的概率,概率通过次数累加得到,累加的单位是fraction。

使用IntStream是因为处理基本类型速度比包装类型快

//串行流实现
public Map<Integer, Double> serialDiceRolls() {
    double fraction = 1.0 / N;
    return IntStream.range(0, N)
            .mapToObj(twoDiceThrows())
            .collect(groupingBy(side -> side, summingDouble(n -> fraction)));
}
//并行流实现
public Map<Integer, Double> parallelDiceRolls() {
    double fraction = 1.0 / N;
    return IntStream.range(0, N)                        
            .parallel()                         
            .mapToObj(twoDiceThrows())          
            .collect(groupingBy(side -> side,   
                    summingDouble(n -> fraction))); 
}
private static IntFunction<Integer> twoDiceThrows() {
    return i -> {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        int firstThrow = random.nextInt(1, 7);
        int secondThrow = random.nextInt(1, 7);
        return firstThrow + secondThrow;
    };
}

在底层, 并行流还是沿用了 fork/join 框架。 fork 递归式地分解问题, 然后每段并行执行,最终由 join 合并结果, 返回最后的值

并行化数组的操作

使用并行化数组操作初始化数组:

public static double[] parallelInitialize(int size) {
    double[] values = new double[size];
    Arrays.parallelSetAll(values, i -> i);
    return values;
}

另外还有parallelPrefix:任意给定一个函数, 计算数组的和(将每一个元素替换为当前元素和其前驱元素的和这里的“ 和” 是一个宽泛的概念,实际是 BinaryOperator),parallelSort:并行化对数组元素排序

另一个案例:在时间序列上增加一个滑动窗口, 计算出窗口中的平均值。 如果输入数据为 0、 1、 2、 3、 4、 3.5, 滑动窗口的大小为 3,则简单滑动平均数为 1、 2、 3、 3.5:

这个方法有点绕:sums经过parallelPrefix操作后,其中的每个元素是自己和所有前面的元素之和,range返回的是值为下标的有序递增的流(模拟滑动窗口不断右移,起始位置是第三个元素,也就是下标n-1),映射的过程很简单,即使当前下标的sum值减去i-n下标的值(该值在右移过程中脱离了滑动窗口需要减掉)

public static double[] simpleMovingAverage(double[] values,int n) {
    double[] sums = Arrays.copyOf(values, values.length);
    Arrays.parallelPrefix(sums, Double::sum);
    int start = n - 1;
    return IntStream.range(start, sums.length) //2,3,4,5
        .mapToDouble(i -> {
            double prefix = i == start ? 0 : sums[i - n];
            return (sums[i] - prefix) / n;
        })
        .toArray();
}

测试、调试和重构

重构

首先看一个打印日志的案例,原先的代码:

Logger logger = new Logger();
    if (logger.isDebugEnabled()) {
    logger.debug("Look at this: " + expensiveOperation());
}

这个方法的问题在于需要调用logger.isDebugEnabled()来做判断,我们可以用lambda来优化:

logger.debug(() -> "Look at this: " + expensiveOperation());

如果使用 Lambda 表达式, 外面的代码根本不需要检查日志级别,即不会暴露内部状态。

ThreadLocal也给我们提供了一个工厂方法,能够更简洁的创建该对象:

//之前的用法
ThreadLocal<Album> thisAlbum = new ThreadLocal<Album> () {
    @Override protected Album initialValue() {
    return database.lookupCurrentAlbum();
    }
};
//替换后
ThreadLocal<Album> thisAlbum = ThreadLocal.withInitial(() -> database.lookupCurrentAlbum());

复用

如果有一个整体上大概相似的模式, 只是行为上有所不同, 就可以试着加入一个 Lambda 表达式。

比如我们提供一个类来统计专辑的相关信息,传统的写法可能如下:

public class OrderImperative extends Order {

    public OrderImperative(List<Album> albums) {
        super(albums);
    }
    public long countRunningTime() {
        long count = 0;
        for (Album album : albums) {
            for (Track track : album.getTrackList()) {
                count += track.getLength();
            }
        }
        return count;
    }
    public long countMusicians() {
        long count = 0;
        for (Album album : albums) {
            count += album.getMusicianList().size();
        }
        return count;
    }
    public long countTracks() {
        long count = 0;
        for (Album album : albums) {
            count += album.getTrackList().size();
        }
        return count;
    }
}

我们可以lambda化:

public class OrderStreams extends Order {

    public OrderStreams(List<Album> albums) {
        super(albums);
    }
    public long countRunningTime() {
        return albums.stream()
                .mapToLong(album -> album.getTracks()
                        .mapToLong(track -> track.getLength())
                        .sum())
                .sum();
    }
    public long countMusicians() {
        return albums.stream()
                .mapToLong(album -> album.getMusicians().count())
                .sum();
    }
    public long countTracks() {
        return albums.stream()
                .mapToLong(album -> album.getTracks().count())
                .sum();
    }
}

然后可以提取公共的部分,从而达到复用代码:

public class OrderDomain extends Order {

    public OrderDomain(List<Album> albums) {
        super(albums);
    }    
    public long countFeature(ToLongFunction<Album> function) {
        return albums.stream()
                .mapToLong(function)
                .sum();
    }
    public long countTracks() {
        return countFeature(album -> album.getTracks().count());
    }
    public long countRunningTime() {
        return countFeature(album -> album.getTracks()
                .mapToLong(track -> track.getLength())
                .sum());
    }
    public long countMusicians() {
        return countFeature(album -> album.getMusicians().count());
    }
}

测试的技巧

现在有一个将字符串第一个字母转为大写的方法:

public static List<String> elementFirstToUpperCaseLambdas(List<String> words) {
    return words.stream()
        .map(value -> { 
            char firstChar = Character.toUpperCase(value.charAt(0));
            return firstChar + value.substring(1);
        })
        .collect(Collectors.<String>toList());
}

我们想测试该方法,其中最关注转换的是否正确,你可能想把这个lambda拆开来赋值到到一个个变量中去,然后通过查看这些中间变量来检验逻辑是否正确。这样显得很麻烦,最简单的就是采用方法引用 ,将核心逻辑抽取成独立方法,针对该独立方法测试就行了:

public static List<String> elementFirstToUppercase(List<String> words) {
    return words.stream()
        .map(Testing::firstToUppercase)
        .collect(Collectors.<String>toList());
}
//针对该方法测试就行了
public static String firstToUppercase(String value) { 
    char firstChar = Character.toUpperCase(value.charAt(0));
    return firstChar + value.substring(1);
}

peek的使用

有时候我们需要打印流中的中间状态,我们可能会选择foreach来执行,但是这回触发求值过程,导致流不能后面继续使用,因此常常需要复制代码。这时候可以使用peek方法,查看流中的元素却同时能继续操作流:

Set<String> nationalities
    = album.getMusicians()
    .filter(artist -> artist.getName().startsWith("The"))
    .map(artist -> artist.getNationality())
    .peek(nation -> System.out.println("Found nationality: " + nation))
    .collect(Collectors.<String>toSet());

设计和架构原则

Lambda对设计模式的影响

命令模式

命令模式的结果如下所示:
在这里插入图片描述
现在看一个案例: 假设有一个GUI Editor 组件, 在上面可以执行 open、 save 等一系列操作,现在我们想
实现宏功能— — 也就是说, 可以将一系列操作录制下来, 日后作为一个操作执行,代码实现:

public interface Editor {
    public void save();
    public void open();
    public void close();
}
public interface Action {
    public void perform();
}
//Save,Close一样
public class Open implements Action {
    private final Editor editor;
    public Open(Editor editor) {
        this.editor = editor;
    }
    @Override
    public void perform() {
        editor.open();
    }
}
public class Macro {
    private final List<Action> actions;
    public Macro() {
        actions = new ArrayList<>();
    }
    public void record(Action action) {
        actions.add(action);
    }
    public void run() {
        actions.forEach(Action::perform);
    }
}

可以看到在run方法中我们使用lambda代替了传统的写法,其他的设计模式也可以类似的改造

使用Lambda表达式的SOLID原则

单一功能原则

程序中的类或方法只能有一个改变的理由

如果你的类有多个功能,一个功能引发的代码变化会影响该类的其他功能;单一功能原则不止于此: 一个类不仅要功能单一, 而且还需将功能封装好。

先看一个案例:

public class SingleResponsibilityPrinciple {

    public static interface PrimeCounter {
        long countPrimes(int upTo);
    }

    //该方法有两个功能,一个是判断是否是质数,一个是计数,违法了单一功能原则
    public static class ImperativeSingleMethodPrimeCounter implements PrimeCounter {
        @Override
        public long countPrimes(int upTo) {
            long tally = 0;
            for (int i = 1; i < upTo; i++) {
                boolean isPrime = true;
                for (int j = 2; j < i; j++) {
                    if (i % j == 0) {
                        isPrime = false;
                    }
                }
                if (isPrime) {
                    tally++;
                }
            }
            return tally;
        }
    }
    //抽取成两个方法,但是大多都是循环,有些代码重复的味道
    public static class ImperativeRefactoredPrimeCounter implements PrimeCounter {
        @Override
        public long countPrimes(int upTo) {
            long tally = 0;
            for (int i = 1; i < upTo; i++) {
                if (isPrime(i)) {
                    tally++;
                }
            }
            return tally;
        }

        private boolean isPrime(int number) {
            for (int i = 2; i < number; i++) {
                if (number % i == 0) {
                    return false;
                }
            }
            return true;
        }
    }
    //使用lambda来构造,解决问题
    public static class FunctionalPrimeCounter implements PrimeCounter {

        @Override
        public long countPrimes(int upTo) {
            return IntStream.range(1, upTo)
                    .filter(this::isPrime)
                    .count();
        }

        private boolean isPrime(int number) {
            return IntStream.range(2, number)
         			//判断流中所有的元素是否达到要求
                    .allMatch(x -> (number % x) != 0);
        }
    }
}

开闭原则

软件应该对扩展开放, 对修改闭合

对开闭原则的另外一种理解和传统的思维不同, 那就是使用不可变对象实现开闭原则。 不可变对象是指一经创建就不能改变的对象。我们说不可变对象实现了开闭原则, 是因为它们的内部状态无法改变, 可以安全地为其增
加新的方法。新增加的方法无法改变对象的内部状态, 因此对修改是闭合的。 但它们又增加了新的行为, 因此对扩展是开放的。

并发编程

这里主要举例CompletableFuture的使用,先看使用Future的实现:

可以看到 如果要将 Future 对象的结果传给其他任务, 会阻塞当前线程的执行。 这会成为一个性能问题, 任务不是平行执行了, 而是( 意外地) 串行执行,也就是查询操作不必等待所有登录操作完成后才能执行

public Album lookupByName(String albumName) {
    Future<Credentials> trackLogin = loginTo("track");
    Future<Credentials> artistLogin = loginTo("artist");

    try {
        Future<List<Track>> tracks = lookupTracks(albumName, trackLogin.get());
        Future<List<Artist>> artists = lookupArtists(albumName, artistLogin.get());

        return new Album(albumName, tracks.get(), artists.get());
    } catch (InterruptedException | ExecutionException e) {
        throw new AlbumLookupException(e.getCause());
    }
}

我们可以使用CompletableFuture来修改实现:

使用 thenCompose 方法将 Credentials 对象转换成包含艺术家信息的 CompletableFuture对象

使用thenCombine方法将一个 CompletableFuture 对象的结果和另一个 CompletableFuture 对象组合起来

CompletableFuture 对象实现了 Future 接口, 可以调用 get 方法获取值。CompletableFuture 对象包含 join 方法, 我们在处调用了该方法, 它的作用和 get 方法是一样的, 而且它没有使用 get 方法时令人倒胃口的检查异常

public Album lookupByName(String albumName) {
    CompletableFuture<List<Artist>> artistLookup
        = loginTo("artist")
        .thenCompose(artistLogin -> lookupArtists(albumName, artistLogin));

    return loginTo("track")
        .thenCompose(trackLogin -> lookupTracks(albumName, trackLogin))
        .thenCombine(artistLookup, (tracks, artists)
                     -> new Album(albumName, tracks, artists))
        .join();
}

CompletableFuture其他的一些方法,含义看方法名就可知:

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                       Executor executor)
public boolean complete(T value)    
public boolean completeExceptionally(Throwable ex)
public static <U> CompletableFuture<U> completedFuture(U value)

附录

###Jdk中对Stream的说明文档摘译

Stream和流的区别:

  1. 无存储。Stream不是一个存储元素的数据结构,它会将一个源,比如一个数据结构,数组,生成函数(generator function)等,在执行一些操作的pipeline中遍历(Traversal)
  2. 天然函数式的。一个stream中的操作会产生一个结果,但是不会修改源数据,比如filter会产生新的stream,而不会删除原有集合中的元素
  3. 懒惰获取(Laziness-seeking):许多Stream操作,比如filter、map等可以懒惰实现,提供了再次优化的机会,例如“找到第一次出现3个连续元音的字符串”,该操作的实现不用检查所有的输入字符串(注:可以先filter不需要排查的)。Stream操作可以分为中间操作(产生Stream)和终端(ternimal)操作(求值或者产生副作用的操作),中间操作总是lazy的
  4. 基本无限制。集合的大小是有有限的,但是stream没有,一些操作像limit(n)或者findFirst()的调用能够允许在有限的流上进行计算
  5. 可消费的,一个stream中的元素只能被访问一次,如果要重新访问源数据的元素,需要生产一个新的stream

Stream操作和pipelines

Stream操作分为中间操作和终端操作,这两者组成了pipeline。中间操作会返回Stream,注意这些中间操作并不会马上执行,在终端操作执行后,源数据在pipeline中的遍历才正式开始,并且认为该stream pipeline被消费了,之后不会再被使用。在大部分情况下,终端操作是“急切的”,它会结束元数据的在流中的遍历,并在返回之前处理pipeline,例外是iterator()和splititerator()这两个终端操作,它们会让外部client控制pipeline的遍历。

懒惰的处理能够极大地提升效率,不会产生中间状态,较少了操作步骤(通过一行数据传递完成像filter,map,sum操作),以及减少不必要的数据检查(filter中)。

中间操作又可以分为无状态操作有状态操作,filter、map是属于无状态操作,他不会保留之前遍历的元素的状态,即每个元素的操作时互相独立的,但是对于distinct和sorted却不一样,它们在处理新的元素时候会参考之前处理的元素状态。有状态的操作需要处理完整的输入才能得到最后的结果。

并行

使用显式for来处理元素时天然是顺序执行的,和这些命令式操作是针对逐个元素不同的是,Stream使用集合了一系列操作的pipeline来帮助并行执行,stream操作可以顺序或者并行执行,Jdk中一般创建的顺序stream,除非显式地调用像Collection.parallelStream()或者BaseStream.parallel()方法才会创建并行的stream。案例代码:

int sumOfWeights = widgets.parallelStream()
    .filter(b -> b.getColor() == RED)
    .mapToInt(b -> b.getWeight())
    .sum();

BaseStream.isParallel()方法也可以用来判断是否是并行的stream,BaseStream.sequential()也可以切换为顺序的。除了一些结果不确定的方法之外,并行流和顺序流操作结果需要一致。特别是在有状态的操作中,并行执行会产生不一致的结果:

Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
 stream.parallel().map(e -> { if (seen.add(e)) return 0; else return e; })

副作用

有副作用的操作时不建议的,它们会破坏无状态的要求,并导致一些线程安全问题。

一个将有副作用的pipeline转为没有副作用的pipeline的案例:

ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
    .forEach(s -> results.add(s));  // Unnecessary use of side-effects!
List<String>results =
    stream.filter(s -> pattern.matcher(s).matches())
    .collect(Collectors.toList());  // No side-effects!

Reduction操作

R操作也称为折叠操作,它接收一系列的输入元素,通过重复的使用一个组合操作,然后将这些元素整合到一个结果里面,比如求数组的和、最大值等,也包括一些特定的R形式像sum(),max(),或者count()。案例代码如下:

//命令式操作
int sum = 0;
for (int x : numbers) {
    sum += x;
}
//R操作
int sum = numbers.stream().reduce(0, (x,y) -> x+y);
//或者
int sum = numbers.stream().reduce(0, Integer::sum);

更一般的reduce形式是有三个参数:

<U> U reduce(U identity,
              BiFunction<U, ? super T, U> accumulator,
              BinaryOperator<U> combiner);

identity是初始值和默认值,accumulator参数有两个,第一个是部分结果,第二个是下一个元素,然后生成新的部分结果,combiner结合两个部分结果然后生成新的部分结果(在并行执行的时候是必需的)。因此更一般的可以写为:

int sumOfWeights = widgets.stream()
    .reduce(0,(sum, b) -> sum + b.getWeight(),Integer::sum);

reduce还有一个只传accumulator参数的方法,返回的结果是个Optional,也就是结果可能为空

可变reduction(collect)

可变R是累积输入到一个可变的容器里面,比如Collection和StringBuilder,先看一个例子:

String concatenated = strings.reduce("", String::concat)

由于String是不可变的,因此会出现String的复制,影响性能,我们可以想到最好用StringBuilder来替换。可变R操作被称为collect(),正如它将结果收集到一个结果容器比如Collection,一个collect操作要求三个函数参数:一个supplier函数用来构建新的结果容器,一个accumulator函数来将输入元素吸收进结果容器中,以及一个combining函数用来合并一个容器的结果到另一个容器,和上面的reduce十分类似:

 <R> R collect(Supplier<R> supplier,
               BiConsumer<R, ? super T> accumulator,
               BiConsumer<R, R> combiner);

下面是一个案例:顺序式编程式这样的:

ArrayList<String> strings = new ArrayList<>();
for (T element : stream) {
    strings.add(element.toString());
}

然后我们可以使用可并行的collect形式:

ArrayList<String> strings = stream.collect(() -> new ArrayList<>(),
                                                (c, e) -> c.add(e.toString()),
                                                (c1, c2) -> c1.addAll(c2));

更进一步,我们可以将其中的映射操作提取出来:

List<String> strings = stream.map(Object::toString)
                       .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

collect的三个参数是高度耦合的,因此,JDK给我们抽象出了Collectors类:

List<String> strings = stream.map(Object::toString)
                                  .collect(Collectors.toList());

Collector类给我提供了更多的组合可能,比如统计员工的薪水,可以写成:

//这里的?表示不关心其类型
Collector<Employee, ?, Integer> summingSalaries
         = Collectors.summingInt(Employee::getSalary);

统计不同部门内的员工薪水总和可以写成:

Map<Department, Integer> salariesByDept =employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,summingSalaries));
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java8函数式编程 的相关文章

  • Grails 3.x bootRun 失败

    我正在尝试在 grails 3 1 11 中运行一个项目 但出现错误 失败 构建失败并出现异常 什么地方出了错 任务 bootRun 执行失败 进程 命令 C Program Files Java jdk1 8 0 111 bin java
  • 如何默认将 Maven 插件附加到阶段?

    我有一个 Maven 插件应该在编译阶段运行 所以在项目中consumes我的插件 我必须做这样的事情
  • 为什么 i++ 不是原子的?

    Why is i Java 中不是原子的 为了更深入地了解 Java 我尝试计算线程中循环的执行频率 所以我用了一个 private static int total 0 在主课中 我有两个线程 主题 1 打印System out prin
  • Java EE:如何获取我的应用程序的 URL?

    在 Java EE 中 如何动态检索应用程序的完整 URL 例如 如果 URL 是 localhost 8080 myapplication 我想要一个可以简单地将其作为字符串或其他形式返回给我的方法 我正在运行 GlassFish 作为应
  • 在 java 类和 android 活动之间传输时音频不清晰

    我有一个android活动 它连接到一个java类并以套接字的形式向它发送数据包 该类接收声音数据包并将它们扔到 PC 扬声器 该代码运行良好 但在 PC 扬声器中播放声音时会出现持续的抖动 中断 安卓活动 public class Sen
  • Android MediaExtractor seek() 对 MP3 音频文件的准确性

    我在使用 Android 时无法在eek 上获得合理的准确度MediaExtractor 对于某些文件 例如this one http www archive org download emma solo librivox emma 01
  • Spark 1.3.1 上的 Apache Phoenix(4.3.1 和 4.4.0-HBase-0.98)ClassNotFoundException

    我正在尝试通过 Spark 连接到 Phoenix 并且在通过 JDBC 驱动程序打开连接时不断收到以下异常 为简洁起见 下面是完整的堆栈跟踪 Caused by java lang ClassNotFoundException org a
  • 路径中 File.separator 和斜杠之间的区别

    使用有什么区别File separator和一个正常的 在 Java 路径字符串中 与双反斜杠相反 平台独立性似乎不是原因 因为两个版本都可以在 Windows 和 Unix 下运行 public class SlashTest Test
  • Java按日期升序对列表对象进行排序[重复]

    这个问题在这里已经有答案了 我想按一个参数对对象列表进行排序 其日期格式为 YYYY MM DD HH mm 按升序排列 我找不到正确的解决方案 在 python 中使用 lambda 很容易对其进行排序 但在 Java 中我遇到了问题 f
  • 如何将 pfx 文件转换为 jks,然后通过使用 wsdl 生成的类来使用它来签署传出的肥皂请求

    我正在寻找一个代码示例 该示例演示如何使用 PFX 证书通过 SSL 访问安全 Web 服务 我有证书及其密码 我首先使用下面提到的命令创建一个 KeyStore 实例 keytool importkeystore destkeystore
  • 加密 JBoss 配置中的敏感信息

    JBoss 中的标准数据源配置要求数据库用户的用户名和密码位于 xxx ds xml 文件中 如果我将数据源定义为 c3p0 mbean 我会遇到同样的问题 是否有标准方法来加密用户和密码 保存密钥的好地方是什么 这当然也与 tomcat
  • 玩!框架:运行“h2-browser”可以运行,但网页不可用

    当我运行命令时activator h2 browser它会使用以下 url 打开浏览器 192 168 1 17 8082 但我得到 使用 Chrome 此网页无法使用 奇怪的是它以前确实有效 从那时起我唯一改变的是JAVA OPTS以启用
  • simpleframework,将空元素反序列化为空字符串而不是 null

    我使用简单框架 http simple sourceforge net http simple sourceforge net 在一个项目中满足我的序列化 反序列化需求 但在处理空 空字符串值时它不能按预期工作 好吧 至少不是我所期望的 如
  • 静态变量的线程安全

    class ABC implements Runnable private static int a private static int b public void run 我有一个如上所述的 Java 类 我有这个类的多个线程 在里面r
  • Firebase 添加新节点

    如何将这些节点放入用户节点中 并创建另一个节点来存储帖子 我的数据库参考 databaseReference child user getUid setValue userInformations 您需要使用以下代码 databaseRef
  • 捕获的图像分辨率太大

    我在做什么 我允许用户捕获图像 将其存储到 SD 卡中并上传到服务器 但捕获图像的分辨率为宽度 4608 像素和高度 2592 像素 现在我想要什么 如何在不影响质量的情况下获得小分辨率图像 例如我可以获取或设置捕获的图像分辨率为原始图像分
  • 使用 JMF 创建 RTP 流时出现问题

    我正处于一个项目的早期阶段 需要使用 RTP 广播DataStream创建自MediaLocation 我正在遵循一些示例代码 该代码目前在rptManager initalize localAddress 出现错误 无法打开本地数据端口
  • 如何修复 JNLP 应用程序中的“缺少代码库、权限和应用程序名称清单属性”?

    随着最近的 Java 更新 许多人都遇到了缺少 Java Web Start 应用程序的问题Codebase Permissions and Application name体现属性 尽管有资源可以帮助您完成此任务 但我找不到任何资源综合的
  • 将 List 转换为 JSON

    Hi guys 有人可以帮助我 如何将我的 HQL 查询结果转换为带有对象列表的 JSON 并通过休息服务获取它 这是我的服务方法 它返回查询结果列表 Override public List
  • 节拍匹配算法

    我最近开始尝试创建一个移动应用程序 iOS Android 它将自动击败比赛 http en wikipedia org wiki Beatmatching http en wikipedia org wiki Beatmatching 两

随机推荐

  • Unity XR 教程专栏引导

    下面对我出过的 Unity XR 开发教程做个分类 XR Interaction Toolkit 系列教程 该专栏介绍了用 OpenXR XR Interaction Toolkit 开发的基础用法 学习后能够对 XR 开发中的手部动画 移
  • WebStorm 2018 最新激活码 license server

    最新的激活码 可以用的 还是热的 将地址 http active chinapyg com 或者 http idea toocruel net 任意一个复制到License Server中
  • eclipse运行程序时只有run on server

    最近写jsp的程序比较多 写java程序时 发现一点击运行按钮就开始启动服务器了 这是因为没有写主函数的原因 注意这个问题
  • Django基础 one

    一 DRF视图 1 Django REST framework是一个建立在Django上的Web开发框架 可以快速开发REST API接口 2 它提供了序列化器Serialzier ModelSerializer 的定义 可以帮助我们简化序
  • mongodb教程_MongoDB教程

    mongodb教程 Welcome to the MongoDB tutorial index post MongoDB is one of the most widely used NoSQL database 欢迎使用MongoDB教程
  • 终于搞定了部分网站无法打开的问题

    最近机器出现一个烦人的问题 有些网站无法打开 最初以为是实验室网络的问题 后来发现别人的机器能打开 于是开始折腾自己的机器了 hosts文件没有异常 关掉杀毒软件 防火墙 症状依旧 在浏览器地址栏中敲入url回车之后 浏览器很快报错无法访问
  • 使用过滤器,格式化超过1万和1千的数字,保留一位小数

    filters handleCount count if count gt 10000 count count count 1000 10000 W else if count gt 1000 count count count 100 1
  • 如何查找出Linux使用的shell版本号

    一 找出正在使用的shell类别 有很多种方法可以找出目前正在使用的shell类别 最简单的方法是使用特殊的shell参数 1 通过特殊的shell参数 可以查找出正在运行的shell的PID 参数是只读的不能修改 下面的指令也可以显示正在
  • uni-app image懒加载

    1 uni app官方给的文档注意看 lazy load Boolean false 图片懒加载 只针对page与scroll view下的image有效 微信小程序 百度小程序 字节跳动小程序 飞书小程序 只针对page与scroll v
  • windows 安装 Linux 子系统教程 (wsl)

    目录 背景 安装 wsl 安装 php 背景 因为日常工作中有一些场景需要使用 Linux 环境 并且有时候需要写一些自动化脚本来方便提效 而 wsl 具备安装便捷 占用轻量 使用便捷的特性 所以 wsl 无疑是最好的选择 安装 wsl 使
  • cmake 返回当前路径的上层路径 string(REGEX REPLACE...)

    返回当前路径的上层路径 先上实现代码 string REGEX REPLACE learn basic 1 PROJECT INIT PATH PROJECT SOURCE DIR 说明 CMakeLists txt 所在路径 可由cmak
  • 3、Jupyter Notebook,Matplotlib的使用

    目录 1 Jupyter Notebook使用 1 1 界面启动 创建文件 1 1 1 界面启动 1 1 2 新建notebook文档 2 Matplotlib使用 2 1 实现一个简单的Matplotlib画图 2 2 Matplotli
  • USB OTG的工作原理

    USB OTG的工作原理 OTG补充规范对USB 2 0的最重要的扩展是其更具节能性的电源管理和允许设备以主机和外设两种形式工作 OTG有两种设备类型 两用OTG设备 Dualrole device 和外设式OTG设备 Peripheral
  • xorl %eax, %eax

    这是GNU的汇编 xorl eax eax 这句起什么作用 按位异或 相同的位置为0 不同的位置为1 eax和eax的每一位都相同 所以相当于清零 movl 8 ebp ecx testl ecx ecx 这句起什么作用 jle L3 8
  • redhat 个人版注册订阅实现可以使用yum安装软件

    Redhat个人版使用入门 第一步安装redhat虚拟机 redhat注册订阅 创建账号 过程可能比较慢 所以需要耐心等待 第一步安装redhat虚拟机 略 如果你还不会使用虚拟机安装系统 请先移步百度如何使用虚拟机安装linux系统 re
  • mac配置vim语法高亮

    mac可能不同于linux macos都会内置了vim 和 vi 但都没有语法高亮 找到vimrc文件的位置 macos一般是在 usr share vim路径下 即 usr share vim vimrc 如果找不到这个路径 打开vim
  • GitHub的注册与使用(详细图解)

    首先 你需要注册一个 github账号 最好取一个有意义的名字 比如姓名全拼 昵称全拼 如果被占用 可以加上有意义的数字 本文中假设用户名为 chuaaqiCSDN 我的博客名的全拼 一 gihub账号注册与仓库创建 1 注册账号 地址 h
  • 解决adobe firefly 无法加载msvcp.dll的

    问题 在运行adobe firefly的运行程序的时候报错 adobe photoshop firefly unable to load a required component msvcp140 dll 解决方法 1 访问网页 Lates
  • xxl-job的使用以及与spring boot整合

    官网教程 中文教程 gitee https gitee com xuxueli0323 xxl job github https github com xuxueli xxl job xxl job主要分为调度中心和执行器项目 调度中心对应
  • Java8函数式编程

    文章目录 Java8函数式编程 简介 为什么需要再次修改Java 什么是函数式编程 Lambda表达式 引用值 而不是变量 函数接口 类型推断 流 常用的流操作 生成流 collect toList 返回集合 map将一个流中的值转为另一个