一、数组和集合的区别
数组不是面向对象的,存在明显的缺陷,集合弥补了数组的缺点。比数组更灵活,更实用,而且不同的集合框架类可适用不同场合。如下:
- 数组能存放基本数据类型和对象,而集合类存放的都是对象,集合类不能存放基本数据类型,数组和集合存放的对象皆为对象的引用地址。
- 数组容易固定无法动态改变,集合类容量动态改变;
- 数组无法判断其中实际存有多少元素,length只告诉了数组的容量,而集合的size()可以确切知道元素的个数。
- 集合有多种实现方式和不同适用场合,不像数组仅采用顺序表方式。
- 集合以类的形式存在,具有封装、继承、多态等类的特性,通过简单的方法和属性即可实现各种复杂操作。
二、 java的集合主要有List、Set、Map
1.List、Set继承至Collection接口,Map为独立接口
- List下有ArrayList、LinkedList、Vector
- Set下有HashSet、LinkedHashSet,TreeSet
- Map下有HashMap、LinkedHashMap,TreeMap、Hashtable
2.List集合
有序列表,允许存放重复元素;
实现类:
- ArrayList:数组实现,查询快、增删慢,轻量级;(线程不安全)
- LinkedList:双向链表实现,增删快,查询慢(线程不安全)
- Vector:数组实现,重量级(线程安全,使用少)
List常用方法:
- add(Object element): 向列表的尾部添加指定的元素。
- size(): 返回列表中的元素个数。
- get(int index): 返回列表中指定位置的元素,index从0开始。
- add(int index, Object element): 在列表的指定位置插入指定元素。
- set(int i, Object element): 将索引i位置元素替换为元素element并返回被替换的元素。
- clear(): 从列表中移除所有元素。
- isEmpty(): 判断列表是否包含元素,不包含元素则返回 true,否则返回false。
- contains(Object o): 如果列表包含指定的元素,则返回 true。
- remove(int index): 移除列表中指定位置的元素,并返回被删元素。
- remove(Object o): 移除集合中第一次出现的指定元素,移除成功返回true,否则返回false。
- iterator(): 返回按适当顺序在列表的元素上进行迭代的迭代器。
ArrayList
优点:底层数据结构是数组,查询快,增删慢
缺点:线程不安全,效率高
底层是Object数组,所以ArrayList具有数组的查询速度快的有点以及增删慢的缺点;而LinkedList的底层是一种双向循环链表。在此链表上每一个数据节点都由三部分组成;前指针(指向前面的节点的位置),数据,后指针(指向后面的节点的位置)。最后一个节点的后指针指向第一个节点的前指针,形成一个循环。双向循环链表的查询效率低但是增删效率高。ArrayList和LinkedList在用法上没有区别,但是功能上还是有区别的。
ArrayList自动扩充机制:
实现机制:ArrayList.ensureCapaCity(int minCapacity);首先得到当前elementData 属性的长度oldCapacity。然后通过判断oldCapacity和minCapacity参数谁大来决定是否需要扩容, 如果minCapacity大于 oldCapacity,那么我们就对当前的List对象进行扩容。 扩容的的策略为:取(oldCapacity * 3)/2 + 1和minCapacity之间更大的那个。然后使用数组拷 贝的方法,把以前存放的数据转移到新的数组对象中 如果minCapacity不大于oldCapacity那么就不进行扩容。
常用方法:
ArrayList可以使用list的所有方法
LinkedList
优点:底层数据结构是链表,查询慢,增删快;
缺点:线程不安全,效率高;
LinkedList顾名思义是链表,LinkedList间接的实现了List接口(说明LinkedList是有List的特性的,add、remove等)、实现了Cloneable(可复制)、Serializable(进行了序列化),除此之外还实现了Queue(队列,说明是有一些队列的特性,pop等),LinkedList为双链表,维护的是一个firsrt指针和last指针,而每个节点有item自身、prev和next两个节点来维护双链表的关系,其他的功能都是围绕我们的双链表来进行的。
常用方法:
增加:
add(E e):在链表后添加一个元素; 通用方法
addFirst(E e):在链表头部插入一个元素; 特有方法
addLast(E e):在链表尾部添加一个元素; 特有方法
push(E e):与addFirst方法一致
offer(E e):在链表尾部插入一个元素 add(int index, E element):在指定位置插入一个元素。
offerFirst(E e):JDK1.6版本之后,在头部添加; 特有方法 offerLast(E e):JDK1.6版本之后,在尾部添加; 特有方法
删除:
remove() :移除链表中第一个元素; 通用方法
remove(E e):移除指定元素; 通用方法
removeFirst(E e):删除头,获取元素并删除; 特有方法
removeLast(E e):删除尾; 特有方法
pollFirst():删除头; 特有方法
pollLast():删除尾; 特有方法
pop():和removeFirst方法一致,删除头。
poll():查询并移除第一个元素 特有方法
查:
get(int index):按照下标获取元素; 通用方法
getFirst():获取第一个元素; 特有方法
getLast():获取最后一个元素; 特有方法
peek():获取第一个元素,但是不移除; 特有方法
peekFirst():获取第一个元素,但是不移除;
peekLast():获取最后一个元素,但是不移除;
pollFirst():查询并删除头; 特有方法
pollLast():删除尾; 特有方法
poll():查询并移除第一个元素 特有方法
Vector
优点:底层数据结构是数组,查询快、增删慢;
缺点:线程安全,效率低;
Vector实现了List接口,与ArrayList一样可以维护一个插入顺序,但是ArrayList比Vector快,它是非同步的,若涉及到多线程,用Vector会好一点,在非多线程环境中,Vector对于元素的查询、添加、删除和更新的操作效果并不是很好。
Vector可实现自动增长的对象数组。java.util.vector提供了向量类(Vector)以实现类似动态数组的功能。创建了一个向量类的对象后,可以往其中随意插入不同类的对象,既不需要顾及类型也不需要预先选定向量的内容,并可以方便的进行查找。对于预先不知或者不愿预先定义数组大小,并且需要频繁进行查找、插入、删除工作的情况,可以考虑使用向量类。
常用方法:
向量类提供了三种构造方法:
public vector()
public vector(int initialcapacity,int capacityIncrement)
public vector(int initialcapacity)
使用第一种方法系统会自动对向量进行管理,若使用后两种方法。则系统将根据参数,initialcapacity设定向量对象的容量(即向量对象可存储数据的大小),当真正存放的数据个数超过容量时。系统会扩充向量对象存储容量。
参数capacityincrement给定了每次扩充的扩充值。当capacityincrement为0的时候,则没次扩充一倍,利用这个功能可以优化存储。在Vector类中提供了各种方法方便用户的使用:
增加:
(1)public final synchronized void adddElement(Object obj)
将obj插入向量的尾部。obj可以是任何类型的对象。对同一个向量对象,亦可以在其中插入不同类的对象。但插入的应是对象而不是数值,所以插入数值时要注意将数组转换成相应的对象。
例如:要插入整数1时,不要直接调用v1.addElement(1),正确的方法为:
Vector v1 = new Vector();
Integer integer1 = new Integer(1);
v1.addElement(integer1);
(2)public final synchronized void setElementAt(Object obj,int index)
将index处的对象设置成obj,原来的对象将被覆盖。
(3)public final synchronized void insertElement(Object obj,int index)
在index指定的位置插入obj,原来对象以及此后的对象依次往后顺延。
删除:
(1)public final synchronized void removeElement(Object obj)
从向量中删除obj,若有多个存在,则从向量头开始试,删除找到的第一个与obj相同的向量成员。
(2)public final synchronized void removeAllElement();
删除向量所有的对象
(3)public fianl synchronized void removeElementAt(int index)
删除index所指的地方的对象
查询:
(1)public final int indexOf(Object obj)
从向量头开始搜索obj,返回所遇到的第一个obj对应的下标,若不存在此obj,返回-1.
(2)public final synchronized int indexOf(Object obj,int index)
从index所表示的下标处开始搜索obj.
(3)public final int lastindexOf(Object obj)
从向量尾部开始逆向搜索obj.
(4)public final synchornized int lastIndex(Object obj,int index)
从index所表示的下标处由尾至头逆向搜索obj.
(5)public final synchornized firstElement()
获取向量对象中的首个obj
(6)public final synchornized Object lastElement()
获取向量对象的最后一个obj
Vector的线程安全与ArrayList的非线程安全
线程安全就是多线程访问的时候,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其它的线程不能进行访问直到该线程读取完,其它线程才可以使用。不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
Vector内部如何实现线程安全
public class Vector
{
Object[] elementData; // 存放元素的数组
int elementCount; // 存放元素的实际数量,默认的容量(capacity)是10
int capacityIncrement; // 当容量占满时,扩容量,如果未指定,则原先的2倍(doubled)
// 构造函数
public Vector(int initialCapacity/* 初始容量 */,int capacityIncrement/*扩容量*/){}
}
Vector类中的capacity()/size()/isEmpty()/indexOf()/lastIndexOf()/removeElement()/addElement() 等方法均是 sychronized 的,所以,对Vector的操作均是线程安全的。
对于Vector的操作均是线程安全这句话还需要注意一点是:如果是单个方法进行调用是线程安全的,但是如果是组合的方式进行调用则需要再次进行同步处理,例如:
if (!vector.contains(element))
vector.add(element);
...
}
这是经典的 put-if-absent 情况,尽管 contains, add 方法都正确地同步了,但作为 vector 之外的使用环境,仍然存在 race condition: 因为虽然条件判断 if (!vector.contains(element))与方法调用 vector.add(element); 都是原子性的操作 (atomic),但在 if 条件判断为真后,那个用来访问vector.contains 方法的锁已经释放,在即将的 vector.add 方法调用之间有间隙,在多线程环境中,完全有可能被其他线程获得 vector的 lock 并改变其状态, 此时当前线程的vector.add(element); 正在等待(只不过我们不知道而已)。只有当其他线程释放了 vector 的 lock 后,vector.add(element); 继续,但此时它已经基于一个错误的假设了。
单个的方法 synchronized 了并不代表组合(compound)的方法调用具有原子性,使 compound actions 成为线程安全的可能解决办法之一还是离不开intrinsic lock (这个锁应该是 vector 的,但由 client 维护):
// Vector v = ...
public boolean putIfAbsent(E x) {
synchronized(v) {
boolean absent = !contains(x);
if (absent) {
add(x);
}
}
return absent;
}
所以在回答Vector与ArrayList的区别时,应该这样回答:
Vector 和 ArrayList 实现了同一接口 List, 但所有的 Vector 的方法都具有 synchronized 关键修饰。但对于复合操作,Vector 仍然需要进行同步处理。
为什么ArrayList线程不安全
举个栗子:
一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:
在 Items[Size] 的位置存放此元素;
增大 Size 的值。 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1; 而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。现在看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
虽然ArrayList是非线程安全的,要想实现线程安全的ArrayList,可在ArrayList的基础上通过同步块来实现,或者使用同步包装器(Collections.synchronizedList),还可以使用J.U.C中的CopyOnWriteArrayList。
ArrayList的非线程安全带来的问题:
final ArrayList<String> list = new ArrayList<String>(); // 多线程共享的ArrayList
for(int i=0;i<100;i++) // 多个线程同时进行写操作
{
new Thread(new Runnable(){
@Override
public void run() {
for(int j=0;j<1000;j++)
{
list.add("hello"); // 多线程下,此处引发ArrayIndexOutOfBoundsException
}
}}).start();
}
ArrayList的内部原理:
public class ArrayList<E>
{
private Object[] elementData; // 存储元素的数组。其分配的空间长度是capacity。
private int size; // elementData存储了多少个元素。
public ArrayList(){this(10);}; // 默认capacity是10
boolean add(E e)
{
ensureCapacityInternal(size + 1); // capacity至少为 size+1
elementsData[size++]=e; // size++
return true;
}
void ensureCapacityInternal(int minCapacity){
if(minCapacity > elementData.length) // 扩容
grow(minCapacity);
}
void grow(int minCapacity){
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 约是原先的1.5倍。
elementData = Arrays.copyOf(elementData,newCapacity );
}
}
如何实现线程安全的ArrayList:
#1:自己手动同步
public static List<E> list = ... ;
lock.lock();
list.add();
lock.unlock();
#2:使用同步包装器
List<E> syncList = Collections.synchronizedList(new ArrayList<E>());
迭代时,需要包含在同步块当中
synchronized(syncList){
while(Iterator<E> iter = syncList.iterator();iter.hasNext();){}
}
#3:使用J.U.C中的CopyOnWriteArrayList。
3.set集合
set集合不可以存放重复元素,元素的存取是无序的;
如何选取set集合及其子类;
- set——>不保留存储顺序并且去掉重复元素
- TreeSet——>适用于元素排列
- HashSet——>如果不需要排序请选择它,比TreeSet效率高
- LinkedHashSet——>如果需要保留存储顺序,又要过滤重复元素选择它;
(1)HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
具体实现唯一性的比较过程:存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,就该存储了,此时就要采用哈希的解决地址冲突算法,在当前hashCode值处类似一个新的链表, 在同一个hashCode值的后面存储存储不同的对象,这样就保证了元素的唯一性。
Set的实现类的集合对象中不能够有重复元素,HashSet也一样他是使用了一种标识来确定元素的不重复,HashSet用一种算法来保证HashSet中的元素是不重复的, HashSet采用哈希算法,底层用数组存储数据。默认初始化容量16,加载因子0.75。
Object类中的hashCode()的方法是所有子类都会继承这个方法,这个方法会用Hash算法算出一个Hash(哈希)码值返回,HashSet会用Hash码值去和数组长度取模, 模(这个模就是对象要存放在数组中的位置)相同时才会判断数组中的元素和要加入的对象的内容是否相同,如果不同才会添加进去。
Hash算法是一种散列算法。
Set hs=new HashSet();
hs.add(o);
|
o.hashCode();
|
o%当前总容量 (0–15)
|
| 不发生冲突
是否发生冲突—————–直接存放
|
| 发生冲突
| 假(不相等)
o1.equals(o2)——————-找一个空位添加
|
| 是(相等)
不添加
覆盖hashCode()方法的原则:
1、一定要让那些我们认为相同的对象返回相同的hashCode值
2、尽量让那些我们认为不同的对象返回不同的hashCode值,否则,就会增加冲突的概率。
3、尽量的让hashCode值散列开(两值用异或运算可使结果的范围更广)
HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成,我们应该为保存到HashSet中的对象覆盖hashCode()和equals(),因为再将对象加入到HashSet中时,会首先调用hashCode方法计算出对象的hash值,接着根据此hash值调用HashMap中的hash方法,得到的值& (length-1)得到该对象在hashMap的transient Entry[] table中的保存位置的索引,接着找到数组中该索引位置保存的对象,并调用equals方法比较这两个对象是否相等,如果相等则不添加,注意:所以要存入HashSet的集合对象中的自定义类必须覆盖hashCode(),equals()两个方法,才能保证集合中元素不重复。在覆盖equals()和hashCode()方法时, 要使相同对象的hashCode()方法返回相同值,覆盖equals()方法再判断其内容。为了保证效率,所以在覆盖hashCode()方法时, 也要尽量使不同对象尽量返回不同的Hash码值。
如果数组中的元素和要加入的对象的hashCode()返回了相同的Hash值(相同对象),才会用equals()方法来判断两个对象的内容是否相同。
(2)LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
(3)TreeSet底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性。根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法;
(4)小结:Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只 是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)Set不保存重复的元素。
Set 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。
list和set总结
1)、List,Set都是继承自Collection接口,Map则不是
(2)、List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
(3).Set和List对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
(4)、ArrayList与LinkedList的区别和适用场景
Arraylist:
优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。
LinkedList:
优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景
缺点:因为LinkedList要移动指针,所以查询操作性能比较低。
适用场景分析:
当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。
ArrayList与Vector的区别和适用场景
ArrayList有三个构造方法:
public ArrayList(int initialCapacity)//构造一个具有指定初始容量的空列表。
public ArrayList() //默认构造一个初始容量为10的空列表。
public ArrayList(Collection<? extends E> c)//构造一个包含指定 collection 的元素的列表
Vector有四个构造方法:
public Vector()//使用指定的初始容量和等于0的容量增量构造一个空向量。
public Vector(int initialCapacity)//构造一个空向量,使其内部数据数组的大小,其标准容量增量为零。
public Vector(Collection<? extends E> c)//构造一个包含指定 collection 中的元素的向量
public Vector(int initialCapacity,int capacityIncrement)//使用指定的初始容量和容量增量构造一个空的向量
ArrayList和Vector都是用数组实现的,主要有这么三个区别:
(1).Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰,这样就导致了Vector在效率上无法与ArrayList相比;
(2)两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
*(3)*Vector可以设置增长因子,而ArrayList不可以。
*(4)*Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。
适用场景分析:
1.Vector是线程同步的,所以它也是线程安全的,而ArrayList是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用ArrayList效率比较高。
2.如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用Vector有一定的优势。
.TreeSet 是二差树(红黑树的树据结构)实现的,Treeset中的数据是自动排好序的,不允许放入null值
2.HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束
3.HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例
适用场景分析:HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。
4.Map
Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,他们都可以使用任何引用类型的数据,但key不能重复,所以通过制定的key就可以取出对应的value。
(1)、请注意!!!, Map 没有继承 Collection 接口, Map 提供 key 到 value 的映射,你可以通过“键”查找“值”。一个 Map 中不能包含相同的 key ,每个 key 只能映射一个 value 。 Map 接口提供 3 种集合的
(2)、Map:
(3)hashMap与hashTable比较
(4)treeMap
(5)Map的其他类
IdentityHashMap和HashMap的具体区别,IdentityHashMap使用 == 判断两个key是否相等,而HashMap使用的是equals方法比较key值。有什么区别呢?
对于==,如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等; 如果作用于引用类型的变量,则比较的是所指向的对象的地址。
对于equals方法,注意:equals方法不能作用于基本数据类型的变量
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
小结:
HashMap 非线程安全
HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。 TreeMap:非线程安全基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
适用场景分析:
HashMap和HashTable:HashMap去掉了HashTable的contains方法,但是加上了containsValue()和containsKey()方法。HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允许空键值,而HashTable不允许。HashMap:适用于Map中插入、删除和定位元素。
Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
总结:
LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;
HashMap是非线程安全的,HashTable是线程安全的;
StringBuilder是非线程安全的,StringBuffer是线程安全的。
ArrayXxx:底层数据结构是数组,查询快,增删慢
LinkedXxx:底层数据结构是链表,查询慢,增删快
HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序