最新带你快速看完9(5),深入浅出

最后

权威指南-第一本Docker书

引领完成Docker的安装、部署、管理和扩展,让其经历从测试到生产的整个开发生命周期,深入了解Docker适用于什么场景。并且这本Docker的学习权威指南介绍了其组件的基础知识,然后用Docker构建容器和服务来完成各种任务:利用Docker为新项目建立测试环境,演示如何使用持续集成的工作流集成Docker,如何构建应用程序服务和平台,如何使用Docker的API,如何扩展Docker。

总共包含了:简介、安装Docker、Docker入门、使用Docker镜像和仓库、在测试中使用Docker、使用Docker构建服务、使用Fig编配Docke、使用Docker API、获得帮助和对Docker进行改进等9个章节的知识。

image

image

image

image

关于阿里内部都在强烈推荐使用的“K8S+Docker学习指南”—《深入浅出Kubernetes:理论+实战》、《权威指南-第一本Docker书》,看完之后两个字形容,爱了爱了!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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)) {

words.collect(groupingBy(word -> alphabetize(word)))

.values().stream()

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

.forEach(g -> System.out.println(g.size() + ": " + g));

}

}

// alphabetize method is the same as in original version

private static String alphabetize(String s) {

char[] a = s.toCharArray();

Arrays.sort(a);

return new String(a);

}

}

它在一个try-with-resources块中打开文件,获得一个由文件中的所有代码的Stream。Stream中的pipeline没有中间操作,终止操作是将所有单词集合到一个映射中,按照它们的换位词对单词进行分组

values().stream()

打开了一个新的Stream<List<String>>,这个Stream里的元素都是换位词,filter进行了过滤,忽略大小小于minGroupSize的所有组,最后由终结操作forEach打印剩下的同位词组。

提高Stream代码的可读性有两个要求:

  • 在没有显式类型的情况下,认真命名Lambda参数

  • 使用辅助方法(上面的alphabetize),因为pipeline缺少显式类型信息和命名临时变量


需要提醒一点,使用Stream处理char类型的数据有风险:

例四:

“Hello world!”.chars().forEach(System.out::print);

发现它打印 721011081081113211911111410810033。这是因为“Hello world!”.chars()返回的Stream的元素不是char值,而是int,修改方法是加一个强制类型转换:

“Hello world!”.chars().forEach(x -> System.out.print((char) x));

所以应该避免使用Stream来处理char值


综上所述,Stream适合完成下面这些工作:

  • 统一转换元素序列

  • 过滤元素序列

  • 使用单个操作组合元素序列(例如添加、连接或计算最小值)

  • 将元素序列累积到一个集合中,可能通过一些公共属性将它们分组

  • 在元素序列中搜索满足某些条件的元素


假设Card是一个不变值类,用于封装Rank和Suit,下面代码求他们的笛卡尔积:

private static List newDeck() {

List result = new ArrayList<>();

for (Suit suit : Suit.values())

for (Rank rank : Rank.values())

result.add(new Card(suit, rank));

return result;

}

基于Stream实现的代码如下:

private static List newDeck() {

return Stream.of(Suit.values())

.flatMap(suit ->

Stream.of(Rank.values())

.map(rank -> new Card(suit, rank)))

.collect(toList());

}

其中用到了 flatMap 方法:这个操作将一个Stream中的每个元素都映射到一个Stream中,然后将这些新的Stream全部合并到一个Stream(或展平它们)。

newDeck的两个版本中到底哪一个更好?这就是仁者见仁智者见智的问题了,取决于你的个人喜好 : )

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


Stream最重要的是把将计算结构构造成一系列变型,其中每个阶段的结果尽可能接近前一阶段结果的纯函数(pure function)。纯函数的结果仅取决于其输入的函数:它不依赖于任何可变状态,也不更新任何状态。为了实现这一点,Stream操作的任何中间操作和终结操作都应该是没有副作用的。

如下代码,将单词出现的频率打印出来:

Map<String, Long> freq = new HashMap<>();

try (Stream words = new Scanner(file).tokens()) {

words.forEach(word -> {

freq.merge(word.toLowerCase(), 1L, Long::sum);

});

}

实际上这根本不是Stream代码,只不过是伪装成Stream的迭代代码。可读性也差,forEach里面逻辑太多了,正确的应该是这么写:

Map<String, Long> freq;

try (Stream words = new Scanner(file).tokens()) {

freq = words

.collect(groupingBy(String::toLowerCase, counting()));

}

所以forEach 操作应仅用于报告Stream计算的结果,而不是进行计算


对于初学者来说,可以忽略Collector接口,将其看做是黑盒对象即可,这个黑盒可以将Stream的元素合并到单个集合里。

有三个这样的CollectortoList()toSet()toCollection(collectionFactory)。基于此,我们可以从频率表中提取排名前10的单词列表:

List topTen = freq.keySet().stream()

.sorted(comparing(freq::get).reversed())

.limit(10)

.collect(toList());

注意上述代码用的是.collect(toList()),而不是.collect(Collectors.toList()),这是因为静态导入了Collectors所有成员,也是一种提高代码可读性的手段。


