SpringBoot2 整合 WebSocket 简单实现聊天室功能

一个很简单的 Demo,可以用 WebSocket 实现简易的聊天室功能

一反常态,我们先来看一下效果,如下:

嫌麻烦的可以直接去我的 GitHub 获取完整无码 Demo。

概述

  • WebSocket 是什么?

WebSocket 是一种网络通信协议。RFC6455 定义了它的通信标准。

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

  • 为什么需要 WebSocket ?

了解计算机网络协议的人,应该都知道:HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。

这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步 JavaScript 和 XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

  • WebSocket 如何工作?

Web浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。由于 WebSockets 连接长期存在,与典型的 HTTP 连接不同,对服务器有重要的影响。

基于多线程或多进程的服务器无法适用于 WebSockets,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接。任何实际的 WebSockets 服务器端实现都需要一个异步服务器。

实现

首先去 start.spring.io 快速下载一个 springboot Demo,记得选中 Websocket 依赖。

然后将项目导入你的 IDE 中。

新建一个 config 类用来注册我们的 websocket bean。

我的是 WebSocketConfig.java :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class WebSocketConfig {

/**
* 自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}

}

该加上的注解别忘了加,项目启动时 springboot 会自动去扫描注解的类。

然后是消息接收处理 websocket 连接、关闭等钩子。

MyWebSocket.java :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
@ServerEndpoint(value = "/websocket")
@Component
public class MyWebSocket {

// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;

// concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();

// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this); // 加入set中
addOnlineCount(); // 在线数加1
System.out.println("有新连接加入!当前在线人数为 : " + getOnlineCount());
try {
sendMessage("您已成功连接!");
} catch (IOException e) {
System.out.println("IO异常");
}
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); // 从set中删除
subOnlineCount(); // 在线数减1
System.out.println("有一连接关闭!当前在线人数为 : " + getOnlineCount());
}

/**
* 收到客户端消息后调用的方法
*
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);

// 群发消息
for (MyWebSocket item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}

public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
// this.session.getAsyncRemote().sendText(message);
}

/**
* 群发自定义消息
*/
public static void sendInfo(String message) throws IOException {
for (MyWebSocket item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
continue;
}
}
}

public static synchronized int getOnlineCount() {
return onlineCount;
}

public static synchronized void addOnlineCount() {
MyWebSocket.onlineCount++;
}

public static synchronized void subOnlineCount() {
MyWebSocket.onlineCount--;
}
}

关键就是@OnOpen@OnClose等这几个注解了。每个对象有着各自的 session,其中可以存放个人信息。当收到一个客户端消息时,往所有维护着的对象循环 send 了消息,这就简单实现了聊天室的聊天功能了。

其中 websocket session 发送文本消息有两个方法:getAsyncRemote()和 getBasicRemote()。 getAsyncRemote 是非阻塞式的,getBasicRemote 是阻塞式的。

然后我用了 Controller 来简单跳转测试页面,也可以直接访问页面。

InitController.java :

1
2
3
4
5
6
7
8
9
@Controller
public class InitController {

@RequestMapping("/websocket")
public String init() {
return "websocket.html";
}

}

websocket.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My WebSocket Test</title>
</head>
<body>

Welcome<br/>
<input id="text" type="text" />
<button onclick="send()">Send</button>
<button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>

</body>

<script type="text/javascript">

var websocket = null;

//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
websocket = new WebSocket("ws://localhost:8080/websocket");
}
else{
alert('Not support websocket')
}

//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};

//连接成功建立的回调方法
websocket.onopen = function(event){
setMessageInnerHTML("open");
}

//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}

//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}

//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}

//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}

//关闭连接
function closeWebSocket(){
websocket.close();
}

//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>

要注意,这里没有用到任何模板引擎,所有直接把 websocket.html 放在 static 文件夹下就可以访问了。

所有的这些搞好就可以运行了,一个简单的效果就能出来。

End.

参考


Java 运行时(RUNTIME)注解详解

参考博文:Java注解解析-运行时注解详解(RUNTIME)

个人博客:DoubleFJ の Blog

整理测试后并附上完整代码


注解定义

注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明 。如果要对于元数据的作用进行分类,还没有明确的定义,不过我们可以根据它所起的作用,注解不会改变编译器的编译方式,也不会改变虚拟机指令执行的顺序,它更可以理解为是一种特殊的注释,本身不会起到任何作用,需要工具方法或者编译器本身读取注解的内容继而控制进行某种操作。大致可分为三类:

  • 编写文档:通过代码里标识的元数据生成文档。
  • 代码分析:通过代码里标识的元数据对代码进行分析。
  • 编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查。

注解用途

因为注解可以在代码编译期间帮我们完成一些复杂的准备工作,所以我们可以利用注解去完成我们的一些准备工作。可以在编译期间获取到注解中的内容以便之后的数据处理,完全可以写好逻辑代码就等着编译时将值传入。

注解详解

Java JDK 中包含了三个注解分别为 @Override(校验格式),@Deprecated:(标记过时的方法或者类),@SuppressWarnnings(注解主要用于抑制编译器警告)等等。JDK 1.8 之后有新增了一些注解像 @FunctionalInterface()这样的,对于每个注解的具体使用细节这里不再论述。我们来看一下 @Override 的源码。

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

通过源代码的阅读我们可以看出生命注解的方式为 @interface,每个注解都需要不少于一个的元注解的修饰,这里的元注解其实就是修饰注解的注解,可以理解成最小的注解单位吧。下面详细的看下每个注释注解的意义吧:

@Target

说明了 Annotation 所修饰的对象范围,也就是我们这个注解是用在那个对象上面的:Annotation 可被用于 packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在 Annotation 类型的声明中使用了target可更加明晰其修饰的目标。以下属性是多选状态,我们可以定义多个注解作用域,比如:

1
2
3
4
5
6
7
8
9
@Target({ElementType.METHOD,ElementType.FIELD}),单个的使用 @Target(ElementType.FIELD)。    
(1).CONSTRUCTOR:构造方法声明。
(2).FIELD:用于描述域也就是类属性之类的,字段声明(包括枚举常量)。
(3).LOCAL_VARIABLE:用于描述局部变量。
(4).METHOD:用于描述方法。
(5).PACKAGE:包声明。
(6).PARAMETER:参数声明。
(7).TYPE:类、接口(包括注释类型)或枚举声明 。
(8).ANNOTATION_TYPE:注释类型声明,只能用于注释注解。

官方解释:指示注释类型所适用的程序元素的种类。如果注释类型声明中不存在 Target 元注释,则声明的类型可以用在任一程序元素上。如果存在这样的元注释,则编译器强制实施指定的使用限制。 例如,此元注释指示该声明类型是其自身,即元注释类型。它只能用在注释类型声明上:

1
2
3
@Target(ElementType.ANNOTATION_TYPE)
public @interface MetaAnnotationType {
}

此元注释指示该声明类型只可作为复杂注释类型声明中的成员类型使用。它不能直接用于注释:

1
2
3
4
@Target({}) 
public @interface MemberType {
...
}

这是一个编译时错误,它表明一个 ElementType 常量在 Target 注释中出现了不只一次。例如,以下元注释是非法的:

1
2
3
4
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.FIELD})
public @interface Bogus {
...
}

@Retention

定义了该 Annotation 被保留的时间长短:某些 Annotation 仅出现在源代码中,而被编译器丢弃;而另一些却被编译在 class 文件中;编译在 class 文件中的 Annotation 可能会被虚拟机忽略,而另一些在 class 被装载时将被读取(请注意并不影响 class 的执行,因为 Annotation 与 class 在使用上是被分离的)。使用这个 meta-Annotation 可以对 Annotation 的“生命周期”限制。来源于 java.lang.annotation.RetentionPolicy 的枚举类型值:

1
2
3
(1).SOURCE:在源文件中有效(即源文件保留)编译成class文件将舍弃该注解。 
(2).CLASS:在class文件中有效(即class保留) 编译成dex文件将舍弃该注解。
(3).RUNTIME:在运行时有效(即运行时保留) 运行时可见。

也就是说注解处理器能处理这三类的注解,我们通过反射的话只能处理 RUNTIME 类型的注解。

官方解释:指示注释类型的注释要保留多久。如果注释类型声明中不存在 Retention 注释,则保留策略默认为 RetentionPolicy.CLASS。只有元注释类型直接用于注释时,Target 元注释才有效。如果元注释类型用作另一种注释类型的成员,则无效。

@Documented

指示某一类型的注释将通过 javadoc 和类似的默认工具进行文档化。应使用此类型来注释这些类型的声明:其注释会影响由其客户端注释的元素的使用。如果类型声明是用 Documented 来注释的,则其注释将成为注释元素的公共 API 的一部。Documented 是一个标记注解,没有成员。

@Inherited

元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了 @Inherited 修饰的 annotation 类型被用于一个 class ,则这个 annotation 将被用于该 class 的子类。 注意:@Inherited annotation 类型是被标注过的 class 的子类所继承。类并不从它所实现的接口继承 annotation,方法并不从它所重载的方法继承 annotation。当 @Inherited annotation 类型标注的 annotation 的 Retention 是 RetentionPolicy.RUNTIME,则反射 API 增强了这种继承性。如果我们使用 java.lang.reflect 去查询一个 @Inherited annotation 类型的 annotation 时,反射代码检查将展开工作:检查 class 和其父类,直到发现指定的 annotation 类型被发现,或者到达类继承结构的顶层。

