整理一些 JDK 中 Integer 实用但不常用的方法

直接开搞。

toString

该方法进行了重载,一种是 toString(int i, int radix),另一个是 toString(int i)。一个参数的方法就相当于 toString(int i, 10),看代码便知,何况其官网注释也有:

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
public static String toString(int i, int radix) {
if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
radix = 10;

/* Use the faster version */
if (radix == 10) {
return toString(i);
}
// int 32位
char buf[] = new char[33];
boolean negative = (i < 0);
int charPos = 32;

if (!negative) {
i = -i;
}
// 根据进制取余转换
while (i <= -radix) {
buf[charPos--] = digits[-(i % radix)];
i = i / radix;
}
buf[charPos] = digits[-i];

if (negative) {
buf[--charPos] = '-';
}

return new String(buf, charPos, (33 - charPos));
}

不过该方法需注意: If the first argument is negative, the first element of the result is the ASCII minus character '-' ('\u005Cu002D'). If the first argument is not negative, no sign character appears in the result. 例如:

1
2
Integer.toString(-44, 2)  // -101100
Integer.toBinaryString(-44) // 11111111111111111111111111010100

若是负数,用该方法求得的值只是正数前加了个 “-“ 。

toBinaryString

类似的几个方法也一并列出了。 toBinaryString(int i) 转二进制方法,toOctalString(int i) 转八进制方法,toHexString(int i) 转十六进制方法。

1
2
3
Integer.toBinaryString(-44) // 11111111111111111111111111010100
Integer.toOctalString(44) // 54
Integer.toHexString(44) // 2c

parseUnsignedInt

与 toString 方法一样进行了重载。 parseUnsignedInt(String s)parseUnsignedInt(String s, int radix) 这是 JDK 1.8 新增的方法,作用就是将字符串参数解析为第二个参数指定的基数中的无符号整数。

1
2
3
Integer.parseUnsignedInt("11111111111111111111111111010100", 2) // -44
Integer.parseUnsignedInt("44", 10) // 44
Integer.parseUnsignedInt("44") // 44

decode

该方法将 String 解码为整数。 接受指定语法的十进制,十六进制和八进制数。源码如下:

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 Integer decode(String nm) throws NumberFormatException {
int radix = 10;
int index = 0;
boolean negative = false;
Integer result;

if (nm.length() == 0)
throw new NumberFormatException("Zero length string");
char firstChar = nm.charAt(0);
// Handle sign, if present
if (firstChar == '-') {
negative = true;
index++;
} else if (firstChar == '+')
index++;

// Handle radix specifier, if present
if (nm.startsWith("0x", index) || nm.startsWith("0X", index)) {
index += 2;
radix = 16;
}
else if (nm.startsWith("#", index)) {
index ++;
radix = 16;
}
else if (nm.startsWith("0", index) && nm.length() > 1 + index) { // 0 后面长度要大于 1
index ++;
radix = 8;
}

if (nm.startsWith("-", index) || nm.startsWith("+", index))
throw new NumberFormatException("Sign character in wrong position");

try {
result = Integer.valueOf(nm.substring(index), radix);
result = negative ? Integer.valueOf(-result.intValue()) : result;
} catch (NumberFormatException e) {
// If number is Integer.MIN_VALUE, we'll end up here. The next line
// handles this case, and causes any genuine format error to be
// rethrown.
String constant = negative ? ("-" + nm.substring(index))
: nm.substring(index);
result = Integer.valueOf(constant, radix);
}
return result;
}

使用测试如下:

1
2
3
4
Integer.decode("0xff") // 255
Integer.decode("#ff") // 255
Integer.decode("-07") // -7
Integer.decode("-071") // -57

highestOneBit

该方法返回一个 int 值,该值最多只有一位,位于指定 int 值中最高位(“最左侧”)1 的位置。 如果指定的值在其二进制补码表示中没有一位,即,如果它等于零,则返回零。

1
Integer.highestOneBit(44) // 32

44 对应的二进制为 0010 1100,只选中其最左侧的 “1” 那就是 0010 0000,也就是 25 = 32

lowestOneBit

该方法返回一个 int 值,该值最多只有一位,位于指定 int 值中最低位(“最右侧”)1 的位置。 如果指定的值在其二进制补码表示中没有一位,即,如果它等于零,则返回零。

1
Integer.lowestOneBit(44) // 4

44 对应的二进制为 0010 1100,只选中其最右侧的 “1” 那就是 0000 0100,也就是 22 = 4

numberOfLeadingZeros

