带你快速看完9,springcloud架构图及讲解

    • 42 Lambda优先于匿名类
  • 43 方法引用优先于Lambda

  • 44 优先使用标准的函数式接口

  • 45 谨慎使用Stream

  • 46 优先选择Stream中无副作用的函数

  • 47 Stream要优先用Collection作为返回类型

  • 48 谨慎使用Stream并行

42 Lambda优先于匿名类


自从JDK 1.1于1997年发布以来,创建函数对象的主要手段就是匿名类。下面代码是按照字符串⻓度顺序对列表进行排序,使用匿名类创建排序的比较方法:

Collections.sort(words, new Comparator() {

public int compare(String s1, String s2) {

return Integer.compare(s1.length(), s2.length());

}

});

匿名类适用于需要函数对象的经典面向对象设计模式,特别是策略模式。Comparator接口代表一种排序的抽象策略;上面的匿名类是排序字符串的具体策略。

在Java 8中,“带有单个抽象方法的接口”是特殊的,他们被称作函数式接口,Java允许利用Lambda表达式创建这些接口的实例。

Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

Lambda的类型是Comparator <String>,其参数s1,s2的类型是String,返回值类型int全都没有出现在代码里

编译器使用一个叫类型推断的过程从上下文中推断出这些类型。

如果编译器产生错误消息,无法推断出Lambda参数的类型,那就手动指定它。

如果用Lambda表达式代替Comparator的构造方法,代码会更加简练:

Collections.sort(words, comparingInt(String::length));

如果用Java 8中List接口里的sort方法,代码还可以更简洁:

words.sort(comparingInt(String::length));

Java中增加了Lambda之后,使得之前不能使用函数对象的地方现在也能用了。例如,以34条里的Operation枚举类型为例:

public enum Operation {

PLUS(“+”) {

public double apply(double x, double y) {

return x + y;

}

},

MINUS(“-”) {

public double apply(double x, double y) {

return x - y;

}

},

TIMES(“*”) {

public double apply(double x, double y) {

return x * y;

}

},

DIVIDE(“/”) {

public double apply(double x, double y) {

return x / y;

}

};

private final String symbol;

Operation(String symbol) {

this.symbol = symbol;

}

@Override

public String toString() {

return symbol;

}

public abstract double apply(double x, double y);

}

使用Lambda改造的话,只要给每个枚举常量的构造器传递一个实现其行为的Lambda即可:

public enum Operation {

PLUS(“+”, (x, y) -> x + y),

MINUS(“-”, (x, y) -> x - y),

TIMES(“*”, (x, y) -> x * y),

DIVIDE(“/”, (x, y) -> x / y);

private final String symbol;

private final DoubleBinaryOperator op;

Operation(String symbol, DoubleBinaryOperator op) {

this.symbol = symbol;

this.op = op;

}

@Override

public String toString() {

return symbol;

}

public double apply(double x, double y) {

return op.applyAsDouble(x, y);

}

}

构造方法将Lambda存储在实例属性中,apply 方法将调用转发给Lambda,代码量更少,逻辑也更清晰。


使用Lambda需要注意的地方是:Lambda没有名称和文档;如果计算比较复杂,或者代码量超过几行,就不要用Lambda了。

Lambda表达式一行是最好的,三行是极限!不能再多了

传递给枚举构造方法的参数是在静态环境中计算的。因此,枚举构造方法中的Lambda表达式不能访问枚举的实例成员。如果枚举类型具有难以理解的特殊方法,使用原先的实现方式仍是首选。

匿名类在Lambda时代并未过时,可以依据以下几点来进行选择:

1. 如果想创建抽象类的实例,可以用匿名类来完成

2. 如果一个接口有多个抽象方法,创建实例要用匿名类

3. Lambda无法获得对自身的引用

在Lambda中,关键字this指向外围实例,这通常是我们想要的

不要序列化一个 Lambda 或 匿名类实例,如果想要可序列化的函数对象,如Comparator,就使用私有静态嵌套类的实例。

43 方法引用优先于Lambda


如果方法引用看起来更简短更清晰,就用方法引用;否则还是用Lambda

Java提供了一种生成函数对象的方法,比lambda还要简洁:方法引用(method references),就是常说的::运算符

下面代码是用来保持从任意键到Integer的映射:

map.merge(key, 1, (count, incr) -> count + incr);

代码使用了Java 8中的 Map 接口中的merge方法,如果没有给定key的映射,就插入默认值(上面代码里的1);如果映射已经存在,则将函数应用于当前值和指定值,并用结果覆盖当前值。这里的函数是将现有值count递增incr