官方解释:指示注释类型被自动继承。如果在注释类型声明中存在 Inherited 元注释,并且用户在某一类声明中查询该注释类型,同时该类声明中没有此类型的注释,则将在该类的超类中自动查询该注释类型。此过程会重复进行,直到找到此类型的注释或到达了该类层次结构的顶层 (Object) 为止。如果没有超类具有该类型的注释,则查询将指示当前类没有这样的注释。

注意,如果使用注释类型注释类以外的任何事物,此元注释类型都是无效的。还要注意,此元注释仅促成从超类继承注释;对已实现接口的注释无效。

@Repeatable

Repeatable可重复性,JDK 1.8 新特性,其实就是把标注的注解放到该元注解所属的注解容器里面。以下是一个完整 Demo :

MyTag.java : 自定义注解

1
2
3
4
5
6
7
8
9
10
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.CLASS)
@Repeatable(MyCar.class) // 注解可重复使用 将 MyTag 作为 MyCar 中 value 的值,即放入了 MyCar 注解容器中
public @interface MyTag {

// default 后为其默认值
String name() default "";

int size() default 0;
}

MyCar.java : MyTag 的注解容器

1
2
3
4
5
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCar {
MyTag[] value(); // 注解里面属性的返回值是 Tag 注解的数组,即 MyTag 注解容器
}

Car.java : 测试实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Car {

private String name;

private int size;

public Car(String name, int size) {
this.name = name;
this.size = size;
}

// 省略了 set get

@Override
public String toString() {
return "Car [name=" + name + ", size=" + size + "]";
}

}