该方法计算首部零的个数。

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
/**
* 首先在 jvm 中一个 int 类型的数据占 4 个字节,共 32 位,其实就相当于一个长度为 32 的数组。
*
* 那我们要计算首部 0 的个数,就是从左边第一个位开始累加 0 的个数,直到遇到一个非零值。
*/
public static int numberOfLeadingZeros(int i) {
// HD, Figure 5-6
if (i == 0)
return 32;
int n = 1;
// 下面的代码就是定位从左边开始第一个非零值的位置,在定位过程中顺便累加从左边开始 0 的个数
// 将 i 无符号右移 16 位后,有二种情况;
// 情况1. i=0,则第一个非零值位于低 16 位,i 至少有 16 个 0,同时将 i 左移 16 位(把低 16 位移到原高 16 位的位置,这样情况 1 和情况 2 就能统一后续的判断方式)
// 情况2. i!=0,则第一个非零值位于高 16 位,后续在高 16 位中继续判断
// 这个思路就是二分查找,首先把32位的数分为高低 16 位,如果非零值位于高 16 位,后续再将高 16 位继续二分为高低 8 位,一直二分到集合中只有 1 个元素
if (i >>> 16 == 0) { n += 16; i <<= 16; }
// 判断第一个非零值是否位于高 8 位
if (i >>> 24 == 0) { n += 8; i <<= 8; }
// 判断第一个非零值是否位于高 4 位
if (i >>> 28 == 0) { n += 4; i <<= 4; }
// 判断第一个非零值是否位于高 2 位
if (i >>> 30 == 0) { n += 2; i <<= 2; }
n -= i >>> 31;
return n;
}

测试看看:

1
Integer.numberOfLeadingZeros(44) // 26

int 4 个字节,一个字节八位,所以有 32 位。44 对应完整二进制就是 0000 0000 0000 0000 0000 0000 0010 1100。所以从左边开始数起共有 26 个零。

numberOfTrailingZeros

返回指定 int 值的二进制补码表达式中最低位(“最右侧”)1 之后的零位数。

1
Integer.numberOfTrailingZeros(44) // 2

44 对应二进制 0010 1100。其最右侧 “1” 之后的零的个数就是 2。

bitCount

返回指定 int 值的二进制补码中 1 的个数。

1
2
Integer.bitCount(44) // 3
Integer.bitCount(-44) // 28

44 对应的二进制补码为 0000 0000 0000 0000 0000 0000 0010 1100。1 有 3 个。

-44 对应的二进制补码为 1111 1111 1111 1111 1111 1111 1101 0100。1 有 28 个。


动态规划之 0-1 背包问题详解

前言

背包问题是比较经典的动态规划算法题,之前没接触过算法都没听说过这个,也是后来在 leetcode 中刷题时才了解到,惭愧惭愧啊。算法的世界太奇妙,数学一直都是那么令人着迷。今天来总结一下这个 01 背包问题。注:这里的物品不可拆分。

动态规划

首先了解下什么是动态规划。动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

详情可见:动态规划

问题详解

问题描述

给定 N 种物品和一个背包。物品i的重量是 Wi,其价值位 Vi,背包的容量为 C。问应该如何选择装入背包的物品,使得转入背包的物品的总价值为最大。

问题分析

在选择物品的时候,对每种物品只有两种选择,要么装入,要么不装入。因此,此为一个 0-1 背包问题。
01 背包的递归公式为:

1
2
3
m[i,0] = m[0,j] = 0
m[i,j] = m[i-1,j] ,j < wi
m[i,j] = max(m[i-1,j-wi]+vi, m[i-1,j]) ,j >= wi

其中,m[i,j]为前 i 件物品中选择若干件,放入承重为 j 的背包中,得到的最大的价值。

wi 为第 i 件商品的重量。

vi 为第 i 件商品的价值。

例题讲解

有编号为 a,b,c,d,e 的五件物品,他们的重量分别为 4,5,6,2,2,价值分别为 6,4,5,3,6,现在给你一个承重为 10 的背包,怎么实现价值最大。

根据上述公式可以得到一个数据表,表从上向下生成:

name weight value 1 2 3 4 5 6 7 8 9 10
a 4 6 0 0 0 6 6 6 6 6 6 6
b 5 4 0 0 0 6 6 6 6 6 10 10
c 6 5 0 0 0 6 6 6 6 6 10 11
d 2 3 0 3 3 6 6 9 9 9 10 11
e 2 6 0 6 6 9 9 12 12 15 15 15

