Java8函数式编程

qchen
• 阅读 1677

初步认识Java8

需求: 给定一个字符串列表:

["1","2","bilibili","of","xiaoming","5","at","BILIBILI","xiaoming","23","CHEERS","6"]

找出所有长度>=5的字符串,并且忽略大小写、去除重复字符串,然后按字母排序,最后用“❤”连接成一个字符串输出!

思路: 1、首先判断输入字符是字母还是数字 2、遍历字符串存入Set集合去重,同时进行大小写转换、长度判断 3、遍历用“❤”拼接结果字符串

方法:使用Java8的Stream流式操作:

public class Test1 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("bilibili");
        list.add("of");
        list.add("xiaoming");
        list.add("5");
        list.add("at");
        list.add("BILIBILI");
        list.add("23");
        list.add("CHEERS");

        String result = list.stream()
                .filter(i -> !isNum(i))
                .filter(i -> i.length() >= 5)
                .map(i -> i.toLowerCase())
                .distinct()
                .sorted(Comparator.naturalOrder())
                .collect(Collectors.joining("❤"));
        System.out.println(result);
    }

    private static boolean isNum(String str) {
        for (int i = 0; i < str.length(); i++) {
            if (!Character.isDigit(str.charAt(i))){
                return false;
            }
        }
        return true;
    }
}

阅读书籍《Java8函数式编程》

1. Lambda表达式

1.1 辨别Lambda表达式

public class Test01 {
    public static void main(String[] args) {
        Button button = new Button();
        // 给Button注册一个事件监听器
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("button clicked");
            }
        });

        // 1、实现了actionPerformed方法
        button.addActionListener(e -> System.out.println("button clicked!"));

        // 2、无参,实现了Runnable接口,重写了内部run()方法
        Runnable noArguments = () -> System.out.println("Hello World");

        // 3、一个参数,和1一样
        ActionListener oneArguments = e -> System.out.println("button clicked");

        // 4、Lambda表达式的主体还可以是一段代码块
        Runnable multiStatement = () -> {
            System.out.println("Hello");
            System.out.println("World");
        };

        // 5、变量 add 的类型是 BinaryOperator<Long>, 它不是两个数字的和,而是将两个数字相加的那行代码。
        BinaryOperator<Long> add = (x, y) -> x + y;

        // 6、上述所有Lambda表达式中的参数类型都是依赖于上下文环境由编译器推断得出的,也可以使用()显式声明参数类型
        BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
    }
}

1.2 引用值,而不是变量

Lambda表达式中引用的局部变量必须是final或既成事实上的final变量,否则编译器会报错,其中既成事实上的final是指只能给该变量赋值一次。

// 正确引用:
String name = getUserName();
button.addActionListener(e -> {System.out.println("hi " + name)});

// 试图给该变量多次赋值,然后在Lambda表达式中引用它,编译器就会报错
// 并显示出错信息: local variables referenced from a Lambda expression must be final or effectively final
String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));

1.3 函数接口

定义:函数接口是只有一个抽象方法的接口,用作Lambda表达式的类型。

例如:

public interface ActionListener extends EventListener {
    /**
     * Invoked when an action occurs.
     */
    // 由于actionPerformed定义在一个接口里,因此 abstract 关键字不是必需的
    public void actionPerformed(ActionEvent e);
}

Java中重要的函数接口: |接口|参数|返回类型|示例| |-|-|-|-| |Predicate|T|boolean|这张唱片是否以及发行| |Consumer|T|void|输出一个值| |Function<T,R>|T|R|获得Artist对象的名字| |Supplier|None|T|工厂方法| |UnaryOperator|T|T|逻辑非(!)| |BinaryOperator|(T,T)|T|求两个数的乘积(*)|

1.4 类型推断

Lambda表达式中的类型推断,实际上是Java 7中就引入的目标类型推断的扩展。

Java 7中的菱形操作符:

Map<String, Integer> map1 = new HashMap<String, Integer>();
Map<String, Integer> map2 = new HashMap<>();

Java 8中的类型推断:

Predicate<Integer> atLeast5 = x -> x > 5;

// 源码:
public interface Predicate<T> {
    boolean test(T t);
}

BinaryOperator<Long> addLongs = (x,y) -> x + y;

BinaryOperator add = (x, y) -> x + y;  // 编译错误,无法判定类型

