一、集合优化、List接口
在这三种不同的实现中,ArrayList和Vector使用数组实现,可以认为ArrayList或者Vector封装了对内部数组的操作,比如向数组中添加、删除、插入新的元素或者数组的拓展和重定义。对ArrayList和Vector的操作,等价于对内部对象数据的操作。
ArrayList和Vector几乎使用了相同的算法,它们的唯一区别是对多线程的支持,ArrayList没有对任何一个方法做线程同步,因此不是线程安全的。Vector绝大部分方法都做了线程同步,是一种线程安全的实现。因此ArrayList和Vector的性能相差无几,从理论上来说,没有实现线程同步的ArrayList稍好于Vector,但实际表现并不是非常明显。
LinkedList使用了循环双向链表数据结构,与基于数组的List相比这两种截然不同的实现技术,这也决定了它们适用于不同的使用场景。
LinkedList由一系列的表项链接而成。
无论LinkedList是否为空,链表内都有一个header表项,它既表示链表的开始,也表示链表的结尾,表项header的后驱表项便是链表的第一个元素,表项header的前驱表项便是链表的最后一个元素。
List add()方法源码:
//默认容量的大小
private static final int DEFAULT_CAPACITY = 10;
//空数组常量
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认的空数组常量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放元素的数组,从这可以发现ArrayList的底层实现就是一个Object数组
transient Object[] elementData;
//数组中包含的元素个数
private int size;
//数组的最大上限
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
public static int max(int a, int b) {
return (a >= b) ? a : b;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
ArrayList的add方法也很好理解,在插入元素之前,它会先检查是否需要扩容,然后再把元素添加到数组
中最后一个元素的后面。在ensureCapacityInternal方法中,我们可以看见,如果当elementData为
空数组时,它会使用默认的大小去扩容。所以说,通过无参构造方法来创建ArrayList时,它的大小其实
是为0的,只有在使用到的时候,才会通过grow方法去创建一个大小为10的数组。
第一个add方法的复杂度为O(1),虽然有时候会涉及到扩容的操作,但是扩容的次数是非常少的,
所以这一部分的时间可以忽略不计。如果使用的是带指定下标的add方法,则复杂度为O(n),
因为涉及到对数组中元素的移动,这一操作是非常耗时的。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
grow方法是在数组进行扩容的时候用到的,从中我们可以看见,ArrayList每次扩容都是扩1.5倍,
然后调用Arrays类的copyOf方法,把元素重新拷贝到一个新的数组中去。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
只要ArrayList的当前容量足够大时,add()操作的效率是非常高的,只有当ArrayList对容量的需求超过当前数组时,才需要进行扩容,扩容的过程,会进行大量的数组复制操作。而数组复制时,最终将调用System.arraycopy()方法,因此,add()操作的效率还是相当高的。
LinkedList的add()操作
LinkedList add方法源码
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList由于使用了链表的数据结构,因此不需要维护容量大小,从这点上说,它比ArrayList有一定的性能优势,然而每次添加元素都需要创建Node对象,并进行更多的赋值操作,对性能会产生一定影响。
对于插入元素到末尾来说,ArrayList性能优于LinkedList。
对于向任意位置插入元素和删除来说,LinkedList优于ArrayList。因为ArrayList是基于数组实现的,而数组是一块连续的内存空间,如果在数组的任意位置插入元素,必然导致在该位置的所有元素需要重新排列。因此其效率比较低。
有效的评估ArrayList容量大小能够优化ArrayList的使用性能。
遍历List
for循环对ArrayList遍历效果最好。迭代循环对LinkedList遍历效果最好。
对于ArrayList这些基于数组实现来说,随机访问的速度是很快的,在遍历这些List对象时,可以优先考虑随机访问。但是对于LinkedList等基于链表的实现,随机访问性能是最差的,应该避免使用。
Map接口
围绕着Map接口,最主要的实现类有:Hashtable、HashMap、LinkedHashMap和TreeMap。
在Hashtable的子类中,还有Properties类的实现。
HashMap和Hashtable都实现了Map接口。内部实现上两者有着微小的差异。
Hashtable大部分方法做了线程同步,而HashMap没有,因此,HashMap不是线程安全的。
其次,Hashtable不允许key或者value值使用null值。而HashMap可以。
尽管有着诸多的不同,但是这两套实现的性能相差无几,在对Hashtable和HashMap以及同步的HashMap(使用Conllections.synchronizedMap()方法产生)做10万次get操作。耗时分别是422ms、406ms、407ms可以认为三者并无明显差异。
HashMap实现原理:
HashMap就是将key做Hash算法,然后将Hash值映射到内存地址,直接取得key所对应的数据。在HashMap中,底层数据结构使用的数组。所谓的内存地址就是数组下标的索引。
HashMap的高性能必须保证以下几点:
hash算法必须高效。
hash值到内存地址(数组索引)的算法是快速的。
根据内存地址(数组索引)可以直接取得对应的值。
HashMap用于计算key的hash值,它分别调用了Object类的hashCode方法和HashMap内部函数的hash方法。hashCode方法默认是native的实现,基本不存在性能问题。hash函数全部基于位运算,因此也是高效的。
native方法比一般的方法快,因为它直接调用操作系统本地链接库的API,由于hashCode方法是可以重载的,因此为了保证HashMap的性能,需要确保相关的hashCode是高效的。而位运算也比算术和逻辑运算快。
HashMap冲突:
有效的指定HashMap容量大小和优秀的hash算法是对HashMap比较大的性能提升。
LinkedHashMap–有序的HashMap
HashMap的性能表现非常不错,但是它最大的功能缺点是无序性,如果希望元素保存输入时的顺序,则需要使用LinkedHashMap代替。
LinkedHashMap继承自HashMap,因此它具备HashMap的优良特性,高性能,在HashMap的基础上,LinkedHashMap又在内部增加了一个链表,用以存放元素的顺序。
LinkedHashMap可以提供两种类型数据:
一是元素插入时的顺序
二是最近访问的顺序
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
其中accessOrder为true时,按照元素的最后访问时间排序,当accessOrder为false时,按照插入顺序排序。默认为false。
在内部实现中LinkedHashMap通过通过继承HashMap.Entry类,实现了LinkedHashMap.Entry,为HashMap.Entry增加了before和after属性用以记录某一表项的前驱和后继并构成循环链表
所有集合不允许在迭代器模式中去修改集合的结构,一般认为put()方法和remove()方法会修改集合中的结,因此不能在迭代中使用。
TreeMap–Map的另一种实现
HashMap通过Hash算法可以快速的进行put()和get()操作,TreeMap则提供了一种完全不同的实现,从功能上讲,TreeMap有着比HashMap更为强大的功能,它实现了SortedMap接口,这意味着它可以对元素进行排序。然而TreeMap的性能却略低于HashMap。一个拥有10万个元素的TreeMap比相同大小的HashMap相差25%。
虽然性能上有所不足,但是在开发上,需要对元素进行排序,那么就可以选择TreeMap,对TreeMap的迭代输出将会以元素的顺序进行。
TreeMap和LinkedHashMap是不同的。LinkedHashMap是根据元素进入集合的顺序或者访问的先后顺序。TreeMap则是基于元素的固有顺序(由Comparator或者Comparable确定。)
LinkedHashMap根据元素的增加或者访问者的先后顺序进行排序,而TreeMap则根据元素的key进行排序,为了确定key的排序算法,可以使用两种方式指定:
(1)在TreeMap的构造函数中注入一个 Comparator:
(2)使用一个实现了Comparable的key。
要正常使用TreeMap,一定要通过其中一种方式将排序规则传递给TreeMap。如果既不指定Comparator,又不去实现Comparable接口,那么在put()操作时,就会抛出java.lang.ClassCastException异常。
TreeMap的内部实现是基于红黑树的,红黑树是一种平衡查找树,它的统计性能要优于平衡二叉树。它具有良好的最坏情况运行时间,可以在O(log n)时间内做查找、插入和删除,n表示树中元素数目。
实例:以学生成绩排序
建立实体类并实现comparable接口,一会作为Map的key使用
public class Student implements Comparable<Student>{
String name;
int score;
public Student(String name,int score) {
this.name=name;
this.score=score;
}
//这是必须的告诉TreeMap如何排序
@Override
public int compareTo(Student o) {
if(o.score<this.score) {
return 1;
}else if(o.score>this.score){
return -1;
}
return 0;
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("name:");
sb.append(name);
sb.append("----");
sb.append("score:");
sb.append(score);
return sb.toString();
}
}
建立具体详细信息类:作为value值使用
public class StudentDetail{
Student s;
public StudentDetail(Student s) {
this.s=s;
}
@Override
public String toString() {
return s.name+"+++"+s.score;
}
}
测试TreeMap排序取值等
public class Demo {
public static void main(String[] args) throws Exception {
Student stu1=new Student("张三",33 );
Student stu2=new Student("李四",44 );
Student stu3=new Student("王五",55 );
Student stu4=new Student("赵七",77 );
Student stu5=new Student("徐六",66 );
Map<Student, StudentDetail> map=new TreeMap<Student, StudentDetail>();
map.put(stu1, new StudentDetail(stu1));
map.put(stu2, new StudentDetail(stu2));
map.put(stu3, new StudentDetail(stu3));
map.put(stu4, new StudentDetail(stu4));
map.put(stu5, new StudentDetail(stu5));
//查询成绩44-66之间的成绩
Map m1=((TreeMap)map).subMap(stu2, stu5);
//查询小于44的成绩
Map m2=((TreeMap)map).headMap(stu2);
//查询大于等于66的成绩
Map m3=((TreeMap)map).tailMap(stu5);
System.out.println(m1);
System.out.println(m2);
System.out.println(m3);
}
}
结果:
{name:李四----score:44=李四+++44, name:王五----score:55=王五+++55}
{name:张三----score:33=张三+++33}
{name:徐六----score:66=徐六+++66, name:赵七----score:77=赵七+++77}
如果确实需要将排序功能加入HashMap,强烈建议使用TreeMap,而不是在应用程序中实现排序。
Set接口:
Set接口并没有在Collection接口之上增加额外的操作,Set集合中的元素是不能重复的。其中最重要的是HashSet,LinkedHashSet,TreeSet。
与上面的Map实现相对应,HashSet对应HashMap的封装,LinkedHashSet对应LinkedHashMap封装,TreeSet对应TreeMap封装,简单说就是实现原理都是一样的。
HashSet的输出毫无规律可言,LinkedHashMap的输出顺序和输入顺序完全一致。TreeSet将所有的输出从小到大排序。
优化集合访问代码
主要介绍如何提高集合访问速度和执行效率的。
假设有一个集合,分别存放一下String对象:“north65”、“west20”、“east30”、“south40”、
“west33”、“south20”、“north10”、“east9”。统计east、west总的出现次数。
这是没优化之前的代码:
public class Demo {
public static void main(String[] args) throws Exception {
List<String> list=new ArrayList<String>();
list.add("north65");
list.add("west20");
list.add("east30");
list.add("south40");
list.add("west33");
list.add("south20");
list.add("north10");
list.add("east9");
int num=0;
for(int i=0;i<list.size();i++) {
if(list.get(i).indexOf("east")!=-1||
list.get(i).indexOf("west")!=-1
) {
num++;
}
}
System.out.print(num);
}
}
优化:去掉重复调用的代码
list.size(),每次循环都要获得集合大小,因为集合大小固定,直接提到for循环外面。
list.get(i)这个也是重复去找元素,因为一次循环只能是一个具体值,所以没必要重复或得两次。
改成一次获得。
public class Demo {
public static void main(String[] args) throws Exception {
List<String> list=new ArrayList<String>();
list.add("north65");
list.add("west20");
list.add("east30");
list.add("south40");
list.add("west33");
list.add("south20");
list.add("north10");
list.add("east9");
int num=0;
String s=null;
int len=list.size();
for(int i=0;i<len;i++) {
if((s=list.get(i)).indexOf("east")!=-1||
s.indexOf("west")!=-1
) {
num++;
}
}
System.out.print(num);
}
}
结果:
4i
如果可以尽量直接访问内部元素,而不要调用对应接口,函数调用是需要系统资源的,直接访问元素会更高效。(size和get等 这些都是对应接口。)
RandomAccess接口
RandomAccess接口是一个标志接口,本身并没有提供任何方法:
public interface RandomAccess {
}
任何实现RandomAccess接口的对象都可以认为是支持快速随机访问对象,此接口的主要目的是标识那些可支持快速随机访问的List实现。
在JDK的实现中,任何一个基于数组的List实现都实现了RandomAccess接口,而基于链表的实现则没有。因为只有数组能够进行快速随机访问,而对于链表的随机访问,需要进行链表的遍历。这个接口的好处是:可以在应用程序中知道正在处理的List对象是否可以进行快速随机访问,从而针对不同的List进行不同的操作,以提高程序性能。
当元素数量较多时,通过随机访问比通过迭代的方式提高大约10%的程序性能。
如果应用程序需要通过索引下标对List做随机访问,尽量不要使用LinkedList,而使用ArrayList 或者Vector都是不错的选择。