AnnotationCar.java : 最关键的注解处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* Car 注解处理类
*
* @author ffj
*
*/
public class AnnotationCar {

private AnnotationCar() {
}

private static volatile AnnotationCar annotationCar;

public static AnnotationCar instance() {
// 单例 双重检查
if (annotationCar == null) {
synchronized (AnnotationCar.class) {
if (annotationCar == null) {
annotationCar = new AnnotationCar();
}
}
}
return annotationCar;
}

public void inject(Object o) {
Class<?> aClass = o.getClass();
Field[] declaredFields = aClass.getDeclaredFields(); // 获取所有声明的字段
for (Field field : declaredFields) {
if (field.getName().equals("car")) {
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation : annotations) { // 注解的对象类型
Class<? extends Annotation> className = annotation.annotationType();
System.out.println("className :" + className);
}
MyCar annotation = field.getAnnotation(MyCar.class); // MyCar 类型输出
MyTag[] tags = annotation.value();
for (MyTag tag : tags) {
System.out.println("name :" + tag.name());
System.out.println("size :" + tag.size());
try {
field.setAccessible(true); // 类中的成员变量为 private,故必须进行此操作
field.set(o, new Car(tag.name(), tag.size())); // 重新赋值对象
System.out.println("注解对象为 :" + field.get(o).toString());
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
}

AnnotationTest.java : 测试运行类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AnnotationTest {

@MyTag(name = "野马", size = 222)
@MyTag(name = "兰博基尼", size = 333)
Car car;

public void printAnno() {
AnnotationCar.instance().inject(this);
}

public static void main(String[] args) {
new AnnotationTest().printAnno();

}
}

最终运行结果便是:

1
2
3
4
5
6
7
className :interface com.tonglei.test.MyCar
name :野马
size :222
注解对象为 :Car [name=野马, size=222]
name :兰博基尼
size :333
注解对象为 :Car [name=兰博基尼, size=333]

总结

不知这样可否清晰。这里运行时注解就是在程序编译时扫描到类下的字段上的注解,就可以知道该字段上的元注解的类型,进而将注解中元素的值得到进行你自己的业务操作。这个 Demo 是利用了 @Repeatable 注解,不用该注解直接用元注解 RUNTIME 类型也是一样的,只要注解类逻辑稍微修改即可。结合这个可以更好地理解了反射和注解以及 class 的注入。


书香八月(2018)
又是一月悄悄溜走,蓦然回首,措不及防,顿有种惊魂失措之意

昨日刚看了些沈复写的《浮生六记》——浮生若梦,为欢几何

多么美好令人羡艳的爱情,平淡如水,举案齐眉,彼此心心想通

可以想象沈复当年回忆并写下这些往事时是种什么样的心情

俱往矣。


  • 如今游戏已没有当年学生时代那么为之疯狂,想想当时一时兴起就会叫上几个同学坐上人力车或是残疾三轮摩托(我们老家称之田鸡蹦),照着熟之不能再熟的小路在那黑网吧相聚。为什么我说是黑网吧,自然是因为那时我们都还未成年,只需报个身份证即可入座。这一坐往往就是个通宵。

  • 最近趁着开学季,有些APP上图书优惠幅度较大,便又是忍不住入手了几本,其中自然有专业书籍也有些闲暇时候用于静心的读物。

  • 可能是我太害怕孤独故连我早已习惯了孤独,都不知。犹记早在小学两三年级我便时常晚上一人在家睡觉,但我已不记得那时我的心中所想,只记得往往是被子过头,闷闷入睡。儿时的睡眠质量倒不像如今,睡得早也睡得着,早上自己也能早起,偶尔自做一碗红烧牛肉面用于饱腹,或是去街上花个一元买个大饼。

  • 依稀还是能记得儿时的事情,不过实在太久远的我便一脸茫然。就如家人常常饭桌上提起的那件“大事”:那时我们一家还在义乌,爸妈还在那做生意,我可能三岁,或是更小吧。有一日我和我姐(亲姐大我四岁)去摊位上,不知怎的当我姐回头看我时,我已不在她视线中。听闻全家跑遍了整个商场寻我,最终还是我姐在商场顶楼寻到了我,我那时白白胖胖一双小短腿,殊不知怎的能一步一步迈着上了四楼,真可谓神乎其技啊。还有一事便是我在托儿所抄起凳子准备与老师干架。由于父母忙于生意,无再有精力将我带在身边,而我那时也已三岁,到了该读书学习的年纪了。于是就将我安排进了托儿所与我姐一个学校,只不过我是小班,她在大班。每每到了午饭时间我便要拿着我的家伙跑到姐姐的班级去要我姐喂我吃(听着傲娇也是从小就开始的啊),姐姐无奈也只得喂我。然老师不从,要将我“赶回”自己所在班级,我那时也是有脾气(果真性子在小时候就已经定型了啊)顺手就抄起身下的小板凳准备扔向该老师,做好了大干一场的风范。次次谈论此,我以手掩面,不语不语。


  • 08年大雪,雪灾。十年后的18年,也是多灾多难。

  • 如今的网络舆论力量不容小觑,听闻国家也是准备出政策实名发表舆论,不知会如何。

  • 以前人们没有手机没有网络,两耳不闻窗外事,生活无忧,也无需顾此失彼,做一件事便能专心做到极致。网络的力量使人们不知名有了一种使命感、一种自我重要感,便会觉得我不能缺,缺我不可。想太多,心累,吃好睡好,长生不老。

  • 前几日猛然想起之前听闻爷爷的死因是前列腺癌,而前列腺癌遗传的概率极高,不知我会不会中招,忐忑不安忐忑不安啊。人生不尽人意之事多矣。

  • 魔都寸土寸金,在小房间里无地安置写字桌,便买了个小桌放置床头用于平日里书写干活。人生不会一直如此,美妙人生须有美丽心人。


  • 恍然又快一年,去年元旦同大学室友游逛了美丽苏州,除了遍地黄牛与无良卖家,一切还是如此美好。尤是夜晚的街道显得古韵十足。

  • 前几日我们便定了中秋相聚游玩之地,本是准备花个大价钱去重庆探一探辣妹底子,可还是奈不住干瘪的钱袋子,于是便定了去杭州。计划是先去杭州再去横店玩耍,若时间有余,还可去我家静坐一番,亲自入园采摘葡萄也是极好(浙江浦江巨峰葡萄远近闻名,我家尚有几亩,养老足矣)。


平日主还是务实基础,提升自我价值。

以上便是八月之总结。写于 戊戌年 狗年 庚申月 戊戌日。抗战胜利日。


Java中的数组拷贝以及对象Bean拷贝

数组拷贝方式

直接先贴出测试代码:

Student :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.tonglei.test;

/**
* 学生实体测试类
*
* @author ffj
*
*/
public class Student {

private int age;
private int height;
private String sex;

public Student(int age, int height, String sex) {
this.age = age;
this.height = height;
this.sex = sex;
}

// 省略set、get方法

@Override
public String toString() {

return "Student :[" + this.age + " ," + this.height + " ," + this.sex + "]";
}
}

运行测试 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static void main(String[] args) {

Student[] stu = new Student[3];
stu[0] = new Student(11, 110, "男");
stu[1] = new Student(12, 120, "女");
stu[2] = new Student(13, 130, "嬲");
System.out.println("stu length :" + stu.length);
System.out.println("stu :" + Arrays.toString(stu));
System.out.println("stu address :" + stu);

System.out.println("<------------------------------------->");

// Arrays.copyOf 原数组 新数组长度
Student[] arrayCopyStu = Arrays.copyOf(stu, stu.length + 1);
arrayCopyStu[stu.length] = new Student(14, 140, "奻");
System.out.println("arrayCopyStu length :" + arrayCopyStu.length);
System.out.println("arrayCopyStu :" + Arrays.toString(arrayCopyStu));
System.out.println("arrayCopyStu address :" + arrayCopyStu);

System.out.println("<------------------------------------->");

// System.arraycopy 原数组 原数组拷贝起始地址 目标数组 目标数组拷贝起始地址 拷贝长度
Student[] systemCopyStu = new Student[4];
System.arraycopy(stu, 0, systemCopyStu, 0, stu.length);
System.out.println("systemCopyStu length :" + systemCopyStu.length);
System.out.println("systemCopyStu :" + Arrays.toString(systemCopyStu));
System.out.println("systemCopyStu address :" + systemCopyStu);

System.out.println("<------------------------------------->");
System.out.println("<---------改变了原数组第一个对象的age------->");
System.out.println("<------------------------------------->");

// 改变原数组的数据
stu[0].setAge(99);
System.out.println("stu :" + Arrays.toString(stu));
System.out.println("arrayCopyStu :" + Arrays.toString(arrayCopyStu));
System.out.println("systemCopyStu :" + Arrays.toString(systemCopyStu));

/**
* 总结:Arrays.copyOf 和 System.arraycopy 都可将结果生成一个新数组,
* 不过两者的区别在于,Arrays.copyOf()不仅仅只是拷贝数组中的元素,在拷贝元素时,会创建一个新的数组对象。而System.arrayCopy只拷贝已经存在数组元素。
* Arrays.copyOf()的源码中可知其底层还是调用了System.arrayCopyOf()方法
* 当修改了原数组中对象的属性时目标数组中也随之改变,故两者都是地址引用,其中元素指向的还是原数组的地址
*/
}

测试运行结果 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
stu length :3
stu :[Student :[11 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲]]
stu address :[Lcom.tonglei.test.Student;@70dea4e
<------------------------------------->
arrayCopyStu length :4
arrayCopyStu :[Student :[11 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲], Student :[14 ,140 ,奻]]
arrayCopyStu address :[Lcom.tonglei.test.Student;@5c647e05
<------------------------------------->
systemCopyStu length :4
systemCopyStu :[Student :[11 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲], null]
systemCopyStu address :[Lcom.tonglei.test.Student;@33909752
<------------------------------------->
<---------改变了原数组第一个对象的age------->
<------------------------------------->
stu :[Student :[99 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲]]
arrayCopyStu :[Student :[99 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲], Student :[14 ,140 ,奻]]
systemCopyStu :[Student :[99 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲], null]

Arrays.copyOf

Arrays.copyOf方法返回一个新数组,不仅仅只是拷贝原数组中的元素会创建一个新的数组对象

上述测试结果 :

1
2
3
4
5
<------------------------------------->
arrayCopyStu length :4
arrayCopyStu :[Student :[11 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲], Student :[14 ,140 ,奻]]
arrayCopyStu address :[Lcom.tonglei.test.Student;@5c647e05
<------------------------------------->

可以看出,copy原数组中元素并扩容了一长度,同时arrayCopyStu[stu.length] = new Student(14, 140, "奻");对新增元素赋值,从而打印出的便是上述内容。

System.arraycopy

System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)方法只拷贝已经存在数组元素,参数依次为原数组、原数组拷贝起始地址、目标数组、目标数组拷贝起始地址、拷贝长度。

上述测试结果 :

1
2
3
4
5
<------------------------------------->
systemCopyStu length :4
systemCopyStu :[Student :[11 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲], null]
systemCopyStu address :[Lcom.tonglei.test.Student;@33909752
<------------------------------------->

可见,新数组只从原数组中拷贝了存在的指定个数元素并可以指定拷贝到目标数组中。
Arrays.copyOf()的源码中可知其底层还是调用了System.arrayCopyOf()方法。

进而探究

两种均可对数组进行copy,但是Arrays.copyOf()System.arrayCopyOf()两种方法所copy生成的新的数组对象中的元素对象到底是新的还是依旧指向原先的呢?来一探究竟!

然而细心的同学已经心领神会..测试代码早早贴在了上面!

1
2
3
4
5
6
7
8
9
System.out.println("<------------------------------------->");
System.out.println("<---------改变了原数组第一个对象的age------->");
System.out.println("<------------------------------------->");

// 改变原数组的数据
stu[0].setAge(99);
System.out.println("stu :" + Arrays.toString(stu));
System.out.println("arrayCopyStu :" + Arrays.toString(arrayCopyStu));
System.out.println("systemCopyStu :" + Arrays.toString(systemCopyStu));

这段代码我改变了原数组中第一个stu对象元素中的age属性值,我将其改为了99。

结果显示为 :

1
2
3
4
5
6
<------------------------------------->
<---------改变了原数组第一个对象的age------->
<------------------------------------->
stu :[Student :[99 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲]]
arrayCopyStu :[Student :[99 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲], Student :[14 ,140 ,奻]]
systemCopyStu :[Student :[99 ,110 ,男], Student :[12 ,120 ,女], Student :[13 ,130 ,嬲], null]

显而易见了,不管是原数组还是两个copy的数组,其值均由原先的11变成了99。

总结:当修改了原数组中对象的属性时目标数组中也随之改变,故两者都是地址引用,其中元素指向的还是原数组的地址。

对象拷贝方式

测试类还是上面的Student,只不过我又新增了一个Teacher类来方便测试,其结构与Student一致。

Teacher :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.tonglei.test;

/**
* 教师实体测试类
*
* @author ffj
*
*/
public class Teacher {

private int age;
private int height;
private String sex;

public Teacher() {
}

public Teacher(int age, int height, String sex) {
this.age = age;
this.height = height;
this.sex = sex;
}

// 省略了set、get方法

@Override
public String toString() {

return "Teacher :[" + this.age + " ," + this.height + " ," + this.sex + "]";
}
}

测试的初始代码 :

1
2
Student stu = new Student(18, 180, "男");
Teacher tea = new Teacher();

SpringBeanUtils

BeanUtils.copyProperties(Object source, Object target) 该方法为spring中方法,故需要导入相应jar包。参数依次为:源数组、目标数组。(方法间参数有差异,需注意!)

测试代码 :

1
2
BeanUtils.copyProperties(stu, tea);
System.out.println(tea);

在运行测试之前,我先将原先Teacher中的age字段属性类型稍稍修改了下。改为了private String age,结果为Could not copy properties from source to target; nested exception is java.lang.IllegalArgumentException,抛了个异常错误。而后我将其类型改回,输出Teacher :[18 ,180 ,男]故:拷贝的目标数组与源数组中的元素对象其属性名称一样,类型就必须一致,否则会报错

commonsBeanUtils

该包下有两个copy方法:BeanUtils.copyProperties(Object dest, Object orig)PropertyUtils.copyProperties(Object dest, Object orig),同样使用其方法前需要导入对应org.apache.commonsjar包。

BeanUtils.copyProperties

BeanUtils.copyProperties(Object dest, Object orig)其中参数分别为:目标数组、源数组。(对了,跟spring中方法参数顺序不一样)

测试代码 :

1
2
3
System.out.println(stu);
org.apache.commons.beanutils.BeanUtils.copyProperties(tea, stu);
System.out.println(tea);

还是老步骤:在运行测试之前,我先将原先Teacher中的age字段属性类型稍稍修改了下。改为了private String age,结果显示为:

1
2
Student :[18 ,180 ,男]
Teacher :[18 ,180 ,男]

我再将Teacher中的sex字段改为了private int sex,结果显示为:

1
2
Student :[18 ,180 ,男]
Teacher :[18 ,180 ,0]

由此可知:拷贝的目标数组与源数组中的元素对象其属性名称一样,类型不一致则会强转该值,若是强转不了就为初始值

PropertyUtils.copyProperties

PropertyUtils.copyProperties(Object dest, Object orig)其中参数分别为:目标数组、源数组。

测试代码 :

1
2
3
System.out.println(stu);
PropertyUtils.copyProperties(tea, stu);
System.out.println(tea);

一样,在运行测试之前,我先将原先Teacher中的age字段属性类型稍稍修改了下。改为了private String age,结果显示为:Cannot invoke com.tonglei.test.Teacher.setAge on bean class 'class com.tonglei.test.Teacher' - argument type mismatch - had objects of type "java.lang.Integer" but expected signature "java.lang.String",报错抛出类型不匹配异常信息,再将类型改回,输出:

1
2
Student :[18 ,180 ,男]
Teacher :[18 ,180 ,男]

故:拷贝的目标数组与源数组中的元素对象其属性名称一样,类型就必须一致,否则会报错(与SpringBeanUtils差不多不过报错信息更为详细,纯粹这次简单测试个人体会)

参考博文


具体方法具体场景各自选择。 END.


Java8集合框架新增方法汇总

本想着自己总结记录汇总下这些新方法的,结果一搜,一点,乖乖,真的是太详细了,然后我就搬过来了。。原文地址

其中当然也涉及Lambda表达式,简化代码,何乐不为。


Collection

forEach()

该方法的签名为void forEach(Consumer<? super E> action),作用是对容器中的每个元素执行action指定的动作,其中Consumer是个函数接口,里面只有一个待实现方法void accept(T t)(后面我们会看到,这个方法叫什么不重要,甚至不需要记住它的名字)。

需求:假设有一个字符串列表,需要打印出其中所有长度大于3的字符串

Java7及以前我们可以用增强的for循环实现:

1
2
3
4
5
6
// 使用曾强for循环迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(String str : list) {
if(str.length() > 3)
System.out.println(str);
}

现在使用forEach()方法结合匿名内部类,可以这样实现:

1
2
3
4
5
6
7
8
9
// 使用forEach()结合匿名内部类迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(new Consumer<String>() {
@Override
public void accept(String str) {
if(str.length() > 3)
System.out.println(str);
}
});

上述代码调用forEach()方法,并使用匿名内部类实现Comsumer接口。到目前为止我们没看到这种设计有什么好处,但是不要忘记Lambda表达式,使用Lambda表达式实现如下:

1
2
3
4
5
6
// 使用forEach()结合Lambda表达式迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach( str -> {
if(str.length() > 3)
System.out.println(str);
});

上述代码给forEach()方法传入一个Lambda表达式,我们不需要知道accept()方法,也不需要知道Consumer接口,类型推导帮我们做了一切

removeIf()

该方法签名为boolean removeIf(Predicate<? super E> filter),作用是删除容器中所有满足filter指定条件的元素,其中Perdicate是一个函数接口,里面只有一个待实现方法boolean test(T t),同样的这个方法的名字根本不重要,因为用的时候不需要书写这个名字。

需求:假设有一个字符串列表,需要删除其中所有长度大于3的字符串。

我们知道如果需要在迭代过程中对容器进行删除操作必须使用迭代器,否则会抛出ConcurrentModificationException,所以上述任务传统的写法是:

1
2
3
4
5
6
7
// 使用迭代器删除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while(it.hasNext()) {
if(it.next().length() > 3) // 删除长度大于3的元素
it.remove();
}

现在使用removeIf()方法结合匿名内部类,我们可以这样实现:

1
2
3
4
5
6
7
8
// 使用removeIf()结合匿名名内部类实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(new Predicate<String>() { // 删除长度大于3的元素
@Override
public boolean test(String str) {
return str.length() > 3;
}
});

