黑马程序员--java集合的底层数据结构

------- android培训java培训、期待与您交流! ----------


一:ArrayList

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    ......

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer.
     */
    private transient E[] elementData;


    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;
   ......
}

由上面可以看出,ArrayList 的底层最重要的两个属性:Object 数组和 size 属性。

看看它如何实现Add操作:

 public boolean add(E o) {
	ensureCapacity(size + 1);  // Increments modCount!!
	elementData[size++] = o;
	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 = (E[])new Object[newCapacity];
	    System.arraycopy(oldData, 0, elementData, 0, size);
	}
    }
可以看出,ArrayList通过将底层Object数组复制(System.arraycopy)的方式来处理数组元素的变化

当容量不足时,先扩容至当前容量的1.5倍,然后判断是否满足,如果不满足,直接把容量扩至当前所需容量。

这种扩容策略有一个好处:在添加大量元素前,通过 ensureCapacity 操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。

    public boolean remove(Object o) {
	if (o == null) {
            for (int index = 0; index < size; index++)
		if (elementData[index] == null) {
		    fastRemove(index);
		    return true;
		}
	} else {
	    for (int index = 0; index < size; index++)
		if (o.equals(elementData[index])) {
		    fastRemove(index);
		    return true;
		}
        }
	return false;
    }

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, 
                             numMoved);
        elementData[--size] = null; // Let gc do its work
    }

由上面的两段源码可以看出,ArrayList删除元素后,也是通过Object数组的复制来处理数组的变化。size总是记录当前数组的大小。

这就解释了,ArrayList添加和删除元素的效率低(数组复制过程消耗资源较多),而查找和更新元素的效率比较高的原因。

ArrayList和Vector的区别:

1.Vector是线程同步的,所以它也是线程安全的。而ArratList是线程异步的,不安全。如果不考虑安全因素,一般用Arralist效率比较高,查看JDK文档,给出提示:

  如果要实现Arraylist线程同步,可以通过下面方式:

如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须 保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedList 方法将该列表“包装”起来。这最好在创建时完成,以防止意外对列表进行不同步的访问:

        List list = Collections.synchronizedList(new ArrayList(...)); 
2.如果集合中的元素数量大于当前集合数组的长度时,Vector的增长率是目前数组长度的100%,而ArryaList增长率为目前数组长度的50%。所以,如果集合中使用数据量比较大的数据,用Vector有一定优势。

二:HashMap

1. HashMap概述:

   HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
 

2. HashMap的数据结构:

    HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。首先,HashMap类的属性中定义了Entry类型的数组。Entry类实现java.ultil.Map.Entry接口,同时每一对key和value是作为Entry类的属性被包装在Entry的类中。

如图所示,HashMap的数据结构:


 

 

 

   HashMap的部分源码如下:

/** 
 * The table, resized as necessary. Length MUST Always be a power of two. 
 */  
  
transient Entry[] table;  
   
static class Entry<K,V> implements Map.Entry<K,V> {  
    final K key;  
    V value;  
    Entry<K,V> next;  
    final int hash;  
    ……  
} 

 

可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表,当新建一个HashMap,就会初始化一个数组,table数组元素的类型就是Entry类型的,每个Entry就是一个键值对,并且持有一个指向下一个Entry元素的引用。

3.    HashMap的存取实现:


   1) 添加元素:

当我们往HashMap中put元素的时候,先根据key的重新计算元素的hashCode,根据hashCode得到这个元素在table数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

HashMap的部分源码如下:

public V put(K key, V value) {  
   // HashMap允许存放null键和null值。  
   // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。  
   if (key == null)  
       return putForNullKey(value);  
   // 根据key的keyCode重新计算hash值。  
   int hash = hash(key.hashCode());  
   // 搜索指定hash值在对应table中的索引。  
   int i = indexFor(hash, table.length);  
   // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。  
   for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
       Object k;  
      // 如果发现 i 索引处的链表的某个Entry的hash和新Entry的hash相等且两者的key相同,则新Entry覆盖旧Entry,返回。  
       if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
           V oldValue = e.value;  
           e.value = value;  
           e.recordAccess(this);  
           return oldValue;  
       }  
   }  
   // 如果i索引处的Entry为null,表明此处还没有Entry。  
   modCount++;  
   // 将key、value添加到i索引处。  
   addEntry(hash, key, value, i);  
   return null; 

 

 2) 读取元素:

有了上面存储时的hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

HashMap的部分源码如下:

public V get(Object key) {  
    if (key == null)  
        return getForNullKey();  
    int hash = hash(key.hashCode());  
    for (Entry<K,V> e = table[indexFor(hash, table.length)];  
        e != null;  
        e = e.next) {  
        Object k;  
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
            return e.value;  
    }  
    return null;  
}  


   3) 归纳起来简单地说,HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象,HashMap底层采用了一个Entry[]数组来保存所有的键值对,当需要存储一个Entry对象时,先通过hash算法算出其在数组中位置,在根据equals方法决定在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,然后通过equal方法从该位置的链表上取出Entry

