读书,收获,分享
建议后面的五角星仅代表笔者个人需要注意的程度。
Talk is cheap.Show me the code
建议60:性能考虑,数组是首选★★★☆☆
在Java中由于数组没有List、Set、Map这些集合类用起来方便,于是数组在实际的系统开发中用得越来越少了,我们通常只有在阅读一些开源项目时才会看到它们的身影。但是在基本类型处理方面,数组还是占优势的,而且集合类的底层也都是通过数组实现的。
如例:
//对数组求和
public static int sum1(int[] arr) {
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
//对列表求和
public static int sum2(List<Integer> list) {
int sum = 0;
for (int i = 0; i < list.size(); i++) {
//此处做了自动拆箱操作
sum += list.get(i);
}
return sum;
}
//所以,效率:sum1() > sum2();
//在实际测试中发现:对基本类型进行求和计算时,数组的效率是集合的10倍。
基本类型是在栈内存中操作的,而对象则是在堆内存中操作的,栈内存的特点是速度快,容量小,堆内存的特点是速度慢,容量大(从性能上来讲,基本类型的处理占优势)。
注意:性能要求较高的场景中使用数组替代集合。
建议61:若有必要,使用变长数组★★☆☆☆
Java中的数组是定长的,经过初始化声明就不可改变长度,这在实际使用中非常不方便。但是可以通过对数组扩容“婉转”地解决该问题,代码如下:
public static <T> T[] expandCapacity(T[] datas, int newLen) {
newLen = newLen < 0 0 : newLen;
//生成一个新数组,并拷贝原值
return Arrays.copyOf(datas, newLen);
}
注意:在实际开发中,如果确实需要变长的数据集,数组也是在考虑范围之内的,不能因固定长度而将其否定。
建议62:警惕数组的浅拷贝★★★☆☆
通过 Arrays.copyOf()
方法产生的数组是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型是直接拷贝值,其他都是拷贝引用地址。数组的clone()
方法也是与此相同的,同样是浅拷贝,而且集合的clone()
方法也都是浅拷贝。
注意:虽然很多时候浅拷贝可以解决业务问题,但更多时候会留下隐患,需要我们提防又提防
建议63:在明确的场景下,为集合指定初始容量★★★☆☆
通过阅读ArrayList
的 add()
方法来看,它是怎么自动管理长度的,代码如下:
public boolean add(E e) {
//扩展长度
ensureCapacity(size + 1);
//追加元素
elementData[size++] = e;
return true;
}
public void ensureCapacity(int minCapacity) {
//修改计数器
modCount++;
//上次(原始)定义的数组长度
int oldCapacity = elementData.length;
//当前需要的长度超过了数组长度
if (minCapacity > oldCapacity) {
Object[] oldData = elementData;
//计算新数组长度
int newCapacity = (oldCapacity * 3) / 2 + 1;
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
//数组拷贝,形成新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
/**
* 注意看新数组的长度计算方法,并不是增加一个元素,elementData的长度就加1,
* 而是在达到elementData长度的临界点时,才将elementData扩容1.5倍,
* 这样实现有什么好处呢?好处是避免了多次调用copyOf方法的性能开销,
* 否则每加一个元素都要扩容一次,那性能岂不是非常糟糕?!
* 这里为什么是1.5倍,而不是2.5倍、3.5倍?
* 原因是一次扩容太大(比如扩容2.5倍),占用的内存也就越大,浪费的内存也就越多
* (1.5倍扩容,最多浪费33%的数组空间,而2.5倍则最多可能浪费60%的内存);
* 而一次扩容太小(比如每次扩容1.1倍),则需要多次对数组重新分配内存,性能消耗严重。
* 经过测试验证,扩容1.5倍即满足了性能要求,也减少了内存消耗。
*/
elementData
的初始容量为10,代码如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
// ***省略***
}
从ArrayList
的扩容机制可以看出,如果不设置初始容量,系统就按照1.5倍的规则扩容,每次扩容都是一次数组的拷贝,如果数据量很大,这样的拷贝会非常耗费资源,而且效率非常低下。如果我们已经知道一个ArrayList
的可能长度,然后对ArrayList
设置一个初始容量则可以显著提高系统性能(在大数据量下,是否指定容量会使性能相差5倍以上)。
其他类型集合的扩容处理方式与ArrayList
相似,只是数组的长度计算方式不同而已。
注意:非常有必要在集合初始化时声明容量。
建议64:多种最值算法,适时选择★★☆☆☆
对一批数据进行排序,然后找出其中的最大值或最小值,有多种实现方式,示例如下:
//(1)自行实现,快速查找最大值
public static int max1(int[] data) {
int max = data[0];
for (int i : data) {
max = max > i max : i;
}
return max;
}
//(2)先排序,后取值
public static int max2(int[] data) {
//先排序,clone()是为了不改变数组原有顺序
Arrays.sort(data.clone());
return data[data.length - 1];
}
//增加复杂度,查找仅次于最大值的元素的实现
public static int getSecond(Integer[] data) {
//先转为列表
List<Integer> dataList = Arrays.asList(data);
//在转为TreeSet,去重并升序排列
TreeSet<Integer> ts = new TreeSet<Integer>(dataList);
return ts.lower(ts.last());
}
首先max1()
的效率肯定是高于max2()
,但在实际测试中我们发现,如果数组数量少于1万,两者基本上没有差别,在同一个毫秒级别里,此时就可以不用自己写算法了,直接使用数组先排序后取值的方式。
至于getSecond()
的要求稍微复杂些,使用集合是最简单的方式,若从性能方面来考虑,数组是最好的选择。
注意:最值计算时使用集合最简单,使用数组性能最优,需适时选择。
建议65:避开基本类型数组转换列表陷阱★☆☆☆☆
示例如下:
public static void main(String[] args) {
int[] data = {1, 2, 3, 4, 5};
List list = Arrays.asList(data);
System.out.println(list.size());
//运行结果1
}
//asList源代码
public static <T> List<T> asList(T... a) {
return new Arrays.ArrayList<>(a);
}
/**
* asList方法输入的是一个泛型变长参数,但基本类型是不能泛型化的
* 于是在main()中把data(一个int类型的数组)作为了T的类型。
* 所以,在上述例子中,用基本类型的包装类即可。
* */
注意:基本类型数组不能作为
asList()
的输入参数,否则会引起程序逻辑混乱。
建议66:asList方法产生的List对象不可更改★☆☆☆☆
asList()
源码:
public static <T> List<T> asList(T... a) {
//此ArrayList非java.util.ArrayList,而是Arrays工具类的一个内置类
return new ArrayList<>(a);
}
//内置类
private static class ArrayList<E> extends AbstractList<E> implements
RandomAccess, java.io.Serializable {
//存储列表元素的数组
private final E[] a;
//唯一的构造函数
ArrayList(E[] array) {
if (array == null)
throw new NullPointerException();
a = array;
}
//***省略余下源码***
/**
* 1. ArrayList是一个静态私有内部类,除了Arrays能访问外,其他类都不能访问
* 2. List.add和List.remove方法它都没有实现, 于是数组是多长,转换成的列表也就是多长(不可变性)
*/
}
建议67:不同的列表选择不同的遍历方法★★☆☆☆
以ArrayList
和LinkedList
为例:
//测试得出,使用最优的遍历方式,性能提升了60%以上
public static int avg(List<Integer> list) {
int sum = 0;
if (list instanceof RandomAccess) {//ArrayList数组实现了RandomAccess接口(随机存取接口)
//可以随机存取,则使用下标遍历
for (int i = 0; i < list.size(); i++) {
sum += list.get(i);
}
} else {
//有序存取,使用foreach方式
for (int i : list) {
sum += i;
}
}
return sum / list.size();
}
RandomAccess接口(随机存取接口) : 实现了RandomAccess则表明这个类可以随机存取,对ArrayList来说也就标志着其数据元素之间没有关联,即两个位置相邻的元素之间没有相互依赖和索引关系,可以随机访问和存储。
Java中的foreach语法是iterator(迭代器)的变形用法,也就是说对于ArrayList,需要先创建一个迭代器容器,然后屏蔽内部遍历细节,对外提供hasNext、next等方法。问题是ArrayList实现了RandomAccess接口,已表明元素之间本来没有关系,可是,为了使用迭代器就需要强制建立一种互相“知晓”的关系,比如上一个元素可以判断是否有下一个元素,以及下一个元素是什么等关系,这也就是通过foreach遍历耗时的原因。
LinkedList类不是随机存取的,而是有序存取的,它实现了双向链表,每个数据结点中都有三个数据项:前节点的引用(Previous Node)、本节点元素(Node Element)、后继节点的引用(Next Node),也就是说在LinkedList中的两个元素本来就是有关联的,我知道你的存在,你也知道我的存在,使用foreach也就是迭代器方式效率更高。
注意:列表遍历不是那么简单的,其中很有“学问”,适时选择最优的遍历方式,不要固化为一种
建议68:频繁插入和删除时使用LinkedList★☆☆☆☆
下面以ArrayList
和LinkedList
的元素插入算法为例:
ArrayList
的插入(add方法)算法:
public void add(int index, E element) {
/*省略校验代码*/
//若需要扩容,则增大底层数组的长度
ensureCapactiy(size + 1);
//给index下标之后的元素(包括当前的元素)的下标加1,空出index位置
System.arraycopy(elementData, index, elementData, index + 1, size - index);
//赋值index位置元素
elementData[index] = element;
//列表长度+1
size++;
}
/**
* 注意看arraycopy方法,只要是插入一个元素,其后的元素就会向后移动一位,
* 虽然arraycopy是一个本地方法,效率非常高,但频繁的插入,每次后面的元素都要拷贝一遍,效率就变低了,
* 特别是在头位置插入元素时。
* */
LinkedList
的插入(add方法)算法:
public void add(int index, E element) {
///调用了私有addBefore方法
addBefore(element, (index == size header : entry(index)));
}
//该方法实现了在一个元素之前插于元素的算法
private Entry<E> addBefore(E e, Entry<E> entry) {
//组装一个新节点,previous指向节点的前节点,next指向原节点
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
//前节点next指向自己
newEntry.previous.next = newEntry;
//后节点的previous指向自己
newEntry.next.previous = newEntry;
//长度+1
size++;
//修改计数器+1;
modCount++;
return newEntry;
}
/**
* 这是一个典型的双向链表插入算法,把自己插入到链表,
* 然后再把前节点的next和后节点的previous指向自己。
* 这样一个插入元素(也就是Entry对象)的过程中,没有任何元素会有拷贝过程,只是引用地址改变了,
* 那效率当然就高了。
* */
经过实际测试得知,LinkedList的插入效率比ArrayList快50倍以上
LinkedList
删除和插入效率高,ArrayList
修改元素效率高,具体源码,不再粘贴。
得出,如果有大量的写操作(更多的是插入和删除动作),推荐使用LinkedList
,不过何为少量,何为大量呢?毕竟80%的性能是消耗在20%的代码上的,所以还是要适时选择,比如一个实时交易的系统,即使写作操再少,使用LinkedList也比ArrayList合适,因为此类系统是争分夺秒的,多N个毫秒可能就会造成交易数据不准确;而对于一个批量系统来说,几十毫秒、几百毫秒,甚至是几千毫秒的差别意义都不大,这时是使用LinkedList还是ArrayList就看个人爱好了,当然,如果系统已经处于性能临界点了那就必须使用LinkedList。
建议69:列表相等只需关心元素数据★★☆☆☆
示例代码:
public static void main(String[] args) {
ArrayList<String> strs = new ArrayList<>();
strs.add("A");
Vector<String> strs2 = new Vector<>();
strs2.add("A");
System.out.println(strs.equals(strs2));
//运行结果:true
}
//AbstractList中equals的源代码:
public boolean equals(Object o) {
if (o == this)
return true;
//判断是否是列表,注意:只需要实现List接口
if (!(o instanceof List))
return false;
//通过迭代器访问所有元素
ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
//遍历两个list的元素
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
//只要存在不相等就退出
if (!(o1==null o2==null : o1.equals(o2)))
return false;
}
//长度是否相等
return !(e1.hasNext() || e2.hasNext());
}
得出,列表只是一个容器,只要是同一种类型的容器(如List
),不用关心容器的细节差别(如ArrayList
与LinkedList
),只要确定所有的元素数据相等,那这两个列表就是相等的。
其他的集合类型,如Set
、Map
等与此相同,也是只关心集合元素,不用考虑集合类型。
注意:判断集合是否相等时只须关注元素是否相等即可。
建议70:子列表只是原列表的一个视图★★☆☆☆
List
的subList
方法示例:
public static void main(String[] args) {
//定义一个列表list
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
//构造一个包含list的列表list1
List<String> list1 = new ArrayList<>(list);
//subList生成与list相同的列表list2
List<String> list2 = list.subList(0, list.size());
//list2增加一个元素
list2.add("C");
System.out.println(list.equals(list1));
System.out.println(list.equals(list2));
//运行结果:
//false,true
}
为什么subList
生成的新列表,在添加了新元素后,还与原列表相等呢?
subList
源码:
public List<E> subList(int fromIndex, int toIndex) {
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
/**
*subList方法是由AbstractList实现的,它会根据是不是可以随机存取来提供不同的SubList实现方式,
* 不过,随机存储的使用频率比较高,而且RandomAccessSubList也是SubList子类,
* 所以所有的操作都是由SubList类实现的(除了自身的SubList方法外)
* */
//SubList类的代码
class SubList<E> extends AbstractList<E> {
//原始列表
private final AbstractList<E> l;
//偏移量
private final int offset;
private int size;
//构造函数,list参数就是我们的原始列表
SubList(AbstractList<E> list, int fromIndex, int toIndex) {
/***省略校验代码***/
//传递原始列表
l = list;
offset = fromIndex;
//子列表的长度
size = toIndex - fromIndex;
this.modCount = l.modCount;
}
//获取指定位置的元素
public E get(int index) {
/***省略校验代码***/
//从原始字符串中获得指定位置的元素
return l.get(index+offset);
}
//增加或插入
public void add(int index, E element) {
/***省略校验代码***/
//直接增加到原始字符串上
l.add(index+offset, element);
this.modCount = l.modCount;
size++;
}
/***其他代码省略***/
}
从源码得出:subList
方法的实现原理,它返回的SubList
类也是AbstractList
的子类,其所有的方法如get
、set
、add
、remove
等都是在原始列表上的操作,它自身并没有生成一个数组或是链表,也就是子列表只是原列表的一个视图(View),所有的修改动作都反映在了原列表上。
最后,为什么变量list
与list1
不相等
因为通过ArrayList
构造函数创建的List
对象list1
实际上是新列表,它是通过数组的copyOf
动作生成的,所生成的列表list1
与原列表list
之间没有任何关系(虽然是浅拷贝,但元素类型是String
,也就是说元素是深拷贝的),虽然此时equals
是相等的,但是随后list
又增加了元素,因此equals
比较肯定不相等了。
注意:subList产生的列表只是一个视图,所有的修改动作直接作用于原列表。
建议71:推荐使用subList处理局部列表★★☆☆☆
示例代码:
//需求:一个列表有10个元素,现在要删除索引位置为2~6的元素
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
//一行代码就解决问题(subList方法所有的操作都是在原始列表上进行的)
list.subList(2, 6).clear();
}
建议72:生成子列表后不要再操作原列表★★☆☆☆
前面说了,subList
生成的子列表是原列表的一个视图,那在subList
执行完后,如果修改了原列表的内容会怎样呢?
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
//子列表
List<String> subList = list.subList(0, 2);
//原列表增加一个元素
list.add("D");
System.out.println("list长度:" + list.size());
System.out.println("subList长度:" + subList.size());
}
运行结果:subList
的size
方法异常 (并发修改异常)
list长度:4
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
at java.util.ArrayList$SubList.size(ArrayList.java:1040)
at com.jyswm.demo1.t151.Client.main(Client.java:19)
/**
* 因为subList取出的列表是原列表的一个视图,原数据集(代码中的list变量)修改了,
* 但是subList取出的子列表不会重新生成一个新列表(这点与数据库视图是不相同的),
* 后面在对子列表继续操作时,就会检测到修改计数器与预期的不相同,
* 于是就抛出了并发修改异常。
*/
SubList
的size
方法的源码
private class SubList extends AbstractList<E> implements RandomAccess {
public int size() {
checkForComodification();
return this.size;
}
//checkForComodification方法就是用于检测是否并发修改的
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
}
解决办法:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//子列表
List<String> subList = list.subList(0, 2);
//设置原列表list为只读状态
list = Collections.unmodifiableList(list);
}
注意:
subList
生成子列表后,保持原列表的只读状态。
建议73:使用Comparator进行排序★★☆☆☆
在Java中,要想给数据排序,有两种实现方式,一种是实现Comparable
接口,一种是实现Comparator
接口,这两者有什么区别呢?示例代码:
/**
* 员工类
*/
@Data
public class Employee implements Comparable<Employee> {
/**
* 工号
*/
private int id;
/**
* 职位
*/
private Position position;
public Employee(int id, Position position) {
this.id = id;
this.position = position;
}
@Override
public int compareTo(Employee o) {
return new CompareToBuilder()
.append(id, o.id).toComparison();
}
}
public enum Position {
Boss, Manager, Staff
}
先按id
工号来排序
public static void main(String[] args) {
List<Employee> list = new ArrayList<>(5);
list.add(new Employee(1001, Position.Boss));
list.add(new Employee(1005, Position.Manager));
list.add(new Employee(1003, Position.Staff));
list.add(new Employee(1002, Position.Staff));
list.add(new Employee(1004, Position.Staff));
//按工号排序
Collections.sort(list);
for (Employee employee : list) {
System.out.println(employee);
}
/**
* 运行结果:
* Employee(id=1001, position=Boss)
* Employee(id=1002, position=Staff)
* Employee(id=1003, position=Staff)
* Employee(id=1004, position=Staff)
* Employee(id=1005, position=Manager)
*/
}
此时,我们希望按照职位来排序,并不重构Employee类,那怎么做呢?
Collections.sort
方法,它有一个重载方法Collections. sort(List<T> list, Comparator<?super T>c)
,可以接受一个Comparator
实现类,用法如下:
class PositionComparator implements Comparator<Employee> {
@Override
public int compare(Employee o1, Employee o2) {
//按照职位降序
return o1.getPosition().compareTo(o2.getPosition());
}
}
//扩展: 按职位临时倒序排列呢?注意只是临时的,是否要重写一个排序器?完全不用,有两个解决办法:
//1.直接使用Collections. reverse(List<?>list)方法实现倒序排列。
//2.通过Collections.sort(list, Collections.reverseOrder(new PositionComparator()))也可以实现倒序排列。
回到原题:在Java中,为什么要有两个排序接口呢?
实现了Comparable
接口的类表明自身是可比较的,有了比较才能进行排序;
而Comparator
接口是一个工具类接口,它的名字(比较器)也已经表明了它的作用:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑。
从这方面来说,一个类可以有很多的比较器,只要有业务需求就可以产生比较器,有比较器就可以产生N多种排序,而Comparable
接口的排序只能说是实现类的默认排序算法,一个类稳定、成熟后其compareTo
方法基本不会改变,也就是说一个类只能有一个固定的、由compareTo
方法提供的默认排序算法。
注意:
Comparable
接口可以作为实现类的默认排序法,Comparator
接口则是一个类的扩展排序工具。
建议74:不推荐使用binarySearch对列表进行检索★★☆☆☆
示例代码:
public static void main(String[] args) {
List<String> cities = new ArrayList<>();
cities.add("上海");
cities.add("广州");
cities.add("广州");
cities.add("北京");
cities.add("天津");
//indexOf方法取得索引值
System.out.println("indexOf索引值:"+cities.indexOf("广州"));
//binarySearch方法取得索引值
System.out.println("binarySearch索引值:"+Collections.binarySearch(cities, "广州"));
/**
* 运行结果:
* indexOf索引值:1
* binarySearch索引值:2
*/
}
binarySearch
方法返回结果错误了,为什么?
JDK上对binarySearch
的描述:使用二分搜索法搜索指定列表,以获得指定对象。
但是二分法查询的一个首要前提是:数据集已经实现升序排列,否则二分法查找的值是不准确的。
简单解决办法:使用Collections.sort
排下序,再用binarySearch
检索。
需要注意的场景:元素数据是从Web或数据库中传递进来的,原本是一个有规则的业务数据,我们为了查找一个元素对它进行了排序,也就是改变了元素在列表中的位置,那谁来保证业务规则此时的正确性呢?所以说,binarySearch
方法在此处受限了。当然,拷贝一个数组,然后再排序,再使用binarySeach
查找指定值,也可以解决该问题(除非处于性能的的考虑,否则使用indexOf
简单、好用,而且也不会出错)。
binarySearch
的二分法查找比indexOf
的遍历算法性能上高几十倍。
注意从:性能方面考虑,
binarySearch
是最好的选择。
建议75:集合中的元素必须做到compareTo和equals同步★★☆☆☆
示例代码:
@Data
static class City implements Comparable<City> {
private String code;
private String name;
public City(String code, String name) {
this.code = code;
this.name = name;
}
@Override
public int compareTo(City o) {
//安装城市名称排序
return new CompareToBuilder()
.append(name, o.name).toComparison();
}
@Override
public boolean equals(Object obj) {
/***省略其他判断***/
City city = (City) obj;
//根据code判断是否相等
return new EqualsBuilder()
.append(code, city.code).isEquals();
}
}
public static void main(String[] args) {
//把多个城市对象放在一个List中,然后使用不同的方法查找同一个城市,看看返回值有什么异常。
List<City> cities = new ArrayList<>();
cities.add(new City("021", "上海"));
cities.add(new City("021", "沪"));
//排序
Collections.sort(cities);
//查找对象
City city = new City("021", "沪");
//indexOf方法取得索引值
System.out.println("indexOf索引值:" + cities.indexOf(city));
//binarySearch方法取得索引值
System.out.println("binarySearch索引值:" + Collections.binarySearch(cities, city));
/**
* 运行结果:
* indexOf索引值:0
* binarySearch索引值:1
*/
}
indexOf
返回的是第一个元素,而binarySearch
返回的是第二个元素,怎么回事呢?
这是因为indexOf
是通过equals
方法判断的,equals
等于true
就认为找到符合条件的元素了,而binarySearch
查找的依据是compareTo
方法的返回值,返回0即认为找到符合条件的元素。
从这个例子中,我们可以理解两点:
-
indexOf
依赖equals
方法查找,binarySearch
则依赖compareTo
方法查找。 -
equals
是判断元素是否相等,compareTo
是判断元素在排序中的位置是否相同。
注意:实现了
compareTo
方法,就应该覆写equals
方法,确保两者同步。
建议76:集合运算时使用更优雅的方式★☆☆☆☆
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
list1.add("A");
List<String> list2 = new ArrayList<>();
list2.add("B");
}
-
并集
list1.addAll(list2);
-
交集
list1.retainAll(list2);
-
差集
list1.removeAll(list2);
-
无重复并集
//删除list1中出现的元素 list2.removeAll(list1); //把剩余的list2元素加到list1 list1.addAll(list2);
建议77:使用shuffle打乱列表★☆☆☆☆
示例代码:
public static void main(String[] args) {
int num = 10;
List<String> tags = new ArrayList<>(num);
for (int i = 0; i < num; i++) {
tags.add(i+"");
}
//打乱顺序
Collections.shuffle(tags);
}
建议78:减少HashMap中元素的数量★★★☆☆
问题描述:同一环境中,分别向HashMap
和List
中添加40万个字符串元素,向Map
中添加元素的程序报内存溢出,向List
中添加元素的程序却正常。
原因一:内部存储结构,代码如下:
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
//键
final K key;
//值
V value;
//相同哈希码的下一个元素
Entry<K,V> next;
/***省略其他代码***/
}
HashMap
底层的数组变量名叫table
,它是Entry
类型的数组,保存的是一个一个的键值对(在我们的例子中Entry
是由两个String类型组成的)。HashMap
在底层虽然也是以数组方式保存元素的,其中每一个键值对就是一个元素,但是HashMap
把键值对封装成了一个Entry
对象,然后再把Entry
放到了数组中。HashMap
比ArrayList
多了一次封装,把String
类型的键值对转换成Entry
对象后再放入数组,这就多了40万个对象
原因二:它的扩容机制,代码如下:
//在插入键值对时,会做长度校验,如果大于或等于阀值(threshold变量),则数组长度增大一倍
if (size++ >= threshold) {
resize(2 * table.length)
}
//也就是说只要HashMap的size大于数组长度的0.75倍时,就开始扩容
threshold = (int) (newCapacity * 0.75)
由上得出,Map
在扩容的时候预留了太多空间。在内存占用的临界点,由于内存剩余不足,无法提供下次扩容所需要的内存,导致扩容失败。但真正需要添加的数据,可能只是需要再扩容一个单位就可以放下。所以有时会产生,在我们看来,系统所分配的内存完全可以存放下这些数据,但是在用Map
存储时却内存溢出了。
注意:尽量让HashMap中的元素少量并简单。
**建议79:集合中的哈希码不要重复★☆☆☆☆
以HashMap
的查找元素为例:
- 以
key
的hashCode
定位元素的位置,所以查找元素比较快(比List
快)。 - 当
hashCode
冲突时,则遍历对比(和List
的遍历对比类似),所以当hashCode
冲突时,查找元素的效率会大大降低。
注意:HashMap中的hashCode应避免冲突。
建议80:多线程使用Vector或HashTable★★☆☆☆
Vector
是ArrayList
的多线程版本,Vector
的每个方法前都加上了synchronized
关键字,同时只会允许一个线程进入该方法,确保了程序的可靠性。
HashTable
是HashMap
的多线程版本,它的实现与Vector
相同。
建议81:非稳定排序推荐使用List★☆☆☆☆
Set
与List
的最大区别就是Set
中的元素不可以重复(这个重复指的equals
方法的返回值相等),其他方面则没有太大的区别了,在Set
的实现类中有一个比较常用的类:TreeSet
,该类实现了类默认排序为升序的Set
集合,如果插入一个元素,默认会按照升序排列(是根据Comparable
接口的compareTo
的返回值确定排序位置了)
注意: SortedSet
接口(TreeSet
实现了该接口)只是定义了在给集合加入元素时将其进行排序,并不能保证元素修改后的排序结果,因此TreeSet
适用于不变量的集合数据排序,比如String
、Integer
等类型,但不适用于可变量的排序,特别是不确定何时元素会发生变化的数据集合。
注意:
SortedSet
中的元素被修改后可能会影响其排序位置。
建议82:由点及面,一叶知秋—集合大家族★☆☆☆☆
Java中的集合类实在是太丰富了,有常用的ArrayList
、HashMap
,也有不常用的Stack
、Queue
,有线程安全的Vector
、HashTable
,也有线程不安全的LinkedList
、TreeMap
,有阻塞式的ArrayBlockingQueue
,也有非阻塞式的PriorityQueue
等,整个集合家族非常庞大,而且也是错综复杂,可以划分为以下几类:
(1)List
实现List接口的集合主要有:ArrayList
、LinkedList
、Vector
、Stack
,其中ArrayList
是一个动态数组,LinkedList
是一个双向链表,Vector
是一个线程安全的动态数组,Stack
是一个对象栈,遵循先进后出的原则。
(2)Set
Set
是不包含重复元素的集合,其主要的实现类有:EnumSet
、HashSet
、TreeSet
,其中EnumSet
是枚举类型的专用Set
,所有元素都是枚举类型;HashSet
是以哈希码决定其元素位置的Set
,其原理与HashMap
相似,它提供快速的插入和查找方法;TreeSet
是一个自动排序的Set
,它实现了SortedSet
接口。
(3)Map
Map
是一个大家族,它可以分为排序Map
和非排序Map
,排序Map
主要是TreeMap
类,它根据Key
值进行自动排序;非排序Map
主要包括:HashMap
、HashTable
、Properties
、EnumMap
等,其中Properties
是HashTable
的子类,它的主要用途是从Property
文件中加载数据,并提供方便的读写操作;EnumMap
则是要求其Key
必须是某一个枚举类型。
Map
中还有一个WeakHashMap
类需要说明,它是一个采用弱键方式实现的Map
类,它的特点是:WeakHashMap
对象的存在并不会阻止垃圾回收器对键值对的回收,也就是说使用WeakHashMap
装载数据不用担心内存溢出的问题,GC
会自动删除不用的键值对,这是好事。但也存在一个严重问题:GC
是静悄悄回收的(何时回收?God knows!),我们的程序无法知晓该动作,存在着重大的隐患。
(4)Queue
队列,它分为两类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括:ArrayBlockingQueue
、PriorityBlockingQueue
、LinkedBlockingQueue
,其中ArrayBlockingQueue
是一个以数组方式实现的有界阻塞队列,PriorityBlockingQueue
是依照优先级组建的队列,LinkedBlockingQueue
是通过链表实现的阻塞队列;另一类是非阻塞队列,无边界的,只要内存允许,都可以持续追加元素,我们最经常使用的是PriorityQueue
类。
还有一种队列,是双端队列,支持在头、尾两端插入和移除元素,它的主要实现类是:ArrayDeque
、LinkedBlockingDeque
、LinkedList
。
(5)数组
数组与集合的最大区别就是数组能够容纳基本类型,而集合就不行,更重要的一点就是所有的集合底层存储的都是数组。
(6)工具类
数组的工具类是java.util.Arrays
和java.lang.reflect.Array
,集合的工具类是java.util.Collections
,有了这两个工具类,操作数组和集合会易如反掌,得心应手。
(7)扩展类
集合类当然可以自行扩展了,想写一个自己的List?没问题,但最好的办法还是“拿来主义”,可以使用Apache
的commons-collections
扩展包,也可以使用Google的google-collections
扩展包,这些足以应对我们的开发需要。
注意:
commons-collections
、google-collections
是JDK之外的优秀数据集合工具包,使用拿来主义即可。