学习集合的小结(一)

集合框架

参考:

https://blog.csdn.net/feiyanaffection/article/details/81394745《java集合超详解》

https://blog.csdn.net/FOR_AnMin/article/details/78546322《Java 集合-实现原理总结》

https://blog.csdn.net/djh2717/article/details/81608264《Java集合框架底层实现 --源码》

https://blog.csdn.net/baidu_16757561/article/details/49850471《HashMap遍历方法和实现原理分析》

概要:

对集合的层次进行梳理,并通过阅读源码等方式尝试模拟实现ArrayList ,LinkedList,HashSet

在这里插入图片描述
常用集合的分类:
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序

Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序

└ ———IdentifyHashMap

Collection

一个collection(集合)是用一个对象来代表一组对象,其中的每个对象作为collection的一个元素

在这里插入图片描述

List

有序可重复集

在这里插入图片描述

ArrayList:

ArrayList是List接口的一个可变长数组实现。

底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素

实现方法:数组的拷贝

public class ArrayT<E> {
	/*存储数据的源数组*/
	private Object[] arr;
	/*初始存储量*/
	private int initCapacity=10;
	//初始索引
	private int index;
	
	/*构造函数初始化长度为10的数组*/
	public ArrayT(){
		arr = new Object[initCapacity];
	}
	/*构造函数 初始化长度为len的数组*/
	public ArrayT(int len){
		arr=new Object[len];
	}
		
	/*返回列表中的元素个数*/
	public int size (){
		return index;
	}
	
	/*返回指定位置的元素*/
	public Object get(int index){
		return arr[index];
	}
	
	/*设置指定位置元素*/
	public void set(int index, E obj){
		this.arr[index]=obj;
	}
	
	/*在列表末尾增加元素*/
	public void add(E obj){
		//检查容量
		ensureCapacity();
		
		arr[index]=obj;
		this.index++;
	}
	
	/*在列表指定位置插入元素*/
	public void add(int index, E obj){
		//检查容量
		ensureCapacity();
		//向后移一位
		System.arraycopy(arr, index, arr, index+1, arr.length-(index+1));
		//插入
		arr[index]=obj;
		this.index++;
	}
	
	/*删除列表中所有元素*/
	public void clear(){
        for (int i = 0; i < index; i++)
            arr[i] = null;
        index = 0;
	}
	
	/*删除列表中指定位置元素,返回被删除的元素*/
	public E remove(int index){
		E target =arr(index);		
		arr[index]=null;
		System.arraycopy(arr, index+1, arr, index, arr.length-(index+1));	
		this.index--;	
		return target;
	}
	
	/*判断列表中指定对象是否存在*/
	public boolean contains(E obj){
		return indexOf(obj) >= 0;	
	}
	
	/*返回指定元素在集合第一次出现的索引*/
	public int indexOf(Object o){
		//判断o是否为null,为null的话就不能用equal,这可以找到第一个空值
		if (o == null) {
            for (int i = 0; i < index; i++)
                if (arr[i]==null)
                    return i;
        } else {
            for (int i = 0; i < index; i++)
                if (o.equals(arr[i]))
                    return i;
        }
        return -1;
	}
	
	//将Object数组转换成E数组
	@SuppressWarnings("unchecked")
	E arr(int in){
		return (E) arr[index];
	}
	
	/*判断容量*/	
	private void ensureCapacity(){
		if(index==arr.length){
			//容量已占满,扩充容量(扩充为原来容量的1.5倍)
			Object[] temp=new Object[arr.length+arr.length/2];
			System.arraycopy(arr, 0, temp, 0, arr.length);
			//将源数组的指针指向新数组
			arr=temp;
			temp=null;
		}
	}
}
LinkedList

底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素 。

双链表

public class LinkList<E> {
	//大小
	int size;
	//头结点
	Node<E> first;
	//尾结点
	Node<E> last;
	
	/*构造函数*/
	public LinkList() {
	}
	
	/*返回此列表中的元素数。*/
	 public int size() {
		 return size;
	 }
 
	 //链表的头结点加个前驱
	 private void linkFirst(E e) {
	        final Node<E> f = first;
	        final Node<E> newNode = new Node<>(null, e, f);
	        first = newNode;
	        if (f == null)
	            last = newNode;
	        else
	            f.prev = newNode;
	        size++;
	 }