注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

   Map m = Collections.synchronizedMap(new HashMap(...));

三.HashSet

HashSet
HashSet是按照哈希算法来存取集合中的对象,具有很好的存取和查找性能,当向集合中加入一个对象时,HashSet会调用对象的hashCode()方法来获取哈希码,然后根据这个哈希吗来计算对象在集合中的存放位置。
在Object类中定义了hashCode()和equal(),equal()是按照内存地址比较对象是否相同,如果object1.equal(object1)w为true时,则表明了object1和object2变量实际上引用了同一个对象,那么object1和object2的哈希码也是肯定相同。

看看它的源码:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{

    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();
        public HashSet() {
        map = new HashMap<E,Object>();
    }

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
}
可以看出它的内部其实就是一个HashMap的实例,set中元素就是这个HashMap的Key值,每次添加元素,实际是向map中添加一个key-value(值恒定)对,map会根据key进行查找,如果发现有key,会返回一个oldValue,如果没有找到key,添加到map中,并且返回null,可以看出。关于HashMap看上面的说明。

注意,此实现不是同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:

   Set s = Collections.synchronizedSet(new HashSet(...));


类似的TreeSet其内部就是一个NavigableMap的实例。

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    private transient NavigableMap<E,Object> m;

    private static final Object PRESENT = new Object();

    public boolean add(E e) {
    return m.put(e, PRESENT)==null;
    }

}

四.String

相比大家都很熟悉,看看源码,看个明白:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    private static final ObjectStreamField[] serialPersistentFields =
            new ObjectStreamField[0];

    public String() {
        this.value = new char[0];
    }

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

其内部是一个char型数组,而且是final,不可改变的,这说一个问题:

String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且大量浪费有限的内存空间。 

String a = "a"; //假设a指向地址0x0001
a = "b";//重新赋值后a指向地址0x0002,但0x0001地址中保存的"a"依旧存在,但已经不再是a所指向的,a 已经指向了其它地址。
因此String的操作都是改变赋值地址而不是改变值操作。


五.StringBuffer和StringBulider

看他们的源码说明:

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    static final long serialVersionUID = 3388685877147921107L;

    /**
     * Constructs a string buffer with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuffer() {
        super(16);
    }

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /** use serialVersionUID for interoperability */
    static final long serialVersionUID = 4383685877147921099L;

    /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {
        super(16);
    }

发现这两个都是继承AbstractStringBuilder

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    int count;

    AbstractStringBuilder() {
    }
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

    public int length() {
        return count;
    }

    public int capacity() {
        return value.length;
    }
可以看出其内部是一个char型数组和其长度count,看看它是如何添加的:

 public AbstractStringBuilder append(StringBuffer sb) {
        if (sb == null)
            return append("null");
        int len = sb.length();
        ensureCapacityInternal(count + len);
        sb.getChars(0, len, value, count);
        count += len;
        return this;
    }

    // Documentation in subclasses because of synchro difference
    public AbstractStringBuilder append(CharSequence s) {
        if (s == null)
            s = "null";
        if (s instanceof String)
            return this.append((String)s);
        if (s instanceof StringBuffer)
            return this.append((StringBuffer)s);
        return this.append(s, 0, s.length());
    }
public AbstractStringBuilder append(char[] str) {
        int len = str.length;
        ensureCapacityInternal(count + len);
        System.arraycopy(str, 0, value, count, len);
        count += len;
        return this;
    }

由上面的代码可以看出,所谓的添加一个新的String,其实就是在value数组后面再添加元素进入。

看看它如何添加boolean类型:

public AbstractStringBuilder append(boolean b) {
        if (b) {
            ensureCapacityInternal(count + 4);
            value[count++] = 't';
            value[count++] = 'r';
            value[count++] = 'u';
            value[count++] = 'e';
        } else {
            ensureCapacityInternal(count + 5);
            value[count++] = 'f';
            value[count++] = 'a';
            value[count++] = 'l';
            value[count++] = 's';
            value[count++] = 'e';
        }
        return this;
    }

最后看看这个函数:

  public abstract String toString();

说明是一个抽象函数,由子类自己完成:

StringBuilder类

 public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

StringBuffer:

   public synchronized String toString() {
        return new String(value, 0, count);
    }

可以看出,StringBuffer类中和数据操作有关的函数都是线程同步函数,也就是安全操作。而StringBuilder类都是普通函数

StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。 每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量。 

StringBuffer和StringBuilder类功能基本相似,主要区别在于StringBuffer类的方法是多线程、安全的,而StringBuilder不是线程安全的,相比而言,StringBuilder类会略微快一点。对于经常要改变值的字符串应该使用StringBuffer和StringBuilder类。 








  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值