zoukankan      html  css  js  c++  java
  • java基础知识--- 集合类

    常用集合类的使用

        

    java中的集合,不管是List,Set,还是Map,都是继承自collection接口,这个接口主要定义了集合类的一些公关方法,比如isEmpty(), remove(),add()等,在使用集合类的时候除了顺序遍历,还提供了一种方便的迭代器遍历的方法,在遍历过程中需要remove元素的必须试用迭代器遍历(在删除元素时,集合内部的index会发生变化,使用顺序遍历可能会产生unindex的情况)。
    为了实现迭代遍历,定义了Iterable接口,collection接口继承了Iterable接口,以支持所有集合类实现迭代遍历。

    1.List
    1.1 ArrayList
    ArrayList 是一个动态数组,以一个数组的格式进行存储,但是可以动态增长,继承自AbstractList,实现了List接口

    ArrayList是一个非线程安全的类,使用时最好在一个线程中操作,构造函数可以提供一个数组的初始化大小,不提供默认按16来进行数组的初始化,往ArrayList增加元素时,都会先判断是否还有空间存在元素,如果没有空间,则会重新申请一个(((以前长度*3)/2)+1)大小的数组空间,并将原来的元素全部copy到新的数组中,这样会带来效率上的损耗。建议在初始化时,能够确定存储空间的尽量提供合适的初始化数组大小,避免因为数组动态扩容带来的效率损耗。
    1.2 Vector
    Vector 基本上很少用到了,和ArrayList很相似,唯一的不同是Vector的方法都是线程安全的,而Array List是非线程安全的,从这点上来说,ArrayList的效率要比Vector的效率高,另外Vector是以2倍的方式扩展数组容量的

    1.3 LinkedList
    LinkedList 同时实现了List,Deque,所以也可以用来作为双向队列使用。LinkedList是以双向列表存储的,它是按照元素的先后顺序进行存储的,所以访问也是按照顺序来访问的。

    2 Set
    Set 继承了Collection,Set不保存重复的元素,存入Set里的元素都是唯一的,区分存入的元素是否重复是通过调用Equals方法来进行判断,所有存入Set中的元素类必须实现Equals方法
    2.1 HashSet
    Hash Set是讲存入Set中的元素以Hash链表的方式存储起来,所有元素存入Set中的位置通过调用hashCode方法获取hash值来决定,所以需要设计好的hashcode方法尽量将存入的值散列开有利于提升HashSet的访问效率。
    2.2 LinkedHashSet
    继承与HashSet,和HashSet的唯一区别是维护了一个双向列表来维护元素的顺序,所有访问是按顺序访问

    2.3 TreeSet
    TreeSet继承于AbstractSet,并且实现了NavigableSet接口
    TreeSet实现了一个顺序访问的不重复元素的Set,底层使用红黑树进行数据的存储,来加快访问的速度
    2.4 EnumSet
    EnumSet只能用来存放Enum类型的数据,也不允许重复数据,性能是最好的

    3 Map
    Map和Set的区别是Set只有值,而Map是一个键值对<key,value>,Set不存重复的元素,Map中的key不能相同
    3.1 HashMap
    和HashSet相似,用hash链表来存储,讲键值对作为一个元素存储
    3.2 LinkedHashMap
    类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点。而在迭代访问时发而更快,因为它使用链表维护内部次序
    3.3 TreeMap
    基于红黑树数据结构的实现。查看“键”或“键值对”时,它们会被排序(次序由Comparabel或Comparator决定)。TreeMap的特点在于,你得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。3.4 WeakHashMap
    弱键(weak key)Map,Map中使用的对象也被允许释放: 这是为解决特殊问题设计的。如果没有map之外的引用指向某个“键”,则此“键”可以被垃圾收集器回收。

    ArrayList 和 LinkedList 和 Vector 的区别

      三者都属于List的子类,方法基本相同。比如都可以实现数据的添加、删除、定位以及都有迭代器进行数据的查找,但是每个类
    在安全,性能,行为上有着不同的表现。
      Vector是Java中线程安全的集合类,如果不是非要线程安全,不必选择使用,毕竟同步需要额外的性能
    开销,底部实现也是数组来操作,再添加数据时,会自动根据需要创建新数组增加长度来保存数据,并拷贝原有数组数据
      ArrayList是应用广泛的动态数组实现的集合类,不过线程不安全,所以性能要好的多,也可以根据需要增加数组容量,不过与
    Vector的调整逻辑不同,ArrayList增加50%,而Vector会扩容1倍。
      LinkedList是基于双向链表实现,不需要增加长度,也不是线程安全的
      Vector与ArrayList在使用的时候,应保证对数据的删除、插入操作的减少,因为每次对改集合类进行这些操作时,都会使原有数据
    进行移动除了对尾部数据的操作,所以非常适合随机访问的场合。
      LinkedList进行节点的插入、删除却要高效的多,但是随机访问对于该集合类要慢的多。

    SynchronizedList 和 Vector 的区别

      

    Vector是java.util包中的一个类。 SynchronizedList是java.util.Collections中的一个静态内部类。

    在多线程的场景中可以直接使用Vector类,也可以使用Collections.synchronizedList(List list)方法来返回一个线程安全的List。

    那么,到底SynchronizedList和Vector有没有区别,为什么java api要提供这两种线程安全的List的实现方式呢?

    首先,我们知道Vector和Arraylist都是List的子类,他们底层的实现都是一样的。

        所以这里比较如下两个list1list2的区别:

      

    1 List<String> list = new ArrayList<String>();
    2 
    3 List list2 =  Collections.synchronizedList(list);
    4 
    5 Vector<String> list1 = new Vector<String>();

    一、比较几个重要的方法

        Vector的实现

        

     1 public void add(int index, E element) {
     2 
     3     insertElementAt(element, index);
     4 
     5 }
     6 public synchronized void insertElementAt(E obj, int index) {
     7     modCount++;
     8     if (index > elementCount) {
     9         throw new ArrayIndexOutOfBoundsException(index
    10                                                  + " > " + elementCount);
    11     }
    12     ensureCapacityHelper(elementCount + 1);
    13     System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
    14     elementData[index] = obj;
    15     elementCount++;
    16 }
    17 
    18 private void ensureCapacityHelper(int minCapacity) {
    19     // overflow-conscious code
    20     if (minCapacity - elementData.length > 0)
    21         grow(minCapacity);
    22 }

    synchronizedList的实现:

    1 public void add(int index, E element) {
    2    synchronized (mutex) {
    3       list.add(index, element);
    4    }
    5 }

    这里,使用同步代码块的方式调用ArrayList的add()方法。

          ArrayList的add方法内容如下:

     1 public void add(int index, E element) {
     2     rangeCheckForAdd(index);
     3     ensureCapacityInternal(size + 1);  // Increments modCount!!
     4     System.arraycopy(elementData, index, elementData, index + 1,
     5                      size - index);
     6     elementData[index] = element;
     7     size++;
     8 }
     9 private void rangeCheckForAdd(int index) {
    10     if (index > size || index < 0)
    11         throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    12 }
    13 private void ensureCapacityInternal(int minCapacity) {
    14     if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    15         minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    16     }
    17     ensureExplicitCapacity(minCapacity);
    18 }

    从上面两段代码中发现有两处不同: 1.Vector使用同步方法实现,synchronizedList使用同步代码块实现。 2.两者的扩充数组容量方式不一样(两者的add方法在扩容方面的差别也就是ArrayList和Vector的差别。)

    remove方法

    synchronizedList的实现:

    1 public E remove(int index) {
    2     synchronized (mutex) {return list.remove(index);}
    3 }

    ArrayList类的remove方法内容如下:

     1 public E remove(int index) {
     2     rangeCheck(index);
     3     modCount++;
     4     E oldValue = elementData(index);
     5     int numMoved = size - index - 1;
     6     if (numMoved > 0)
     7         System.arraycopy(elementData, index+1, elementData, index,
     8                          numMoved);
     9     elementData[--size] = null; // clear to let GC do its work
    10     return oldValue;
    11 }

    Vector的实现:

     1 public synchronized E remove(int index) {
     2        modCount++;
     3         if (index >= elementCount)
     4             throw new ArrayIndexOutOfBoundsException(index);
     5         E oldValue = elementData(index);
     6         int numMoved = elementCount - index - 1;
     7         if (numMoved > 0)
     8             System.arraycopy(elementData, index+1, elementData, index,
     9                              numMoved);
    10         elementData[--elementCount] = null; // Let gc do its work
    11         return oldValue;
    12     }

    从remove方法中我们发现除了一个使用同步方法,一个使用同步代码块之外几乎无任何区别。

        通过比较其他方法,我们发现,SynchronizedList里面实现的方法几乎都是使用同步代码块包上List的方法。如果该List是ArrayList,那么,SynchronizedList和Vector的一个比较明显区别就是一个使用了同步代码块,一个使用了同步方法。

    区别分析

    数据增长区别

    从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。

    同步代码块和同步方法的区别 1.同步代码块在锁定的范围上可能比同步方法要小,一般来说锁的范围大小和性能是成反比的。 2.同步块可以更加精确的控制锁的作用域(锁的作用域就是从锁被获取到其被释放的时间),同步方法的锁的作用域就是整个方法。 3.静态代码块可以选择对哪个对象加锁,但是静态方法只能给this对象加锁。

    因为SynchronizedList只是使用同步代码块包裹了ArrayList的方法,而ArrayList和Vector中同名方法的方法体内容并无太大差异,所以在锁定范围和锁的作用域上两者并无却别。 在锁定的对象区别上,SynchronizedList的同步代码块锁定的是mutex对象,Vector锁定的是this对象。那么mutex对象又是什么呢? 其实SynchronizedList有一个构造函数可以传入一个Object,如果在调用的时候显示的传入一个对象,那么锁定的就是用户传入的对象。如果没有指定,那么锁定的也是this对象。

    所以,SynchronizedList和Vector的区别目前为止有两点: 1.如果使用add方法,那么他们的扩容机制不一样。 2.SynchronizedList可以指定锁定的对象。

    但是,凡事都有但是。 SynchronizedList中实现的类并没有都使用synchronized同步代码块。其中有listIterator和listIterator(int index)并没有做同步处理。但是Vector却对该方法加了方法锁。 所以说,在使用SynchronizedList进行遍历的时候要手动加锁。

    但是,但是之后还有但是。

    之前的比较都是基于我们将ArrayList转成SynchronizedList。那么如果我们想把LinkedList变成线程安全的,或者说我想要方便在中间插入和删除的同步的链表,那么我可以将已有的LinkedList直接转成 SynchronizedList,而不用改变他的底层数据结构。而这一点是Vector无法做到的,因为他的底层结构就是使用数组实现的,这个是无法更改的。

    所以,最后,SynchronizedList和Vector最主要的区别: 1.SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。 2.使用SynchronizedList的时候,进行遍历时要手动进行同步处理。3.SynchronizedList可以指定锁定的对象。

    HashMap、HashTable、ConcurrentHashMap 区别

    HashMap和HashTable的区别一种比较简单的回答是:

    (1)HashMap是非线程安全的,HashTable是线程安全的。

    (2)HashMap的键和值都允许有null存在,而HashTable则都不行。

    (3)因为线程安全、哈希效率的问题,HashMap效率比HashTable的要高。

    1.  HashMap概述

    Java中的数据存储方式有两种结构,一种是数组,另一种就是链表,前者的特点是连续空间,寻址迅速,但是在增删元素的时候会有较大幅度的移动,所以数组的特点是查询速度快,增删较慢。

    而链表由于空间不连续,寻址困难,增删元素只需修改指针,所以链表的特点是查询速度慢、增删快。

    那么有没有一种数据结构来综合一下数组和链表以便发挥他们各自的优势?答案就是哈希表。哈希表的存储结构如下图所示:

                          

    从上图中,我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点,通过功能类似于hash(key.hashCode())%len的操作,获得要添加的元素所要存放的的数组位置。

    HashMap的哈希算法实际操作是通过位运算,比取模运算效率更高,同样能达到使其分布均匀的目的,后面会介绍。

    键值对所存放的数据结构其实是HashMap中定义的一个Entity内部类,数组来实现的,属性有key、value和指向下一个Entity的next。

    2.  HashMap初始化

    HashMap有两种常用的构造方法:

    第一种是不需要参数的构造方法:

     1 static final int DEFAULT_INITIAL_CAPACITY = 16; //初始数组长度为16
     2 static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量为2的30次方
     3 //装载因子用来衡量HashMap满的程度
     4 //计算HashMap的实时装载因子的方法为:size/capacity
     5 static final float DEFAULT_LOAD_FACTOR = 0.75f; //装载因子
     6 public HashMap() {  
     7     this.loadFactor = DEFAULT_LOAD_FACTOR;  
     8 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
     9 //默认数组长度为16 
    10     table = new Entry[DEFAULT_INITIAL_CAPACITY];
    11     init();  
    12 } 

    第二种是需要参数的构造方法:

     1 public HashMap(int initialCapacity, float loadFactor) {  
     2         if (initialCapacity < 0)  
     3             throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);  
     4         if (initialCapacity > MAXIMUM_CAPACITY)  
     5             initialCapacity = MAXIMUM_CAPACITY;  
     6         if (loadFactor <= 0 || Float.isNaN(loadFactor))  
     7             throw new IllegalArgumentException("Illegal load factor: " + loadFactor);  
     8         // Find a power of 2 >= initialCapacity  
     9         int capacity = 1;  
    10         while (capacity < initialCapacity)  
    11             capacity <<= 1;  
    12         this.loadFactor = loadFactor;  
    13         threshold = (int)(capacity * loadFactor);  
    14         table = new Entry[capacity];  
    15         init();  
    16 } 

    从源码可以看出,初始化的数组长度为capacity,capacity的值总是2的N次方,大小比第一个参数稍大或相等。

    3.  HashMap的put操作

     1 public V put(K key, V value) {  
     2         if (key == null)  
     3           return putForNullKey(value);  
     4         int hash = hash(key.hashCode());  
     5         int i = indexFor(hash, table.length);  
     6         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
     7             Object k;  
     8             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
     9                 V oldValue = e.value;  
    10                 e.value = value;  
    11                 e.recordAccess(this);  
    12                 return oldValue;  
    13             }  
    14         }        
    15 modCount++;  
    16         addEntry(hash, key, value, i);  
    17         return null;  
    18 } 

    3.1  put进的key为null

     从源码中可以看出,HashMap是允许key为null的,会调用putForNullKey()方法:

     1 private V putForNullKey(V value) {  
     2         for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
     3             if (e.key == null) {  
     4                 V oldValue = e.value;  
     5                 e.value = value;  
     6                 e.recordAccess(this);  
     7                 return oldValue;  
     8            }  
     9         }  
    10         modCount++;  
    11         addEntry(0, null, value, 0);  
    12         return null;  
    13 } 
    14 
    15 void addEntry(int hash, K key, V value, int bucketIndex) {  
    16    Entry<K,V> e = table[bucketIndex];  
    17         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
    18         if (size++ >= threshold)  
    19             resize(2 * table.length);  
    20     }  

      putForNullKey方法会遍历以table[0]为链表头的链表,如果存在key为null的KV,那么替换其value值并返回旧值。否则调用addEntry方法,这个方法也很简单,将[null,value]放在table[0]的位置,并将新加入的键值对封装成一个Entity对象,将其next指向原table[0]处的Entity实例。

      size表示HashMap中存放的所有键值对的数量。

      threshold = capacity*loadFactor,最后几行代码表示当HashMap的size大于threshold时会执行resize操作,将HashMap扩容为原来的2倍。扩容需要重新计算每个元素在数组中的位置,indexFor()方法中的table.length参数也证明了这一点。

      但是扩容是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。比如说我们有1000个元素,那么我们就该声明new HashMap(2048),因为需要考虑默认的0.75的扩容因子和数组数必须是2的N次方。若使用声明new HashMap(1024)那么put过程中会进行扩容。
    3.2  put进的key不为null

      将上述put方法中的相关代码复制一下方便查看:

      

     1 int hash = hash(key.hashCode());  
     2 int i = indexFor(hash, table.length);  
     3 for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
     4    Object k;  
     5     if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
     6         V oldValue = e.value;  
     7         e.value = value;  
     8         e.recordAccess(this);  
     9         return oldValue;  
    10        }  
    11 }        
    12 modCount++;  
    13 addEntry(hash, key, value, i);  
    14 return null;  
    15 }

      从源码可以看出,第1、2行计算将要put进的键值对的数组的位置i。第4行判断加入的key是否和以table[i]为链表头的链表中所有的键值对有重复,若重复则替换value并返回旧值,若没有重复则调用addEntry方法,上面对这个方法的逻辑已经介绍过了。

      至此HashMap的put操作已经介绍完毕了。
    4.  HashMap的get操作

     1 public V get(Object key) {  
     2    if (key == null)  
     3        return getForNullKey();  
     4    int hash = hash(key.hashCode());  
     5    for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {  
     6             Object k;  
     7             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
     8                 return e.value;  
     9         }  
    10    return null;  
    11 }  
    12 private V getForNullKey() {  
    13    for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
    14    if (e.key == null)  
    15      return e.value;  
    16     }  
    17     return null;  
    18 }  

      如果了解了前面的put操作,那么这里的get操作逻辑就很容易理解了,源码中的逻辑已经非常非常清晰了。需要注意的只有当找不到对应value时,返回的是null

      或者value本身就是null。这是可以通过containsKey()来具体判断。了解了上面HashMap的put和get操作原理,可以通过下面这个小例题进行知识巩固,题目是打印在数组中出    现 n/2以上的元素,我们便可以使用HashMap的特性来解决。
      

     1 public class HashMapTest {  
     2     public static void main(String[] args) {  
     3         int [] a = {2,1,3,2,0,4,2,1,2,3,1,5,6,2,2,3};  
     4         Map<Integer, Integer> map = new HashMap<Integer,Integer>();  
     5         for(int i=0; i<a.length; i++){  
     6             if(map.containsKey(a[i])){  
     7                 int tmp = map.get(a[i]);  
     8                 tmp+=1;  
     9                 map.put(a[i], tmp);  
    10             }else{  
    11                map.put(a[i], 1);  
    12             }  
    13         }  
    14         Set<Integer> set = map.keySet();        
    15 for (Integer s : set) {  
    16             if(map.get(s)>=a.length/2){  
    17                 System.out.println(s);  
    18             }  
    19         }
    20    }  
    21 }  

    5. HashMap和HashTable的对比

    HashTable和HashMap采用相同的存储机制,二者的实现基本一致,

      不同的是:

    (1)HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都经过synchronized修饰。

    (2)因为同步、哈希性能等原因,性能肯定是HashMap更佳,因此HashTable已被淘汰。

    (3) HashMap允许有null值的存在,而在HashTable中put进的键值只要有一个null,直接抛出NullPointerException。

    (4)HashMap默认初始化数组的大小为16,HashTable为11。前者扩容时乘2,使用位运算取得哈希,效率高于取模。而后者为乘2加1,都是素数和奇数,这样取模哈希结果更均匀。 看下两种集合的hash算法。看源码也不难理解。

     1 //HashMap的散列函数,这里传入参数为键值对的key
     2 static final int hash(Object key) {
     3     int h;
     4     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
     5 } 
     6 //返回hash值的索引,h & (length-1)操作等价于 hash % length操作, 但&操作性能更优
     7 static int indexFor(int h, int length) {
     8     // length must be a non-zero power of 2
     9     return h & (length-1);
    10 }
    11 //HashTable的散列函数直接在put方法里实现了
    12 int hash = key.hashCode();
    13 int index = (hash & 0x7FFFFFFF) % tab.length;

    6.  HashTable和ConCurrentHashMap的对比

    先对ConcurrentHashMap进行一些介绍吧,它是线程安全的HashMap的实现。HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。ConcurrentHashMap算是对上述问题的优化,其构造函数如下,默认传入的是16,0.75,16。

     1 public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  {  
     2     //
     3     int i = 0;  
     4     int j = 1;  
     5     while (j < paramInt2) {  
     6       ++i;  
     7       j <<= 1;  
     8     }  
     9     this.segmentShift = (32 - i);  
    10     this.segmentMask = (j - 1);  
    11     this.segments = Segment.newArray(j);  
    12     //
    13     int k = paramInt1 / j;  
    14     if (k * j < paramInt1)  
    15       ++k;  
    16     int l = 1;  
    17     while (l < k)  
    18       l <<= 1;  
    19     for (int i1 = 0; i1 < this.segments.length; ++i1)  
    20       this.segments[i1] = new Segment(l, paramFloat);  
    21   }  
    22 
    23  
    24 
    25 public V put(K paramK, V paramV)  {  
    26 
    27     if (paramV == null)  
    28 
    29       throw new NullPointerException();  
    30 
    31     int i = hash(paramK.hashCode()); //这里的hash函数和HashMap中的不一样
    32 
    33     return this.segments[(i >>> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false);  
    34 
    35 }  

        ConcurrentHashMap引入了分割(Segment),上面代码中的最后一行其实就可以理解为把一个大的Map拆分成N个小的HashTable,在put方法中,会根据hash(paramK.hashCode())来决定具体存放进哪个Segment,如果查看Segment的put操作,我们会发现内部使用的同步机制是基于lock操作的,这样就可以对Map的一部分(Segment)进行上锁,这样影响的只是将要放入同一个Segment的元素的put操作,保证同步的时候,锁住的不是整个Map(HashTable就是这么做的),相对于HashTable提高了多线程环境下的性能,因此HashTable已经被淘汰了。
    7.  HashMap和ConCurrentHashMap的对比

      

    最后对这俩兄弟做个区别总结吧:

    (1)经过4.2的分析,我们知道ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

    (2)HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

    Set 和 List 区别

    set和list都是集合接口 

    set --其中的值不允许重复,无序的数据结构

     list --其中的值允许重复,因为其为有序的数据结构

     List的功能方法

    实际上有两种List: 一种是基本的ArrayList,其优点在于随机访问元素,另一种是更强大的LinkedList,它并不是为快速随机访问设计的,而是具有一套更通用的方法。

     List : 次序是List最重要的特点:它保证维护元素特定的顺序。List为Collection添加了许多方法,使得能够向List中间插入与移除元素(这只推荐LinkedList使用。)一个List可以生成ListIterator,使用它可以从两个方向遍历List,也可以从List中间插入和移除元素。

     ArrayList : 由数组实现的List。允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。ListIterator只应该用来由后向前遍历ArrayList,而不是用来插入和移除元素。因为那比LinkedList开销要大很多。

     LinkedList : 对顺序访问进行了优化,向List中间插入与删除的开销并不大。随机访问则相对较慢。(使用ArrayList代替。)还具有下列方法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和removeLast(), 这些方法 (没有在任何接口或基类中定义过)使得LinkedList可以当作堆栈、队列和双向队列使用。

     Set的功能方法

     Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)Set不保存重复的元素(至于如何判断元素相同则较为负责)

    Set : 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。

    HashSet : 为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。 

    TreeSet : 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。

    LinkedHashSet : 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。

     Java 8 中 stream 相关用法  

    1.java8中为什么加入stream   

      tream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。

    Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。尤其是对于数据从业人员来说,对数据做各种操作转换是再正常不过的需求,基本每天都会用到。例如下面这么一个简单的小需求:求一个集合中字符串长度小于5的数量。
      在java8之前,我们一般这么做:

     1    @Test
     2     public void lenIter() {
     3         List<String> list = Arrays.asList("java", "scala", "python", "shell", "ruby");
     4         int num = 0;
     5         for(String lan: list) {
     6             if(lan.length() < 5) {
     7                 num++;
     8             }
     9         }
    10         System.out.println(num);
    11     }

    这段代码逻辑很简单,但是显得很冗长,可读性嘛也就呵呵了。如果用Stream,我们可以这样:

    1   @Test
    2     public void lenStream() {
    3         List<String> list = Arrays.asList("java", "scala", "python", "shell", "ruby");
    4         long num = list.parallelStream().filter(x -> x.length() < 5).count();
    5         System.out.println(num);
    6     }

    代码量明显减少而且逻辑特别清楚,即使不懂代码的人看到也能猜出来是什么意思。如果大家了解过函数式编程,就会觉得特别亲切自然。

      2.什么是stream 

      Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
    Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
    而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。

    Stream和Collection的区别主要有:
    1.stream本身并不存储数据,数据是存储在对应的collection里,或者在需要的时候才生成的;
    2.stream不会修改数据源,总是返回新的stream;
    3.stream的操作是懒执行(lazy)的:仅当最终的结果需要的时候才会执行,比如上面的例子中,结果仅需要前3个长度大于7的字符串,那么在找到前3个长度符合要求的字符串后,filter()将停止执行;

    使用stream的步骤如下:
    1.创建stream;
    2.通过一个或多个中间操作(intermediate operations)将初始stream转换为另一个stream;
    3.通过中止操作(terminal operation)获取结果;该操作触发之前的懒操作的执行,中止操作后,该stream关闭,不能再使用了;


      3.创建stream的方法

        最常用的为使用静态方法创建

    1     @Test
    2     public void numberStreamConstruct() {
    3         IntStream.of(new int[] {1, 2, 3}).forEach(System.out::println);
    4         IntStream.range(1, 3).forEach(System.out::println);
    5         IntStream.rangeClosed(1, 3).forEach(System.out::println);
    6     }

      4.stream的转换

      Stream最大的用途就是各种转换了。跟Spark中的Rdd类似,Rdd里面也是各种transfer操作。


      1.  filter操作。即使原stream中满足条件的元素构成新的stream:

    1    @Test
    2     public void lenStream() {
    3         List<String> list = Arrays.asList("java", "scala", "python", "shell", "ruby");
    4         long num = list.parallelStream().filter(x -> x.length() < 5).count();
    5         System.out.println(num);
    6     }

    结果是:2      得到长度小于5的单词个数

     2.map操作。map算是最常用的一种操作了,遍历原stream中的元素,转换后构成新的stream:

    1     @Test
    2     public void turnUpperCase() {
    3         List<String> list = Arrays.asList(new String[] {"a", "b", "c"});
    4         List<String> result = list.stream().map(String::toUpperCase).collect(Collectors.toList());
    5         result.forEach(x -> System.out.print(x + " "));
    6     }

     3.distinct操作。distinct也是常用的操作之一。

    1    @Test
    2     public void distinctStream() {
    3         Stream<String> distinctStream = Stream.of("bj","shanghai","tianjin","bj","shanghai").distinct();
    4         Stream<String> sortedStream = distinctStream.sorted(Comparator.comparing(String::length));
    5         sortedStream.forEach(x -> System.out.print(x + " "));
    6     }

    结果:  bj tianjin shanghai

    4.排序操作

      

     1    @Test
     2     public void sortStream() {
     3         Stream<Integer> sortedStream = Stream.of(1,3,7,4,5,8,6,2).sorted();
     4         sortedStream.collect(Collectors.toList()).forEach(x -> System.out.print(x + " "));
     5         System.out.println();
     6 
     7         Stream<Integer> sortedReverseStream = Stream.of(1,3,7,4,5,8,6,2).sorted(new Comparator<Integer>() {
     8             @Override
     9             public int compare(Integer o1, Integer o2) {
    10                 return o1 - o2;
    11             }
    12         });
    13         Stream<Integer> sortedReverseStreamV2 = Stream.of(1,3,7,4,5,8,6,2).sorted((Integer o1, Integer o2) -> o2 - o1);
    14         sortedReverseStreamV2.collect(Collectors.toList()).forEach(x -> System.out.print(x + " "));
    15     }

    最终的结果:  1 2 3 4 5 6 7 8

            8 7 6 5 4 3 2 1  

     5.reduction操作

      1.reduction就是从stream中取出结果,是terminal operation,因此经过reduction后的stream不能再使用了。主要包含以下操作: findFirst()/findAny()/allMatch/anyMatch()/noneMatch等等

    1     @Test
    2     public void reductionStream() {
    3         Stream<String> wordList = Stream.of("bj","tj","sh","yy","yq").distinct();
    4         Optional<String> firstWord = wordList.filter(word -> word.startsWith("y")).findFirst();
    5         System.out.println(firstWord.orElse("unknown"));
    6     }

      结果:yy

      reduce方法。与其他语言里的reduce方法一样的逻辑。

    1    @Test
    2     public void reduceTest() {
    3         Stream<Integer> list = Stream.of(1,2,3,4,5);
    4         Optional<Integer> result = list.reduce((x, y) -> x + y);
    5         System.out.println(result);
    6     }

    结果如下: Optional[15]

     6.collect

      collect()方法可以对stream中的元素进行各种处理后,得到stream中元素的值。并且Collectors接口提供了很方便的创建Collector对象的工厂方法。

     1    @Test
     2     public void collectTest() {
     3         List<String> list = Stream.of("hello", "world", "hello", "java").collect(Collectors.toList());
     4         list.forEach(x -> System.out.print(x + " "));
     5         System.out.println();
     6         Set<String> set = Stream.of("hello", "world", "hello", "java").collect(Collectors.toSet());
     7         set.forEach(x -> System.out.print(x + " "));
     8         System.out.println();
     9         Set<String> treeset = Stream.of("hello", "world", "hello", "java").collect(Collectors.toCollection(TreeSet::new));
    10         treeset.forEach(x -> System.out.print(x + " "));
    11         System.out.println();
    12         String resultStr = Stream.of("hello", "world", "hello", "java").collect(Collectors.joining(","));
    13         System.out.println(resultStr);
    14     }

    最后的输出结果为:

    1 hello world hello java 
    2 world java hello 
    3 hello java world 
    4 hello,world,hello,java
  • 相关阅读:
    游戏对战练习
    扩展属性 配置文件
    数据操作类:增删改查(三大类)
    作业
    泛型集合
    Linux下查看文件和文件夹大小
    reids客户端 redis-cli用法
    生产环境下JAVA进程高CPU占用故障排查
    MySQL主从复制与读写分离
    最全面 Nginx 入门教程 + 常用配置解析
  • 原文地址:https://www.cnblogs.com/udbyygysai/p/10412781.html
Copyright © 2011-2022 走看看