JAVA编程规范-集合、并发(阿里手册)
(五
)集合
1.
【强制】关于
hashCode
和
equals
的处理,遵循如下规则:
1
) 只要重写
equals
,就必须重写
hashCode
。
2
) 因为
Set
存储的是不重复的对象,依据
hashCode
和
equals
进行判断,所以
Set
存储的
对象必须重写这两个方法。
3
) 如果自定义对象做为
Map
的键,那么必须重写
hashCode
和
equals
。
说明:
String
重写了
hashCode
和
equals
方法,所以我们可以非常愉快地使用
String
对象
作为
key
来使用。
2.
【强制】
ArrayList
的
subList
结果不可强转成
ArrayList
,否则会抛出
ClassCastException
异常,即
java
.
util
.
RandomAccessSubList cannot be cast to java
.
util
.
ArrayList
.
说明:
subList
返回的是
ArrayList
的内部类
SubList
,并不是
ArrayList
,而是
ArrayList
的一个视图,对于
SubList
子列表的所有操作最终会反映到原列表上。
3.
【强制】在
subList
场景中,
高度注意
对原集合元素个数的修改,会导致子列表的遍历、增加、
删除均会产生
ConcurrentModificationException
异常。
4.
【强制】使用集合转数组的方法,必须使用集合的
toArray(T[] array)
,传入的是类型完全
一样的数组,大小就是
list
.
size()
。
说明:
使用
toArray
带参方法,入参分配的数组空间不够大时,
toArray
方法内部将重新分配
内存空间,并返回新数组地址
;
如果数组元素大于实际所需,下标为
[ list
.
size() ]
的数组
元素将被置为
null
,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素
个数一致。
正例:
List<String> list = new ArrayList<String>(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);
反例:
直接使用
toArray
无参方法存在问题,此方法返回值只能是
Object[]
类,若强转其它
类型数组将出现
ClassCastException
错误。
5.
【强制】使用工具类
Arrays
.
asList()
把数组转换成集合时,不能使用其修改集合相关的方
法,它的
add
/
remove
/
clear
方法会抛出
UnsupportedOperationException
异常。
说明:
asList
的返回对象是一个
Arrays
内部类,并没有实现集合的修改方法。
Arrays
.
asList
体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { "you", "wu" };
List list = Arrays.asList(str);
第一种情况:
list.add("yangguanbao");
运行时异常。
第二种情况:
str[0] = "gujin";
那么
list.get(0)
也会随之修改。
6.
【强制】泛型通配符
<?
extends T
>
来接收返回的数据,此写法的泛型集合不能使用
add
方
法,而
<? super T>
不能使用
get
方法,做为接口调用赋值时易出错。
说明:
扩展说一下
PECS(Producer Extends Consumer Super)
原则:第一、频繁往外读取内
容的,适合用
<?
extends T
>
。第二、经常往里插入的,适合用
<? super T>
。
7.
【强制】不要在
foreach
循环里进行元素的
remove
/
add
操作。
remove
元素请使用
Iterator
方式,如果并发操作,需要对
Iterator
对象加锁。
正例:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (
删除元素的条件
) {
iterator.remove();
}
}
反例:
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
说明:
以上代码的执行结果肯定会出乎大家的意料,那么试一下把
“1”
换成
“2”
,会是同样的
结果吗?
8.
【强制】 在
JDK
7
版本及以上,
Comparator
要满足如下三个条件,不然
Arrays
.
sort
,
Collections
.
sort
会报
IllegalArgumentException
异常。
说明:
三个条件如下
1
)
x
,
y
的比较结果和
y
,
x
的比较结果相反。
2
)
x
>
y
,
y
>
z
,则
x
>
z
。
3
)
x
=
y
,则
x
,
z
比较结果和
y
,
z
比较结果相同。
反例:
下例中没有处理相等的情况,实际使用中可能会出现异常:
new Comparator<Student>() {
@
Override
public int compare(Student o1, Student o2) {
return o1.getId() > o2.getId() ? 1 : -1;
}
};
9.
【推荐】集合初始化时,指定集合初始值大小。
说明:
HashMap
使用
HashMap(int initialCapacity)
初始化,
正例:
initialCapacity
=
(
需要存储的元素个数
/
负载因子
) + 1
。注意
负载因子(即
loader
factor
)默认为
0.75
,
如果暂时无法确定初始值大小,请设置为
16
(即默认值)。
反例:
HashMap
需要放置
1024
个元素,由于没有设置容量初始大小,随着元素不断增加,容
量
7
次被迫扩大,
resize
需要重建
hash
表,严重影响性能。
10.
【推荐】使用
entrySet
遍历
Map
类集合
KV
,而不是
keySet
方式进行遍历。
说明:
keySet
其实是遍历了
2
次,一次是转为
Iterator
对象,另一次是从
hashMap
中取出
key
所对应的
value
。而
entrySet
只是遍历了一次就把
key
和
value
都放到了
entry
中,效
率更高。如果是
JDK
8
,使用
Map
.
foreach
方法。
正例:
values()
返回的是
V
值集合,是一个
list
集合对象
;
keySet()
返回的是
K
值集合,是
一个
Set
集合对象
;
entrySet()
返回的是
K
-
V
值组合集合。
11.
【推荐】高度注意
Map
类集合
K
/
V
能不能存储
null
值的情况,如下表格:
集合类
Key
Value
Super
说明
Hashtable
不允许为
null
不允许为
null
Dictionary
线程安全
ConcurrentHashMap
不允许为
null
不允许为
null
AbstractMap
锁分段技术(
JDK8:CAS
)
TreeMap
不允许为
null
允许为
null
AbstractMap
线程不安全
HashMap
允许为
null
允许为
null
AbstractMap
线程不安全
反例:
由于
HashMap
的干扰,很多人认为
ConcurrentHashMap
是可以置入
null
值,而事实上,
存储
null
值时会抛出
NPE
异常。
12.
【参考】合理利用好集合的有序性
(sort)
和稳定性
(order)
,避免集合的无序性
(unsort)
和
不稳定性
(unorder)
带来的负面影响。
说明:
有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次
序是一定的。如:
ArrayList
是
order
/
unsort
;
HashMap
是
unorder
/
unsort
;
TreeSet
是
order
/
sort
。
13.
【参考】利用
Set
元素唯一的特性,可以快速对一个集合进行去重操作,避免使用
List
的
contains
方法进行遍历、对比、去重操作。
(
六
)
并发处理
1.
【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
说明:
资源驱动类、工具类、单例工厂类都需要注意。
2.
【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread() {
super.setName("TimerTaskThread");
...
}
3.
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资
源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者
“
过度切换
”
的问题。
4.
【强制】线程池不允许使用
Executors
去创建,而是通过
ThreadPoolExecutor
的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:
Executors
返回的线程池对象的弊端如下:
1
)
FixedThreadPool
和
SingleThreadPool
:
允许的请求队列长度为
Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致
OOM
。
2
)
CachedThreadPool
和
ScheduledThreadPool
:
允许的创建线程数量为
Integer.MAX_VALUE
,可能会创建大量的线程,从而导致
OOM
。
5.
【强制】
SimpleDateFormat
是线程不安全的类,一般不要定义为
static
变量,如果定义为
static
,必须加锁,或者使用
DateUtils
工具类。
正例:
注意线程安全,使用
DateUtils
。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@
Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明:
如果是
JDK
8
的应用,可以使用
Instant
代替
Date
,
LocalDateTime
代替
Calendar
,
DateTimeFormatter
代替
SimpleDateFormat
,官方给出的解释:
simple beautiful strong
immutable thread
-
safe
。
6.
【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁
;
能
锁区块,就不要锁整个方法体
;
能用对象锁,就不要用类锁。
说明:
尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用
RPC
方法。
7.
【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造
成死锁。
说明:
线程一需要对表
A
、
B
、
C
依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序
也必须是
A
、
B
、
C
,否则可能出现死锁。
8.
【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加
锁,要么在数据库层使用乐观锁,使用
version
作为更新依据。
说明:
如果每次访问冲突概率小于
20%
,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次
数不得小于
3
次。
9.
【强制】多线程并行处理定时任务时,
Timer
运行多个
TimeTask
时,只要其中之一没有捕获
抛出的异常,其它任务便会自动终止运行,使用
ScheduledExecutorService
则没有这个问题。
10.
【推荐】使用
CountDownLatch
进行异步转同步操作,每个线程退出前必须调用
countDown
方法,线程执行代码注意
catch
异常,确保
countDown
方法被执行到,避免主线程无法执行
至
await
方法,直到超时才返回结果。
说明:
注意,子线程抛出异常堆栈,不能在主线程
try
-
catch
到。
11.
【推荐】避免
Random
实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一
seed
导致的性能下降。
说明:
Random
实例包括
java
.
util
.
Random
的实例或者
Math
.
random()
的方式。
正例:
在
JDK
7
之后,可以直接使用
API ThreadLocalRandom
,而在
JDK
7
之前,需要编码保
证每个线程持有一个实例。
12.
【推荐】在并发场景下,通过双重检查锁
(
double
-
checked locking
)
实现延迟初始化的优
化问题隐患
(
可参考
The
"
Double
-
Checked Locking is Broken
"
Declaration)
,推荐解
决方案中较为简单一种
(
适用于
JDK
5
及以上版本
)
,将目标属性声明为
volatile
型
。
反例:
class Singleton {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
// other methods and fields...
}
13.
【参考】
volatile
解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,
但是如果多写,同样无法解决线程安全问题。如果是
count
++
操作,使用如下类实现:
AtomicInteger count
=
new AtomicInteger(); count
.
addAndGet(
1
);
如果是
JDK
8
,推
荐使用
LongAdder
对象,比
AtomicLong
性能更好
(
减少乐观锁的重试次数
)
。
14.
【参考】
HashMap
在容量不够进行
resize
时由于高并发可能出现死链,导致
CPU
飙升,在
开发过程中可以使用其它数据结构或加锁来规避此风险。
15.
【参考】
ThreadLocal
无法解决共享对象的更新问题,
ThreadLocal
对象建议使用
static
修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享
此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象
(
只
要是这个线程内定义的
)
都可以操控这个变量。