上述代码使用removeIf()方法,并使用匿名内部类实现Precicate接口。相信你已经想到用Lambda表达式该怎么写了:

1
2
3
// 使用removeIf()结合Lambda表达式实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(str -> str.length() > 3); // 删除长度大于3的元素

使用Lambda表达式不需要记住Predicate接口名,也不需要记住test()方法名,只需要知道此处需要返回一个boolean类型的Lambda表达式就行了。

replaceAll()

该方法签名为void replaceAll(UnaryOperator<E> operator),作用是对每个元素执行operator指定的操作,并用操作结果来替换原来的元素。其中UnaryOperator是一个函数接口,里面只有一个待实现函数T apply(T t)

需求:假设有一个字符串列表,将其中所有长度大于3的元素转换成大写,其余元素不变。

Java7及之前似乎没有优雅的办法:

1
2
3
4
5
6
7
// 使用下标实现元素替换
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for (int i=0; i<list.size(); i++) {
String str = list.get(i);
if (str.length() > 3)
list.set(i, str.toUpperCase());
}

使用replaceAll()方法结合匿名内部类可以实现如下:

1
2
3
4
5
6
7
8
9
10
// 使用匿名内部类实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(new UnaryOperator<String>() {
@Override
public String apply(String str) {
if(str.length() > 3)
return str.toUpperCase();
return str;
}
});

上述代码调用replaceAll()方法,并使用匿名内部类实现UnaryOperator接口。我们知道可以用更为简洁的Lambda表达式实现:

1
2
3
4
5
6
7
// 使用Lambda表达式实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
if(str.length() > 3)
return str.toUpperCase();
return str;
});

sort()

该方法定义在List接口中,方法签名为void sort(Comparator<? super E> c),该方法根据c指定的比较规则对容器元素进行排序。Comparator接口我们并不陌生,其中有一个方法int compare(T o1, To2)需要实现,显然该接口是个函数接口。

需求:假设有一个字符串列表,按照字符串长度增序对元素排序。

由于Java7以及之前sort()方法在Collections工具类,所以代码要这样写:

1
2
3
4
5
6
7
8
// Collections.sort()方法
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String str1, String str2) {
return str1.length() - str2.length();
}
});

现在可以直接使用List.sort()方法,结合Lambda表达式,可以这样写:

1
2
3
// List.sort()方法结合Lambda表达式
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length() - str2.length());

spliterator()

方法签名为Spliterator<E> spliterator(),该方法返回容器的可拆分迭代器。从名字来看该方法跟iterator()方法有点像,我们知道Iterator是用来迭代容器的,Spliterator也有类似作用,但二者有如下不同:

  • Spliterator即可以像Iterator那样逐个迭代,也可以批量迭代。批量迭代可以降低迭代的开销。
  • Spliterator是可以拆分的,一个Spliterator可以通过调用Spliterator<T> trySplit()方法来尝试分成两个。一个是this,另一个是新返回的那个,这两个迭代器代表的元素没有重叠。

可通过(多次)调用Spliterator.trySplit()方法来分解负载,以便多线程处理。

stream() / parallelStream()

stream()parallelStream()分别返回容器的stream视图表示,不同之处在于parallelStream()返回并行的Stream。**Stream是Java函数式编程的核心类,我之前也有文章详细介绍过**。

Map

相比CollectionMap中加入了更多的方法,我们以HashMap为例来逐一探秘。

forEach()

该方法签名为void forEach(BiConsumer<? super K, ? super V> action),作用是Map中每个映射执行action指定的操作,其中BiConsumer是一个函数接口,里面有一个待实现方法void accept(T t, U u)BinConsumer接口名字和accept()方法名字都不重要,不须记住。

需求:假设有一个数字到对应英文单词的Map,请输出Map中的所有映射关系

Java7以及之前经典的代码如下:

1
2
3
4
5
6
7
8
// Java7以及之前迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}

使用Map.forEach() 方法,结合匿名内部类,实现如下:

1
2
3
4
5
6
7
8
9
10
11
// 使用forEach()结合匿名内部类迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach(new BiConsumer<Integer, String>() {
@Override
public void accept(Integer k, String v) {
System.out.println(k + "=" + v);
}
});

上述代码调用forEach()方法,并使用匿名内部类实现BiConsumer接口。当然,实际场景中没人使用匿名内部类写法,因为有Lambda表达式:

1
2
3
4
5
6
7
// 使用forEach()结合Lambda表达式迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
}

getOrDefault()

该方法跟Lambda表达式没关系,但是很有用。方法签名为V getOrDefault(Object key, V defaultValue),作用是**按照给定的key查询Map中对应的value,如果没有找到则返回defaultValue**。使用该方法程序员可以省去查询指定键值是否存在的麻烦。

需求:假设有一个数字到对应英文单词的Map,输出4对应的英文单词,如果不存在则输出NoValue

1
2
3
4
5
6
7
8
9
10
11
12
13
// 查询Map中指定的值,不存在时使用默认值
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
// Java7以及之前做法
if (map.containsKey(4)) { // 1
System.out.println(map.get(4));
} else {
System.out.println("NoValue");
}
// Java8使用Map.getOrDefault()
System.out.println(map.getOrDefault(4, "NoValue")); // 2

putIfAbsent()

该方法跟Lambda表达式没关系,但是很有用。方法签名为V putIfAbsent(K key, V value),作用是只有在不存在key值的映射或映射值为null时,才将value指定的值放入到Map中,否则不对Map做更改。该方法将条件判断和赋值合二为一,使用起来更加方便。

remove()

我们都知道Map中有一个remove(Object key)方法,来根据指定key值删除Map中的映射关系;Java8新增了remove(Object key, Object value)方法,只有在当前Map中**key正好映射到value时**才删除该映射,否则什么也不做。

replace()

在Java7及以前,要想替换Map中的映射关系可通过put(K key, V value)方法来实现,该方法总是会用新值替换原来的值,为了更精确的控制替换行为,Java8在Map中加入了两个replace()方法,分别如下:

  • replace(K key, V value),只有在当前Map中**key的映射存在时**才用value去替换原来的值,否则什么也不做。
  • replace(K key, V oldValue, V newValue),只有在当前Map中**key的映射存在且等于oldValue时**才用newValue去替换原来的值,否则什么也不做。

replaceAll()

该方法签名为replaceAll(BiFunction<? super K, ? super V, ? extends V> function),作用是对Map中的每个映射执行function指定的操作,并用function的执行结果替换原来的value,其中BiFunction是一个函数接口,里面有一个待实现方法R apply(T t, U u),不要被如此多的函数接口吓到,因为使用的时候根本不需要知道他们的名字。

需求:假设有一个数字到对应英文单词的Map,请将原来映射关系中的单词都转换成大写

Java7以及之前经典的代码如下:

1
2
3
4
5
6
7
8
// Java7以及之前替换所有Map中所有映射关系
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()) {
entry.setValue(entry.getValue().toUpperCase());
}

使用replaceAll()方法结合匿名内部类,实现如下:

1
2
3
4
5
6
7
8
9
10
11
// 使用replaceAll()结合匿名内部类实现
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll(new BiFunction<Integer, String, String>() {
@Override
public String apply(Integer k, String v) {
return v.toUpperCase();
}
});

上述代码调用replaceAll()方法,并使用匿名内部类实现BiFunction接口。更进一步的,使用Lambda表达式实现如下:

1
2
3
4
5
6
// 使用replaceAll()结合Lambda表达式实现
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());

简洁到让人难以置信。

merge()

该方法签名为merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction),作用是:

  • 如果Mapkey对应的映射不存在或者为null,则将value(不能是null)关联到key上;
  • 否则执行remappingFunction,如果执行结果非null则用该结果跟key关联,否则在Map中删除key的映射。

参数中BiFunction函数接口前面已经介绍过,里面有一个待实现方法R apply(T t, U u)merge()方法虽然语义有些复杂,但该方法的用法很明确,一个比较常见的场景是将新的错误信息拼接到原来的信息上,比如:

1
map.merge(key, newMeg, (v1, v2) -> v1 + v2);

compute()

该方法签名为compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction),作用是remappingFunction的计算结果关联到key上,如果计算结果为null,则在Map中删除key的映射

要实现上述merge()方法中错误信息拼接的例子,使用compute()代码如下:

1
map.compute(key, (k, v) -> v == null ? newMsg : v.concat(newMsg));

