集合

集合(Collection)

 

Collection和Collections有什么区别

  Collection是一个集合接口。提供了对集合对象进行基本操作的通用接口方法,实现接口的类主要是List和Set。

  Collections是针对集合类的一个包装类,提供一系列静态方法以实现对各种集合的搜索、排序、线程安全化等操作,大多用来处理线性表。

  集合中线程安全的类有:vector、stack、hashtable、enumeration。除此之外,均是非线程安全的类和接口。

  collection、set、List都是接口,HashSet继承AbstractSet,实现了Set接口

 

ArrayListLinkedListVector

  List接口有三个实现类,分别是ArrayList、LinkedList和Vector

 

ArrayList

Vector

LinkedList

内存结构

数组

数组

双向链表

数组内存扩展

原数组*1.5+1

有利于节约内存空间

原数组*2

 

插入删除

效率低

效率低

效率高

随机访问

效率高

效率高

效率低

线程安全?

不安全,不同步

安全,同步

不安全,不同步

空间浪费

List列表的结尾预留一定的空间

 

每一个元素都需要消耗相当的空间

备注

 

提供indexOf(obj, start)接口

提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ArrayList本质是顺序存储的线性表,插入和删除操作会引发后续元素移动,效率低,随机访问效率高。ArrayList删除元素后,剩余元素会依次向前移动,因此下标一直在变,size()也会减小。remove(int index)删除索引处元素,remove(Object o)删除元素。

Vector和ArrayList一样,也是通过数组实现,不同的是它支持线程的同步,但实现同步需要很高的花费,因此访问它比访问ArrayList慢。

LinkedList的内存是双向链表储存,链式存储结构插入和删除效率高,不需要移动。但是随机访问的效率低,需要从头开始向后依次访问。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

 

手动实现ArrayList

public class MyArrayList {
    //定义存放数据的数组
    private Object[] elementData;
    private int size;
    
    //获得集合长度的方法
    public int size(){
        return elementData.length;
    }
    
    //默认初始数组长度为10
    public MyArrayList() {
        this(10);
    }
    