2. Stream流

2.1 从外部迭代到内部迭代

例子:使用for循环计算来自伦敦的艺术家人数

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

for循环本质上是一个封装了迭代的语法糖,其工作原理如下:首先调用iterator方法,产生一个新的Iterator对象,进而控制整个迭代过程,即外部迭代。迭代过程通过显式调用 Iterator 对象的 hasNext 和 next方法完成迭代。

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

Java8函数式编程

内部迭代: 首先调用stream()方法,返回内部迭代中的相应接口:Stream

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

Java8函数式编程

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

2.2 实现机制

惰性求值:返回值是Stream 及早求值:返回值是另一个值或空

// 该行码中并未做什么实际性的工作, filter只刻画出了Stream,但没有产生新的集合
allArtists.stream()
          .filter(artist -> artist.isFrom("London"));

2.3 常用的流操作

流操作 解释
collect(toList()) 由 Stream 里的值生成一个列表, 是一个及早求值操作。
map 将一个流中的值转换成一个新的流
filter 遍历数据并检查其中的元素时使用
flatMap 用Stream替换值,然后将多个Stream连接成一个Stream
max & min 求流中的最大值和最小值
通用模式
reduce 可以实现从一组值中生成一个值
整合操作 结合多个流操作
```java
public class Test01 {
public static void main(String[] args) {
// of:将一组初始值生成新的Stream
// collect:将Stream中的值生成一个列表
List list1 = Stream.of("a", "b", "c").collect(toList());
Assert.assertEquals(Arrays.asList("a", "b", "c"), list1);
    // map:将字符转换为大写形式
    List<String> list2 = Stream.of("a", "b", "helloWorld")
            .map(str -> str.toUpperCase())
            .collect(toList());
    Assert.assertEquals(Arrays.asList("A","B","HELLOWORLD"), list2);

    // filter:找出以数字开头的字符串
    List<String> list3 = Stream.of("1abc", "abc")
            .filter(str -> Character.isDigit(str.charAt(0)))
            .collect(toList());
    Assert.assertEquals(Arrays.asList("1abc"), list3);

    // flatMap:将多个Stream流连接成一个Stream流
    List<Integer> list4 = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
            .flatMap(numbers -> numbers.stream())
            .collect(toList());
    Assert.assertEquals(Arrays.asList(1,2,3,4), list4);

    // min:找出最短的字符串
    String res1 = Stream.of("a", "ab", "abc")
            .min(Comparator.comparing(str -> str.length()))
            .get();
    Assert.assertEquals("a" , res1);

    // max:找出最长的字符串
    String res2 = Stream.of("a", "ab", "abc")
            .max(Comparator.comparing(str -> str.length()))
            .get();
    Assert.assertEquals("abc" , res2);

    // reduce:实现累加求和
    // 0 :初始值
    // acc :累加器
    // element:当前元素
    int count1 = Stream.of(1,2,3)
            .reduce(0, (acc, element) -> acc + element);
    Assert.assertEquals(6, count1);

    // 展开reduce操作
    BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
    int count2 = accumulator.apply(accumulator.apply(accumulator.apply(0, 1), 2), 3);
    Assert.assertEquals(6, count2);

}

// 计算字符串中小写字母的个数
public static int countLowerCaseLetters(String string){
    return (int) string.chars()
            .filter(Character::isLowerCase)
            .count();
}

// 在一个字符串列表中,找出包含最多小写字母的字符串。对于空列表,返回Optional<String>对象。
public static Optional<String> mostLowerCaseString(List<String> strings){
    return strings.stream()
            .max(Comparator.comparingInt(Test01::countLowerCaseLetters));
}

}


## 2.4 链式调用
例:找出专辑上所有演出乐队的国籍
```java
Set<String> origins = album.getMusicians()
                           .filter(artist -> artist.getName().startsWith("The"))
                           .map(artist -> artist.getNationality())
                           .collect(toSet());

2.5 高阶函数

定义:如果函数的参数列表里包含函数接口,或该函数返回一个函数接口,那么该函数就是高阶函数。 例如:map 是一个高阶函数, 因为它的 mapper 参数是一个函数。

2.6 总结

  • 内部迭代将更多控制权交给了集合类。
  • 和 Iterator 类似, Stream 是一种内部迭代方式。
  • 将 Lambda 表达式和 Stream 上的方法结合起来, 可以完成很多常见的集合操作。