computeIfAbsent()

该方法签名为v computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction),作用是:只有在当前Map中不存在key值的映射或映射值为null时,才调用mappingFunction,并在mappingFunction执行结果非null时,将结果跟key关联

Function是一个函数接口,里面有一个待实现方法R apply(T t)

computeIfAbsent()常用来对Map的某个key值建立初始化映射,比如我们要实现一个多值映射,Map的定义可能是Map<K, Set<V>>,要向Map中放入新值,可通过如下代码实现:

1
2
3
4
5
6
7
8
9
10
11
Map<Integer, Set<String>> map = new HashMap<>();
// Java7及以前的实现方式
if (map.containsKey(1)){
map.get(1).add("one");
} else {
Set<String> valueSet = new HashSet<String>();
valueSet.add("one");
map.put(1, valueSet);
}
// Java8的实现方式
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");

使用computeIfAbsent()将条件判断和添加操作合二为一,使代码更简洁。

computeIfPresent()

该方法签名为V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction),作用跟computeIfAbsent()相反,即,只有在当前Map中存在key值的映射且非null,才调用remappingFunction,如果remappingFunction执行结果为null,则删除key的映射,否则使用该结果替换key原来的映射。

这个函数的功能跟如下代码是等效的:

1
2
3
4
5
6
7
8
9
10
11
// Java7及以前跟computeIfPresent()等效的代码
if (map.get(key) != null) {
V oldValue = map.get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null)
map.put(key, newValue);
else
map.remove(key);
return newValue;
}
return null;

End.


常用排序算法

在这里做个记录,将常用的三大排序算法列出来,方便查看复习,算法主要是理清思路,最近在刷leetcode发现自己脑子是有点笨的。 For Java.


冒泡排序

冒泡是我觉得最简单的一个排序算法了,也是我记的最早的一个排序算法。

冒泡,顾名思义,泡泡往上冒,也就是每次都将最大值放在末尾,剩余值继续冒泡。

其时间复杂度为O(n²)。详解:冒泡排序

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 冒泡排序
*
* @param arr
* @return
*/
public static int[] MPSort(int[] arr) {
// 外循环一次就将最大的值放最后
for (int i = 1; i < arr.length; i++) {
// 内循环剩余值比较 找出最大值
for (int j = 0; j < arr.length - i; j++) {
// 比较 满足条件互换
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;

}

插入排序

插入排序也是比较好理解的一个排序算法,别看这名字挺粗鲁的,其实它还就那么回事。

插入排序,挨个遍历,发现比前数值还要小的数就把该数前移比较并换值,直到满足的位置为止。

是挺粗鲁的一个算法,不过这很直接,我喜欢。其时间复杂度为O(n²)。详解:插入排序

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 插入排序
*
* @param arr
* @return
*/
public static int[] insertSort(int[] arr) {
System.out.println(Arrays.toString(arr));
// j = ++i 保证了i和j两个数一样
for (int i = 0, j = i; i < arr.length - 1; j = ++i) {
int num = arr[i + 1];
// 有较小值就前移并换值
while (arr[j] > num) {
arr[j + 1] = arr[j];
// 使下标递减去比较
if (j-- == 0) {
break;
}
}
// 较小值新赋值
arr[j + 1] = num;

}
return arr;
}

快速排序

快排就是比较经典了,快排是对冒泡的一种改进,由C. A. R. Hoare在1962年提出。

它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

详解:快速排序

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 快速排序
*
* @param arr
* @param lo
* @param hi
* @return
*/
public static int[] quickSort(int[] arr, int lo, int hi) {
if (lo >= hi)
return arr;
int num = partition(arr, lo, hi);
quickSort(arr, lo, num - 1);
quickSort(arr, num + 1, hi);
return arr;
}

/**
* 排序数组 返回基准值
*
* @param arr
* @param lo
* @param hi
* @return
*/
public static int partition(int[] arr, int lo, int hi) {
int num = arr[lo];
if (lo >= hi)
return 0;
while (lo < hi) {
while (hi > lo && arr[hi] >= num) {
hi--;
}
arr[lo] = arr[hi];
while (hi > lo && arr[lo] <= num) {
lo++;
}
arr[hi] = arr[lo];
}
arr[hi] = num;
return hi;
}

End.


懵逼七月(2018)

又是一个月过去了,许久不更博客,今日来随意写写最近的状况。


  • 最近很不太平

    • 疫苗事件 全国愤怒
    • 娱乐圈税务事件还在继续
    • 吴亦凡虎扑有搞头 rapper diss大战
    • 纪凌尘 阚清子分手事件
    • 还有最近台风挺多的
  • 话说这些大多与我们吃瓜的干系不大,贵圈很乱也不是这一天两天的了。

  • 最近在刷leetcode,发现还是很吃力,想想好歹小时候也是参加奥数竞赛的人啊,脑子真的是要多用用,特别是一些思考问题的方式方法,时间久了就没有那么敏感了。还有就是深刻认识到了English的重要性,要是去官网上直接刷题不用翻译的话,怕是题目描述都看不懂。。

  • 前几天买了点游泳装备,准备周末找时间去游游泳,锻炼锻炼肺部。当然顺道去看看有没有性感比基尼美女了,这不是重点。游泳 健身 了解一下?

  • 周末也找了点话剧、点映、展览什么的看了看,以后周末闲不住了也可以去走一遭,何必一直窝在小小出租屋中呢,还是要多到外面看看景色,呼吸呼吸好空气。

  • 发现自己上半身要保持一直笔直有点困难了,特别是肩部、颈部,弯曲程度稍微严重了些。有时候自己会刻意去纠正、扭动,但是无意识状态就又还是异常姿态。长时间坐在工位办公给我们带来了什么,我觉得这伴随着的健康问题在以后科技技术越来越炫酷的时候,找出个解决方案是至关重要的。毕竟这基数也大,而且健康问题是每个人都不可忽视的,也是国家重视的。

  • ChinaJoy今天开幕,本来是想看看的,之前也没去过,但是没有人陪同自己一个人我也打消了这个念头,花这个钱我还是去多买几本书多游几次泳来得有价值。

  • 买了一个锅专门用来煮鸡胸餐、煎手抓饼,原来的那个锅太小了而且还粘锅。为了健康生活同时减少生活成本,这点投资还是要的。

  • 估计是住的地方太潮了再加上还有个大鱼缸,湿气太重,最近十分不得劲,皮肤略微过敏。


大致就先这样了。


JDK1.8中的Stream详解

别处看到的文章,对其再次进行了整理。收获很多。

Stream简介

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

什么是聚合操作

在传统的J2EE应用中,Java代码经常不得不依赖于关系型数据库的聚合操作来完成某些操作,诸如:

  • 客户每月平均消费金额
  • 最昂贵的在售商品
  • 本周完成的有效订单(排除了无效的)
  • 取十个数据样本作为首页推荐

但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。而Java的集合API中,仅仅有极少量的辅助性方法,更多的时候是程序员需要用Iterator来遍历集合,完成相关的聚合应用逻辑。这是一种远不够高效、笨拙的方法。在Java7中,如果要发现type为grocery的所有交易,然后返回以交易值降序排序好的交易ID集合,我们需要这样写:

Java7的排序、取值实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
List<Transaction> groceryTransactions = new Arraylist<>();

for(Transaction t: transactions){

if(t.getType() == Transaction.GROCERY){

groceryTransactions.add(t);

}

}

Collections.sort(groceryTransactions, new Comparator(){

public int compare(Transaction t1, Transaction t2){

return t2.getValue().compareTo(t1.getValue());

}

});

List<Integer> transactionIds = new ArrayList<>();

for(Transaction t: groceryTransactions){

transactionsIds.add(t.getId());

}

Java8的排序、取值实现

Java8中使用Stream,代码更加简洁易读,而且使用并发模式,程序执行速度更快。

1
2
3
4
5
6
7
8
9
List<Integer> transactionsIds = transactions.parallelStream().

filter(t -> t.getType() == Transaction.GROCERY).

sorted(comparing(Transaction::getValue).reversed()).

map(Transaction::getId).

collect(toList());

Stream总览

什么是流

Stream不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的Iterator。原始版本的Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的Stream,用户只要给出需要对其包含的元素执行什么操作,比如“过滤掉长度大于10的字符串”、“获取每个字符串的首字母”等,Stream会隐式地在内部进行遍历,做出相应的数据转换。

Stream就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

而和迭代器又不同的是,Stream可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方法去遍历时,每个item读完后再读下一个item。而使用并行去遍历时,数据会被分成多段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream的并行操作依赖于Java7中引入的Fork/Join框架(JSR166y)来拆分任务和加速处理过程。Java的并行API演变历程基本如下:

  • 1.0-1.4中的java.lang.Thread
  • 5.0中的java.util.concurrent
  • 6.0中的Phasers等
  • 7.0中的Fork/Join框架
  • 8.0中的Lambda

Stream的另外一大特点是,数据源本身可以是无限的。*

流的构成

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source) → 数据转换 → 执行操作获取想要的结果,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示:

流管道(Stream Pipeline)的构成

有多种方式生成 Stream Source:

  • 从Collection和数组

    • Collection.stream()
    • Collection.parallelStream()
    • Arrays.stream(T array) or Stream.of()
  • 从BufferedReader

    • java.io.BufferedReader.lines()\
  • 静态工厂

    • java.util.stream.IntStream.range()
    • java.nio.file.Files.walk()
  • 自己构建

    • java.util.Spliterator
  • 其它

    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()

流的操作类型分为两种:

  • Intermediate:一个流可以后面跟随零个或多个intermediate操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  • Terminal:一个流只能有一个terminal操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个side effect。

在对于一个Stream进行多次转换操作(Intermediate操作),每次都对Stream的每个元素进行转换,而且是执行多次,这样时间复杂度就是N(转换次数)个for循环里把所有操作都做掉的总和么?其实不是这样的,转换操作都是lazy的,多个转换操作只会在Terminal操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在Terminal操作的时候循环Stream对应的集合,然后对每个元素执行所有的函数。

还有一种操作被称为short-circuiting。用以指:

  • 对于一个intermediate操作,如果它接收的是一个无限大(infinite/unbounded)的Stream,但返回一个有限的新Stream。
  • 对于一个terminal操作,如果它接受的是一个无限大的Stream,但能在有限的时间计算出结果。

当操作一个无限大的Stream,而又希望在有限时间内完成操作,则在管道内拥有一个short-circuiting操作是必要非充分条件。

一个流操作的示例

1
2
3
4
5
6
7
int sum = widgets.stream()

.filter(w -> w.getColor() == RED)

.mapToInt(w -> w.getWeight())

.sum();

stream()获取当前小物件的source,filter和mapToInt为intermediate操作,进行数据筛选和转换,最后一个sum()为terminal操作,对符合条件的全部小物件作重量求和。

流的使用详解

简单说,对Stream的使用就是实现一个filter-map-reduce过程,产生一个最终结果,或者导致一个副作用(side effect)。

流的构造与转换

下面提供最常见的几种构造Stream的样例。

构造流的几种常见方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. Individual values

Stream stream = Stream.of("a", "b", "c");

// 2. Arrays

String [] strArray = new String[] {"a", "b", "c"};

stream = Stream.of(strArray);

stream = Arrays.stream(strArray);

// 3. Collections

List<String> list = Arrays.asList(strArray);

stream = list.stream();

需要注意的是,对于基本数值型,目前有三种对应的包装类型Stream:

IntStream、LongStream、DoubleStream。当然我们也可以用Stream、Stream、Stream,但是boxing和unboxing会很耗时,所以特别为这三种基本数值型提供了对应的Stream。

Java8中还没有提供其它数值型Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种Stream进行。

数据流的构造

1
2
3
4
5
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);