Integer 类(和所有其他包装数字基本类型)提供了一个静态方法sum,只传入这个方法的引用也行:

map.merge(key, 1, Integer::sum);

使用了方法引用的代码更简洁

但有时候Lambda会比方法引用更简洁,大多数情况是方法与lambda相同的类中,例如下面的代码发生在GoshThisClassNameIsHumongous类里:

- 方法引用

service.execute(GoshThisClassNameIsHumongous::action);

- Lambda

service.execute(() -> action());

类似的还有 Function 接口,它用一个静态工厂方法返回 id 函数 Function.identity()。如果使用等效的lambda内联代码:

x -> x

这样会更简洁

许多方法引用是指静态方法,但有4种方法没有引用静态方法:

| 方法引用类型 | 范例 | Lambda等式 |

| — | — | — |

| 静态 | Integer::parseInt | str -> Integer.parseInt(str) |

| 有限制 | Instant.now()::isAfter | Instant then = Instant.now();

t-> then.isAfter(t) |

| 无限制 | String::toLowerCase | str -> str.toLowerCase() |

| 类构造器 | TreeMap<K, V>::new | () -> new TreeMap<K, V> |

| 数组构造器 | int[]::new | len -> new int[len] |

无限制的引用经常用在流管道(Stream pipeline)中作为映射和过滤函数;构造器引用是充当工厂对象

44 优先使用标准的函数式接口


如果标准函数接口能满足要求,应该优先使用它,而不是专⻔自己创建新的函数接口。

LinkedHashMap为例,可以通过重写其protected removeEldestEntry方法将此类用作缓存,每次将新的key值加入到map时都会调用该方法。以下代码重写允许map最多保存100个条目,然后在每次添加新key值时删除最老的条目:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {

return size() > 100;

}

用Lambda可以做得更好,自己定义一个函数接口如下:

@FunctionalInterface

interface EldestEntryRemovalFunction<K,V>{

boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);

}

这个接口可以正常工作,但是没必要,因为java.util.function包提供了大量标准函数式接口以供使用。

许多标准函数式接口都提供了有用的默认方法。如Predicate接口提供了组合判断的方法。标准的BiPredicate<Map<K,V>, Map.Entry<K,V>> 接口应优先于自定义的EldestEntryRemovalFunction接口的使用。

EldestEntryRemovalFunction 接口使用@FunctionalInterface注解进行标注的。这个注解类型本质上与@Override一样,有三个目的:

  1. 告诉读者这个接口是针对Lambda设计的

  2. 这个接口不会进行编译,除非他只有一个抽象方法

  3. 避免后续维护人员不小心给该接口添加抽象方法

始终使用@FunctionalInterface1注解标注自己写的函数式接口

| 接口 | 方法 | 示例 |

| — | — | — |

| UnaryOperator | T apply(T t) | String::toLowerCase |

| BinaryOperator | T apply(T t1, T t2) | BigInteger::add |

| Predicate | boolean test(T t) | Collection::isEmpty |

| Function<T,R> | R apply(T t) | Arrays::asList |

| Supplier | T get() | Instant::now |

| Consumer | void accept(T t) | System.out::println |

  • 这六个基础接口各自还有3种变体(int、long、double),例如predicate的变体IntPredicate

  • Function 接口还有9种变体,LongToIntFunctionDoubleToObjFunction

  • 这三种基础函数接口还有带两个参数的版本,BiPredicate <T,U>BiFunction <T,U,R>BiConsumer <T,U>

  • 还有BiFunction变体用于返回三个相关的基本类型:ToIntBiFunction<T,U>ToLongBiFunction<T,U>ToDoubleBiFunction <T,U>

  • Consumer接口也有带两个参数的变体版本,带一个对象和一个基本类型:ObjDoubleConsumer <T>ObjIntConsumer <T>ObjLongConsumer <T>

  • 还有一个 BooleanSupplier 接口,它是 Supplier 的一个变体,返回boolean

注意:不要用带包装类型的基础函数接口来代替基本函数接口。使用装箱基本类型进行批量操作处理,后果可能是致命的。

什么时候应该自己编写接口呢?

答案是:如果没有一个标准的函数接口能够满足需求时

Comparator <T>为例,它的结构与ToIntBiFunction <T, T>接口相同。Comparator有自己的接口有以下几个原因:

  1. 每当在API中使用时,其名称提供了良好的文档信息

  2. Comparator接口对于如何构成一个有效的实例,有着严格的条件限制

  3. 这个接口配置了大量好用的default方法,可以对Comparator进行转换和合并