3. 类库

...

4. 高级集合类和收集器

4.1 方法引用

Lambda表达式经常调用参数,例如:artist -> artist.getName() Java 8为其提供了一个简写语法,叫作方法引用,例如:Artist::getName 标准语法为 Classname::methodName 凡是使用 Lambda 表达式的地方, 就可以使用方法引用

4.2 元素顺序

直观上看, 流是有序的, 因为流中的元素都是按顺序处理的。 这种顺序称为出现顺序。

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

如果集合本身就是无序的, 由此生成的流也是无序的。 HashSet 就是一种无序的集合,下面程序不一定每次都通过

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

一些操作在有序的流上开销更大, 调用 unordered 方法消除这种顺序就能解决该问题。 大多数操作都是在有序流上效率更高, 比如 filter、 map 和 reduce 等。

4.3 收集器

定义:一种通用的、 从流生成复杂值的结构。 只要将它传给 collect 方法, 所有的流就都可以使用它了。

4.3.1 转换成其他集合

使用toCollection,用定制的集合收集元素

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

4.3.2 转换成值

maxBy 和 minBy 允许用户按某种特定的顺序生成一个值。

// 找出成员最多的乐队
public Optional<Artist> biggestGroup(Stream<Artist> artists){
    Function<Artists, Long> getCount = artist -> artist.getMembers().count();
    return artists.collect(maxBy(comparing(getCount)));
}

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

// 找出一组专辑上曲目的平均值
public double averageNumberOfTracks(List<Alum> albums){
    return albums.stream()
                 .collect(averagingInt(album -> album.getTrackList().size()));
}

4.3.3 数据分块

收集器 partitioningBy, 它接受一个流, 并将其分成两部分。 Java8函数式编程

// 假设有一个艺术家组成的流, 你可能希望将其分成两个部分,
// 一部分是独唱歌手, 另一部分是由多人组成的乐队。
public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists){
    rerurn artists.collect(partitioningBy(artist -> artist.isSolo()));

    // 方法引用
    // rerurn artists.collect(partitioningBy(Artist::isSolo));
}

4.3.4 数据分组

groupingBy 收集器,接受一个分类函数,用来对数据分组,就像 partitioningBy一样,接受一个Predicate 对象将数据分成 ture 和 false 两部分。 Java8函数式编程

// 现在有一个由专辑组成的流, 可以按专辑当中的主唱对专辑分组
public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums){
    return albums.collect(groupingBy(album -> album.getMainMusician()));
}

4.3.5 字符串

Collectors.joining 收集流中的值,该方法可以方便地从一个流得到一个字符串,允许用户提供分隔符( 用以分隔元素)、前缀和后缀。

// 格式化艺术家姓名
String result = artists.stream()
                       .map(Artist::getName)
                       .collect(Collectors.joining(",","[","]"));

4.3.6 组合收集器

groupingBy + countin收集器

// 计算每个艺术家的专辑数量:
// 1、groupingBy先将元素分组,每块都与分类函数 getMainMusician 提供的键值相关联
// 2、然后使用下游的另一个收集器收集每块中的元素
// 3、最后将结果映射为一个 Map。
public Map<Artist, Long> numberOfAlbums(Stream<Album> albums){
    return albums.collect(groupingBy(album -> album.getMainMusician(), counting()));
}

groupingBy + mapping收集器

// 计算每个艺术家的专辑名
public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums){
    return albums.collect(groupingBy(Album::getMainMusician,
                                     mapping(Album::getName, toList())));
}

4.3.7 重构和定制收集器

...

4.4 其他细节

Lambda 表达式的引入也推动了一些新方法被加入集合类,例如Map:

构建 Map 时, 为给定值计算键值是常用的操作之一,一个经典的例子就是实现一个缓存。 传统的处理方式是先试着从 Map 中取值, 如果没有取到, 创建一个新值并返回。

假设使用 Map<String, Artist> artistCache 定义缓存, 我们需要使用费时的数据库操作查 询艺术家信息:

// 使用显式判断空值的方式缓存
public Artist getArtist(String name){
    Artist artist = artistCache.get(name);
    if(artist == null){
        artist = readArtistFromDB(name);
        artistCache.put(name, artist);
    }
    return artist;
}