IntStream.range(1, 3).forEach(System.out::println);

IntStream.rangeClosed(1, 3).forEach(System.out::println);

流转换为其它数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. Array

String[] strArray1 = stream.toArray(String[]::new);

// 2. Collection

List<String> list1 = stream.collect(Collectors.toList());

List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));

Set set1 = stream.collect(Collectors.toSet());

Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));

// 3. String

String str = stream.collect(Collectors.joining()).toString();

注:一个Stream只可以使用一个,以上示例只是为了代码简洁而重复使用数次。

流的操作

接下来,当把一个数据结构包装成Stream后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下:

  • Intermediate:map(mapToint,flatMap等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered
  • Terminal:forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator
  • Short-circuiting:anyMatch、allMatch、noneMatch、findFirst、findAny、limit

接下来下面看一下Stream的比较典型用法。

  • map/flatMap

先来看map。如果熟悉scala这类函数式语言,对这个方法应该很了解,它的作用就是把input Stream的每一个元素,映射成output Stream的另外一个元素。

转换大写

1
2
3
4
5
List<String> output = wordList.stream().

map(String::toUpperCase).

collect(Collectors.toList());

平方数

1
2
3
4
5
6
7
List<Integer> nums = Arrays.asList(1, 2, 3, 4);

List<Integer> squareNums = nums.stream().

map(n -> n * n).

collect(Collectors.toList());

从上面例子可以看出,map生成的是个1:1映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要flatMap。

一对多

1
2
3
4
5
6
7
8
9
10
11
12
13
Stream<List<Integer>> inputStream = Stream.of(

Arrays.asList(1),

Arrays.asList(2, 3),

Arrays.asList(4, 5, 6)

);

Stream<Integer> outputStream = inputStream.

flatMap((childList) -> childList.stream());

flatMap把input Stream中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终output的新Stream里面已经没有List了,都是直接的数字。

  • filter

filter对原始Stream进行某项测试,通过测试的元素被留下来生成一个新Stream。

留下偶数

1
2
3
4
5
Integer[] sixNums = {1, 2, 3, 4, 5, 6};

Integer[] evens =

Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);

经过条件“被2整除”的filter,剩下的数字为{2,4,6}。

把单词挑出来

1
2
3
4
5
6
7
List<String> output = reader.lines().

flatMap(line -> Stream.of(line.split(REGEXP))).

filter(word -> word.length() > 0).

collect(Collectors.toList());

这段代码首先把每行的单词用flatMap整理到新的Stream,然后保留长度不为0的,就是整篇文章中的所有单词了。

  • forEach

forEach方法接收一个Lambda表达式,然后在Stream的每一个元素上执行该表达式。

打印姓名(forEach和pre-java8的对比)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Java 8

roster.stream()

.filter(p -> p.getGender() == Person.Sex.MALE)

.forEach(p -> System.out.println(p.getName()));

// Pre-Java 8

for (Person p : roster) {

if (p.getGender() == Person.Sex.MALE) {

System.out.println(p.getName());

}

}

对一个人员集合遍历,找出男性并打印姓名。可以看出来,forEach是为Lambda而设计的,保持了最紧凑的风格。而且lambda表达式本身是可以重用的,非常方便。当需要为多核系统优化时,可以parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时forEach本身的实现不需要调整,而Java8以前的for循环code可能需要加入额外的多线程逻辑。

但一般认为,forEach和常规for循环的差异不涉及到性能,它们仅仅是函数式风格与传统Java风格的差别。

另外一点需要注意,forEach是terminal操作,因此它执行后,Stream的元素就被“消费”掉了,你无法对一个Stream进行两次terminal运算。下面的代码是错误的:

1
2
3
stream.forEach(element -> doOneThing(element));

stream.forEach(element -> doAnotherThing(element));

相反,具有相似功能的intermediate操作peek可以达到上述目的。如下是出现在该api javadoc上的一个示例。

peek 对每个元素执行操作并返回一个新的 Stream

1
2
3
4
5
6
7
8
9
10
11
Stream.of("one", "two", "three", "four")

.filter(e -> e.length() > 3)

.peek(e -> System.out.println("Filtered value: " + e))

.map(String::toUpperCase)

.peek(e -> System.out.println("Mapped value: " + e))

.collect(Collectors.toList());

forEach不能修改自己包含的本地变量值,也不能用break/return之类的关键字提前结束循环。

  • findFirst

这是一个terminal兼short-circuiting操作,它总是返回Stream的第一个元素,或者空。

这里比较重点的是它的返回值类型:Optional。这也是一个模仿Scala语言中的概念,作为一个容器,它可能含有某值,或者不包含。使用它的目的是尽可能避免NullPointerException。

Optional的两个用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
String strA = " abcd ", strB = null;

print(strA);

print("");

print(strB);

getLength(strA);

getLength("");

getLength(strB);

public static void print(String text) {

// Java 8

Optional.ofNullable(text).ifPresent(System.out::println);

// Pre-Java 8

if (text != null) {

System.out.println(text);

}

}

public static int getLength(String text) {

// Java 8

return Optional.ofNullable(text).map(String::length).orElse(-1);

// Pre-Java 8

// return if (text != null) ? text.length() : -1;

};

在更复杂的if(xx != null)的情况中,使用Optional代码的可读性更好,而且它提供的是编译时检查,能极大的降低NPE这种Runtime Exception对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。

Stream中的findAny、max/min、reduce等方法返回Optional值。还有例如IntStream.average()返回Optional Double等等。

  • reduce

这个方法的主要作用是把Stream元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面Stream的第一个、第二个、第n个元素组合。从这个意义上说,字符串拼接、数值的sum、min、max、average都是特殊的reduce。例如Stream的sum就相当于

1
Integer sum = integers.reduce(0,(a,b) -> a+b);

1
Integer sum = integers.reduce(0,Integer::sum);

也有没有起始值的情况,这时会把Stream的前面两个元素组合起来,返回的是Optional。

reduce的用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 字符串连接,concat = "ABCD"

String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);

// 求最小值,minValue = -3.0

double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);

// 求和,sumValue = 10, 有起始值

int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);

// 求和,sumValue = 10, 无起始值

sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();

// 过滤,字符串连接,concat = "ace"

concat = Stream.of("a", "B", "c", "D", "e", "F").

filter(x -> x.compareTo("Z") > 0).

reduce("", String::concat);

上面代码例如第一个示例的reduce(),第一个参数(空白字符)即为起始值,第二个参数(String::concat)为BinaryOperator。这类有起始值的reduce()都返回具体的对象。而对于第四个示例没有起始值的reduce(),由于可能没有足够的元素,返回的是Optional,请留意这个区间。

  • limit/skip

limit返回Stream的前面n个元素;skip则是扔掉前n个元素(它是由一个叫subStream的方法改名而来)。

limit和skip对运行次数的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public void testLimitAndSkip() {

List<Person> persons = new ArrayList();

for (int i = 1; i <= 10000; i++) {

Person person = new Person(i, "name" + i);

persons.add(person);

}

List<String> personList2 = persons.stream().

map(Person::getName).limit(10).skip(3).collect(Collectors.toList());

System.out.println(personList2);

}

private class Person {

public int no;

private String name;

public Person (int no, String name) {

this.no = no;

this.name = name;

}

public String getName() {

System.out.println(name);

return name;

}

}

结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name1

name2

name3

name4

name5

name6

name7

name8

name9

name10

