线程的使用经验(设置名称、响应中断、使用ThreadLocal)

启动线程的注意事项

方式一

1
2
3
4
5
6
Thread thread = new Thread("thread name") {
public void run() {
// do xxx
}
};
thread.start();

方式二

1
2
3
4
5
6
7
8
9
10
11
public class MyThread extends Thread {
public MyThread() {
super("thread name");
}
public void run() {
// do xxx
}
};
MyThread thread = new MyThread ();
thread.start();

方式三

1
2
3
4
5
6
7
Thread thread = new Thread() {
public void run() {
// do xxx
}
};
thread.setName("thread name");
thread.start();

方式四

1
2
3
4
Thread thread = new Thread(task);
// 传入任务
thread.setName(“thread name");
thread.start();

方式五

1
2
Thread thread = new Thread(task, “thread name");
thread.start();

无论何种方式,启动一个线程,就要给它一个名字!这对排错诊断系统监控有帮助。否则诊断问题时,无法直观知道某个线程的用途。

要响应线程中断

thread.interrupt();
方式一

1
2
3
4
5
6
7
8
9
10
11
Thread thread = new Thread("interrupt test") {
public void run() {
for (;;) {
doXXX();
if (Thread.interrupted()) {
break;
}
}
}
};
thread.start();

方式二

1
2
3
4
5
public void foo() throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
}

方式三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Thread thread = new Thread("interrupt test") {
public void run() {
for (;;) {
try {
doXXX();
} catch (InterruptedException e) {
break;
} catch (Exception e) {
// handle Exception
}
}
}
};
thread.start();

程序应该对线程中断作出恰当的响应。

ThreadLocal

+-------------+---------+
|     ThreadLocal<T>    |
+-------------+---------+
+-------------+---------+
| initialValue() : T    |
| get() : T             |
| set(T value)          |
| remove()              |
+-------------+---------+

顾名思义它是local variable(线程局部变量)。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不 会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。

使用场景

  • To keep state with a thread (user-id, transaction-id, logging-id)
  • To cache objects which you need frequently
  • 隐式传参

注意:使用ThreadLocal,一般都是声明在静态变量中,如果不断的创建ThreadLocal而且没有调用其remove方法,将会导致内存泄露
同时请注意,如果是static的ThreadLocal,一般不需要调用remove。

线程间的协调(lock、condition、wait、notify、notifyAll)

ReentrantLock和Synchronized

Synchronized是Lock的一种简化实现,一个Lock可以对应多个Condition,而synchronized把Lock和Condition合并了,一个synchronized Lock只对应一个Condition,可以说Synchronized是Lock的简化版本。
在JDK 5,Synchronized要比Lock慢很多,但是在JDK 6中,它们的效 率差不多。

不要在Lock和Condition上使用wait、notiffy、notifyAll方法!

Shell编码风格和脚本配置文件格式

Scripting with style 是少见的一篇介绍 Shell 编码风格 的文章,相信对大多数运维人员和linux爱好者都有用,现在将译文奉上。

Shell编码风格

缩进准则

我一般使用2个空格来缩进(尽管大多人使用4个空格),原因是:

  • 输入简单快速;
  • 没有输入一个Tab键,避免不同环境下显示的差异问题;
  • 缩进的效果已经足够,并且没有浪费太多的空间;

译者注:本人也是使用4个空格,如果你也与本文作者的风格不一样,下面说到2个空格的地方请自觉替换成你实际使用的空格数。个人认为,缩进只是一个个人的风格,只要不影响可读性即可。

顺便说一句,尽量不要使用Tab键,它们容易带来麻烦,我只能想到一种情况下它是有用的:here document中的缩进。

分隔长行

如果需要分隔过长的代码,你可以使用下面的任意一种方法:

1) 使用与命令宽度相同的缩进

1
2
activate some_very_long_option \
some_other_option

2) 使用2个空格缩进

1
2
activate some_very_long_option \
some_other_option

从个人的角度来说,除非有特别的需要,我更倾向于第一种形式,因为它突出“上下两行的内容是一起的”这一联系。

分离复合命令

译者注:其实这里的复合命令就是指块语句,例如for/while循环, if分支结构等等。

1
2
3
HEAD_KEYWORD parameters; BODY_BEGIN
BODY_COMMANDS
BODY_END

我习惯于:

  • 将HEAD_KEYWORD和初始化命令或者参数放在第一行;
  • 将BODY_BEGIN同样放在第一行;
  • 复合命令中的BODY部分以2个空格缩进;
  • BODY_END部分独立一行放在最后;

1)if/then/elif/else分支语句

1
2
3
4
5
6
7
if ...; then
...
elif ...; then
...
else
...
fi

2)for循环