// Java 8新方法:computIfAbsent
// 该方法接受一个 Lambda 表达式, 值不存在时使用该 Lambda 表达式计算新值。
public Artist getArtist(String name){
    return artistCache.computeIfAbsent(name, this::readArtistFromDB(name));
}

迭代Map

// 普通迭代遍历Map,代码冗余
Map<Artist, Integer> countOfAlbums = new HashMap<>();
for(Map.Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()){
    Artist artist = entry.getKey();
    List<Album> albums = entry.getValue();
    countOfAlbums.put(artist, albums.size());
}

// Java 8内部迭代遍历Map
Map<Artist, Integer> countOfAlbums = new HashMap<>();
albumsByArtist.forEach((artist, albums) -> {
    countOfAlbums .put(artist, albums.size());
});

5. 数据并行化

5.1 并行和并发

并行:两(多)个任务在同一时间发生 并发:两(多)个任务共享时间段 Java8函数式编程

5.2 并行化流操作

  • 如果已经有一个Stream对 象,调用它的parallel方法就能让其拥有并行操作的能力。
  • 如果想从一个集合类创建一个流,调用parallelStream就能立即获得一个拥有并行能力的流。
// 串行化计算专辑曲目长度
public int serialArraySum(){
    return albums.stream()
                 .flatMap(Album::getTracks)
                 .mapToInt(Track::getLength)
                 .sum();

// 并行化计算专辑曲目长度
public int serialArraySum(){
    return albums.parallelStream()
                 .flatMap(Album::getTracks)
                 .mapToInt(Track::getLength)
                 .sum();
}

5.3 限制

为了发挥并行流框架的优势, 写代码时必须遵守一些规则和限制。

限制一: 调用 reduce 方法,初始值可以为任意值,为了让其在并行化时能工作正常,初值必须为组合函数的恒等值。 reduce 操作求和,组合函数为(acc, element) -> acc + element,则其初值必须为 0,因为任何数字加 0,值不变。 reduce 操作求积,组合函数为(acc, element) -> acc * element,则其初值必须为 1,因为任何数字乘 1,值不变。

限制二: reduce 操作的另一个限制是组合操作必须符合结合律。 这意味着只要序列的值不变, 组合操作的顺序不重要。

与 parallel 对应的是 sequential

5.4 性能

使用串行流还是并行化,取决于以下5个主要因素:

  • 数据大小:分解数据并行处理后合并会带来额外开销
  • 源数据结构:
  • 装箱:处理基本类型比处理装箱类型要快
  • 核的数量:指运行时机器能使用多少核
  • 单元处理开销:花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显
// 并行求和
private int addIntegers(List<Integer> values) {
    return values.parallelStream()
        .mapToInt(i -> i)
        .sum();
}

在底层,并行流还是沿用了fork/join框架。 fork递归式地分解问题,然后每段并行执行,最终由 join 合并结果,返回最后的值。 Java8函数式编程 根据问题的分解方式, 初始的数据源的特性变得尤其重要, 它影响了分解的性能。 根据性能的好坏, 将核心类库提供的通用数据结构分成以下 3 组:

  • 性能好
    • 类似ArrayList、 数组或 IntStream.range数据结构支持随机读取,能轻而易举地被任意分解。
  • 性能一般
    • HashSet、TreeSet,这些数据结构不易公平地被分解
  • 性能差
    • LinkedList、Streams.iterate 和 BufferedReader.lines难于分解

在讨论流中单独操作每一块的种类时, 可以分成两种不同的操作: 无状态的和有状态的。

  • 无状态操作整个过程中不必维护状态,
    • map、filter 和 flatMap
  • 有状态操作则有维护状态所需的开销和限制。
    • sorted、 distinct 和 limit

5.5 并行化数组操作

|方法名|操作| |-|-|-| |parallelPrefix|任意给定一个函数, 计算数组的和| |parallelSetAll|使用 Lambda 表达式更新数组元素| |parallelSort|并行化对数组元素排序|

parallelPrefix操作擅长对时间序列数据做累加,它会更新一个数组,将每一个元素替换为当前元素和其前驱元素的和,这里的“ 和” 是一个宽泛的概念,它不必是加法,可以是任意一个 BinaryOperator

// 计算简单滑动平均数(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)
                    .mapToDouble(i -> {
                        double prefix = i == start ? 0 : sums[i-n];
                        return (sums[i] - prefix) / n;
                    })
                    .toArray();
}
// 使用并行化数组操作初始化数组(改变了传入的数组,没有创建一个新的数组)
public static double[] parallelInitialize(int size){
    double[] values = new double[size];
    Arrays.parallelSetAll(values, i -> i);
    return values;
}
Double[] values = new Double[]{3.0,1.0,2.0};
Arrays.parallelSort(values, ((o1, o2) -> (int) (o2 - o1)));