    public MyArrayList(int initialCapacity) {
        if (initialCapacity < 0) {
            try {
                throw new Exception();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        elementData = new Object[initialCapacity];
    }
    
    //add方法
    public void add (Object o){
        //数组扩容和数据拷贝
        if (size == elementData.length ) {
            Object[] newArray =  new Object[size*2+1];
            System.arraycopy(elementData, 0, newArray, 0, elementData.length);
            elementData = newArray;
        }
        elementData[size++] = o;
    }
    
    //是否为空
    public boolean isEmpty(){
        return size == 0;
    }
    
    //get方法
    public Object get(int index){
        if (index < 0 || index >=size) {
            try {
                throw new Exception();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return elementData[index];
    }
    
    
    
    public static void main(String[] args) {
        MyArrayList m2 = new MyArrayList(3);
        m2.add("111");
        m2.add("222");
        m2.add("333");
        m2.add("444");
        m2.add("555");
        System.out.println(m2.get(2));
    }
}

 

HashMapHashtable的区别

(1)Hashtable的方法是线程同步的,HashMap未经同步

(2)Hashtable不允许null值(key和value都不可以),HashMap允许null(keyvalue都可以)。HashMap插入的时候,检查是否已经存在相同的key,如果不存在,则直接插入,如果存在,则用新的value替换旧的value。

(3)Hashtable使用Enumeration,HashMap使用Iterator

(4)Hashtable有一个contains(Object value),功能和containsValue(Objectvalue)功能一样;

(5)哈希值的使用不同,Hashtable直接使用对象的hashCode,而HashMap重新计算hash值。

(6)Hashtable中hash数组默认大小是11,增加的方式是old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数

(7)HashMap底层是由数组加链表实现的,对于每一个key值,都需要计算哈希值,然后通过哈希值来确定顺序,并不是按照加入顺序来存储,是无序的

 

Set中存放元素根据什么来判断相同

  HashSet中的add()方法,底层是靠HashMap来实现。HashMap中存入元素的时候,首先比较hashCode值,若不相等,则存入,若相同再比用equals()比较。所以HashSet是避免重复是根据hashCode和equals保证的。

 

Set不能有重复元素,且是无序的,要有空值也只能有一个。

List可以有重复元素,是有序的,空值也可以是多个。

 

 遍历集合(Iterator)

通用遍历方式

所有集合都实现Iterator接口,用来遍历集合中的元素。

接口中的方法如下:

  boolean  hasNext()  如果仍有元素可以迭代,则返回 true

  Object    next()    返回迭代的下一个元素。

  void     remove()  从迭代器指向的 collection 中移除迭代器返回的最后一个元素。

用法

public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        
        Iterator<Object> it = list.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }

另一种写法

for (Iterator it = list.iterator(); it.hasNext();) {
    System.out.println(it.next());            
}

 

Map遍历方式

1、通过获取所有的key,按照来遍历

Set<String> set = map.keySet(); //得到所有key的集合
for (String str : map.keySet()) {
    Object obj = map.get(str);//得到每个key多对用value的值
}

2、通过Map.entrySet使用iterator遍历key和value

Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
     Map.Entry<Integer, String> entry = it.next();
     System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

3、通过Map.entrySet遍历key和value,推荐,尤其是容量大时

//Map.entry<Integer,String> 映射项(键-值对)  有几个方法:用上面的名字entry
//entry.getKey() ;entry.getValue(); entry.setValue();
//map.entrySet()  返回此映射中包含的映射关系的 Set视图。
for (Map.Entry<Integer, String> entry : map.entrySet()) {
    System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

 

List遍历方式

1、使用Iterator

2、foreach循环

for (Object object : list) { 
    System.out.println(object); 
}

3、for循环

for(int i = 0 ;i<list.size();i++) {  
    int j= (Integer) list.get(i);
    System.out.println(j);  
}

 

每个遍历方法的实现原理是什么?

1、传统的for循环遍历,基于计数器的:
  遍历者自己在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后,停止。主要就是需要按元素的位置来读取元素。
2、迭代器遍历,Iterator:
  每一个具体实现的数据集合,一般都需要提供相应的Iterator。相比于传统for循环,Iterator取缔了显式的遍历计数器。所以基于顺序存储集合的Iterator可以直接按位置访问数据。而基于链式存储集合的Iterator,正常的实现,都是需要保存当前遍历的位置。然后根据当前位置来向前或者向后移动指针。
3、foreach循环遍历:
  根据反编译的字节码可以发现,foreach内部也是采用了Iterator的方式实现,只不过Java编译器帮我们生成了这些代码。

 

各遍历方式对于不同的存储方式,性能如何?

1、传统的for循环遍历
  因为是基于元素的位置,按位置读取。所以我们可以知道,对于顺序存储,因为读取特定位置元素的平均时间复杂度是O(1),所以遍历整个集合的平均时间复杂度为O(n)。而对于链式存储,因为读取特定位置元素的平均时间复杂度是O(n),所以遍历整个集合的平均时间复杂度为O(n2)。
2、迭代器遍历,Iterator:
  那么对于RandomAccess类型的集合来说,没有太多意义,反而因为一些额外的操作,还会增加额外的运行时间。但是对于Sequential Access(顺序存取)的集合来说,就有很重大的意义了,因为Iterator内部维护了当前遍历的位置,所以每次遍历,读取下一个位置并不需要从集合的第一个元素开始查找,只要把指针向后移一位就行了,这样一来,遍历整个集合的时间复杂度就降低为O(n);
3、foreach循环遍历:
  分析Java字节码可知,foreach内部实现原理,也是通过Iterator实现的,只不过这个Iterator是Java编译器帮我们生成的,所以我们不需要再手动去编写。但是因为每次都要做类型转换检查,所以花费的时间比Iterator略长。时间复杂度和Iterator一样。
 

各遍历方式的适用于什么场合?

1、传统的for循环:
  顺序存储:读取性能比较高。适用于遍历顺序存储集合。
  链式存储:时间复杂度太大,不适用于遍历链式存储的集合。
2、迭代器遍历,Iterator:
  顺序存储:如果不是太在意时间,推荐选择此方式,毕竟代码更加简洁,也防止了Off-By-One(大小差一。。就是指某个变量的最大值和最小值可能会和正常值差1,或者循环多执行一次/少执行一次。一般在临界情况时发生)的问题。
  链式存储:意义就重大了,平均时间复杂度降为O(n),还是挺诱人的,所以推荐此种遍历方式。
3、foreach循环遍历:
  foreach只是让代码更加简洁了,但是他有一些缺点,就是遍历过程中不能操作数据集合(删除等),所以有些场合不使用。而且它本身就是基于Iterator实现的,但是由于类型转换的问题,所以会比直接使用Iterator慢一点,但是还好,时间复杂度都是一样的。所以怎么选择,参考上面两种方式,做一个折中的选择。
 

Java的最佳实践是什么?

  Java数据集合框架中,提供了一个RandomAccess接口,该接口没有方法,只是一个标记。通常被List接口的实现使用,用来标记该List的实现是否支持Random Access。
一个数据集合实现了该接口,就意味着它支持Random Access,按位置读取元素的平均时间复杂度为O(1)。比如ArrayList。而没有实现该接口的,就表示不支持Random Access。比如LinkedList。
  所以看来JDK开发者也是注意到这个问题的,那么推荐的做法就是,如果想要遍历一个List,那么先判断是否支持Random Access,也就是 list instanceof RandomAccess。
ArrayList部分源码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

LinkList部分源码

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
if (list instanceof RandomAccess) {
    //使用传统的for循环遍历。
} else {
    //使用Iterator或者foreach。
}

 

参考博客

[1]java集合遍历的几种方式总结及比较

http://www.cnblogs.com/leskang/p/6031282.html

 

原文地址:https://www.cnblogs.com/ghq120/p/8335152.html