	 //链表的尾结点加个后继
	 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++;
	 }
	 
	 /**
	  * 删除头结点
	  */
	 private E unlinkFirst(Node<E> f) {
	        // assert f == first && f != null;
	        final E element = f.item;
	        final Node<E> next = f.next;
	        f.item = null;
	        f.next = null; 
	        first = next;
	        if (next == null)
	            last = null;
	        else
	            next.prev = null;
	        size--;
	        return element;
	  }

	 /**
	  * 删除尾结点
	  */
	  private E unlinkLast(Node<E> l) {
	        // assert l == last && l != null;
	        final E element = l.item;
	        final Node<E> prev = l.prev;
	        l.item = null;
	        l.prev = null; // help GC
	        last = prev;
	        if (prev == null)
	            first = null;
	        else
	            prev.next = null;
	        size--;
	        return element;
	   }
	 
	 //在指定的不为空的结点前插入一个结点
	 void linkBefore(E e, Node<E> succ) {
	        // assert succ != null;
	        final Node<E> pred = succ.prev;
	        final Node<E> newNode = new Node<>(pred, e, succ);
	        succ.prev = newNode;
	        if (pred == null)
	            first = newNode;
	        else
	            pred.next = newNode;
	        size++;
	}
	 
	 //将某个不为空的结点置空
	E unlink(Node<E> x) {
	        // assert x != null;
	        final E element = x.item;
	        final Node<E> next = x.next;
	        final Node<E> prev = x.prev;

	        //x的前驱为空则它为头结点,把它的后继当做头结点
	        //x的前驱不为空则将其前驱的后继设为x的后继
	        if (prev == null) {
	            first = next;
	        } else {
	            prev.next = next;
	            x.prev = null;
	        }

	        //x的后继为空则它为尾结点,把它的前驱当做尾结点
	        //x的后继不为空则将其后继的前驱设为x的前驱
	        if (next == null) {
	            last = prev;
	        } else {
	            next.prev = prev;
	            x.next = null;
	        }

	        x.item = null;
	        size--;
	        return element;
	    }
	
	 /*找到指定位置的结点,如果index小于size的一半则从头开始遍历,反之从尾开始*/
	 Node<E> node(int index) {
	        // assert isElementIndex(index);
	        if (index < (size >> 1)) {
	            Node<E> x = first;
	            for (int i = 0; i < index; i++)
	                x = x.next;
	            return x;
	        } else {
	            Node<E> x = last;
	            for (int i = size - 1; i > index; i--)
	                x = x.prev;
	            return x;
	        }
	    }
	   
	 /*将指定的元素追加到此列表的末尾*/
	 public boolean add(E e) {
	        linkLast(e);
	        return true;
	 }
	 
	 /*在此列表中的指定位置插入指定的元素。*/
	 void add(int index, E element){
		 checkElementIndex(index);
			 if (index == size)
		            linkLast(element);
		        else
		            linkBefore(element, node(index));
	
	 }

	 /*返回此列表中指定位置的元素。  */
	 E get(int index){
		 checkElementIndex(index);
		 return node(index).item;
	 }

	 /*用指定的元素替换此列表中指定位置的元素。 */
	 E set(int index, E element) {
		  checkElementIndex(index);
	      Node<E> x = node(index);
	      E oldVal = x.item;
	      x.item = element;
	      return oldVal;
	 }

	 /*从列表中删除指定元素的第一个出现(如果存在)。*/
	 boolean remove(Object o) {
		 //删除空结点
	     if (o == null) {
	            for (Node<E> x = first; x != null; x = x.next) {
	                if (x.item == null) {
	                    unlink(x);
	                    return true;
	                }
	            }
	        } else {
	            for (Node<E> x = first; x != null; x = x.next) {
	                if (o.equals(x.item)) {
	                    unlink(x);
	                    return true;
	                }
	            }
	        }
	        return false;	 
	 }

	 /*删除该列表中指定位置的元素。  */
	 E remove(int index) {
		 checkElementIndex(index);	 
		return unlink(node(index));
	 }

	 /*如果此列表包含指定的元素,则返回 true 。 */
	 boolean contains(Object o) {
		return indexOf(o)>=0;	 
	 }

	 /*将元素推送到由此列表表示的堆栈上。  就是放在头上面*/
	 void push(E e) {
		 linkFirst(e);
	 }

	 /*从此列表表示的堆栈中弹出一个元素。 */
	 E pop() {
		 final Node<E> f = first;
	        if (f == null)
	        {
	        	System.out.println("栈为空");
	            return null;
	        }
	        return unlinkFirst(f);		 
	 }

	/*返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。 */
	int indexOf(Object o) {
		int i=0;
		if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {               
                    return i;
                }
                i++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    return i;
                }
                i++;
            }
        }
		return -1;
	}

	/*检查索引越界*/
	private void checkElementIndex(int index) {
		if (index<0||index>size)
			System.out.println("索引越界");
	}	
	
	/*结点数据结构*/
	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;
		}
   }
}
Vector:

底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素

Vector很多方法用了synchronized关键字,当一个线程调用了这些方法时,其他线程不能再同时调用这些方法,保证了其线程安全。

//添加元素
public synchronized boolean add(E e) {
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
}
//判断是否超过最大值
private void ensureCapacityHelper(int minCapacity) {
      if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}
//数组增大的方法
private void grow(int minCapacity) {
    //原数组长度
     int oldCapacity = elementData.length;
    //新数组长度每次增加capacityIncrement,若其为零则新数组长度为原数组的两倍。
     int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
    //若新数组长度还不够则为,长度为需要的大小
     if (newCapacity - minCapacity < 0)
         newCapacity = minCapacity;
    //避免新数组长度超过最大值,MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8
     if (newCapacity - MAX_ARRAY_SIZE > 0)
         newCapacity = hugeCapacity(minCapacity);
    //调用的System.arraycopy()
     elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList与LinkedList

Arraylist:
优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。

LinkedList:
优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景
缺点:因为LinkedList要移动指针,所以查询操作性能比较低。
适用场景分析:
当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。

ArrayList与Vector

ArrayList和Vector都是用数组实现的,主要有这么三个区别:
(1)Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰,这样就导致了Vector在效率上无法与ArrayList相比;
(2)两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
(3)*Vector可以设置增长因子,而ArrayList不可以。
(4)*Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

最主要区别就是Vector类被synchronized修饰

Set

无序无重复集每个具体的 Set 实现类依赖添加的对象的 equals()方法来检查独一性

无特有功能

HashSet

HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素。元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的。

hashcode()

存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,就该存储了,此时就要采用哈希的解决地址冲突算法,在当前hashCode值处类似一个新的链表, 在同一个hashCode值的后面存储存储不同的对象,这样就保证了元素的唯一性。

HashSet采用哈希算法,底层用数组存储数据。默认初始化容量16,加载因子0.75。

HashSet会用Hash码值去和数组长度取模, 模(这个模就是对象要存放在数组中的位置)相同时才会判断数组中的元素和要加入的对象的内容是否相同,如果不同才会添加进去。

HashSet 的实现基本上都是直接调用底层HashMap的相关方法来完成.

public boolean add(Object o) {
	return map.put(o, PRESENT)==null;
}  

要存入HashSet的集合对象中的自定义类必须覆盖hashCode(),equals()两个方法,才能保证集合中元素不重复
模拟实现代码:

    class MyHashSet {
        	private Node[] no;    
        	private static class Node {
            int key;
            Node next = null;
    
        Node(int key){
            this.key=key;
        }
        
        Node remove(int key) {
            if(key == this.key){
                return next;
            }
            if(next != null)
                this.next = next.remove(key);
            return this;
        }
        
        boolean contains(int key) {
            if(this.key == key)
                return true;
            if(next == null)
                return false;
            return next.contains(key);
        }
        
        void add(int key) {
            if(this.key == key)
                return;
            if(next == null) {
   			   next = new Node(key);
            }else{
                next.add(key);
            }
        }
    }
    
    /** Initialize your data structure here. */
    public MyHashSet() {
      no=new Node[500];
    }
    
    private int hash(int key) {
        return key % no.length;
    }   
    
	//添加
    public void add(int key) {
        int index=hash(key);
        if(no[index]==null){
            Node n=new Node(key);
            no[index]=n;
        }else {   
            no[index].add(key);    
        }
    }
    
    //删除
    public void remove(int key) {
        int index=hash(key);
        if(no[index]!=null){
           no[index]=no[index].remove(key);              
        }
    }
    
    /** Returns true if this set contains the specified element */
    public boolean contains(int key) {
        int index=hash(key);
        if(no[index]!=null)
            return no[index].contains(key);
        return false;
    }
}
LinkedHashSet

LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。

TreeSet

TreeSet底层数据结构采用二叉树(红黑树的树据结构)来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性;不允许放入null值 .

自然排序(无参构造)

元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;

比较器排序(有参构造)

较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法;

小结

加入Set的元素必须定义equals()方法以确保对象的唯一性。

list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。

为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。

Map(映射)

在这里插入图片描述

Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。

HashMap

首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。

HashMap可以看作三个视图:key的Set,value的Collection,Entry的Set。 这里HashSet就是其实就是HashMap的一个视图。HashSet内部就是使用Hashmap实现的,和Hashmap不同的是它不需要Key和Value两个值。

HashMap为散列映射,它是基于hash table的一个实现,它可在常量时间内安插元素,或找出一组key-value pair.

	// 静态内部类Entry
	static class Node<K,V> {
		    //哈希值
	        final int hash;
	        //键值,不允许重复
	        final K key;
	        //
	        V value;
	        //当哈希值相等时,就用next连接,比如A,B哈希值相等,先A后B,则B.next = A,Entry[0] = B。
	        //数组中存储的是最后插入的元素。
	        Node<K,V> next;

	        Node(int hash, K key, V value, Node<K,V> next) {
	            this.hash = hash;
	            this.key = key;
	            this.value = value;
	            this.next = next;
	        }

	        public final K getKey()        { return key; }
	        public final V getValue()      { return value; }
	        public final String toString() { return key + "=" + value; }

	        //调用odjects的hascode()求哈希值
	        public final int hashCode() {
	            return Objects.hashCode(key) ^ Objects.hashCode(value);
	        }

	        public final V setValue(V newValue) {
	            V oldValue = value;
	            value = newValue;
	            return oldValue;
	        }

	        //比较两个node是否相等
	        public final boolean equals(Object o) {
	            if (o == this)
	                return true;
	            if (o instanceof Node) {
	            	Node<?,?> e = (Node<?,?>)o;
	                if (Objects.equals(key, e.getKey()) &&
	                    Objects.equals(value, e.getValue()))
	                    return true;
	            }
	            return false;
	        }
	    }

以上代码抄得源码或是参考源码。

HashMap、Hashtable、TreeMap的区别?

**HashMap:**是基于hash表的实现,是Map接口的一个实现类,内部的元素存储顺序是根据key的hash地址进行排序,因此,元素的顺序存储顺序可能跟添加的顺序不一致;HashMap使用链表加数组共同来实现元素存储;HashMap允许空键值出现,但是不允许重复的键存在(值可以重复);HashMap是线程不同步的实现,因此在操作数据时效率较高,但是线程并发是不能保障数据的一致性。如果多线程并发下使用建议使用:java.util.concurrent.ConcurrentHashMap

Hashtable:Hashtable是集合框架出现之前的键值对集合解决方案,从Dictionary类(JDK1.0)继承而来,在JDk1.2出现Map接口后,Hashtable又从改接口实现,Hashtable不允许空键值出现,该类的实现是线程同步,因此在多线程并发操作时对数据的一致性有保障,但是效率低。

TreeMap:是基于二叉树中的红黑树实现,元素的存储按照键的自然顺序排序(键必须实现Comparable接口,或者在创建TreeMap对象时指定的Comparator比较器),TreeMap中的键必须是相同的数据类型,并且不允许空元素出现,同HashMap一样,TreeMap也是线程不同步的实现。

Collections

Collections类是是一个用于对集合进行一些常规操作的工具类(排序,查找,反转等),内部包含的都是静态方法.

Collection、Collections的区别

Collection:JDK1.2之后出现的集合框架的顶层接口

Collections:跟随集合框架共同出现的一个用处理集合(排序,查找等)相关的工具类

iterator

在对集合迭代的时候,如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常;

这个异常是因为modCount与expectedModCount不等。而调用集合本身的remove(),实际上会调用removeNode(),这会导致++modCount,而expectedModCount不变。

要用迭代器进行删除,因为迭代器用得是自己的remove();

Comparator接口

因为集合的底层实现是不同的,且集合内部存储的元素的类型不定,要按上面属性排序也不定,比如学生类可以按学号排序,也可以按年龄排序。所以集合的排序主要有两种方法:

排序比较器

排序比较器,通过继承Comparator接口并实现compare方法。

    public class MyComparator implements Comparator<String>{	
        /*
    		方法内部需要传入两个被比较的对象,数据类型跟接口泛型一直,方法
            的返回值为int值,集合的sort方法会自动根据该返回结果决定如何实现对元素
            的排序
        */
    	@Override
    	public int compare(String a, String b) {
    		return a.compareTo(b);
    	}
    }

names.sort(new MyComparator());//主函数调用sort方法,传入比较器。

匿名内部类实现排序

    names.sort(new Comparator<String>() {   
    			@Override
    			public int compare(String a, String b) {
    				return a.compareTo(b);
    			}
    	});

用lamda表达式简化:

    names.sort((a,b)->a.compareTo(b));

sort调用比较器的具体过程:

sort调用Arrays.sort(),Arrays是集合的工具类。Arrays.sort()调用的则是mergeSort(),也就是归并排序。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值