6. 测试、调式和重构

6.1 Lambda表达式的单元测试

通常,在编写单元测试时,怎么在应用中调用该方法,就怎么在测试中调用。给定一些输入或测试替身,调用这些方法,然后验证结果是否和预期的行为一致。

局限性:因为Lambda 表达式没有名字,无法直接在测试代码中调用。 解决: 1、将Lambda表达式放入一个方法测试,这种方式要测那个方法,而不是Lambda表达式本身

// 将字符串转换为大写形式
public static List<String> allToUpperCase(List<String> words){
    return words.stream()
                .map(string -> string.toUpperCase())
                .collect(Collectors.<String>toList());
}
// 测试大写转换
@Test
public void multiWordsToUppercase(){
    List<String> input = Arrays.toList("a","b","hello");
    List<String> result = Testing.allToUpperCase(input);
    Assert.assertEquals(asList("A", "B","HELLO"), result);
}
// 将列表中元素的第一个字母转换成大写
public static List<String> elementFirstToUpperCaseLambdas(List<String> words){
    return words.stream()
                .map(word -> {
                    char firstChar = Character.toUpperCase(word.charAt(0));
                    return firstChar + word.substring(1);
                })
                .collect(Collector.<String>toList());
}

// 测试,这样测试必须创建一个列表,将所有可能的边界情况考虑到,太繁琐了!
@Test
public void twoLetterStringConvertedToUppercaseLambdas() {
    List<String> input = Arrays.asList("ab");
    List<String> result = Testing.elementFirstToUpperCaseLambdas(input);
    assertEquals(asList("Ab"), result);
}

// 解决:将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);
}

// 测试单独的方法
@Test
public void twoLetterStringConvertedToUppercase() {
    String input = "ab";
    String result = Testing.firstToUppercase(input);
    Assert.assertEquals("Ab", result);
}

6.2 在测试替身时使用Lambda表达式

测试代码时, 使用 Lambda 表达式的最简单方式是实现轻量级的测试存根。 对于countFeature方法的期望行为是为传入的专辑返回某个数值。这里传入 4 张专辑, 测试存根中为每张专辑返回 2,然后断言该方法返回 8,即 2× 4。如果要向代码传入一个Lambda 表达式,最好确保 Lambda 表达式也通过测试。

// 使用 Lambda 表达式编写测试替身, 传给 countFeature 方法
@Test
public void canCountFeatures() {
    OrderDomain order = new OrderDomain(asList(
        newAlbum("Exile on Main St."),
        newAlbum("Beggars Banquet"),
        newAlbum("Aftermath"),
        newAlbum("Let it Bleed")));
    Assert.assertEquals(8, order.countFeature(album -> 2));
}

多数的测试替身都很复杂,使用Mockito这样的框架有助于更容易地产生测试替身。 让我们考虑一种简单情形,为List生成测试替身。我们不想返回List本上的长度,而是返回另一个 List 的长度,为了模拟 List 的 size 方法 我们不想只给出答案, 还想做一些操作, 因此传入一个 Lambda 表达式:

// 结合 Mockito 框架使用 Lambda 表达式
List<String> list = mock(List.class);
when(list.size()).thenAnswer(inv -> otherList.size());
Assert.assertEquals(3, list.size());

6.3 日志和打印消息

以“找出专辑上每位艺术家来自哪个国家”为例

for循环打印中间值

Set<String> nationalities = new HashSet<>();
for(Artist artist : album.getMusicianList()){
    if(artist.getName().startWith("The")){
        String nationality = artist.getNationality();
        System.out.println("Found nationality :" + nationality);
        nationalities.add(nationality);
    }
}

可以使用 forEach 方法打印出流中的值,这同时会触发求值过程。但是这样的操作有个缺点:我们无法再继续操作流了,流只能使用一次。如果我们还想继续,必须重新创建流。