[name4, name5, name6, name7, name8, name9, name10]

这是一个有10000个元素的Stream,但在short-circuiting操作limit和skip的作用下,管道中map操作指定的getName()方法的执行次数为limit所限定的10次,而最终返回结果在跳过前3个元素后只有后面7个返回。

还有一种情况是limit/skip无法达到short-circuiting目的地,就是把它们放在Stream的排序操作后,原因跟sorted这个intermediate操作有关:此时系统并不知道Stream排序后的次序如何,所以sorted中的操作看上去就像完全没有被limit或者skip一样。

limit和skip对sorted后的运行次数无影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Person> persons = new ArrayList();

for (int i = 1; i <= 5; i++) {

Person person = new Person(i, "name" + i);

persons.add(person);

}

List<Person> personList2 = persons.stream().sorted((p1, p2) ->

p1.getName().compareTo(p2.getName())).limit(2).collect(Collectors.toList());

System.out.println(personList2);

首先对5个元素的Stream排序,然后进行limit操作。输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
name2

name1

name3

name2

name4

name3

name5

name4

[stream.StreamDW$Person@816f27d, stream.StreamDW$Person@87aac27]

即虽然最后的返回元素数量是2,但整个管道中的sorted表达式执行次数没有像之前示例一样相应减少。

最后有一种需要注意的是,对一个parallel的Stream管道来说,如果其元素是有序的,那么limit操作的成本会比较大,因为它的返回对象必须是前n个也有一样次序的元素。取而代之的策略是取消元素间的次序,或者不要用parallel Stream。

  • sorted

对Stream的排序通过sorted进行,它比数组的排序更强之处在于你可以首先对Stream进行各类map、filter、limit、skip甚至distinct来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。对之前示例可进行优化:

排序前进行 limit 和 skip

1
2
3
4
5
6
7
8
9
10
11
12
13
List<Person> persons = new ArrayList();

for (int i = 1; i <= 5; i++) {

Person person = new Person(i, "name" + i);

persons.add(person);

}

List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList());

System.out.println(personList2);

结果为:

1
2
3
4
5
name2

name1

[stream.StreamDW$Person@6ce253f1, stream.StreamDW$Person@53d8d10a]

当然这种优化是有business logic上的局限性的:即不要求排序后再取值

  • min/max/distinct

min和max的功能也可以通过对Stream元素先排序,再findFirst来实现,但前者的性能会更好,为O(n),而sorted的成本是O(n log n)。同时它们作为特殊的reduce方法被独立出来也是因为求最大最小值是很常见的操作。

找出最长一行的长度

1
2
3
4
5
6
7
8
9
10
11
12
13
BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));

int longest = br.lines().

mapToInt(String::length).

max().

getAsInt();

br.close();

System.out.println(longest);

找出全文的单词,转小写,并排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<String> words = br.lines().

flatMap(line -> Stream.of(line.split(" "))).

filter(word -> word.length() > 0).

map(String::toLowerCase).

distinct().

sorted().

collect(Collectors.toList());

br.close();

System.out.println(words);
  • Match

Stream有三个match方法,从语义上说:

allMatch:Stream中全部元素符合传入的predicate,返回true
anyMatch:Stream中只要有一个元素符合传入的predicate,返回true
noneMatch:Stream中没有一个元素符合传入的predicate,返回true

它们都不是要遍历全部元素才能返回结果。例如allMatch只要一个元素不满足条件,就skip剩下的所有元素,返回false。

使用Match

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
List<Person> persons = new ArrayList();

persons.add(new Person(1, "name" + 1, 10));

persons.add(new Person(2, "name" + 2, 21));

persons.add(new Person(3, "name" + 3, 34));

persons.add(new Person(4, "name" + 4, 6));

persons.add(new Person(5, "name" + 5, 55));

boolean isAllAdult = persons.stream().

allMatch(p -> p.getAge() > 18);

System.out.println("All are adult? " + isAllAdult);

boolean isThereAnyChild = persons.stream().

anyMatch(p -> p.getAge() < 12);

System.out.println("Any child? " + isThereAnyChild);

输出结果:

1
2
3
All are adult? false

Any child? true

进阶:自己生成流

  • Stream.generate

通过实现Supplier接口,你可以自己来控制流的生成。这种情形通常用于随机数、常量的Stream,或者需要前后元素间维持着某种状态信息的Stream。把Supplier实例传递给Stream.generate()生成的Stream,默认是串行(相对parallel而言)但无序的(相对ordered而言)。由于它是无限的,在管道中,必须利用limit之类的操作限制Stream大小。

生成10个随机整数

1
2
3
4
5
6
7
8
9
10
11
Random seed = new Random();

Supplier<Integer> random = seed::nextInt;

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

//Another way

IntStream.generate(() -> (int) (System.nanoTime() % 100)).

limit(10).forEach(System.out::println);

Stream.generate()还接受自己实现的Supplier。例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算Stream的每个元素值。这些都是维持状态信息的情形。

自实现Supplier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Stream.generate(new PersonSupplier()).

limit(10).

forEach(p -> System.out.println(p.getName() + ", " + p.getAge()));

private class PersonSupplier implements Supplier<Person> {

private int index = 0;

private Random random = new Random();

@Override

public Person get() {

return new Person(index++, "StormTestUser" + index, random.nextInt(100));

}

}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
StormTestUser1, 9

StormTestUser2, 12

StormTestUser3, 88

StormTestUser4, 51

StormTestUser5, 22

StormTestUser6, 28

StormTestUser7, 81

StormTestUser8, 51

StormTestUser9, 4

StormTestUser10, 76
  • Stream.iterate

iterate跟reduce操作很像,接受一个种子值,和一个UnaryOperator(例如f)。然后种子值成为Stream的第一个元素,f(seed)为第二个,f(f(seed))第三个,以此类推。

生成一个等差数列

1
Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));

输出结果:

1
0 3 6 9 12 15 18 21 24 27

与Stream.generate相仿,在iterate时候管道必须有limit这样的操作来限制Stream大小。

进阶:用Collectors来进行reduction操作

java.util.stream.Collectors类的主要作用就是辅助进行各类有用的reduction操作,例如转变输出为Collection,把Stream元素进行归组,

  • groupingBy/partitioningBy

按照年龄归组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map<Integer, List<Person>> personGroups = Stream.generate(new PersonSupplier()).

limit(100).

collect(Collectors.groupingBy(Person::getAge));

Iterator it = personGroups.entrySet().iterator();

while (it.hasNext()) {

Map.Entry<Integer, List<Person>> persons = (Map.Entry) it.next();

System.out.println("Age " + persons.getKey() + " = " + persons.getValue().size());

}

上面的示例,首先生成100人的信息,然后按照年龄归组,相同年龄的人放到同一个list中,可以看到如下的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
Age 0 = 2

Age 1 = 2

Age 5 = 2

Age 8 = 1

Age 9 = 1

Age 11 = 2

……

按照未成年人和成年人归组

1
2
3
4
5
6
7
8
9
Map<Boolean, List<Person>> children = Stream.generate(new PersonSupplier()).

limit(100).

collect(Collectors.partitioningBy(p -> p.getAge() < 18));

System.out.println("Children number: " + children.get(true).size());

System.out.println("Adult number: " + children.get(false).size());

输出结果:

1
2
3
Children number: 23

Adult number: 77

在使用条件“年龄小于18”进行分组后可以看到,不到18岁的未成年人是一组,成年人是另外一组。partitioningBy其实是一种特殊的groupingBy,它依照条件测试的是否两种结果来构造返回的数据结构,get(true)和get(false)能即为全部的元素对象。

结束语

总之,Stream的特性可以归纳为:

  • 不是数据结构
  • 它没有内部存储,它只是用操作管道从source(数据结构、数组、generator function、IO channel)抓取数据
  • 它也绝不修改自己所封装的底层数据结构的数据。例如Stream的filter操作会产生一个不包含被过滤元素的新Stream,而不是从source删除那些元素
  • 所有Stream的操作必须以lambda表达式为参数
  • 不支持索引访问
  • 你可以请求第一个元素,但无法请求第二个,第三个或者最后一个。不过请参阅下一项
  • 很容易生成数组或者List
  • 惰性化
  • 很多Stream操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始
  • Intermediate操作永远是惰性化的
  • 并行能力
  • 当一个Stream是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的
  • 可以是无限的
  • 集合有固定大小,Stream则不必。limit(n)和findFirst()这类的short-circuiting操作可以对无限的Stream进行运算并很快完成

再怎么样也不能废了自己

拥有一个自律的生活

身边有太多的人都因为身体健康而发愁。等到病魔来找你,那就真的晚了,什么都抵不过健康二字。

把自己的身体精神养好,也是对自己的一种负责,更是对父母的负责。