1
2
3
for f in /etc/*; do
...
done

3) while/until循环

1
2
3
while [[ $answer != [YyNn] ]]; do
...
done

4) case分支语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case $input in
hello)
echo "You said hello"
;;
bye)
echo "You said bye"
if foo; then
bar
fi
;;
*)
echo "You said something weird..."
;;
esac

几点注意的地方:

  • 如果不是100%需要,匹配部分左右的括号不需要写(译者注:例如写成hello)而不是(hello));
  • 匹配模式与分支的终止符号;;位于同一缩进级别
  • 分支内部的命令多缩进一层;
  • 尽管是可选的,这里还是把最后一个分支的终止符号也写上了;

语法和编码指引

晦涩的语法结构

我们都喜欢一些晦涩的语法结构,因为它们很简洁。但是如果不是100%需要用到,尽量不要使用它们,否则大多数人无法理解你的代码。

所以有有时候,我们需要在代码的智能,效率与可读性之间找到一个平衡点。

如果你一定要使用这种语法结构,记得在用的地方写上一小段注释。

译者注:Shell提供的一些语法糖很难理解,但是有非常简洁实用,本人也很喜欢用,这样可以省下一大堆精力,而且用熟了也没有什么难以理解的,但是作者说的也有道理,这一点就仁者见仁,智者见智了

变量名

因为所有保留的变量名都是大写的,最安全的方法是仅使用小写字母作为变量名,例如读入用户的输入、循环变量等等……:

  • 变量名尽量选择小写字母;
  • 如果你使用大写的变量名,不要使用保留的变量名(一份不完全的列表参见SUS);
  • 如果你使用大写的变量名,最后在变量名前面加一个独特的前缀(例如下面例子中的MY_);

下面是一个例子:

1
2
3
4
5
6
7
8
#!/bin/bash
#
# the prefix 'MY_'
MY_LOG_DIRECTORY=/var/adm/
for file in "$MY_LOG_DIRECTORY"/*; do
echo "Found Logfile: $file"
done

变量初始化

正如C语言一样,最好的处理是在变量声明的时候初始化。

用户可以将一个变量以环境变量的形式传递到脚本中。如果你盲目地假定你使用的所有变量都是未初始化的,其它人可以以环境变量的形式劫持一个变量。

译者注:一个例子说明这一点:

1
2
3
4
5
6
7
8
9
10
$ cat b.sh
if [ -z "$var" ]; then
echo "$var is not set"
var=1
fi
echo "Now, var is equals to $var"
var=2 sh b.sh
Now, var is equals to 2

解决这个问题的方法很简单,将变量初始化:

1
2
3
my_input=""
my_array=()
my_number=0

参数展开

除非你知道自己做的事情,请在参数展开的地方使用双引号

当然,也有一些地方并不需要使用双引号,例如:

  • [[ ]]测试表达式内部是不会展开的;
  • 在case $WORD in语法中WORD也不会展开的;
  • 在变量赋值var=$WORD的地方也是不会展开的

但是在这些地方使用引号并不会出错,如果你习惯于在每个可能展开参数的地方使用引号,你写得代码会很安全。

如果你要传递一个参数作为一个单词列表,你可以不使用引号,例如:

1
2
3
4
5
6
list="one two three"
#
# you MUST NOT quote $list here
for word in $list; do
...
done

函数名称

函数名称应该采用小写的形式,并且有一个很好的意义。函数名称应该容易让人理解,比如f1这个名称虽然容易输入但是对调试和其它人阅读代码造成了很大的困难,它说明不了任何东西。好的函数名称可以帮助说明代码,而不需要额外的注释。

一个或多或少有趣的是:如果你无意这样做,不要把函数名称命名为常见的命令名,新手往往比较容易将脚本或者函数名命名成test,这样就和UNIX的test命令冲突了。

除非绝对必要,仅使用字母、数字和下划线作为函数名称。/bin/ls也是一个合法的Bash函数名称。

译者注:/bin/ls不是一个合法的函数名称。

命令替换

正如文章the article about command substitution [Bash Hackers Wiki]中提及的,你应该使用$( .. )形式。

不过,如果可移植性是一个问题,你可能必须使用反引号的形式...

在任何情况,如果其它展开或者单词分隔并不是你期望的,你应该将命令替换用双引号引起来。

Eval命令

正如Greg据说的:“If eval is the answer, surely you are asking the wrong question.”

避免它,除非绝对必要:

  • eval can be your neckshot(可能是你的麻烦?)
  • 很有可能有其它的方法来实现你需要的;
  • 如果可能,重新思考下脚本的工作过程,当eval的使用不可避免的时候;
  • 如果你实在需要使用,小心慎用;

脚本的基本结构

一个脚本的基本结构是这样的:

1
2
3
4
5
6
7
#!SHEBANG
CONFIGURATION_VARIABLES
FUNCTION_DEFINITIONS
MAIN_CODE

Shebang

如果可能,请不要忘记shebang。

请小心使用/bin/sh作为shebang,在Linux系统中,/bin/sh就是Bash这是一个错误的观点。

于我而言,shebang有两个目的:

  • 说明直接执行时以哪个解释器来执行;
  • 明确该脚本应该以哪个解释器来执行;

配置变量

在这里,我将这一类变量——可以被用户更改的——叫做配置变量。

让这类变量容易找到,一般放在脚本的头部,给它们有意义的名称并且加上注释说明。正如上面说的,仅当你知道你为什么这么做的时候,才用大写的变量名形式,否则小写形式更加安全。

函数定义

所有函数定义应该在脚本主要代码执行之前,这样可以给人全局的印象,并且确保所有函数在使用之前它是已知的。

你应该使用可移植性高的函数定义形式,即不带function关键字的形式。

脚本行为和健壮性

当脚本检测到问题时尽早退出,以免执行潜在的问题;
如果你需要用到的命令可能并没有安装在系统上,在脚本执行的时候最好检查命令是否存在并且提醒用户缺少什么;
采用有意义的脚本返回值,例如0代码成功,1代码错误或者失败;

其它

输出内容

  • if the script is interactive, if it works for you and if you think this is a nice feature, you can try to save the terminal content and restore it after execution;(译者注:不理解这一点是什么意思)
  • 在屏幕中输出简单易理解的消息;
  • 使用颜色或者特别的前缀区分错误和警告信息;
  • 输出正常的内容到STDOUT,而输出错误、警告或者诊断的信息到STDERR;
  • 在日志文件中输出所有详细的信息;

输入

不要盲目地假设任何事情,如果你希望用户输入一个数字,请在脚本中主动检查它是否真得是一个数字,检查头部是否包含0,等等。我们都应该知道这一点,用户仅仅是用户而不是程序员,他们会做他们想要的,而不是程序想要的。

Sehll脚本配置文件格式

开发过程中为了减少 hardcode,不可避免的需要提供配置文件给用户定制。对于高级编程语言来说,因为有丰富的第三方库,可供选择的配置文件格式有很多,比如 xml、jsno、ini、yaml 等等。

key=value 文本格式配置

而对于 linux shell,基本上很难使用前面提到的各种格式。所以在 unix 系统上,很多 shell 脚本的配置文件都是纯粹的 key=value 文本格式,例如绝大多数的开机服务启动脚本、网络配置文件等。

例子 1:ntp 配置文件

1
2
3
4
5
6
7
8
9
$ cat /etc/sysconfig/ntpd
# Drop root to id 'ntp:ntp' by default.
OPTIONS="-u ntp:ntp -p /var/run/ntpd.pid"
#
# Set to 'yes' to sync hw clock after successful ntpdate
SYNC_HWCLOCK=no
#
# Additional options for ntpdate
NTPDATE_OPTIONS=""

例子 2:网络配置文件

1
2
3
$ cat /etc/sysconfig/network
NETWORKING="yes"
HOSTNAME="xx.com"

而且,要注意得是,一般key=value的等号两边不应该有空格,因为大多数脚本都是直接 source 配置文件的(当然,也有部分脚本是会自己处理配置文件格式),使用起来很简单,基本上没有解析的操作:

1
2
3
4
5
6
$ cat /etc/init.d/network
if [ ! -f /etc/sysconfig/network ]; then
exit 0
fi
#
. /etc/sysconfig/network

理所当然,这种格式无法满足更复杂的配置文件需求,比如 ini 格式的 section。那么,在 shell 中除了满世界去找一个解析库之外,能有什么方法可以实现呢?

扩展 key=value 文本格式配置

假设,我们管理着 n 个集群,每个集群配置项都是一样的,我们需要在 shell 脚本中,可以根据集群的名称来导入对应的配置。

下面我们介绍一种最简单的方法,只需要针对第一种格式扩展下即可。我们创建一个配置文件目录conf.d,在这个目录下存放各个集群的配置文件。每个集群对应一个配置文件,文件名为集群名称,例如:

1
2
3
$ cat conf.d/CLUSTER_A
c_cluster_name="CLUSTER_A"
c_cluster_type=1

在脚本中,我们可以这样来导入相应集群的配置:

1
2
3
4
5
6
7
8
9
10
function load_config()
{
local cluster_name="$1"
if [ -f "conf.d/$cluster_name" ]; then
. conf.d/$cluster_name
fi
}
#
load_config CLUSTER_A

因为各个集群的配置文件相互独立,所以如果包含一些全局范围的配置项,需要在每个配置文件中都增加。或者,再增加一个入口的配置文件:

1
2
3
# cat global.conf
g_conf_dir=conf.d # 配置文件目录
g_version="0.1" # 全局配置

脚本相应调整下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GLOBAL_CONF=/etc/xxx/global.conf
if [ -f "$GLOBAL_CONF" ]; then
. $GLOBAL_CONF
fi
if [[ "$g_conf_dir" != /* ]]; then # 如果是相对路径
g_conf_dir="$(dirname $GLOBAL_CONF)/$g_conf_dir"
fi
function load_config()
{
local cluster_name="$1"
if [ -f "$g_conf_dir/$cluster_name" ]; then
. $g_conf_dir/$cluster_name
fi
}
load_config CLUSTER_A

类 ini 配置格式

第二种方法,基本上已经可以解决我们之前假设中提出的需求,简单而且实现方便,不足的是配置文件比较零散,管理上可能不是很方便。如果,你仍然倾向于一种类似 ini 格式的配置,可以试试下面这种方法。

在这种场景下,每个集群应该是一个独立的 section,所以转换成 ini 格式,配置文件应该是这样的:

1
2
3
4
5
6
[DEFAULT]
g_version="0.1" ; 全局配置
[CLUSTER_A]
c_cluster_name="CLUSTER_A"
c_cluster_type=1

但是,我们前面提到过,原生的 shell 是很难去解析 ini 格式的配置文件的,所以上面的形式还得变化下,我们用 shell 中的函数来模拟 section:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat cluster.conf
# global config
g_version="0.1" # 全局配置
function section_cluster_a()
{
c_cluster_name="CLUSTER_A"
c_cluster_type=1
}
function section_cluster_b()
{
c_cluster_name="CLUSTER_B"
c_cluster_type=1
}

配套的配置文件解析库:

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
$ cat config.sh
#!/bin/echo Warnning, this library must only be sourced!
if [ ! -f "cluster.conf" ]; then
exit 0 # or print error before exit
fi
. cluster.conf
function load_config()
{
local cluster_name=$(echo "$1" | tr A-Z.- a-z__) # 特殊符号转换
section_$cluster_name &>/dev/null # 执行函数,将集群的配置赋值给对应的全局变量
}
function option()
{
local opt_name="$1"
if [[ "$opt_name" != c_* ]]; then # no "c_" prefix
opt_name="c_$opt_name"
fi
echo "${!opt_name}" # indirect reference by variable name
}

测试脚本:

1
2
3
4
5
$ cat test.sh
. config.sh
load_config CLUSTER_B
option cluster_name # puts CLUSTER_B

PET运维工具中,我是采取了第三种方式来维护集群的配置文件,基本上可以满足绝大多数需求。大家也可以贴出你们是怎么设计shell的配置文件的?我们探讨下!

java中的transient, volatile, strictfp关键字用法

Volatile - 修饰符告诉编译器被volatile修饰的变量可以被程序的其他部分改变。在多线程程序中,有时两个或更多地线程共享一个相同的实例变量。考虑效率问题,每个线程可以自己保存该共享变量的私有拷贝。实际的变量副本在不同的时候更新,比如当进入synchronized方法时。

该变量声明为volatile(不稳定的),这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。

volatile修饰变量。在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

Reference:

http://developer.51cto.com/art/201105/264855.htm

http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html

Strictfp - 确保浮点运算,其意思是FP-strict,即 strict float point (精确浮点),也就是说精确浮点的意思。在java虚拟机进行浮点运算时,如果没有指定strictfp关键字时,Java的编译器以及运行环境在对浮点数的表达式是采取一种近似于我行我素的行为来完成这些操作,以至于得到的结果往往无法令你满意。而一旦使用了strictfp来声明一个类,借口或者方法时,那么所声明的范围内java的编译器以及运行环境会完全依照浮点规范IEEE-754来执行。

你可以将一个类、接口以及方法声明为strictfp,但是不允许对接口中的方法以及构造函数声明strictfp关键字。

native 关键字

native是方法修饰符。Native方法是由另外一种语言(如c/c++,FORTRAN,汇编)实现的本地方法。因为在外部实现了方法,所以在java代码中,就不需要声明了,有点类似于借口方法。Native可以和其他一些修饰符连用,但是abstract方法和Interface方法不能用native来修饰。

为什么需要使用native method?请参考:
http://www.javaeye.com/topic/72543 java Native Method初涉

Transient - 声明一个实例变量,当对象存储或序列化时,它的值不需要维持

Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想
用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。
transient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。

Reference:

http://www.blogjava.net/fhtdy2004/archive/2009/06/20/286112.html

1.transient的作用及使用方法

我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

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
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @description 使用transient关键字不序列化某个变量
* 注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致
*
* @author zxc
* @date 2011-12-09
*/
public class TransientTest {
public static void main(String[] args) {
User user = new User();
user.setUsername("zxc");
user.setPasswd("123456");
System.out.println("read before Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
try {
ObjectOutputStream os = new ObjectOutputStream(
new FileOutputStream("/home/zxc/user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/home/zxc/user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close();
System.out.println("\nread after Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class User implements Serializable {
private static final long serialVersionUID = 9294180014912103005L;
private String username;
private transient String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
}

输出为:

    read before Serializable:   
    username: zxc  
    password: 123456  

    read after Serializable:   
    username: zxc  
    password: null

密码字段为null,说明反序列化时根本没有从文件中获取到信息。

2. transient使用小结

  • 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
  • transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
  • 被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
    第三点可能有些人很迷惑,因为发现在User类中的username字段前加上transient关键字后,程序运行结果依然不变,即static类型的username也读出来为“zxc”了,这不与第三点说的矛盾吗?实际上是这样的:第三点确实没错(一个静态变量不管是否被transient修饰,均不能被序列化),反序列化后类中static型变量username的值为当前JVM中对应static变量的值,这个值是JVM中的不是反序列化得出的,不相信?好吧,下面我来证明:
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
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @description 使用transient关键字不序列化某个变量
* 注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致
*
* @author zxc
* @date 2011-12-09
*/
public class TransientTest {
public static void main(String[] args) {
User user = new User();
user.setUsername("zxc");
user.setPasswd("123456");
System.out.println("read before Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
try {
ObjectOutputStream os = new ObjectOutputStream(
new FileOutputStream("/home/zxc/user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
// 在反序列化之前改变username的值
User.username = "alibaba";
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/home/zxc/user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close();
System.out.println("\nread after Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class User implements Serializable {
private static final long serialVersionUID = 9294180014912103005L;
public static String username;
private transient String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
}

输出为:

    read before Serializable:   
    username: zxc  
    password: 123456  

    read after Serializable:   
    username: alibaba  
    password: null

这说明反序列化后类中static型变量username的值为当前JVM中对应static变量的值,为修改后alibaba,而不是序列化时的值zxc。

3. transient使用细节——被transient关键字修饰的变量真的不能被序列化吗?

思考下面的例子:

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
import java.io.Externalizable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
/**
* @descripiton Externalizable接口的使用
*
* @author Alexia
* @date 2011-12-09
*/
public class ExternalizableTest implements Externalizable {
private transient String content = "是的,我将会被序列化,不管我是否被transient关键字修饰";
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
content = (String) in.readObject();
}
public static void main(String[] args) throws Exception {
ExternalizableTest et = new ExternalizableTest();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
new File("test")));
out.writeObject(et);
ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
"test")));
et = (ExternalizableTest) in.readObject();
System.out.println(et.content);
out.close();
in.close();
}
}

content变量会被序列化吗?好吧,我把答案都输出来了,是的,运行结果就是:

    是的,我将会被序列化,不管我是否被transient关键字修饰

这是为什么呢,不是说类的变量被transient关键字修饰以后将不能序列化了吗?

我们知道在Java中,对象的序列化可以通过实现两种接口来实现,若实现的是Serializable接口,则所有的序列化将会自动进行,若实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关。因此第二个例子输出的是变量content初始化的内容,而不是null。

对Java中ThreadLocal的理解

常见线程安全策略

synchronized这类线程同步的机制可以解决多线程并发问题,在这种解决方案下,多个线程访问到的,都是同一份变量的内容。为了防止在多线程访问的过程中,可能会出现的并发错误。不得不对多个线程的访问进行同步,这样也就意味着,多个线程必须先后对变量的值进行访问或者修改,这是一种以延长访问时间来换取线程安全性的策略

而ThreadLocal类为每一个线程都维护了自己独有的变量拷贝。每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了,那就没有任何必要对这些线程进行同步,它们也能最大限度的由CPU调度,并发执行。并且由于每个线程在访问该变量时,读取和修改的,都是自己独有的那一份变量拷贝,变量被彻底封闭在每个访问的线程中,并发错误出现的可能也完全消除了。对比前一种方案,这是一种以空间来换取线程安全性的策略

java.lang.ThreadLocal类的API说明

1
2
ThreadLocal()
//创建一个线程本地变量。
1
2
T get()
//返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
1
2
3
4
protected T initialValue()
//返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。
//若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。
1
2
void remove()
//移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。
1
2
void set(T value)
//将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。

在程序中一般都重写initialValue方法,以给定一个特定的初始值。

应用场景

1.来看一个运用ThreadLocal来实现数据库连接Connection对象线程隔离的例子。

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
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
@Override
protected Connection initialValue() {
Connection conn = null;
try {
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test", "username",
"password");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
public static void setConnection(Connection conn) {
connectionHolder.set(conn);
}
}

通过调用ConnectionManager.getConnection()方法,每个线程获取到的,都是和当前线程绑定的那个Connection对象,第一次获取时,是通过initialValue()方法的返回值来设置值的。通过ConnectionManager.setConnection(Connection conn)方法设置的Connection对象,也只会和当前线程绑定。这样就实现了Connection对象在多个线程中的完全隔离。在Spring容器中管理多线程环境下的Connection对象时,采用的思路和以上代码非常相似。

2.Hiberante的Session 工具类HibernateUtil
这个类是Hibernate官方文档中HibernateUtil类,用于session管理。

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
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory; //定义SessionFactory
static {
try {
// 通过默认配置文件hibernate.cfg.xml创建SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化SessionFactory失败!", ex);
throw new ExceptionInInitializerError(ex);
}
}
//创建线程局部变量session,用来保存Hibernate的Session
public static final ThreadLocal session = new ThreadLocal();
/**
* 获取当前线程中的Session
* @return Session
* @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 如果Session还没有打开,则新开一个Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //将新开的Session保存到线程局部变量中
}
return s;
}
public static void closeSession() throws HibernateException {
//获取线程局部变量,并强制转换为Session类型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}

在这个类中,由于没有重写ThreadLocal的initialValue()方法,则首次创建线程局部变量session其初始值为null,第一次调用currentSession()的时候,线程局部变量的get()方法也为null。因此,对session做了判断,如果为null,则新开一个Session,并保存到线程局部变量session中,这一步非常的关键,这也是“public static final ThreadLocal session = new ThreadLocal()”所创建对象session能强制转换为Hibernate Session对象的原因。

ThreadLocal实现机制

那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

没有什么魔法,在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。

线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。

为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

代码已经说的非常直白,就是获取和设置Thread内的一个叫threadLocals的变量,而这个变量的类型就是ThreadLocalMap,这样进一步验证了上文中的观点:每个线程都有自己独立的ThreadLocalMap对象。打开java.lang.Thread类的源代码,我们能得到更直观的证明:

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

那么接下来再看一下ThreadLocal类中的get()方法,代码是这么说的:

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
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

这两个方法的代码告诉我们,在获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。

进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。

设置到这些线程中的隔离变量,会不会导致内存泄漏呢?ThreadLocalMap对象保存在Thread对象中,当某个线程终止后,存储在其中的线程隔离的变量,也将作为Thread实例的垃圾被回收掉,所以完全不用担心内存泄漏的问题。在多个线程中隔离的变量,光荣的生,合理的死,真是圆满,不是么?

ThreadLocal使用的一般步骤

  • 在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
  • 在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
  • 在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

不适用场景

ThreadLocal变量的这种隔离策略,也不是任何情况下都能使用的。如果多个线程并发访问的对象实例只允许,也只能创建那么一个,那就没有别的办法了,老老实实的使用同步机制来访问吧。

Effective Java读书笔记

创建和销毁对象

第1条:考虑用静态工厂方法代替构造函数

好处

  • 静态工厂方法有名字,表达更清楚。
  • 不需创建新的对象。
  • 可以返回原返回类型的子类型对象。应用:
    a) 返回类型定义为接口,则返回的所有实例对象的细节都可以被隐藏,如Collections。
    b) 返回对象所属的类在写该静态方法时可以不存在。

缺点

  • 类如果不含public或者protected构造函数,就不能被子类化,如不能实例化Collections的任一实现类。
  • 与其他静态方法没有区别,不像构造函数一样明显。但使用标准的命名习惯:valueOf和getInstance,可以客服一定的缺点。

第2条:使用私有构造函数强化singleton属性

将构造函数设为private是为了避免其他类调用已确保唯一性(不写显示构造函数则系统提供默认构造函数且为public)。

两种实现singleton的方法

1
2
3
public static final Singleton INSTANCE = new Singleton();
//
private Singleton(){}

构造方法只在初始化INSTANCE时被调用一次。

1
2
3
4
5
private static final Singleton INSTANCE = new Singleton();
//
private Singleton(){}
//
public static getInstance(){ return INSTANCE};

第一个好处在于类成员明确表明了类是一个singleton;而第二个则提供了灵活性,允许在不改变API的情况下改变类是否为singleton。
singleton类在序列化的时候需要提供readResolve()确保singleton性,否则一个序列化的实例在每次反序列化的时候,都会导致创建一个新的实例。

1
2
3
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}

第3条:通过私有构造函数强化不可实例化的能力

缺点:类不能被子类化,因为子类的构造函数需要调用超类的构造函数。

第4条:避免创建重复的对象

String s = “s”;优于String s = new String(“s”);因为后者每次都创建新的对象而前者不是。

同时提供静态工厂函数和构造函数的非可变类,使用静态工厂函数可以避免每次创建新的对象,如Boolean.valueOf(String)优于Boolean(String)。

不会被修改的可变类

只有重量级的对象才需要尽可能的避免创建对象工作,应该重用则重用。

第5条:消除过期的对象引用

“清空对象引用”这样的操作应该是一种例外,而不是一种规范行为。类自己管理它的内存,就应该警惕内存泄漏问题。一旦一个元素被释放掉,则该元素中包含的任何对象引用应该要被清空。

内存泄漏的另一个常见来源是缓存。

第6条:避免使用终结函数

一般用try-finally块来回收其他的非内存资源。

JLS不仅不保证终结函数会被及时地执行,而且根本就不保证它们会被执行。

如果一个类封装的资源(例如文件或者线程)确实需要回收,只需提供一个显式的终止方法,并要求该类的客户在每个实例不再有用的时候调用这个方法。该实例必须记录下自己是否已经被终止了:显式的终止方法必须在一个私有域中记录下“该对象已经不再有效了”,其他的方法必须检查这个域,如果在对象已经被终止之后,这些方法被调用的话,那么它们应该抛出IllegalStateException异常。

显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。在finally子句内部调用显式的终止方法可以保证:即使在对象被使用的时候有异常被抛出来,该终止方法也会被执行。

finalize()的合理用途:

  • 充当释放资源最后的安全网(虽然效果不确定,但比完全没有好)。
  • 终止非关键的本地资源。

使用了终结函数,就要调用super.finalize。如果要把一个终结函数与一个公有的非final类关联起来,考虑使用终结函数守卫者,以确保即使子类的终结函数未能调用super.finalize,该终结函数也会被执行。

对于所有对象都通用的方法

尽管Object是一个具体类,但是设计它主要是为了扩展。它的所有非final方法(equals、

hashCode、toString、clone和finalize)都有明确的通用约定(general contract),它们都是为了要被改写(override)而设计的。任何一个类在改写这些方法的时候,有责任遵守这些通用约定;如果不能做到这一点,则其他一些依赖于这些约定的类就无法与这些类结合在一起正常运作。

第7条:在改写equals的时候请遵守通用约定

不需改写equals的情况

  • 一个类的每个实例本质上都是惟一的。
  • 不关心一个类是否提供了“逻辑相等(logical equality)”的测试功能。
  • 超类已经改写了equals,从超类继承过来的行为对于子类也是合适的。
  • 一个类是私有的,或者是包级私有的,并且可以确定它的equals方法永远也不会被调用。

但最好改写,以免万一有一天它会被调用到,如下:

1
public boolean equals(Object o){ throw new UnsupportedOperationException();}

equals()约定:等价关系

  • 自反性。x==x。
  • 对称性。x==y,则y==x。
  • 传递性。x==y,y==z,则x==z。
  • 一致性。相等的永远相等,不等的永远不等。
  • 非零性。x!=null,x==null=false。

标准equals()写法

  • if (this == obj) return true; //优化比较
  • if (obj == null) return false; //优化比较
  • if (getClass() != obj.getClass()) return false;
    转换obj类型为比较类型
  • 对象的关键域比较
    float域:使用Float.floatToIntBits转换成int类型的值,然后使用==操作符比较int类型的值。(float存在特殊常量,double类似)
    double域:使用Double.doubleToLongBits转换成long类型的值,然后使用==操作符比较long类型的值。
    引用域:null合法使用field == null ? obj.field == null : field.equals(obj.field)
    优先比较最可能不一致的域或开销最低的域,两者皆满足最好。

equals()建议

  • 写完后验证对称、传递、一致性,特别是前两个。
  • 改写equals()同时改写hashCode()。
  • 让equals()简单而不是过于聪明地适应更多的等级关系。
  • 不要使equals()依赖于不可靠的资源。
  • 不要将equals()声明中的Object对象替换为其他的类型,这样是overload而不是override。

第8条:改写equals时总是要改写hashCode

hashCode()约定

  • 程序运行期,equals()信息不变则hashCode()值不变;程序两次运行中的hashCode()值可以不一致。
  • x.equals(y),则x.hashCode() == y.hashCode()。
    如果x,y的类不改写hashCode(),则默认调用Object.hashCode(),x,y为两个对象,Object.hashCode()每个对象的值都不一样,即x.hashCode() != y.hashCode()。
  • x,y不equals,则它们的hashCode()不必不等,但最好不等以提高性能。

hahsCode()标准写法

  • 把某个非零常数值,例如17,保存在一个叫result的int类型的变量中。
  • 对于对象中每一个关键域f(指equals方法中考虑的每一个域),完成以下步骤:
    a) 为该域计算int类型的散列码c
    1. boolean,计算(f ? 0 : 1)。
    2. byte、char、short或者int,计算(int)f。
    3. long,计算(int)(f ^ (f >>> 32))。
    4. float,则计算Float.floatToIntBits(f)。
    5. double,计算Double.doubleToLongBits(f)得到一个long类型的值,再按iv.对该long型值计算散列值。
    6. 对象引用,且该类的equals方法通过递归调用equals的方式来比较这个域,则同样对这个域递归调用hashCode()。如果要求一个更为复杂的比较,则为这个域计算一个“规范表示(canonicalrepresentation)”,然后针对这个范式表示调用hashCode()。如果这个域的值为null,则返回0(或者其他某个常数,但习惯上使用0)。
    7. 数组,把每一个元素当做单独的域来处理。
      b) result == 37 * result + c(eclipse自动生成hashCode()时是用的31)。
  • 返回result。
  • 检查“是否相等的实例具有相等的散列码”。

第9条:总是要改写toString

主要就是要用toString()将一个对象进行间接而又尽量清楚的说明,其他没什么好说的,具体内容看书吧。

第10条:谨慎地改写clone

clone()注意

  • 新的SDK中已没有不能在clone()中调用构造器的规定。
  • 对于复杂对象(包含对象域的对象),需要递归地调用clone(),但需要注意递归层次过多时容易造成栈溢出。
  • clone结构与指向可变对象的final域的正常用法是不兼容的,因为无法使用除初始化以外的方法给final域赋值。需要找到方案在原始对象和克隆对象之间可以安全地共享此可变对象。
  • 复制复制对象的一个优雅方法:先调用super.clone(),然后把结果对象中的所有域都设置到它们的空白状态,然后调用高层的方法来重新产生对象的状态,但运行起来没有“直接操作对象和其克隆的内部状态的clone方法”快。
  • clone方法不应该在构造过程中,调用新对象中任何非final方法(private方法可以看做final)。
  • Cloneable接口不应该不被其他接口继承,为继承而设计的类也不应实现该接口。

clone()好的替代方法

  • 拷贝构造函数,以本身对象为唯一参数的构造函数。
  • 静态工厂方法,同样以要拷贝的对象为唯一参数。

第11条:考虑实现Comparable接口

一个类实现了Comparable接口,就表明它的实例具有内在的排序关系。

约定

  • x.compareTo(y) == - y.compareTo(x)(这也暗示着,当且仅当y.compareTo(x)抛出一个异常的时候,x.compareTo(y)也必须抛出一个异常)。
  • 可传递性。
  • 若x.compareTo(y) == 0则x.compareTo(z) == y.compareTo(z)。
  • 强烈建议(x.compareTo(y) == 0) == (x.equals(y)),任何实现了Comparable接口的类,若违反了这个条件,应该明确予以说明:“注意:该类具有内在的排序功能,但是与equals不一致。”

注意

  • 没有一种简单的方法可以做到,在扩展一个新的可实例化的类的时候,既增加了新的特征,同时又保持了compareTo约定。
    如果你想为一个实现了Comparable接口的类增加一个关键特征,请不要扩展这个类;而是编写一个不相关的类,其中包含一个域,其类型是第一个类。然后提供一个“视图(view)”方法返回这个域。这样做,你既可以自由地在第二个类上实现compareTo(),同时也允许客户在必要的时候,把第二个类的实例当做第一个类的实例来看待。
  • 在对实参进行类型转换之前,不需要检查实参的类型。
    如果实参的类型不合适,则compareTo()应该抛出ClassCastException异常。如果实参为null,则compareTo()应该抛出NullPointerException异常。
  • 比较对象引用域可以通过递归地调用compareTo()来实现。
    如果一个域并没有实现Comparable接口,或者需要使用一个非标准的排序关系,可以使用一个显式的Comparator。或者编写专门的Comparator,或者使用已有的Comparator。
  • 比较原语类型的域,可以使用关系操作符<和>;比较数组域时,可以把这些指导原则应用到每一个元素上。
    如果一个类有多个关键域,必须从最关键的域开始,逐步进行到所有的重要域。如果有一个域的比较产生了非零的结果,则整个比较操作结束,并返回该结果。如果最关键的域是相等的,则进一步比较次最关键的域,以此类推。如果所有的域都是相等的,则对象是相等的,返回零。

类和接口

第12条:使类和成员可访问能力最小化

尽可能是每一个类或成员不被外界访问

  • 设计了公有API后,将其他所有成员都变成private,确实有必要的情况下才给予默认或protected。
  • 公有类的protected成员是类导出API的一部分,必须永远被支持,每一个protected成员都代表了该类对一个实现细节的公开承诺,应尽量少用。
  • 共有类不应包含公有域,静态final常量除外,这些final域指向的对象应是非可变对象。
    非零长度的数组是可变的(数组长度不变但数组中对象可变),所以具有公有的静态final域数组几乎总是错误的。

第13条:支持非可变性

非可变类实例不能被修改,实例中包含的所有信息必须在该实例被创建的时候就提供出来,即所有域都应该被初始化。

约定

  • 不提供任何会修改对象的方法。
  • 保证没有可被子类改写的方法。
    常有做法是设置类为final,更好的替代方案是将类的构造方法设为private或package,添加公有的静态工厂方法代替公有的构造方法。这种方案最灵活,允许在包内对类进行扩展,而对包外的用户来说类又相当于是final的;同时,静态工厂相对构造函数也有优势。
  • 所有的域都是final。
    真正的要求是没有方法能对对象状态产生外部可见的变化。可以有若干冗余非final域来对一些计算结果进行缓存处理。
  • 所有的域都是private。
  • 保证对于任何可变组件的互斥访问。
    如果类具有指向可变对象的域,必须确保该类客户无法获得指向这些对象的引用;不使用客户提供的引用来初始化这样的域;不在任何一个访问方法中返回该对象的引用;在构造函数、访问方法和readObject()中使用保护性拷贝技术。

注意

  • 非可变类线程安全,不需同步,可以被自由地共享,所以不需要也不应该为其提供clone()或构造拷贝函数。
  • 非可变类对象的内部信息业可以共享。
  • 真正唯一的缺点:对不同值都要有一个单独的对象,某些时候可能对性能影响较大,解决方法是对其添加可变配套类进行优化,如String和StringBuffer。
  • 如果非可变类包含若干指向可变对象的域而又实现了Serializable接口,必须提供一个显示的readObject()或readResolve(),默认的readObject()可以使一个攻击者用非可变类创建可变的实例。
  • 坚决不要为get()编写相应的set(),除非确有必要使一个非可变类成为可变的。总是应该让一些小的值对象成为非可变类。可以考虑将一些大的值对象成为非可变类,在确实有性能优化的必要时才为非可变类提供公有的可变配套类。
  • 如果一个类不能成为非可变类,也仍然要尽量限制其可变性。构造函数应该完全初始化对象并建立所有的约束关系,除非有绝对的理由,否则不要在构造函数之外再提供一个公有的初始化方法或一个“重新初始化”的方法。

第14条:复合(组合)优先于继承

这里的继承仅指类之间的继承(extends)而不包括实现接口(implements)。

包内使用继承安全,包外使用继承危险,因为其打破了包的封装性。除非被继承的超类是专门为继承而设计并有很好的说明文档。

包外的子类改写包内的超类方法时,由于超类没有暴露方法的细节,所以其实现方式很可能和你想象中的准备用在改写子类方法中的方式不一样,从而造成各种问题;同时,在向子类中添加超类没有的新方法时,你也不能确定超类以后会不会添加和该方法冲突的方法。使用复合(组合),将超类作为新类的一个私有域可以解决以上问题。

只有A和B是is a关系时才使用继承,即每一个B都是A。使用复合,说明B本质上和A不完全相同。

第15条:要么专门为继承而设计,并给出说明文档,要么禁止继承

对于专门为继承而设计的类注意

  • 文档必须精确地描述了改写每一个方法所带来的影响:对于每一个public或protected的方法或构造函数,必须指明它调用了哪些可改写的方法,调用的顺序,每个调用结果又是如何处理后续的处理过程的等等。
  • 必须通过某种形式提供适当的钩子(hook),以便能够进入它的内部工作流程中,形式可以是精心选择的protected方法或保护域。
  • 构造函数、clone()和readObject()都不能调用一个可改写的方法,无论是直接或间接地调用。超类的构造函数在子类构造函数之前运行,所以子类中被改写的方法会在子类构造函数运行之前被调用从而造成错误结果。
  • 若实现Serializable并有readResolve()或writeReplace(),则必须使这两个方法称为protected而不是private。

禁止继承

对于那些并非为了安全地进行子类化而设计和编写文档的类,禁止子类化。如果因为某些原因必须要扩展一个类,那么确保这个类不会在方法和构造函数中直接或间接地调用其可被改写的方法(public和protected),即有自用特性(会被本类的其他方法调用)的方法应该是private的。

第16条:接口优于抽象类

接口定义一种服务/功能,实现该接口的类可以提供这种服务/功能。

对于希望导出的重要接口,提供一个抽象的骨架实现类来负责接口实现的相关工作(提供基本的骨架代码)。骨架类是为继承而设计的,当然要遵循设计继承类的要求,中文版这里翻译错了,多了一个“不”,英文原版是 “Because skeletal implementationsare designed for inheritance, you should follow all of the design and documentation guidelines in Item 15.”

第17条:接口只是被用于定义类型

常量接口是对接口的不良使用。定义在接口中的常量或方法应该是永远不变的。导出常量应该采用类型安全枚举类或不可被实例化的工具类来实现。

第18条:优先考虑静态成员类

静态成员类

  • 可以看做普通类,其实例化的时候不需要外围类的实例。
  • 访问规则上可以看做外围类的一个静态成员。
  • 通常的用法是做公有的辅助类与其外部类一起使用,表示类与类之间的关系;私有静态成员类通常用法是代表外围类对象的组件。
  • 如果声明的成员类不需要访问外围类实例,则使用静态成员类,这样效率更好。

非静态成员类

  • 实例化的时候需要一个外围类的实例,实例化后和创建其的外围类实例形成一个一对一的关系,表示实例与实例间的关系。
  • 通常用在适配器模式中,成员类适配为一个和外围类不相关的类,外围类的实例通过成员类取得这个不相关类的功能。和组合中将不相关类作为类的一个私有域类似,但组合中如果没有成员类的话,就只能直接使用已有不相关的类的实例(如果声明私有域的时候用的是接口,那么就必须有其他类实现了这个接口,从而才能使用),而成员类则可以用来对类进行扩展(实现接口或继承类),可以看做能进行扩展的私有域。
  • 如果成员类是导出类的public或protected域,则不能为静态成员类。

匿名类

  • 同时被声明和实例化,只能被用作被实例化的那个点上,被实例化之后不能再被引用,即只能用一次。
  • 不会声明新方法,因为新增方法无法在“一次性”后被访问。
  • 简短不影响代码可读性。
  • 常见用法:创建函数对象(实现接口,或者说实现接口中的方法);创建过程对象(如Thread);用在静态工厂方法内部;在复杂的类型安全枚举类型中(要求为每个实例提供单独的子类)用于公有的静态final域的初始化器中。

C语言结构的替代

虽说这章主要面向熟悉C语言的读者,但对C不是太熟悉的读者来说看一下了解一下思想还是非常有好处的。

第19条:用类代替结构

公有类不应该直接暴露数据域,包级私有或私有嵌套类可以暴露数据域。

第20条:用类层次来代替联合

第21条:用类来代替enum结构

将常量定义在类中的问题:

  • 性能;
  • 常量名和常量值都要暴露给客户;
  • 书写错误在编译时无法知道。

类型安全枚举

  • 方法:定义一个类代表枚举类型,构造函数私有,提供公有的静态final域,使枚举类型中的每一个常量都对应一个该类定义的域。即用预定义的实例来对应枚举类型中的常量。
  • 提供编译时的类型安全性。
  • 常量值没有被编译到客户代码中,可以方便地添加新的常量而无需编译客户代码。
  • 可以改写toString()打印常量名或常量值。
  • 类可以添加方法或实现任何接口提供很多功能。
  • 运算性能同int型常量,但装载类和构造对象时间、空间开销大。

第22条:用类和接口来代替函数指针

函数指针主要是为了实现Strategy模式,Java中用接口来表示策略,为每一个具体策略声明一个实现该接口的类。如果具体策略只用一次,通常使用匿名类来声明和实例化这个策略;如果具体策略需要被导出以重复使用,则一个私有静态成员类来声明和实例化,再通过一个公有final域导出,其类型为策略接口。

方法

第23条:检查参数的有效性

在方法执行计算任务之前检查其参数,除非有效性检查工作代价非常昂贵、不切实际或计算本身隐含检查。

第24条:需要时使用保护性拷贝

防止客户可以对方法或构造函数中提供的对象的内部状态进行修改。在对象作为参数的构造函数中,如果该对象内部状态能被客户修改,则使用对象的副本进行构造(副本不暴露给客户);在方法返回对象时返回给要保护的对象的副本。

对象内部的组件尽量使用非可变对象。

保护性拷贝动作在检查参数的有效性之前进行,且有效性检查拷贝后的对象而不是原始对象。防止其他线程在检查参数有效性后改变参数对象,使之后进行拷贝的对象不正确。

第25条:谨慎设计方法的原型

  • 谨慎选择方法的名字。规范、易懂。
  • 不要过于追求提供便利的方法。对于一个类型支持的每一个动作提供一个功能完全的方法。只有当某操作被频繁使用的时候才考虑提供快捷方法。
  • 避免长长的参数列表。三个参数一般为最大值。减少参数数目的方法:
    a)将方法分解为多个方法,某个方法的结果可以由若干子方法一起使用来得到。
    b)创建辅助类,将一些频繁出现的参数序列封装到一个辅助类中。
  • 对应参数类型,优先使用接口。
  • 谨慎地使用函数对象。

第26条:谨慎地使用重载

对于重载(Overload)的选择是静态的,即执行编译时确定的实例类型中的方法;对于改写(Override)的选择是动态的,即运行时确定执行具体实例中被改写的方法版本,而不管编译时这个实例的类型。

保守但安全的策略是永远不要导出两个具有相同参数数目的方法。(对,是数目,这就意味着使用重载的时候每个重载的方法参数都必须和其他完全不一致,相当的保守……)如果数目必须相同,那就不使用重载,而是换个名字。

对于构造函数来说,你没有改名字机会,所以你会碰到需要导出相同参数数目的构造函数。但由于构造函数不能被改写,所以不用担心重载和改写的相互影响,只需单纯考虑参数数目相同时怎么办。如果两个重载方法的参数序列中对应位置的参数不能相互转换,那么这两个方法就没有问题。所以应该避免同一组参数经过类型转换就可以被传递给不同的重载方法。如果不能避免的话,就让接受同一组参数所有重载的方法行为一致。

第27条:返回零长度的数组而不是null

这样做客户端代码更简单,不用专门来处理null,性能也不差。书上提高性能的例子是实用中可以经常使用的一个技巧。

1
2
3
4
5
6
7
8
private List cheesesInStock =……;
private final static Cheese[] NULL_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses(){
return (Cheese[])cheesesInStock.toArray(NULL_CHEESE_ARRAY);
}

第28条:为所有导出的API元素编写文档注释

  • 每个方法的注释应简洁地描述出它和客户之间的约定,说明这个方法做了什么(除了专门为继承而设计的类中的方法)。
  • 列出方法的前置条件(由@throws标签和某些受影响的参数@param描述)和后置条件(由@return标签描述)。
  • 描述方法的副作用(系统状态中一个可观察到的变化,它不是为了获得后置条件而要求的变化)和类的线程安全性。
  • 每一个文档注释的第一句话是该注释所属元素的概要描述。

通用程序设计

第29条:将局部变量的作用域最小化

  • 在第一次使用局部变量的地方声明它,声明的时候几乎都应该包含一个初始化表达式。
  • 一般情况下for循环优先于while。
  • 让方法小而集中。

第30条:了解和使用库

第31条:如果要求精确的答案,请避免使用float和double

float和double执行二进制浮点运算,能快速取得较为精确的结果,但不完全精确。需要精确结果时使用BigDecimal,或者用int或long加上相应的自定义操作。

第32条:如果其他类型更适合,则尽量避免使用字符串

  • 不适合代替其他的值类型。虽然某些数据在进入程序中时是以字符串的形式存在,但如果数据的本质不是字符串而是如int等,那就应该转换为相适应的类型。
  • 不适合代替枚举类型。应使用类型安全枚举类型或int值。
  • 不适合代替聚集类型。应使用静态嵌套类。
  • 不适合代替能力表。

第33条:了解字符串连接的性能

大规模字符连接用StringBuffer,因为String为非可变对象,n个String进行连接,性能影响为n的平方。

第34条:通过接口引用对象

用接口来引用对象会使程序更灵活,如果没有合适的接口,则使用类层次结构中提供了所需功能的最高类。

第35条:接口优先于映像机制

映像就是“反射”。通常,普通应用在运行时刻不应该以反射方式访问对象。如果需要处理编译时未知的类,尽量使用反射方式实例化对象,访问对象则使用编译时已知的接口或超类。

反射的缺点

  • 损失了编译时类型检查的好处。
  • 代码冗长。
  • 性能比普通访问慢2倍左右。

第36条:谨慎地使用本地方法

使用本地语言提高性能的做法不值得提倡,JVM已经足够快。本地语言不安全、平台相关、进入和退出时有较高的固定开销。

第37条:谨慎地进行优化

不要因为性能而牺牲合理的结构,性能问题应该在设计(API等)的时候考虑,API设计好了性能自然就好了。

即使使用性能剖析工具,结果也会随着JVM的不同而不同。

第38条:遵守普遍接受的命名惯例

java的命名惯例分为两大类:字面的和语法的,字面的基本上可以视为强制性的,一般必须遵守;语法的稍灵活,但也有一般应该遵守的若干建议规则。

语法命名惯例

  • 类:名词/名词短语;接口或者与类相同,或者以”-able”或”-ible”结尾的形容词。
  • 方法:动词/动词短语。
    a)返回boolean,以“is”开头后加一个名词或形容词或短语。
    b)返回其他类型,用一个名词短语,或以”get”开头的动词短语。如方法所在的类是一个Bean,则强制要求以get开头。
    c)类包含对属性操作,用setAttribute或getAttribute格式命名。
  • 转换对象类型的方法:
    a)toType:返回不同类型的独立的对象,如Object.toString()。
    b)asType:返回视图,如Arrays.asList()。
    c)typeValue:返回与被调用对象同值的原语类型,如Integer.intValue()。
  • 静态工厂方法,用valueOf或getInstance。

异常

第39条:只针对不正常的条件才使用异常

创建、抛出和捕获异常的开销昂贵,其初衷是用于不正常的情形,少有JVM会对它进行优化。正常的控制类里不应为了某种原因去使用异常。一个东西应该只用于被设计的用途,只做它该做的事情。

第40条:对应可恢复的条件使用被检查的异常,对于程序错误使用运行时异常

如果希望调用者能够恢复,应使用被检查的异常,并让调用者处理这个异常;如果程序抛出未检查的异常(运行时异常或错误),则说明这是不可恢复的情况,继续执行有害无益。

用运行时异常指明程序错误,实现的所有未检查的抛出结构应该是RuntimeException的子类,不要再实现Error,Error应只由JVM保留。

不清楚是否可恢复的时候使用未检查异常。(我个人觉得在开发的时候尽量不用被检查的异常,后期再根据情况添加)。

第41条:避免不必要地使用被检查的异常

使用被检查异常的两个条件

  • 正确使用API不能阻止异常条件的产生。
  • 用户可以对产生的异常采取有效的动作。一般在catch中写e.printStackTrace();
    System.exit(1);这样的处理不能算得上是有效的。

如果一个方法只抛出一个被检查的异常,可以使用其他方式来避免使用被检查的异常。

方式例子:将抛出异常的方法分为两个方法,第一个方法返回boolean,表明是否应该抛出异常。具体如下:

1
2
3
4
5
6
7
try{
obj.action(args);
}catch(TheCheckedException e){
//Handle exception condition
}

转换为:

1
2
3
4
5
6
7
if(obj.actionPermitted(args)){
obj.action(args));
}else{
//handle exception condition
}

方式不适用的情况:

  • 同步:对象在缺少外部同步的情况下被并发访问或可被外界改变状态。
  • 性能:actionPermitted()里需要重复action()的工作。

第42条:尽量使用标准的异常

常用异常

异常 使用场合
IllegalArgumentException 参数值不合适
IllegalStateException 对于这个方法调用而言,对象的状态不合适(如初始化不恰当)
NullPointerException 在null被禁止的情况下参数值为null
IndexOutOfBoundsException 下标越界
ConcurrentModificationException 在禁止并发修改的情况下,对象检测到并发修改
UnsupportedOperationException 对象不支持客户请求的方法

重用异常时要确保抛出异常的条件与该异常文档中描述的条件一致,而不是从名字上看可以使用。

第43条:抛出的异常要适合于相应的抽象

异常转移:高层的实现应该捕获低层的异常,同时抛出一个可以按照高层抽象进行解释的异常。

处理低层异常

  • 应尽量使低层的方法得到成功执行,不抛出异常。如检查传递给低层方法的参数。
  • 如果无法阻止低层的异常,则可以让高层来处理这些异常,将高层方法的用户与低层方法的问题隔离。如用日志记录低层的异常。
  • 1、2都不行,则使用异常转移。只有在低层方法的规范可以保证其抛出的异常对高层也适用的情况下才可以将异常从低层传播到高层。

第44条:每个方法抛出的异常都要有文档

第45条:在细节消息中包含失败-捕获信息

一个异常的toString()中应该包含所有对异常有贡献的参数和域的值。可以在异常的构造器中以参数的形式引入以上信息。

第46条:努力使失败保存原子性

失败原子性:一个失败的方法调用应该是对象保持“它在被调用之前的状态”。对被检查的异常这尤为重要。

方式

  • 方法中使用非可变对象。
  • 对于使用可变对象的方法,在执行前检查参数的有效性。即在处理过程中调整顺序,是可能失败的部分发生在对象状态被修改前。
  • 编写恢复代码,解释发生的失败,并使对象状态回滚到操作开始之前。
  • 在对象的临时拷贝上进行操作,操作完成后将临时拷贝上的结果复制给原对象。

第47条:不要忽略异常

线程

第48条:对于共享可变数据的同步访问

同步可以保证线程不会看到对象处于不一致的状态(互斥),还可以让线程看到前一线程对对象状态进行的改变(对象的实时改变,通信)。

Java保证读写一个非long或double的变量是原子的(读写引用也是原子的)。

Java的原子性只保证互斥而不能保证通信,即只能保证线程读原子数据不会看到随机值,但不能保证线程写入的值对其他线程可见,所以在读写原子数据时同步不是可以省略的(除非对一个原子数据只读不写,这样的话就是常量了)。

volatile关键字可以保证线程在读一个域的时候会看到最近被写入的值。

第49条:避免过多的同步

在同步区域中,要控制客户的行为,即不要调用可被改写的public或protected方法。

在同步区域中应该做尽可能少的工作:获得锁,检查共享数据,根据需要转换共享数据,释放锁。

第50条:永远不要在循环的外面调用wait

wait()使用的标准模式@唯一模式

1
2
3
4
5
6
7
8
synchronized(obj){
while(<condition does not hold>)
obj.wait();
//Perform action appropriate condition
}

一般情况下优先使用nofifyAll(),其更安全,可以避免不相关线程意外或恶意的等待。但nofifyAll()在处理信号量、有界缓冲区、读写锁时性能将退化到平方级。如果处于等待状态的所有线程都在等待同一个条件且每次只有一个线程可以从这个条件中被唤醒(如只有一个线程在特定的对象上等待),可以(不是一定要)使用notify()。

第51条:不要依赖于线程调度器

不同JVM线程调度器实现策略不同,依赖于线程调度器而达到正确性或性能要求的程序很可能是不可移植的。

编写健壮的、响应良好的、可移植的多线程应用程序的最好办法:尽可能确保在任何给定时刻只有少量的可运行线程。

保存可运行线程数量尽可能少的主要技术:让每个线程做少量的工作,使用Object.wait()等待某个条件的发生,或者使用Thread.sleep()睡眠一段时间。线程不应该忙等,即反复地检查一个数据结构以等待某些事件的发生。

不要通过Thread.yield()来修正程序。同一个yield()在不同JVM上性能不同,可能变好,也可能变差。一般情况下,yield()的唯一用途是在测试期间人为地增加一个程序的并发性。

不推荐调整线程优先级。线程优先级是Java平台上最不可移植的特征,也会因为JVM的不同存在差异,通过调整线程优先级来解决严重的活性问题是不合理的。

yield()和线程优先级都是影响调度器的设施,可以用来提高一个正常工作的系统的服务质量,但不应用来修正一个不能工作的程序。

第52条:线程安全性的文档化

方法声明中出现synchronized只说明这是一个实现细节,而不是导出API的一部分。有了synchronized并不能确定方法是线程安全的。

线程安全级别

  • 非可变的(immutable):非可变对象,不需要外部同步。
  • 线程安全的(thread-safe):可变对象,有足够的内部同步手段,可以并发使用无需外部同步。并发调用表现为按照某种全局一致的顺序依次执行,如Random和java.util.Timer。
  • 有条件的线程安全(conditionally thread-safe):类(或关联类)中某些方法必须被顺序调用且不能受到其他线程的干扰,即执行这些方法序列时需要获得适当的锁。除此之外,该线程安全级别与thread-safe相同。如HashTable和Vector中的itretor需要外部同步。
  • 线程兼容的(thread-compatible):每个方法调用都需要外部同步才能被安全地并发使用,如HashMap和ArrayList。
  • 线程对立的(thread-hostlie):不能安全地被并发使用。根源在于类中方法会修改静态数据,这些静态数据可能会影响其他线程。Java平台中这样的类或方法很罕见。

对象锁

获得锁一般只实例自身的锁,但如果一个对象代表另一个对象的视图,那需要获得后台对象上的锁。

使用公有可访问的锁对象存在安全隐患。使用内部私有锁对象作为特别适合于专门为继承而设计的类。否则如果超类以它的实例作为锁对象,则子类可能会其造成妨碍。

第53条:避免使用线程组

序列化

第54条:谨慎地实现Serializable

实现Serializable的代价

  • 改变类的实现的灵活性将大大降低。需要仔细设计一个高质量的序列化形式并长期使用。
  • 增加了bug和安全漏洞的可能性。反序列化过程中需要保证由构造函数建立的约束关系且不允许用户访问正在构造的对象的内部信息。
  • 类的新版本发型会增加测试负担。需要测试在不同版本之间相互序列化和反序列化。

判断是否实现Serializable

  • 为继承而设计的类应很少实现Serializable,接口也应很少扩展它。
  • 为继承而设计的不可序列化的类,应该考虑提供一个无参数构造器供子类需要实现Serializable时使用。
  • 内部类应该很少实现Serializable,静态成员类可以实现Serializable。

第55条:考虑使用自定义的序列化形式

理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示应该是独立的。

使用默认序列化的情况

  • 对象的物理表示等同于它的逻辑内容,可考虑默认的序列化方式。
  • 若默认序列化合适,还需要提供一个readObject()以保证约束关系和安全性。

对象物理表示与逻辑内容有实质区别,使用默认的序列化的缺点

  • 类的导出API被永久束缚在类的内部表示上。
  • 消耗过多的空间。将不必要的实现细节也序列化了。
  • 消耗过多的时间。需要进行昂贵的图遍历。
  • 引起栈溢出。需要进行对象图的递归遍历。

Transient域

  • 对象所有域都是transient,最好也不要省略defaultReadObject()和defaultWriteObject(),这样可以在以后增加非transient域时仍能够保持兼容性。
  • 非transient域要保证它的值是该对象逻辑状态的一部分。
  • 自定义序列化中,大多数甚至全部域都应该被标记为transient,在自定义序列化中手动的将这些域设为正确的值。

序列化时,要为自己编写的可序列化类声明一个显示的UID(serial version UID),可以提高兼容性和性能,格式如下:

1
private static final long serialVersionUID = randomLongValue;

randomLongValue可用工具生成,也可以自己随意生成。

第56条:保护性地编写readObject方法

对象被反序列化的时候,对于包含了客户不该拥有的引用的域必须要进行保护性拷贝。

健壮readObject()应遵循的原则

  • 将链接到私有引用域的对象进行保护性拷贝。
  • 检查类的约束条件是否满足,不满足则抛出InvalidObjectException。检查动作在所有保护性拷贝之后。
  • 对象图在反序列化之后整个对象图要保证有效,应使用ObjectInputValidation接口。
  • 不要直接或间接地调用类中被改写的方法。

第57条:必要时提供一个readResolve方法

readResolve()中返回的对象引用将替代反序列化中创建的对象,后者将立即成为gc的回收对象。

用法

  • 用于实例受控的类(如singleton和类型安全枚举类型)。
  • 作为保护性readObject()的一种保守的替代选择。(个人觉得readResolve()比保护性readObject()更好,相比而言代码少,也无需考虑更多细节,性能降低可以忽略)

缺点

不适合允许包外继承的类(有域是protected或public)。如果超类readResolve()为final或者子类没有改写readResolve(),则对子类进行反序列化时会得到一个超类实例从而造成结果不正确;如果子类恶意改写,则会造成安全性问题。

Thinking In java读书笔记(java编程思想)

寒假在家正好一直没事情,就一直看书,看了不下五本左右的编程专业书籍,是该花时间整理下写写读书笔记否则读了忘没有用,读书的时候是把书读厚,写笔记目的是把书写薄,下面是 Think in java 这本书的读书笔记,估算了下大概花了三个月左右时间去看它,内容很多很全,很符合入门级程序员和中级但偶尔有些迷茫的程序员的口味.

第一章:对象简介

1、Java的五大特征,也是面向对象的五大特征:

  • Everything is an object: 万物皆对象
  • A program is a bunch of objects telling each other what to do by sending messages: 程序就是一组相互之间传递信息,告诉对方该干些什么的对象
  • Each object has its own memory made up of other objects: 每个对象都利用别的对象来组建它自己的记忆
  • Every object has a type: 对象都有类型
  • All objects of a particular type can receive the same messages: 所有属于同一类型的对象能接收同样的信息

2、作者在第一章讲的是一些OOP概念上的东西,在我看来也有许多哲学上的东西,多看几遍或许会顿悟.

第二章:万物皆对象

1、在Java中,我们直接操控的不是类本身,而是类的一个实例,或者说是Reference。Java没有地址传递之说。

2、Java把对象,也就是类存放在“堆”里,而把其他数据和对象的reference存放在“栈”里,对操作来收,栈比堆要快。

3、因为栈比堆要快,所以作为特例,Java的primitive类型的变量也存放在栈里,这样可以提高效率,另外一方面来说,primitive类型的数据不是类,所以,它们也没有reference。

4、Java不允许在同一个方法中定义同样名称的变量,如:

1
2
3
4
5
6
{
int x = 12;
{
int x = 16;
}
}

这在C++中是允许的,在Java中却会出现编译错误.

5、无需关心清理不再被使用的reference,Java的Gabage Collector会帮你做这一切的.

6、对于primitive类型的变量,如果这个变量是类的成员,则类会对其进行初始化,如果不是类的成员,则不会对其初始化,它可能是一个任意的值。

7、javadoc非常强大,但要求我们写程序的时候要有丰富的注释和良好的习惯。

第三章:流程控制

1、几乎所有的运算符都只能作用于primitive。但”=”、”==”、”!=”是例外,它们可以运用于所有对象,此外,String类也支持”+”和”+=”。

2、Reference变量的赋值,会使表达式左边的reference丢失原来的对象,原来的对象成了没有reference的内存垃圾。

3、Java的书籍总是强调Java没有地址传递,但我觉得reference传递就是地址传递。

4、equals比较的是reference

1
2
3
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1.equals(n2)); //println true

打印的结果是true,不要认为理所当然就是这样,其实,equals比较的是reference,这里是两个reference,显然不会相等,之所以输出true,是因为Integer类中已经对equals函数做了处理,如果是自己写的类,而没有重载equals,那么打印的肯定是false。

5、Java提供了位操作符,但我觉得没有必要使用它。

6、在Java中,逗号运算符只能用在for循环中。

7、switch只能用char、byte、short、int。

第四章:初始化与清理

1、primitive类型的重载慎用。

2、返回值不能重载,因为存在虽然方法有返回值,但程序中并不关注返回值的情况,如:
定义:

1
2
int f(){};
String f(){};

调用 f(),此时虚拟机就不知道该调用哪个f了。

3、类中的普通方法不能调用构造函数,构造函数能相互调用,但只能用this关键字。

4、一般来说,类中无需使用finalize(),因为虚拟机会自动进行垃圾清理,但有种特殊情况,声明了一个对象,但并没有refrence,比如:

1
2
3
class Aclass(){....};
...
new Aclass();

因为没有refrence,那么虚拟机会认为它已经没有用了,就可以回收了,但此时如果你不希望它回收,那么可以在finalize函数中实现,具体可参考书本的例子。

5、内部变量在使用之前必须初始化;所谓“使用”是指出现在表达式右边、方法的参数等,而不是表达式的左边,例如:

1
2
3
String s = "i love java";
int i;
i = s.length();

是正确的;而

1
2
int i;
i++;

是错误的。因为i++相对于i = i + 1
但是,如果不是内部变量而是类的的成员数据,则不需要初始化,因为编译器不知道会在哪个方法中被初始化。对于primitive的成员数据,Java会自动赋予初始值,如:

1
2
3
4
5
6
7
boolean = false
char = (char)0 //----空格
byte = 0
int = 0
long = 0
float = 0
double = 0

对于对象的成员数据,没有初始化之前等于null,所以,primitive的成员数据没有初始化之前被使用并不会发生错误,但对象数据在运行时就会发生exception。
有一种特殊情况,如:

1
int [] a = new int[4];

此时,看上去只初始化了数组的大小而没有初始化数组的成员,但在java中,这也是没有问题的,java给每个数组成员自动进行了初始化。

6、类实例化的时候总是先执行成员数据的定义(如果在定义的时候进行初始化的话此时就初始化了),然后再执行构造函数,而不管在代码顺序上成员数据在前还是构造函数在前。

7、对于static类型的成员变量,static类型的成员变量总是比其他成员变量先初始化,static类型的成员变量只初始化一次,只有被用到的static成员变量才会被初始化。
此处,“被用到”在我来理解,是从main函数开始检查的,如果main函数中定义了静态变量或者被main函数调用的其他类中定义了静态变量,这些静态变量就“被用到”了。

8、primitive类型的数组可以用new来初始化,如

1
int[] a = new int[]{1,2,3,4}

也可以直接用形如

1
int[] a = {1,2,3,4}

的方式来初始化,但如果不是primitive的变量就只能用new来初始化。

9、数组初始化的时候,int[] a = {1,2,3,4,}; 4后面有逗号,对不对?答:对,最后的逗号有与没有都可以。

第五章:隐藏实现

1、并非每一个java文件都需要一个public类,但一个java文件只能有一个public类。如果没有public类,那么文件名可以随便取。

2、使用import的时候,引用的类库要么能在CLASSPAHT中找到,要么在当前路径在加上import的相对路径中能找到,如:

1
import com.spring.util,

如果当前路径是D:\JavaWork,那么这些被import的类库可以在D:\workspace\com\spring\util目录下。

3、关于Java的package,如果打包成jar文件,必须把这个文件放到CLASSPATH能找到的路径中去。

4、Java的访问符包括,public、protected、private和没有访问符(package),此处的访问符是指类的成员的访问符。其访问权限分别是:

1
public>package>protected>private

package的访问权限不仅仅是使同一个package中其他类能访问这个类的public、protected、package成员,也能访问private成员。

5、相对于成员的访问权限,类没有private的访问权限,因为private的类没有任何价值;同时也没有protected的访问权限。

第六章:复用类

1、toString是一个特殊的方法,当编译器需要一个String而你的类是一个对象的时候,编译器会自动调用toString方法,当然,你得保证你的类里面有这个方法。

2、往每个类中都写一个main函数,会使调试方便得多。

3、如果基类中没有默认的(即没有参数的)构造函数而有有参数的构造函数,则在子类的构造函数中必须调用基类的构造函数,否则编译会出错。也就是说,当子类实例化的时候虚拟机自动去调用基类的默认的构造函数,除非在子类的构造函数中显式地调用了基类的非默认的构造函数。

4、编译器会强制将你基类的构造函数的调用放在派生类的构造函数的最前面。也就是说,在它之前不能有任何东西。

5、虽然编译器会强制你对基类进行初始化,并且会要求你在构造函数的开始部分完成初始化,但它不会检查你是不是进行了成员对象的初始化。

6、合成还是继承?一般来说,合成用于新类要使用旧类的功能,而不是其接口的场合。也就是说,把对象嵌进去,用它实现新类的功能,但是用户看到的是新累的接口,而不是嵌进去的对象的接口。

7、一般情况下,应该将类的成员数据定义成private。

8、上传(upcasting)总是安全的。

9、private方法都隐含有final的意思。由于你不能访问private的方法,因此你也不能复写它。你可以给一个private方法加final修饰符,但这样做什么意义也没有。

10、9中提到的不能复写private函数,是指,该函数在基类中是private的,并且在派生类中也是private的。如果在派生类中不是private的,则可以复写。

第七章:多态性

1、”封装”通过将数据的特征与行为结合在一起,创建了一种新的数据类型。“隐藏实现”通过将细节设置成private,完成了接口与实现的分离。而多态性是站在“类”的角度来处理这种逻辑上的分离的。

2、”上传”使得类的动态性得以实现,但需要注意的是,只有基类是public的情况下,扩展类的复写才可以实现,比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
private void f() {
System.out.println("private f()");
}
#
public static void main(String[] args) {
Test po = new Derived();
po.f();
}
}
#
class Derived extends Test {
public void f() {
System.out.println("public f()");
}
}

例子中,Test.f是private的,所以Derived.f其实不是Test.f的复写,Derived.f是一个全新的方法,它连重载都算不上,因为Derived根本看不到基类的f()。
所以,输出的应该是private f(),而不是public f()。

3、如果类包含一个或多个抽象方法,那么这个类必须定义为abstract,但如果类没有abstract方法,也可以将类定义为abstract。

4、构造函数的调用顺序:

  • 调用基类的构造函数。这是一个递归过程,因此会先创建继承体系的根,然后是下一级派生类,依此类推,直到最后一个继承类的构造函数
  • 成员对象按照其声明的顺序进行初始化
  • 执行继承类的构造函数的正文。

其实,还不如这样表述表决方便:在一个类的初始化过程中,先按成员对象的声明顺序初始化这些成员变量,然后执行其构造函数;如果有基类,则先初始化基类。

5、如果要内存清理,则可以从基类开始写一个函数,函数名可以自己定,但调用的时候,必须从顶层开始执行,这刚好与构造函数的调用顺序相反。

6、开始做Java的时候,不要想着把整个程序做成一个类系,比较好的办法是合成。

第八章:接口与内部类

1、接口中的方法是自动public的,即,如果你没有对其进行设置,它不会象类一样认为它是package,而是认为是public,另外,接口中的方法是不允许为private和protected的。

2、在继承和实现同时进行的时候,在声明一个类的时候,应该先继承后实现。

3、使用接口还是抽象类?如果没有成员数据和方法的实现,则应该优先考虑使用接口。

4、接口中的成员变量自动就是public和final的,所以不必特别声明,这样可以实现与C语言的enum相似的功能。如:

1
2
3
public interface Months {
int JANUARY = 1, FEBRUARY = 2, MARCH = 3, APRIL = 4, MAY = 5, JUNE = 6, JULY = 7, AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,NOVEMBER = 11, DECEMBER = 12;
}

5、内部类是一种非常有价值的特性,它能让你在逻辑上将相互从属的类组织起来,并且在类的内部访问控制权限。但是切记,内部类和合成是截然不同的。

6、内部类可以被创建在方法里,甚至是任意一个作用域里。

7、内部类能访问宿主类的任何成员。

8、嵌套类就是static的内部类。

9、每个内部类都可以独立地继承某个“实现(implementation)。因此,内部类不会受“宿主类是否已经继承了别的实现”的约束。

10、虽然作者说了很多内部类的好处,但我觉得我还是看不太懂,留待以后慢慢琢磨吧。

第九章:异常带来处理错误

1、如果一个方法在声明的时候抛出一个异常,比如public void f() throws SimpleException,那么在调用的时候必须进行异常捕捉。

2、打印错误信息是时候,System.err比System.out好,因为后者可能不重定向。

3、自己创建的异常类可以有带参数的构造函数,也可以有其他的成员。

4、Throwable类的printStackTrace方法返回“被调用的方法是经过怎样一个顺序到达异常发生地点”的信息。

5、异常说明(即在方法后面跟上throws关键字和要抛出的异常的类名称)可以让程序员很明确的知道这个方法可能会抛出什么样的异常。

6、异常NullPointerReference虚拟机会自动抛出,不必处处调用。

7、无论是否抛出异常,finally块总是会被执行。

8、异常运用原则:

  • 在合适的地方处理问题。(避免在自己还不知道该如何处理的情况下去捕捉异常)
  • 把问题解决掉,然后重新调用那个引起问题的方法
  • 修正一下问题,然后染过那个方法在继续下去
  • 用一些别的,不准备让这个方法返回的数字来进行计算
  • 把当前允许环境下能做的事情全部做完,然后把相同的异常抛到更高层
  • 把当前允许环境下能做的事情全部做完,然后把抛一个不同的异常到更高层
  • 中止程序
  • 简化(如果异常结构把事情搞得太复杂了,那用起来会非常痛苦也很烦人)
  • 把类库和程序做得更安全(这既是在为调试作短期投资,也是在为程序的健壮性作长期投资)

Hello World

Welcome to Lawrence-zxc’s Blog! Thank you!

for Java

1
2
3
4
5
public class Hello{
public static void main(String[] args){
System.out.println("Hello, World!");
}
}

for Python

1
2
#!/usr/bin/env python
print("Hello, World!")

for Ruby

1
2
#!/usr/bin/ruby
puts "Hello, World!"

for PHP

1
2
3
4
5
6
<?php
echo 'Hello, world!';
print 'Hello, world!';
?>
/* or another */
<?= "Hello, World!"?>

for C

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
printf("Hello, World!");
return 0;
}

for Lua

1
print "Hello, World!"

for JavaScript

1
2
3
4
5
alert("Hello, World!");
//or another
console.log("Hello, World!");
//or another
document.write("Hello, World!");

for Css

1
2
*:before,
*:after { content:'Hello, Word!' }

for Html

1
2
3
4
5
6
7
8
9
10
11
<!-- Direct output... -->
Hello, World!
<!-- or -->
<html>
<head>
<title> Hello, World! </title>
</head>
<body>
Hello, World!
</body>
</html>

for Shell

1
2
3
echo "Hello, World!"
#or another
printf "Hello, World!\n"

for Perl

1
2
3
4
5
6
7
8
#!/usr/bin/env perl
print "Hello, World!\n";
#or another
#!/usr/bin/env perl
use 5.010;
say "Hello, World!";

for Awk

1
awk 'BEGIN { print "Hello, World!" }'

for Sed

1
sed -ne '1s/.*/Hello, World!/p'

for SQL

1
2
3
4
5
6
7
8
CREATE TABLE MESSAGE (TEXT CHAR(15));
INSERT INTO MESSAGE (TEXT) VALUES ('Hello, World!');
SELECT TEXT FROM MESSAGE;
DROP TABLE MESSAGE;
-- or another
SELECT 'Hello, World!';