如果所需要的函数接口与Comparator一样具有以下特征,就需要自己编写专用的函数接口了:

  1. 通用,并且将受益于描述性的名称

  2. 具有与其关联的严格的契约

  3. 将受益于定制的缺省方法

45 谨慎使用Stream


在Java 8中添加了Stream API,以简化串(并)行执行批量操作的任务。

Stream表示有限或无限的数据元素序列,Stream pipeline,表示对这些元素的多级计算。Stream中的元素可以来自集合、数组、文件、正则表达式模式匹配器、伪随机数生成器和其他Stream。数据可以是对象引用或基本类型(int、long、double)。

一个Stream pipeline包含一个 源Stream,几个中间操作,1个终止操作。每个中间操作都以某种方式转换Stream,比如过滤操作。终止操作会对Stream执行一个最终计算,比如返回一个List,打印所有元素等。

  • Stream pipeline是lazy的:直到调用终止操作时才会开始计算

没有终止操作的的Stream pipeline是静默的,所以终止操作千万不能忘

  • Stream API是fluent的:所有包含pipeline的调用可以链接成一个表达式

介绍完Stream之后,肯定就会有小伙伴们开始思考了,我们应该在什么时候用呢?

其实并没有任何硬性的规定,但可以从以下例子中得到启发:

例一:

读取字典中的单词,打印出单词出现次数大于某值的所有“换位词”

换位词:包含相同字母,但顺序不同的单词

如果换位词一样,这里就认为是同一个单词

public class Anagrams {

public static void main(String[] args) throws IOException {

File dictionary = new File(args[0]);

int minGroupSize = Integer.parseInt(args[1]);

Map<String, Set> groups = new HashMap<>();

try (Scanner s = new Scanner(dictionary)) {

while (s.hasNext()) {

String word = s.next();

groups.computeIfAbsent(alphabetize(word),

(unused) -> new TreeSet<>()).add(word);

}

}

for (Set group : groups.values())

if (group.size() >= minGroupSize)

System.out.println(group.size() + ": " + group);

}

private static String alphabetize(String s) {

char[] a = s.toCharArray();

Arrays.sort(a);

return new String(a);

}

}

将每个单词插入到map中中使用了computeIfAbsent方法,computeIfAbsent 方法对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hashMap 中。

语法为:

hashmap.computeIfAbsent(K key, Function remappingFunction)

参数说明:

  • key - 键

  • remappingFunction - 重新映射函数,用于重新计算value

例二:

这个例子大量使用了Stream

public class Anagrams {

public static void main(String[] args) throws IOException {

Path dictionary = Paths.get(args[0]);

int minGroupSize = Integer.parseInt(args[1]);

try (Stream words = Files.lines(dictionary)) {

words.collect(

groupingBy(word -> word.chars().sorted()

.collect(StringBuilder::new,

(sb, c) -> sb.append((char) c),

StringBuilder::append).toString()))

.values().stream()

.filter(group -> group.size() >= minGroupSize)

.map(group -> group.size() + ": " + group)

.forEach(System.out::println);

}

}

}

如果你发现这段代码难以阅读,别担心,我也难看懂吗,在工作里面也是不提倡的,所以滥用Stream会使得程序代码难以读懂和维护

例三:

下面的代码和例二的逻辑相同,它没有过度使用Stream,代码可读性很强:

public class Anagrams {

public static void main(String[] args) throws IOException {

Path dictionary = Paths.get(args[0]);

int minGroupSize = Integer.parseInt(args[1]);

try (Stream words = Files.lines(dictionary)) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

读者福利

秋招我借这份PDF的复习思路,收获美团,小米,京东等Java岗offer

更多笔记分享

秋招我借这份PDF的复习思路,收获美团,小米,京东等Java岗offer

ary)) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-twACqpwF-1711857559311)]
[外链图片转存中…(img-Fox4UrK3-1711857559312)]
[外链图片转存中…(img-awtwlLYf-1711857559312)]
[外链图片转存中…(img-Ev9Dy8eV-1711857559313)]
[外链图片转存中…(img-DD67ozy5-1711857559313)]
[外链图片转存中…(img-FQhtnaRJ-1711857559314)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-CFXdPpcz-1711857559315)]

读者福利

[外链图片转存中…(img-RGbhcFWe-1711857559315)]

更多笔记分享

[外链图片转存中…(img-hTFdFm9P-1711857559315)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值