album.getMusicians()
     .filter(artist -> artist.getName().startsWith("The"))
     .map(artist -> artist.getNationality())
     .forEach(nationality -> System.out.println("Found: " + nationality));

Set<String> nationalities
    album.getMusicians()
         .filter(artist -> artist.getName().startsWith("The"))
         .map(artist -> artist.getNationality())
         .collect(Collectors.<String>toSet());

解决办法:peek能查看每个值,同时能继续操作流

// 使用 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());

使用 peek 方法还能以同样的方式,将输出定向到现有的日志系统中,比如 log4j、java.util.logging 或者 slf4j。

7. 设计和架构的原则

7.1 Lambda表达式改变了设计模式

7.2 使用Lambda表达式的SOLID原则

SOLID原则:

  • Single responsibility
  • Open/closed
  • Liskov substitution
  • Interface segregation
  • Dependency inversion

7.2.1 单一功能原则

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

// 计算质数个数, 一个方法里塞进了多重职责
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;
}
// 将 isPrime 重构成另外一个方法后, 计算质数个数的方法
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;
}
// 使用 Java 8 的集合流重构上述代码
public long countPrimes(int upTo){
    return IntStream.range(1, upTo)
                    .filter(this::isPrime)
                    .count();
}

public boolean isPrime(int number){
    return IntStream.range(2, number)
                    .allMatch(x -> (number % x) != 0);
}
// 并行流处理
public long countPrimes(int upTo){
    return IntStream.range(1, upTo)
                    .parallel()
                    .filter(this::isPrime)
                    .count();
}

public boolean isPrime(int number){
    return IntStream.range(2, number)
                    .allMatch(x -> (number % x) != 0);
}

7.2.2 开闭原则

软件应该对扩展开放,对修改闭合。 借助于抽象实现!

例:我们有描述计算机花在用户空间、 内核空间和输入输出上的时间散点图。 我将负责显示这些指标的类叫作 MetricDataGraph

class MetricDataGraph {
    public void updateUserTime(int value);
    public void updateSystemTime(int value);
    public void updateIoTime(int value);
}

如果添加新的时间点,需要修改MetricDataGraph类,这里新建一个“时间点”的抽象类TimeSeries接口

public interface TimeSeries{

}

每种时间点都实现这个抽象类

public class UserTimeSeries implments TimeSeries{
    ...
}
public class SystemTimeSeries implments TimeSeries{
    ...
}
public class IoTimeSeries implments TimeSeries{
    ...
}
// 新增时间点
public class StealTimeSeries implments TimeSeries{
    ...
}
// 重构
class MetricDataGraph {
    public void addTimeSeries(TimeSeries values);
}

对于高阶函数,比如 ThreadLocal 有一个特殊的变量, 每个线程都有一个该变量的副本并与之交互。 该类的静态方法 withInitial 是一个高阶函数, 传入一个负责生成初始值的Lambda 表达式。

// ThreadLocal 日期格式化器

// 实现
ThreadLocal<DateFormat> localFormatter
    = ThradLocal.withInitial(() -> new SimpleDateFormat());

// 使用
DateFomat formatter = localFormatter.get();

通过传入不同的 Lambda 表达式, 可以得到完全不同的行为。

// ThreadLocal 标识符
// 实现
AtomicInteger threadId = new AtomicInteger();
ThreadLocal<Integer> localId
    = ThreadLocal.withInitial(() - > threadId.getAndIncrement());

// 使用
int idForThisThread = localId.get();

对开闭原则的另外一种理解和传统的思维不同, 那就是使用 不可变对象 实现开闭原则

不可变性

  • 观测不可变:指在其他对象看来, 该类是不可变的
  • 实现不可变:指对象本身不可变


java.lang.String 宣称是不可变的,但事实上只是观测不可变,因为它在第一次调用hashCode 方法时缓存了生成的散列值。在其他类看来,这是完全安全的,它们看不出散列值是每次在构造函数中计算出来的,还是从缓存中返回的。

7.2.3 依赖反转原则

抽象不应依赖细节, 细节应该依赖抽象。 ...

8. 使用Lambda表达式编写并发程序

待更新...

8.1 非阻塞I/O

8.2 回调

8.3 Future

8.4 CompletableFuture

8.5 响应式编程

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
3年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
qchen
qchen
Lv1
谁家玉笛暗飞声,散入春风满洛城。
文章
4
粉丝
2
获赞
8