[appendix]
= JAVA
== 常用技巧
=== 日志输出模式
在日志输出时,在拼接字符串时,请使用字符串模版方式。
[source,java]
----
log.info(object + " message"); // not good
log.info("{} message", object); // good
----
=== 遍历 Map
[source,java]
----
Map<String,Object> map =new HashMap<>();
for (Map.Entry<String,Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
}
----
== Java 8 Time Api
Java 8 为 Date 和 Time 引入了新的 API,以解决旧 java.util.Date 和 java.util.Calendar 的缺点。
=== 旧的时间API(java8之前)的问题
* 线程安全 - Date 和 Calendar 类不是线程安全的,使开发者难以调试这些 api 的并发问题,需要编写额外的代码来处理线程安全。Java 8 中引入的新的 Date 和 Time API 是不可变的和线程安全的,使得这些痛点得以解决。
* API 设计和易于理解 - 旧的时间 api 非常难以理解,操作都非常复杂,非常绕口,没有提供一些常用的解析转换方法。新的时间 API 是以 ISO 为中心的,并遵循 date, time, duration 和 periods 的一致域模型。提供了一些非常实用方法以支持最常见的操作。不再需要我们自己封装一些时间操作类,而且描述语义化。
* ZonedDate 和 Time - 在旧的时间 api 中开发人员必须编写额外的逻辑来处理旧 API 的时区逻辑,而使用新的 API,可以使用 Local 和 ZonedDate / Time API 来处理时区。无需过多关心时区转换问题。
=== 使用 LocalDate,LocalTime 和 LocalDateTime
上下文相结合的本地日期/时间。这些类主要用于不需要在上下文中明确指定时区的情况。
==== LocalDate
[source,java]
----
// 获取当前系统时钟下的日期
LocalDate localDate = LocalDate.now();
// 构建指定日期
LocalDate localDate =LocalDate.of(2015, 02, 20);
LocalDate localDate =LocalDate.parse("2015-02-20");
// 当前日期加一天
LocalDate tomorrow =LocalDate.now().plusDays(1);
// 当前日期减一个月
LocalDate previousMonthSameDay =LocalDate.now().minus(1, ChronoUnit.MONTHS);
// 星期几
DayOfWeek sunday =LocalDate.parse("2019-06-12").getDayOfWeek();
// 月中的第几天
int twelve =LocalDate.parse("2016-09-12").getDayOfMonth();
// 是否闰年
boolean leapYear =LocalDate.now().isLeapYear();
// 日期先后
boolean notBefore =LocalDate.parse("2019-06-12").isBefore(LocalDate.parse("2019-06-11"));
boolean isAfter =LocalDate.parse("2019-06-12").isAfter(LocalDate.parse("2019-06-11"));
// 开始时间
LocalDateTime beginningOfDay =LocalDate.parse("2019-06-12").atStartOfDay();
// 月初
LocalDate firstDayOfMonth =LocalDate.parse("2019-09-12").with(TemporalAdjusters.firstDayOfMonth());
----
==== LocalTime
[source,java]
----
// 当前时间
LocalTime now =LocalTime.now();
// 构建时间
LocalTime sixThirty =LocalTime.parse("06:30");
LocalTime sixThirty =LocalTime.of(6, 30);
// 当前时间加一小时
LocalTime sevenThirty =LocalTime.parse("06:30").plus(1, ChronoUnit.HOURS);
// 获取时间的小时
int six =LocalTime.parse("06:30").getHour();
// 时间先后
boolean isbefore =LocalTime.parse("06:30").isBefore(LocalTime.parse("07:30"));
// 最大时间
LocalTime maxTime =LocalTime.MAX;
----
==== LocalDateTime
[source,java]
----
// 当前日期时间
LocalDateTime now =LocalDateTime.now();
// 构建日期时间
LocalDateTime datetime =LocalDateTime.of(2019, Month.FEBRUARY, 20, 06, 30);
LocalDateTime datetime =LocalDateTime.parse("2019-02-20T06:30:00");
// 获取组成部分
int month =LocalDateTime.now().getMonth();
// 加减
LocalDateTime.now().plusDays(1);
LocalDateTime.now().minusHours(2);
----
=== 使用 ZonedDateTime
[source,java]
----
// 获取上海时区
ZoneId zoneId =ZoneId.of("Aisa/Shanghai");
// 获取所有时区
Set<String> allZoneIds =ZoneId.getAvailableZoneIds();
// 构建时区日期时间
ZonedDateTime zonedDateTime =ZonedDateTime.of(localDateTime, zoneId);
ZonedDateTime zonedDateTime =ZonedDateTime.parse("2019-06-03T10:15:30+01:00[Aisa/Shanghai]");
// 偏移量日期时间
ZoneOffset offset =ZoneOffset.of("+02:00");
OffsetDateTime offSetByTwo =OffsetDateTime.of(LocalDateTime.now(), offset);
----
=== 使用 Period 和 Duration
* Period : 用于计算两个日期(年月日)间隔。
* Duration : 用于计算两个时间(秒,纳秒)间隔。
[source,java]
----
// Period
LocalDate initialDate = LocalDate.parse("2007-05-10");
LocalDate finalDate = initialDate.plus(Period.ofDays(5));
int five = Period.between(finalDate, initialDate).getDays();
int five = ChronoUnit.DAYS.between(finalDate , initialDate);
// Duration
LocalTime initialTime = LocalTime.of(6, 30, 0);
LocalTime finalTime = initialTime.plus(Duration.ofSeconds(30));
int thirty = Duration.between(finalTime, initialTime).getSeconds();
int thirty = ChronoUnit.SECONDS.between(finalTime, initialTime);
----
=== Date 和 Calendar 实例转换为新的 Date Time API
[source,java]
----
LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault());
LocalDateTime.ofInstant(Calendar.getInstance().toInstant(), ZoneId.systemDefault());
LocalDateTime.ofEpochSecond(1465817690, 0, ZoneOffset.UTC);
----
=== 日期和时间格式化
[source,java]
----
LocalDateTime localDateTime = LocalDateTime.of(2019, Month.JANUARY, 25, 6, 30);
String localDateString = localDateTime.format(DateTimeFormatter.ISO_DATE);
localDateTime.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
localDateTime.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.UK);
----
== 安全随机数
[source,java]
----
SecureRandom random =new SecureRandom();
random.nextBoolean();
random.nextBytes();
random.nextInt();
random.nextDouble();
----
== 常用的 Http 认证方式
=== HTTP Basic Authentication
HTTP Basic Authentication 又叫基础认证,它简单地使用 Base64 算法对用户名、密码进行加密,并将加密后的信息放在请求头 Header 中,本质上还是明文传输用户名、密码,并不安全,所以最好在 Https 环境下使用。其认证流程如下:
image::9999-appendix/java/001.webp[]
客户端发起 GET 请求 服务端响应返回 401 Unauthorized, www-Authenticate 指定认证算法,realm 指定安全域。然后客户端一般会弹窗提示输入用户名称和密码,输入用户名密码后放入 Header 再次请求,服务端认证成功后以 200 状态码响应客户端。
=== HTTP Digest Authentication
为弥补 BASIC 认证存在的弱点就有了 HTTP Digest Authentication 。它又叫摘要认证。它使用随机数加上 MD5 算法来对用户名、密码进行摘要编码,流程类似 Http Basic Authentication ,但是更加复杂一些:
image::9999-appendix/java/002.webp[]
. 步骤1:跟基础认证一样,只不过返回带 WWW-Authenticate 首部字段的响应。该字段内包含质问响应方式认证所需要的临时咨询码(随机数,nonce)。 首部字段 WWW-Authenticate 内必须包含 realm 和 nonce 这两个字段的信息。客户端就是依靠向服务器回送这两个值进行认证的。nonce 是一种每次随返回的 401 响应生成的任意随机字符串。该字符串通常推荐由 Base64 编码的十六进制数的组成形式,但实际内容依赖服务器的具体实现
. 步骤2:接收到 401 状态码的客户端,返回的响应中包含 DIGEST 认证必须的首部字段 Authorization 信息。首部字段 Authorization 内必须包含 username、realm、nonce、uri 和 response 的字段信息,其中,realm 和 nonce 就是之前从服务器接收到的响应中的字段。
. 步骤3:接收到包含首部字段 Authorization 请求的服务器,会确认认证信息的正确性。认证通过后则会返回包含 Request-URI 资源的响应。
并且这时会在首部字段 Authorization-Info 写入一些认证成功的相关信息。
=== SSL 客户端认证
SSL 客户端认证就是通常我们说的 HTTPS 。安全级别较高,但需要承担 CA 证书费用。SSL 认证过程中涉及到一些重要的概念,数字证书机构的公钥、证书的私钥和公钥、非对称算法(配合证书的私钥和公钥使用)、对称密钥、对称算法(配合对称密钥使用)。相对复杂一些这里不过多讲述。
=== Form 表单认证
Form 表单的认证方式并不是HTTP规范。所以实现方式也呈现多样化,其实我们平常的扫码登录,手机验证码登录都属于表单登录的范畴。
表单认证一般都会配合 Cookie,Session 的使用,现在很多 Web 站点都使用此认证方式。用户在登录页中填写用户名和密码,
服务端认证通过后会将 sessionId 返回给浏览器端,浏览器会保存 sessionId 到浏览器的 Cookie 中。
因为 HTTP 是无状态的,所以浏览器使用 Cookie 来保存 sessionId。下次客户端会在发送的请求中会携带 sessionId 值,
服务端发现 sessionId 存在并以此为索引获取用户存在服务端的认证信息进行认证操作。认证过则会提供资源访问。
登录后返回 JWT Token 一文其实也是通过 Form 提交来获取 Jwt 其实 Jwt 跟 sessionId 同样的作用,
只不过 Jwt 天然携带了用户的一些信息,而 sessionId 需要去进一步获取用户信息。
=== Json Web Token 的认证方式 Bearer Authentication
我们通过表单认证获取 Json Web Token ,那么如何使用它呢?
通常我们会把 Jwt 作为令牌使用 Bearer Authentication 方式使用。
Bearer Authentication 是一种基于令牌的 HTTP 身份验证方案,用户向服务器请求访问受限资源时,
会携带一个 Token 作为凭证,检验通过则可以访问特定的资源。
最初是在 RFC 6750 中作为 OAuth 2.0 的一部分,但有时也可以单独使用。
我们在使用 Bear Token 的方法是在请求头的 Authorization 字段中放入 Bearer <token> 的格式的加密串(Json Web Token)。
请注意 Bearer 前缀与 Token 之间有一个空字符位,与基本身份验证类似,Bearer Authentication 只能在HTTPS(SSL)上使用。
== Java 8 Stream Api
Java 8 提供了非常好用的 Stream API ,可以很方便的操作集合。
今天我们来探讨两个 Stream 中间操作 map(Function<? super T, ? extends R> mapper)
和 flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
=== map 操作
map 操作是将流中的元素进行再次加工形成一个新流。这在开发中很有用。
比如我们有一个学生集合,我们需要从中提取学生的年龄以分析学生的年龄分布曲线。
放在 Java 8 之前 我们要通过新建一个集合然后通过遍历学生集合来消费元素中的年龄属性。
现在我们通过很简单的流式操作就完成了这个需求。
[source,java]
----
public class Test {
public static void main(String[] args) {
List<Student> students =new ArrayList<>();
students.add(new Student("name1",10));
students.add(new Student("name2",20));
students.add(new Student("name3",30));
students.add(new Student("name4",40));
List<Integer> ints =students.stream().map(Student::getAge).collect(Collectors.toList());
for(int i : ints){
System.out.println(i);
}
}
private static class Student {
private String name;
private int age;
public Student(String name,int age){
this.name =name;
this.age =age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}
----
=== map 操作
通过上面的例子,map 操作应该非常好理解。那么 flatMap 是干嘛的呢?
这样我们把上面的例子给改一下,如果是以班级为单位,提取所有班级下的所有学生的年龄以分析学生的年龄分布曲线。
这时我们使用上面的方法还行得通吗?
[source,java]
----
List<List<Student>> studentGroup= gradeList.stream().map(Grade::getStudents).collect(Collectors.toList());
----
通过上面的一顿操作,我们只能得到每个班的学生集合的集合 List<List<Student>>。
我们还需要嵌套循环才能获取学生的年龄数据,十分不便。如果我们能返回全部学生的集合 List<Students> 就方便多了。
没错!flatMap 可以搞定!
[source,java]
----
List<Integer> ages = grades.stream().flatMap(grade -> grade.getStudents().stream())
.map(Student::getAge).collect(Collectors.toList());
----
正如上面的伪代码所示,我们使用 flatMap 将所有的学生汇聚到一起。
然后再使用 map 操作提取年龄。 flatMap 不同于 map 地方在于 map 只是提取属性放入流中,
而 flatMap 先提取属性放入一个比较小的流,然后再将所有的流合并为一个流。有一种 “聚沙成塔” 的感觉。
=== skip
skip(lang n) 是一个跳过前 n 个元素的中间流操作。我们编写一个简单的方法来进行skip操作,将流剩下的元素打印出来。
[source,java]
----
public static void skip(long n) {
Stream<Integer> integerStream = Stream.of(10, 9, 8, 7, 6, 5);
integerStream.skip(2).forEach(integer -> System.out.println("integer = " + integer));
// out put:
// integer = 8
// integer = 7
// integer = 6
// integer = 5
}
----
skip(long n) 方法跳过前 n (非负)个元素,返回剩下的流,有可能为空流。
=== limit
[source,java]
----
public static void skip(long n) {
Stream<Integer> integerStream = Stream.of(10, 9, 8, 7, 6, 5);
integerStream.limit(2).forEach(integer -> System.out.println("integer = " + integer));
// out put:
// integer = 10
// integer = 9
}
----
=== peek
peek 操作接收的是一个 Consumer<T> 函数。
顾名思义 peek 操作会按照 Consumer<T> 函数提供的逻辑去消费流中的每一个元素,
同时有可能改变元素内部的一些属性。
这里我们要提一下这个 Consumer<T> 以理解 什么是消费。
[source,java]
----
Stream<String> stream = Stream.of("hello", "world");
List<String> strs= stream.peek(System.out::println).collect(Collectors.toList());
// out put:
// hello
// world
----
peek 操作一般用于不想改变流中元素本身的类型或者只想元素的内部状态时;
而 map 则用于改变流中元素本身类型,即从元素中派生出另一种类型的操作。
这是他们之间的最大区别。
那么 peek 实际中我们会用于哪些场景呢?
比如对 Stream<T> 中的 T 的某些属性进行批处理的时候用 peek 操作就比较合适。
如果我们要从 Stream<T> 中获取 T 的某个属性的集合时用 map 也就最好不过了。
=== concat
Stream 流合并的前提是元素的类型能够一致
[source,java]
----
Stream<Integer> stream = Stream.of(1, 2, 3);
Stream<Integer> another = Stream.of(4, 5, 6);
Stream<Integer> concat = Stream.concat(stream, another);
List<Integer> collect = concat.collect(Collectors.toList());
List<Integer> expected = Lists.list(1, 2, 3, 4, 5, 6);
Assertions.assertIterableEquals(expected, collect);
// 多个流的合并我们也可以使用上面的方式进行“套娃操作”:
Stream.concat(Stream.concat(stream, another), more);
Stream<Integer> stream = Stream.of(1, 2, 3);
Stream<Integer> another = Stream.of(4, 5, 6);
Stream<Integer> third = Stream.of(7, 8, 9);
Stream<Integer> more = Stream.of(0);
Stream<Integer> concat = Stream.of(stream,another,third,more).flatMap(integerStream -> integerStream);
List<Integer> collect = concat.collect(Collectors.toList());
List<Integer> expected = Lists.list(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);
Assertions.assertIterableEquals(expected, collect);
----
== JOSE
JSON Web Token (JWT) 其实目前已经广为软件开发者所熟知了,但是 JOSE (Javascript Object Signing and Encryption) 却鲜有人知道。
=== JOSE 概述
JOSE 是一种旨在提供在各方之间安全传递声明(claims)的方法的规范集。
我们常用的 JWT 就包含了允许客户端访问特定应用下特定资源的声明。
JOSE 制定了一系列的规范来达到此目的。目前该规范还在不断的发展,我们常用的包含以下几个 RFC :
* JWS(RFC 7515) -JSON Web签名,描述生成和处理签名消息
* JWE(RFC 7516) -JSON Web加密,描述了保护和处理加密 消息
* JWK(RFC 7517) -JSON Web密钥,描述 Javascript 对象签名和加密中加密密钥的 格式和处理
* JWA(RFC 7518) -JSON Web算法,描述了 Javascript 对象签名和加密中使用的 加密 算法
* JWT(RFC 7519) -JSON Web令牌,描述以 JSON 编码并由 JWS 或 JWE 保护的声明的表示形式
=== JWT
JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties.
直译过来:JSON Web令牌(JWT)是一种紧凑的URL安全方法,用于表示要在两方之间转移的声明。
也就是说我们通常说的 JWT 实际上是一个对声明进行 JOSE 处理方式的统称。我们之前用的应该叫 JWS(JSON Web Signature)**,
是 **JWT 的一种实现,除了 JWS , JWT 还有另一种实现 JWE(JSON Web Encryption) 。它们之间的关系应该是这样的:
image::9999-appendix/java/003.png[]
=== JWE
JWS 我们就不说了,就是通常我们所说的 JWT。
JWS 仅仅是对声明(claims)作了签名,保证了其不被篡改,但是其 payload(中段负载) 信息是暴露的。
也就是 JWS 仅仅能保证数据的完整性而不能保证数据不被泄露。所以我以前也说过它不适合传递敏感数据。
JWE 的出现就是为了解决这个问题的。具体的可以看下图:
image::9999-appendix/java/004.png[]
从上面可以看出 JWE 的生成非常繁琐,作为 Token 可能比较消耗资源和耗时。用作安全的数据传输途径应该不错。
=== Spring Security jose 相关
这里需要简单提一下 Spring Security 提供了 JOSE 有关的类库 spring-security-oauth2-jose ,你可以使用该类库来使用 JOSE 。
如果 Java 开发者要在 Spring Security 安全框架中使用 OAuth2.0 ,这个类库也是需要研究一下的。
== cron 表达式
cron 表达式是一个字符串,该字符串由 6 个空格分为 7 个域,每一个域代表一个时间含义。 格式如下:
[秒] [分] [时] [日] [月] [周] [年]
通常定义 “年” 的部分可以省略,实际常用的由 前六部分组成
=== cron 各部定义
关于 cron 的各个域的定义如下表格所示:
|===
| 域 | 是否必填 | 值以及范围 | 通配符
| 秒 | 是 | 0-59 | , - * /
| 分 | 是 | 0-59 | , - * /
| 时 | 是 | 0-23 | , - * /
| 日 | 是 | 1-31 | , - * ? / L W
| 月 | 是 | 1-12 或 JAN-DEC | , - * /
| 周 | 是 | 1-7 或 SUN-SAT | , - * ? / L #
| 年 | 否 | 1970-2099 | , - * /
|===
上面列表中值范围还是比较好理解的,但是比较令开发者难以理解的就是通配符,
其实 cron 表达式的难点也在于通配符。我们在下一个章节进行说明:
=== cron 中的通配符
[cols="1,10"]
|===
| 通配符 | 说明
| , | 这里指的是在两个以上的时间点中都执行,如果我们在 “分” 这个域中定义为 8,12,35 ,则表示分别在第8分,第12分 第35分执行该定时任务。
| - | 这个比较好理解就是指定在某个域的连续范围,如果我们在 “时” 这个域中定义 1-6,则表示在1到6点之间每小时都触发一次,用 , 表示 1,2,3,4,5,6
| * | 表示所有值,可解读为 “每”。 如果在“日”这个域中设置 *,表示每一天都会触发。
| ? | 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的8号触发一个操作,但不关心是周几,我们可以这么设置 0 0 0 8 * ?
| / | 在某个域上周期性触发,该符号将其所在域中的表达式分为两个部分,其中第一部分是起始值,除了秒以外都会降低一个单位,比如 在 “秒” 上定义 5/10 表示从 第 5 秒开始 每 10 秒执行一次,而在 “分” 上则表示从 第 5 秒开始 每 10 分钟执行一次。
| L | 表示英文中的LAST 的意思,只能在 “日”和“周”中使用。在“日”中设置,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年), 在“周”上表示周六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在“周”上设置”7L”这样的格式,则表示“本月最后一个周六”
| W | 表示离指定日期的最近那个工作日(周一至周五)触发,只能在 “日” 中使用且只能用在具体的数字之后。若在“日”上置”15W”,表示离每月15号最近的那个工作日触发。假如15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果是 “1W” 就只能往本月的下一个最近的工作日推不能跨月往上一个月推。
| # | 表示每月的第几个周几,只能作用于 “周” 上。例如 ”2#3” 表示在每月的第三个周二。
|===
=== 示例
[source,bash]
----
每隔1分钟执行一次:0 */1 * * * ?
每天22点执行一次:0 0 22 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周周六凌晨3点实行一次:0 0 3 ? * L
在24分、30分执行一次:0 24,30 * * * ?
----
== java 8 Collectors
Collectors 是 Java 8 加入的操作类,位于 java.util.stream 包下。
它会根据不同的策略将元素收集归纳起来,比如最简单常用的是将元素装入 Map、Set、List 等可变容器中。
特别对于 Java 8 Stream Api 来说非常有用。它提供了collect() 方法来对 Stream 流进行终结操作派生出基于各种策略的结果集。
我们就借助于 Stream 来熟悉一下 Collectors 吧。
[source,java]
----
public class Student {
private String id;
private String name;
private LocalDate birthday;
private int age;
private double score;
public Student(String id,String name,LocalDate birthday,int age,double score){
this.id =id;
this.name =name;
this.birthday =birthday;
this.age =age;
this.score =score;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDate getBirthday() {
return birthday;
}
public void setBirthday(LocalDate birthday) {
this.birthday = birthday;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "(" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", birthday=" + birthday +
", age=" + age +
", score=" + score +
')';
}
public static void main(String[] args) {
final List<Student> students =new ArrayList<>();
students.add(new Student("1", "张三", LocalDate.of(2009, Month.JANUARY, 1), 12, 12.123));
students.add(new Student("2", "李四", LocalDate.of(2010, Month.FEBRUARY, 2), 11, 22.123));
students.add(new Student("3", "王五", LocalDate.of(2011, Month.MARCH, 3), 10, 32.123));
students.add(new Student("4", "赵六", LocalDate.of(2011, Month.FEBRUARY, 3), 10, 50.123));
// counting
long count =students.stream().collect(Collectors.counting());
System.out.println("counting: " + count);
// counting: 4
// averagingDouble
double averagingDouble =students.stream().collect(Collectors.averagingDouble(Student::getScore));
System.out.println("averagingDouble: " + averagingDouble);
// averagingDouble: 29.122999999999998
// averagingInt
double averagingInt =students.stream().collect(Collectors.averagingInt(Student::getAge));
System.out.println("averagingInt: " + averagingInt);
// averagingInt: 10.75
// averagingLong
double averagingLong =students.stream().collect(Collectors.averagingLong(Student::getAge));
System.out.println("averagingLong: " + averagingLong);
// averagingLong: 10.75
// summingInt
int summingInt =students.stream().collect(Collectors.summingInt(Student::getAge));
System.out.println("summingInt: " + summingInt);
// summingInt: 43
// summingLong
long summingLong =students.stream().collect(Collectors.summingLong(Student::getAge));
System.out.println("summingLong: " + summingLong);
// summingLong: 43
// summingDouble
double summingDouble =students.stream().collect(Collectors.summingDouble(Student::getAge));
System.out.println("summingDouble: " + summingDouble);
// summingDouble: 43.0
// minBy
Optional<Student> minBy =students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge)));
System.out.println("minBy: " + minBy.get());
// minBy: (id='3', name='王五', birthday=2011-03-03, age=10, score=32.123)
// maxBy
Optional<Student> maxBy =students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge)));
System.out.println("maxBy: " + maxBy.get());
// maxBy: (id='1', name='张三', birthday=2009-01-01, age=12, score=12.123)
// toList
final List<String> idList = students.stream().map(Student::getId).collect(Collectors.toList());
System.out.println("toList: " + idList);
// toList: [1, 2, 3, 4]
// toSet
final Set<String> idSet = students.stream().map(Student::getId).collect(Collectors.toSet());
System.out.println("toSet: " + idSet);
// toSet: [1, 2, 3, 4]
// toCollection
final Collection<String> idTreeSet = students.stream().map(Student::getId).collect(Collectors.toCollection(TreeSet::new));
System.out.println("toCollection: " + idTreeSet);
// toCollection: [1, 2, 3, 4]
// toMap1
final Map<String, Student> map1 = students.stream().collect(Collectors.toMap(Student::getId, Function.identity()));
System.out.println("toMap1: " + map1);
// toMap1: {1=(id='1', name='张三', birthday=2009-01-01, age=12, score=12.123), 2=(id='2', name='李四', birthday=2010-02-02, age=11, score=22.123), 3=(id='3', name='王五', birthday=2011-03-03, age=10, score=32.123), 4=(id='4', name='赵六', birthday=2011-02-03, age=10, score=50.123)}
// toMap2
final Map<String,Student> map2 =students.stream().collect(Collectors.toMap(Student::getId,Function.identity(),(x,y)->x));
System.out.println("toMap2: " + map2);
// toMap2: {1=(id='1', name='张三', birthday=2009-01-01, age=12, score=12.123), 2=(id='2', name='李四', birthday=2010-02-02, age=11, score=22.123), 3=(id='3', name='王五', birthday=2011-03-03, age=10, score=32.123), 4=(id='4', name='赵六', birthday=2011-02-03, age=10, score=50.123)}
// toMap3
final Map<String, String> map3 = students.stream().collect(Collectors.toMap(Student::getId, Student::getName, (x, y) -> x));
System.out.println("toMap3: " + map3);
// toMap3: {1=张三, 2=李四, 3=王五, 4=赵六}
// toMap4 年龄相同的采用分数最高的
final Map<Integer, Student> map4 = students.stream().collect(Collectors.toMap(Student::getAge, Function.identity(), BinaryOperator.maxBy(Comparator.comparing(Student::getScore))));
System.out.println("toMap4: " + map4);
// toMap4: {10=(id='4', name='赵六', birthday=2011-02-03, age=10, score=50.123), 11=(id='2', name='李四', birthday=2010-02-02, age=11, score=22.123), 12=(id='1', name='张三', birthday=2009-01-01, age=12, score=12.123)}
// groupingBy
final Map<Integer, List<Student>> group1 = students.stream().collect(Collectors.groupingBy(Student::getAge));
System.out.println("groupingBy: " + group1);
// groupingBy: {10=[(id='3', name='王五', birthday=2011-03-03, age=10, score=32.123), (id='4', name='赵六', birthday=2011-02-03, age=10, score=50.123)], 11=[(id='2', name='李四', birthday=2010-02-02, age=11, score=22.123)], 12=[(id='1', name='张三', birthday=2009-01-01, age=12, score=12.123)]}
// groupingBy
final Map<Integer, Set<Student>> group2 = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.toSet()));
System.out.println("groupingBy2: " + group2);
// groupingBy2: {10=[(id='3', name='王五', birthday=2011-03-03, age=10, score=32.123), (id='4', name='赵六', birthday=2011-02-03, age=10, score=50.123)], 11=[(id='2', name='李四', birthday=2010-02-02, age=11, score=22.123)], 12=[(id='1', name='张三', birthday=2009-01-01, age=12, score=12.123)]}
// partitioningBy
// partitioningBy 与 groupingBy 的区别在于,partitioningBy 借助 Predicate 断言,可以将集合元素分为 true 和 false 两部分。比如,按照年龄是否大于 11 分组:
final Map<Boolean, List<Student>> partitioningBy = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 11));
System.out.println("partitioningBy: " + partitioningBy);
// partitioningBy: {false=[(id='2', name='李四', birthday=2010-02-02, age=11, score=22.123), (id='3', name='王五', birthday=2011-03-03, age=10, score=32.123), (id='4', name='赵六', birthday=2011-02-03, age=10, score=50.123)], true=[(id='1', name='张三', birthday=2009-01-01, age=12, score=12.123)]}
// partitioningBy
final Map<Boolean, Set<Student>> partitioningBy2 = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 11,Collectors.toSet()));
System.out.println("partitioningBy2: " + partitioningBy2);
// partitioningBy2: {false=[(id='3', name='王五', birthday=2011-03-03, age=10, score=32.123), (id='4', name='赵六', birthday=2011-02-03, age=10, score=50.123), (id='2', name='李四', birthday=2010-02-02, age=11, score=22.123)], true=[(id='1', name='张三', birthday=2009-01-01, age=12, score=12.123)]}
// joining
System.out.println("joining: " + Stream.of("java", "go", "sql").collect(Collectors.joining()));
// joining: javagosql
// joining2
System.out.println("joining2: " + Stream.of("java", "go", "sql").collect(Collectors.joining(",")));
// joining2: java,go,sql
// joining3
System.out.println("joining3: " + Stream.of("java", "go", "sql").collect(Collectors.joining(",","(",")")));
// joining3: (java,go,sql)
// mapping
System.out.println("mapping: " + students.stream().collect(Collectors.mapping(Student::getName, Collectors.toList())));
// mapping: [张三, 李四, 王五, 赵六]
System.out.println("mapping2: " + students.stream().map(Student::getName).collect(Collectors.toList()));
// mapping2: [张三, 李四, 王五, 赵六]
// reducing
System.out.println("reducing: " + students.stream().map(Student::getScore).collect(Collectors.reducing(Double::sum)));
// reducing: Optional[116.49199999999999]
// reducing2
System.out.println("reducing2: " + students.stream().map(Student::getScore).collect(Collectors.reducing(10.0,Double::sum)));
// reducing2: 126.49199999999999
// reducing3
System.out.println("reducing3: " + students.stream().collect(Collectors.reducing(0.0, Student::getScore, Double::sum)));
// reducing3: 116.49199999999999
// reducing4
System.out.println("reducing4: " + students.stream().map(Student::getScore).reduce(0.0,Double::sum));
// reducing4: 116.49199999999999
}
}
----
== Java Collection 移除元素
操作集合是一个 Java 编程人员几乎每天都在重复的事情。
=== for 循环并不一定能从集合中移除元素
[source,java]
----
for (String server : servers) {
if (server.startsWith("F")) {
servers.remove(server);
}
}
----
使用传统的 foreach 循环移除 F 开头的假服务器,但是你会发现这种操作引发了 ConcurrentModificationException 异常。
难道 for 循环就不能移除元素了吗?当然不是!我们如果能确定需要被移除的元素的索引还是可以的。
[source,java]
----
for (int i = 0; i < servers.size(); i++) {
if (servers.get(i).startsWith("F")) {
servers.remove(i);
}
}
----
但是这种方式我目前只演示了 ArrayList,其它的类型并没有严格测试,留给你自己探索。
=== 迭代器 Iterator 可以删除集合中的元素
在传统方式中我们使用 Iterator 是可以保证删除元素的:
[source,java]
----
Iterator<String> iterator = servers.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.startsWith("F")) {
iterator.remove();
}
}
----
=== 遍历删除元素的缺点
* 我们需要遍历集合的每一个元素并对它们进行断言,哪怕你删除一个元素。
* 尽管我们可以通过迭代的方式删除特定的元素,但是操作繁琐,根据集合类型的不同有潜在的 ConcurrentModificationException 异常。
* 根据数据结构的不同,删除元素的时间复杂度也大大不同。比如数组结构的 ArrayList 在删除元素的速度上不如链表结构的 LinkedList。
=== 新的集合元素删除操作
Java 8 提供了新的集合操作 API 和 Stream 来帮助我们解决这个问题。
==== Collection.removeIf()
新的 Collection Api removeIf(Predicate<? super E> filter) 。
该 Api 提供了一种更简洁的使用 Predicate (断言)删除元素的方法,于是我们可以更加简洁的实现开始的需求:
[source,java]
----
servers.removeIf(s-> s.startsWith("F"));
----
=== 通过 filter 断言实现
我们可以使用 Stream 的 filter 断言。filter 断言会把符合断言的流元素汇集成一个新的流,然后归纳起来即可,于是我们可以这么写:
[source,java]
----
List<String> newServers = servers.stream().filter(s -> !s.startsWith("F")).collect(Collectors.toList());
----
这个优点上面已经说了不会影响原始数据,生成的是一个副本。缺点就是可能会有内存占用问题。