这里我就记录一下最近自己的生活状态,再怎么样也不能废了自己。


  • 早上

    • 七点一个闹钟,往往过个十分钟才动身起床,也就是七点十分左右(工作日)
    • 起床烧一壶开水,泡一杯燕麦,拉几个拉力绳
    • 上厕所,洗脸,刷牙
    • 偶尔做个手抓饼(自己网上买的食材)
    • 打开电脑,编辑一篇情爱的诗词丢上公众号(自娱自乐)
    • 给龟崽子喂点吃的(鱼干、虾干、面包虫…)
    • 自己吃个面包,喝了那杯燕麦,再拉几个拉力绳,准备出门上班
    • 周末的话早上可能起得迟一点,起床后就打开电脑看看电影玩玩游戏
  • 上班

    • 上班找小黄车,距离上班地点6公里左右,骑车半小时
    • 由于地铁站距离比较远,加上最近需要减脂就还是骑车最舒服了(常年坐工位体脂率高)
    • 中午公司包餐,伙食一般般,吃的也不多,所以有时候需要下午自己补给一下
    • 朝九晚六,基本上七点能回家,当然也是骑车回去
  • 晚上

    • 下班骑车回去,基本上两三天要去菜市场补点货
    • 鸡胸肉、黄瓜、胡萝卜、西红柿、生菜…
    • 周二、四、六晚上我会选择去跑步,一般都是环形,一起算上5公里吧也不多
    • 不跑步的时候会躺床上先看个电影,练练口琴或者练练pop
    • 运动回去缓一缓,拉几个拉力绳,准备动手自己做鸡胸晚餐了
    • 基本上全部弄完要将近九点了,再休息休息洗个澡
    • 最近晚上有球赛的话我会用投影看球赛,没有的话就趴在床上自己看看书,犯困就睡觉
    • 睡眠质量不太好,所以基本睡觉都比较晚,我知道这对身体很不利,最近也在慢慢改
  • 周末

    • 周末的话稍微就自由放松一点,平时没睡够的觉可以补一补
    • 上周发现附近有个学校,去那边操场跑了跑
    • 准备周末带着书去那边的图书馆泡着,晚上还可以顺带跑跑步

总是在你回头的时候才会感慨时间流逝得快。

有一句话我一直忘不了:
学习和健身是两种最便宜而有效的改变人的方式

再怎么样,我们也不能废了自己。保持自律。


indexedDB数据库使用总结

indexedDB简介

indexedDB是一个前端存储数据库,之前也没有什么了解,这次项目中需要用到,然后就去找了相关资料。数据库有两种,一种是关系型数据库,另一种是非关系型数据库。indexedDB是第二种,它是非关系型数据库,它不需要你去写一些特定的sql语句来对数据库进行操作,数据形式使用的是json。

与其他前端存储方式对比

也许熟悉前端存储的会说,不是有了LocalStorage和Cookies吗?为什么还要推出indexedDB呢?其实对于在浏览器里存储数据,你可以使用cookies或local storage,但它们都是比较简单的技术,而IndexedDB提供了类似数据库风格的数据存储和使用方式。

首先说说Cookies,英文直接翻译过来就是小甜点,听起来很好吃,实际上并不是,每次HTTP接受和发送都会传递Cookies数据,它会占用额外的流量。例如,如果你有一个10KB的Cookies数据,发送10次请求,那么,总计就会有100KB的数据在网络上传输。Cookies只能是字符串。浏览器里存储Cookies的空间有限,很多用户禁止浏览器使用Cookies。所以,Cookies只能用来存储小量的非关键的数据。

其次说说LocalStorage,LocalStorage是用key-value键值模式存储数据,但跟IndexedDB不一样的是,它的数据并不是按对象形式存储。它存储的数据都是字符串形式。如果你想让LocalStorage存储对象,你需要借助JSON.stringify()能将对象变成字符串形式,再用JSON.parse()将字符串还原成对象。但如果要存储大量的复杂的数据,这并不是一种很好的方案。毕竟,localstorage就是专门为小数量数据设计的,所以它的api设计为同步的。而IndexedDB很适合存储大量数据,它的API是异步调用的。IndexedDB使用索引存储数据,各种数据库操作放在事务中执行。IndexedDB甚至还支持简单的数据类型。IndexedDB比localstorage强大得多,但它的API也相对复杂。对于简单的数据,你应该继续使用localstorage,但当你希望存储大量数据时,IndexedDB会明显的更适合,IndexedDB能提供你更为复杂的查询数据的方式。

indexedDB特性

  • 对象仓库
    indexedDB中没有表的概念,而是objectStore,一个数据库中可以包含多个objectStore,objectStore是一个灵活的数据结构,可以存放多种类型数据。也就是说一个objectStore相当于一张表,里面存储的每条数据和一个键相关联。我们可以使用每条记录中的某个指定字段作为键值(keyPath),也可以使用自动生成的递增数字作为键值(keyGenerator),也可以不指定。选择键的类型不同,objectStore可以存储的数据结构也有差异。

    键类型 存储数据
    不使用 任意值,但是每添加一条数据的时候,需指定键参数
    keyPath 对象,eg: {keyPath: 'id'}
    keyGenerator 任意值 eg: {autoincrement: true}
    keyPath and KeyGenerator 都使用 对象,如果对象中有keyPath指定的属性则不生成新的键值,如果没有自动生成递增键值,填充keyPath指定的属性
  • 事务性
    在indexedDB中,每一个对数据库操作是在一个事务的上下文中执行的。事务范围一次影响一个或多个object stores,你通过传入一个object store名字的数组到创建事务范围的函数来定义。例如:db.transaction(storeName, ‘readwrite’),创建事务的第二个参数是事务模式。当请求一个事务时,必须决定是按照只读还是读写模式请求访问。

  • 基于请求
    对indexedDB数据库的每次操作,描述为通过一个请求打开数据库,访问一个object store,再继续。IndexedDB API天生是基于请求的,这也是API异步本性指示。对于你在数据库执行的每次操作,你必须首先为这个操作创建一个请求。当请求完成,你可以响应由请求结果产生的事件和错误。

  • 异步
    在IndexedDB大部分操作并不是我们常用的调用方法,返回结果的模式,而是请求—响应的模式,所谓异步API是指并不是这条指令执行完毕,我们就可以使用request.result来获取indexedDB对象了,就像使用ajax一样,语句执行完并不代表已经获取到了对象,所以我们一般在其回调函数中处理。

使用示例

打开数据库

  • 判断浏览器是否支持indexedDB数据库
    1
    2
    3
    4
    5
    var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
    if(!indexedDB)
    {
    console.log("你的浏览器不支持IndexedDB");
    }
  • 创建请求打开indexedDB,IndexedDB需要你创建一个请求来打开它。
    1
    var request = indexedDB.open(name, version);
    第一个参数是数据库的名称,第二个参数是数据库的版本号。版本号可以在升级数据库时用来调整数据库结构和数据。但你增加数据库版本号时,会触发onupgradeneeded事件,这时可能会出现成功、失败和阻止事件三种情况:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    request.onerror = function(e) { // 失败
    console.log(e.currentTarget.error.message);
    };

    request.onsuccess = function(e) { // 成功
    myDB.db = e.target.result;
    console.log('成功打开DB');
    };

    request.onupgradeneeded = function(e) {
    var db = e.target.result;
    if (!db.objectStoreNames.contains('person')) {
    console.log("我需要创建一个新的存储对象");
    //如果表格不存在,创建一个新的表格(keyPath,主键 ; autoIncrement,是否自增),会返回一个对象(objectStore)
    var objectStore = db.createObjectStore('person', {
    keyPath: "id",
    autoIncrement: true
    });

    //指定可以被索引的字段,unique字段是否唯一

    objectStore.createIndex("name", "name", {
    unique: false
    });

    objectStore.createIndex("phone", "phone", {
    unique: false
    });

    }
    console.log('数据库版本更改为: ' + version);
    };
    onupgradeneeded事件在第一次打开页面初始化数据库时会被调用,或在当有版本号变化时。所以,你应该在onupgradeneeded函数里创建你的存储数据。如果没有版本号变化,而且页面之前被打开过,你会获得一个onsuccess事件。

添加数据

  • 创建一个事务,并要求具有读写权限
    1
    var transaction = db.transaction(storeName, 'readwrite');
  • 获取objectStore,调用add方法添加数据
    1
    2
    3
    4
    var store = transaction.objectStore(storeName); //访问事务中的objectStore
    data.forEach(function (item) {
    store.add(item);//保存数据
    });

删除数据

  • 创建事务,然后调用删除接口,通过key删除对象
    1
    2
    3
    4
    5
    var transaction = db.transaction(storeName, 'readwrite');

    var store = transaction.objectStore(storeName);

    store.delete(key);

查找数据

  • 按key查找 开启事务,获取objectStore,调用往get()方法,往方法里传入对象的key值,取出相应的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var transaction = db.transaction(storeName, 'readwrite');

    var store = transaction.objectStore(storeName);

    var request = store.get(key);

    request.onsuccess = function(e) {

    data = e.target.result;

    console.log(student.name);

    };
  • 使用索引查找
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var transaction = db.transaction(storeName);

    var store = transaction.objectStore(storeName);

    var index = store.index(search_index);

    index.get(value).onsuccess = function(e) {

    data = e.target.result;

    console.log(student.id);

    }
  • 游标遍历数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    var transaction = db.transaction(storeName);

    var store = transaction.objectStore(storeName);

    var request = store.openCursor();//打开游标

    var dataList = new Array();

    var i = 0;

    request.onsuccess = function(e) {

    var cursor = e.target.result;

    if (cursor) {

    console.log(cursor.key);

    dataList[i] = cursor.value;

    console.log(dataList[i].name);

    i++;

    cursor.continue();

    }

    data = dataList;

    };

更新对象

更新对象,首先要把它取出来,修改,然后再放回去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var transaction = db.transaction(storeName, 'readwrite');

var store = transaction.objectStore(storeName);

var request = store.get(key);

request.onsuccess = function(e) {

var data = e.target.result;

for (a in newData) {

//除了keypath之外



data.a = newData.a;

}

store.put(data);

};

关闭与删除数据库

关闭数据库可以直接调用数据库对象的close方法
1
2
3
4
5
function closeDB(db) {

db.close();

}
删除数据库使用数据库对象的deleteDatabase方法
1
2
3
4
5
function deleteDB(name) {

indexedDB.deleteDatabase(name);

}

参考资料