故可以根据公式码出如下实现代码:

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
public static void main(String[] args) {

int n = 5;// 5件物品,物品编号为a,b,c,d,e(下面为多加一件物品,第一个物品为虚拟的物品)
int weight[] = { 0, 4, 5, 6, 2, 2 };// 物品的重量
int value[] = { 0, 6, 4, 5, 3, 6 }; // 对应物品的价值
int c = 10; // 背包容量
int state[] = { 0, 0, 0, 0, 0, 0 };// 开始状态
char name[] = { ' ', 'a', 'b', 'c', 'd', 'e' };
int maxValue = getMaxValue(n, weight, value, state, c);
System.out.println("最大价值为 = " + maxValue);
System.out.print("放入的物品为 :");
for (int i = 1; i <= 5; i++) {
if (state[i] == 1) {
System.out.print(name[i] + " ");
}
}

// System.out.println();
}

/**
*
* @param n
* 物品数量
* @param weight
* 物品对应重量(数组下标从0开始,故第一个物品为虚拟物品)
* @param value
* 物品对应价值(数组下标从0开始,故第一个物品为虚拟物品)
* @param state
* 物品的开始状态
* @param c
* 背包的容量
* @return
*/
public static int getMaxValue(int n, int weight[], int value[], int state[], int c) {
// n 为物品的数量,数组时需要加 1,此时可以从 0,1,...n 个物品,共 n+1 个商品,其中第 0 个为虚构物品
// 对于物品的价值,可以写成 2 维数组
int m[][] = new int[n + 1][c + 1]; // n 为 0,1,2...(n-1),背包重量为 0,1,2...C
int i, j;

for (i = 0; i <= n; i++) {
m[i][0] = 0;
}
for (j = 0; j <= c; j++) {
m[0][j] = 0;
}

for (i = 1; i <= n; i++) {
// System.out.println();
for (j = 1; j <= c; j++) {
if (j < weight[i]) { // 新的物品太重,无法放下
m[i][j] = m[i - 1][j];
} else {// 分为放和不放 取较大值
m[i][j] = Math.max(m[i - 1][j - weight[i]] + value[i], m[i - 1][j]);
}
// System.out.print("m["+i+"]["+j+"]="+m[i][j]+" ");
// System.out.print(m[i][j]+" ");
}
}

// 根据其最大价值,反向推断是否添加了物品 i

j = c;
for (i = n; i > 0; i--) {
if (m[i][j] > m[i - 1][j]) {// 物品 i 添加到了序列列表
state[i] = 1;
j = j - weight[i];
} else { // 没有添加
state[i] = 0;
}
}

return m[n][c]; // 最大价值
}

输出结果为:

1
2
最大价值为 = 15
放入的物品为 :a d e

具体实现看上述代码即可,注释齐全,简单易懂。

参考资料


笑口常开

摘录自贾平凹先生散文集《自在独行》中的《笑口常开》。在生活中找寻乐而开笑之事。

著作得以出版,殷切切送某人一册,扉页上恭正题写:“赠 xxx 先生存正。”一月过罢,偶尔去废旧书报收购店见到此册,遂折价买回,于扉页上那条提款下又恭正题写:“再赠 xxx 先生存正。”写毕邮走,踅进一家酒馆坐喝,不禁乐而开笑。

大学毕业,年届三十,婚姻难就,累得三朋四友八方搭线,但一次一次介绍终未能成就。忽一日,又有人送来游票,郑重讲明已物色着一位姑娘,同意明日去公园 xx 桥第三根栏杆下见面。黎明早起,赶去约会,等候的姑娘竟是两年前曾经别人介绍见过面的。姑娘说:“怎么又是你?”掉身而去。木木在桥上立了半晌,不禁乐而开笑。

好友 x 君,编辑十五年杂志,清苦贫困,英年早逝。保存下那一支笔和一副深度近视镜。租三轮车送亡友去火葬场火化,待化的队列冗长,忽见墙上张贴有“本场优待知识分子”,立即返回取来编辑证书,果然火化提前,免受尸体臭烂,不禁乐而开笑。

入厕所大便完毕,发现未带手纸,见旁边有被揩过的一片脏纸,应急欲用,缺进来一个人蹲坑,只好等着那人便后先走。但那人也是没手纸,为难半天,也发现那片脏纸,企图我走后应急。如此相持许久,均心照不宣,后同时欲先下手为强,偏又进来一人,背一篓,拄一铁条,为捡废纸者,铁条一点,扎去脏纸入篓走了。两人对视,不禁乐而开笑。

居住于 A 城的伯父,沉沦于二十年右派生涯,早妻离子散,平反后已垂垂暮老,多回忆早年英武及故友。我以他大学的一位女生名义去信慰藉,不想他立即复信,只好信来信往,谈当年的友情,谈数十年的思念,谈现在鳏寡人的处境,及至发展到黄昏恋。我半月一封,连续四年不断,且信中一再说要去见他,每次日期将至又以患病推延。伯父终老弱病倒,我去看他,临咽气说:“我等不及她来了。她来了,你把这个箱子交她。”又说一句:“我总没白活。”安详瞑目。掩埋了伯父,打开箱子,竟是我写给他的近百封信,得意为他在爱的幸福中度过晚年,不禁乐而开笑。