接下来介绍Collector中比较重要的三个方法:

1. toMap(keyMapper、valueMapper)

它接受两个函数:一个将Stream元素映射到键,另一个将它映射到值。例如下面将枚举的字符串形式映射到枚举本身:

private static final Map<String, Operation> stringToEnum =

Stream.of(values()).collect(toMap(Object::toString, e -> e));

还有带三个参数的toMap,假设有一个Stream代表不同艺术家(artists)的专辑(albums),可以得到每个歌唱家最畅销的那一张专辑,用map来存储:

Map<Artist, Album> topHits = albums.collect(toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));

比较器使用静态工厂方法maxBy,它是从BinaryOperator import进来的。此方法将Comparator<T> 转换为BinaryOperator<T>,用于计算指定比较器产生的最大值。

对于`toMap`,阿里巴巴开发规约也专门做了要求:

在这里插入图片描述

在这里插入图片描述

2. groupingBy

该方法返回Collector,基于分类函数(classifier function)将元素分类,返回值是一个map,value是存储了每个类别的所有元素的List

words.collect(groupingBy(word -> alphabetize(word)))

上面代码就返回的是collect,key是alphabetize(word),value是word列表

还有传入两个参数的groupingBy,传入counting()作为下游收集器,这样会生成一个映射,将每个类别与该类别中的元素数量关联起来

Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));

3. groupingByConcurrent

该方法提供了提供了groupingBy所有三种重载的变体,支持并发安全性,最终返回的也是ConcurrentHashMap实例

4. joining

它仅对 CharSequence 实例(如字符串)的Stream进行操作。可以传入一个参数CharSequence delimiter作为分界符。如果传入一个逗号作为分隔符,Collector就会返回一个用逗号隔开的字符串

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


先说结论:

在编写会返回一系列元素的方法时,某些程序员可能希望将它们作为 Stream 处理,其他程序员可能希望使用迭代方式(Iterable)。

如何做到兼顾呢?

  • 如果可以返回集合,就返回集合

  • 如果集合中已经有元素,或者元素数量不多,就返回一个标准集合,比如 ArrayList

  • 否则,就需要自定义集合,如下文将提到的幂集

  • 如果不能返回集合,则返回Stream或Iterable


如果想要用for-each循环遍历返回序列的话,必须将方法引用转换成合适的Iterable类型:

for (ProcessHandle ph : (Iterable)ProcessHandle.allProcesses()::iterator)

但是上面的代码在实际使用时过于杂乱、不清晰。解决方案就是写一个适配器:

public static Iterable iterableOf(Stream stream) {

return stream::iterator;

}

有了这个适配器,就可以使用 for-each 语句迭代任何Sream了:

总结:心得体会

既然选择这个行业,选择了做一个程序员,也就明白只有不断学习,积累实战经验才有资格往上走,拿高薪,为自己,为父母,为以后的家能有一定的经济保障。

学习时间都是自己挤出来的,短时间或许很难看到效果,一旦坚持下来了,必然会有所改变。不如好好想想自己为什么想进入这个行业,给自己内心一个答案。

面试大厂,最重要的就是夯实的基础,不然面试官随便一问你就凉了;其次会问一些技术原理,还会看你对知识掌握的广度,最重要的还是你的思路,这是面试官比较看重的。

最后,上面这些大厂面试真题都是非常好的学习资料,通过这些面试真题能够看看自己对技术知识掌握的大概情况,从而能够给自己定一个学习方向。包括上面分享到的学习指南,你都可以从学习指南里理顺学习路线,避免低效学习。

大厂Java架构核心笔记(适合中高级程序员阅读):

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

ble`


如果想要用for-each循环遍历返回序列的话,必须将方法引用转换成合适的Iterable类型:

for (ProcessHandle ph : (Iterable)ProcessHandle.allProcesses()::iterator)

但是上面的代码在实际使用时过于杂乱、不清晰。解决方案就是写一个适配器:

public static Iterable iterableOf(Stream stream) {

return stream::iterator;

}

有了这个适配器,就可以使用 for-each 语句迭代任何Sream了:

总结:心得体会

既然选择这个行业,选择了做一个程序员,也就明白只有不断学习,积累实战经验才有资格往上走,拿高薪,为自己,为父母,为以后的家能有一定的经济保障。

学习时间都是自己挤出来的,短时间或许很难看到效果,一旦坚持下来了,必然会有所改变。不如好好想想自己为什么想进入这个行业,给自己内心一个答案。

面试大厂,最重要的就是夯实的基础,不然面试官随便一问你就凉了;其次会问一些技术原理,还会看你对知识掌握的广度,最重要的还是你的思路,这是面试官比较看重的。

最后,上面这些大厂面试真题都是非常好的学习资料,通过这些面试真题能够看看自己对技术知识掌握的大概情况,从而能够给自己定一个学习方向。包括上面分享到的学习指南,你都可以从学习指南里理顺学习路线,避免低效学习。

大厂Java架构核心笔记(适合中高级程序员阅读):

[外链图片转存中…(img-ZH8G9xPb-1715670616197)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>