陪领导去某地开会,讨论席上,领导突然脖子发痒,用手去摸,摸出一个肉肉的小东西,脸色微红旋又若无其事说:“我还以为是个虱子哩!”随手丢到地上。我低头往地上瞅,说:“噢,我还以为不是个虱子哩!”会后领导去风景区旅游,而我被命令返回,列车上买一个鸡爪边嚼边想,不禁乐而开笑。

有了妻子便有了孩子,仍住在那不足十平方米的单间里。出差马上就要走了,一走又是一月,夫妻想亲热一下,孩子偏死不离家。妻说:“小宝,爸爸要走了,你去商店打些酱油,给你爸爸做一顿好吃的吧!”孩子提了酱油瓶出门,我说:“拿这个去,”给了一大口浅底盘子,“别洒了啊!”孩子走了,关门立即行动。毕,赶忙去车站,于巷口远远看见孩子双手捧盘,一步一小心地回来,不禁乐而开笑。

夜里正在床上半醒半睡,有人影推门闪进来,在立柜里翻,翻出一堆破衣服和书报,扔了;再往架板上翻,翻出各类米袋子、面袋子和书报,扔了;在桌斗里又翻,是一堆读书卡片,凑眼前看了看,扔了。咕哝了一句顺门便走,我在床上说:“朋友,把门拉上,夜里有风的。”小偷把门拉上了。天明起来整理房间,一地乱书乱报,竟发现找了好久未找着的一份资料,不禁乐而开笑。

上大街回来,挤了一身臭汗,牢骚道:“用枪得在街十字路口扫一通!”回家一杯茶未喝尽,楼梯上步声杂乱,巷中有人呼:“大街上有人用枪打死几十个人了!”遂也往街上跑,街上人山人海,弯腰往里挤,问:“尸体在哪儿?”一熟人说:“不是说是你讲的吗?”忽记得那一句顺口的牢骚,不禁乐而开笑。

剧场里正巧和一位官太太邻座,太太把持不住放一屁,四周骚哗;骂问:“谁放的?不文明!”太太窘极不语,骂问声更甚。我站起说:“我放的!”众人骚哗即息,却以手作扇风状,太太也扇,畏我如臭物,回望她不禁乐而开笑。

出外突然有人迎面过来打招呼,立即停下,作疑惑状。“你不认识我了?”“怎不认识?”于是握手,互问哪儿来,到哪儿去,互问老人康健孩子可乖,互说又胖了,又瘦了,半天的淡而无味的话。分手了,终想不起这是谁,不禁乐而开笑。

弄文学的穷朋友来家侃山,酒瘾发而酒瓶仅能空出一杯酒,取马鬃四根,各人蘸吮,却大声划拳:“八匹马,五魁手……你一盅(鬃)!我一盅(鬃)!”窗外卖茶蛋的老妪对老翁说:“怪不得咱出钱让人家写文章宣传咱不干,人家钱多酒量也大,喝了整晌也未醉!”听着不禁乐而开笑。

路过一条小巷,忽见有长队排出,以为又在出售紧俏物件了,急忙列入其中,排到跟前,方见是巷口唯一的厕所,居民等候出恭,不禁乐而开笑。

去给孩子买一双袜子,昨日看时价是一元,今日是一元二角,怏怏出店门,打响一个喷嚏,喷带出一口痰。正想是售货员在嘲笑我,我方有喷嚏打出,一位戴“卫管员”袖章的人却责斥我吐了痰要罚五角钱。掏出那一元钱,卫管员没零钱找,遂再当地吐一口,愤愤而走,走过十步,不禁乐而开笑。

出差去旅社住宿,服务员开发票将“作协”写成“做鞋”,不禁乐而开笑。

夏月偏停电,爬十二层楼梯去办公室,气喘吁吁到门口了,门钥匙却和自行车钥匙系在一起,遗忘在车子锁孔了,不禁乐而开笑。

路遇一女子,回望我嫣然一笑,极感幸福,即趋而前去搭话,女子闪进一家商店,尾随入店,玻璃上映出自己衣服纽扣错位,不禁乐而开笑。

名字是自己的,别人却用得最多,不禁乐而开笑。

写完《笑口常开》草稿,去吸一根烟,返身要眷写时,草稿不见了,妻说:“是不是一大页写过的纸,我上厕所用了。”惊呼:“那是一篇散文!”妻说:“白纸舍不得用,我只说写过的纸就没用了。”急奔厕所,幸而已臭但未全湿,捂鼻子抄出此份,不禁乐而开笑。


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

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

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


大致就先这样了。