Java集合框架之Collections接口及实现类

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

集合框架(collections framework)

现实生活中:很多相同事物凑在一起,比如人群
数学中的集合:具有共同属性的事物的总体
java中的集合框架:是一种工具类,就像是容器,储存任意数量的具有共同属性的对象

其实说白了,可以把一个集合看成一个微型数据库,操作不外乎“增删改查”四种操作,我们在学习使用一个具体的集合类时,需要把这四个操作的时空复杂度弄清楚了,基本上就可以说掌握这个类了。

两大基类Collection与Map

在集合框架的类继承体系中,最顶层有两个接口:

  • Collection表示一组纯数据
  • Map表示一组key-value对

一般继承自Collection或Map的集合类,会提供两个“标准”的构造函数:

  • 没有参数的构造函数,创建一个空的集合类
  • 有一个类型与基类(Collection或Map)相同的构造函数,创建一个与给定参数具有相同元素的新集合类

因为接口中不能包含构造函数,所以上面这两个构造函数的约定并不是强制性的,但是在目前的集合框架中,所有继承自Collection或Map的子类都遵循这一约定。

Collection

Java集合框架之Collections接口及实现类_第1张图片

如上图所示,Collection类主要有三个接口:

  • Set表示不允许有重复元素的集合(A collection that contains no duplicate elements)
  • List表示允许有重复元素的集合(An ordered collection (also known as a sequence))
  • Queue JDK1.5新增,与上面两个集合类主要是的区分在于Queue主要用于存储数据,而不是处理数据。(A collection designed for holding elements prior to processing.)

Collection接口部分源码

public interface Collection extends Iterable {

    int size();
    boolean isEmpty();
    boolean contains(Object o);
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection c);
    boolean addAll(Collection c);
    boolean removeAll(Collection c);
    void clear();

    Iterator iterator();
    Object[] toArray();
}

以上是Collection接口的常用方法,因为3个子接口都继承了这个接口,因此在它们各自特有方法外,都会实现以上方法。

Map

Java集合框架之Collections接口及实现类_第2张图片

Java 中有四种常见的Map实现——HashMap, TreeMap, Hashtable和LinkedHashMap:

  • HashMap就是一张hash表,键和值都没有排序。
  • TreeMap以红黑树结构为基础,键值可以设置按某种顺序排列。
  • LinkedHashMap保存了插入时的顺序。
  • Hashtable是同步的(而HashMap是不同步的)。所以如果在线程安全的环境下应该多使用HashMap,而不是Hashtable,因为Hashtable对同步有额外的开销,不过JDK 5之后的版本可以使用conncurrentHashMao代替HashTable。

说到Map接口的话大家也许在熟悉不过了。Map接口实现的是一组Key-Value的键值对的组合。 Map中的每个成员方法由一个关键字(key)和一个值(value)构成。Map接口不直接继承于Collection接口(需要注意啦),因为它包装的是一组成对的“键-值”对象的集合,而且在Map接口的集合中也不能有重复的key出现,因为每个键只能与一个成员元素相对应。

另外,Set接口的底层是基于Map接口实现的。Set中存储的值,其实就是Map中的key,它们都是不允许重复的。

Map接口部分源码

public interface Map {

    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    V put(K key, V value);
    void putAll(Map m);
    V remove(Object key);
    void clear();

    Collection values();
    Set keySet();
    Set> entrySet();
    interface Entry {
        K getKey();
        V getValue();
        V setValue(V value);

    }
}

源码中的方法大家也比较熟悉。在遍历Map时,我们可以通过keySet()方法获取到所有key,它返回的是一个Set对象,遍历Set,通过key获取value。也可以通过entrySet()的方式获取Entry的Set,遍历Set,通过Entry的getValue()getKey()方法获取值和对象,这些在后面会详细讲到。

List接口

List接口对Collection进行了简单的扩充,因此它继承了Collection接口。其中大部分方法和其继承的Collection相同,至于不同之处也不太常用,大家可以参考源码。

特点:

List中存储的元素是有序的,而且可以重复的存储相关元素。

ArrayList

特点:

ArrayList的底层使用数组实现,当元素的数量超过数组长度时,通过新建更大容量数组,将原数组内容拷贝一份,然后新增元素的方式存储元素。

优点:

类似数组的形式进行存储,因此它的随机访问速度极快。

缺点:

不适合于在线性表中间需要频繁进行插入和删除操作。因为每次插入和删除都需要移动数组中的元素。

可以这样理解ArrayList就是基于数组的一个线性表,只不过数组的长度可以动态改变而已。

对于ArrayList的详细使用信息以及创建的过程可以查看jdk中ArrayList的源码,这里不做过多的讲解。

对于使用ArrayList的开发者而言,下面几点内容一定要注意啦,尤其找工作面试的时候经常会被问到。作者去年面试的时候,都已经被问烦了

注意啦!!!!!!!!

1、关于扩容问题:

默认ArrayListde的默认构造函数ArrayList()会构造一个长度为10的数组。
ArrayList(int initialCapacity)构造函数会构造一个指定长度的数组。
添加元素时,如果超出了长度,则以每次旧长度的3/2倍增长。

例:

new ArrayList(20);扩容几次?
答案: 0次,因为直接产生了一个长度20的数组

2、ArrayList是线程不安全的,在多线程的情况下不要使用。

如果一定在多线程使用List的,你可以使用Vector,因为Vector和ArrayList基本一致,区别在于Vector中的绝大部分方法都使用了同步关键字修饰,这样在多线程的情况下不会出现并发错误哦,还有就是它们的扩容方案不同,ArrayList是通过原始容量*3/2,而Vector是允许设置默认的增长长度,Vector的默认扩容方式为原来的2倍(可以通过构造函数设置,如设置为2,扩容后长度为旧长度+2)。

切记Vector是ArrayList的多线程的一个替代品。

3、ArrayList实现遍历的几种方法

        ArrayList list = new ArrayList();
        list.add("hello");
        list.add(",");
        list.add("world");

        // 第一种遍历方式使用foreach遍历List,编译器编译时,会将这种方式转化为迭代器方式
        for (String str : list) {
            System.out.print(str);
        }
        System.out.println();

        // 第二种遍历方式使用for循环依次得到元素
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i));
        }
        System.out.println();

        // 第三种遍历方式使用迭代器遍历
        Iterator iterator = list.iterator();
        while(iterator.hasNext()) {
            System.out.print(iterator.next());
        }
        System.out.println();

LinkedList

特点:

LinkedList的底层用双向链表实现。另外建议阅读其源码,并不难,会让你有醍醐灌顶的感觉。

优点:

链表相对于实现ArrayList的数组来说,其存储空间是散列的而不是连续的,因此在链表中间插入和删除元素时,无需移动后面的元素,只需要改变3个节点的关联即可。

缺点:

因为LinkedList不是空间连续的,因此随机读取时,需要从头到尾的读取,因此不如ArrayList来得快。另外,在使用双向链表实现时,需要额外提供空间供记录前驱节点和后继节点的地址,消耗了额外空间。

对于使用LinkedList而言,下面几点内容一定要注意啦

注意啦!!!!!!!!

1、LinkedList和ArrayList的区别和联系

主要从底层实现、优缺点(随机读取、新增、删除)等方面总结,详见之前总结的特点、优缺点,不再赘述。

2、LinkedList的内部实现

强烈建议大家去看看源码,其内部使用双链表实现,如果你实在不想看,下面的代码提供了其节点结构,和最常用的add()remove()方法。

    // 长度
    transient int size = 0;
    
    // 节点的表示
    private static class Node {
        E item;
        Node next;
        Node prev;

        Node(Node prev, E element, Node next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
    
    // 此段不是源码,仅供理解思想
    private void add(Node node, E data) {
        Node newNode = new Node(node.prev, data, node);
        newNode.prev.next = newNode;
        node.prev = newNode;
        size++;
    }

    // 此段不是源码,仅供理解思想
    private E remove(Node node) {
        node.next.prev = node.prev;
        node.prev.next = node.next;
        size--;
        return node.item;
    }

3、LinkedList不是线程安全的

注意LinkedList和ArrayList一样也不是线程安全的,如果在对线程下面访问可以自己重写LinkedList

然后在需要同步的方法上面加上同步关键字synchronized

4、LinkedList的遍历方法

同ArrayList

        LinkedList list = new LinkedList();
        list.add("hello");
        list.add(",");
        list.add("world");

        // 第一种遍历方式使用foreach遍历List
        for (String str : list) {
            System.out.print(str);
        }
        System.out.println();

        // 第二种遍历方式使用for循环依次得到元素,这种方式用到了随机读取,特别特别不推荐
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i));
        }
        System.out.println();

        // 第三种遍历方式使用迭代器遍历
        Iterator iterator = list.iterator();
        while(iterator.hasNext()) {
            System.out.print(iterator.next());
        }
        System.out.println();
    }

5、LinkedList可以被当做堆栈来使用

由于LinkedList实现了接口Dueue,所以LinkedList可以被当做堆栈来使用,这个你自己研究吧。

Vector

Vector和ArrayList不论在实现,还是使用上,都大同小异。因此也就不细说,他们主要的不同就是Vector是线程安全的,它在一些方法上加了synchronized关键字。

划重点了!!!!!!!!

1、Arraylist与Vector的区别

  • Vector是线程安全的,ArrayList不是线程安全的。
  • ArrayList在底层数组不够用时在原来的基础上扩展0.5倍(3/2),Vector是扩展1倍。

参见 https://zhuanlan.zhihu.com/p/28241176

Set接口

Set接口也是Collection接口的一个常用子接口,它区别于List接口的特点在于:

Set中的元素实现了不重复,有点象集合的概念,无序,不允许有重复的元素,最多允许有一个null元素对象。

需要注意的是:虽然Set中元素没有顺序,但是元素在set中的位置是有由该元素的HashCode决定的,其具体位置其实是固定的。

此外需要说明一点,在set接口中的不重复是由特殊要求的。

举一个例子:对象A和对象B,本来是不同的两个对象,正常情况下它们是能够放入到Set里面的

但是

如果对象A和B的都重写了hashcode和equals方法,并且重写后的hashcode和equals方法是相同的话。那么A和B是不能同时放入到Set集合中去的

也就是Set集合中的去重和hashcode与equals方法直接相关。

Set接口的常见实现类有HashSet,LinedHashSet和TreeSet这三个,现在依次介绍这三个类:

HashSet

HashSet是Set接口的最常见的实现类了。其最底层是通过Hash表(一个元素为链表的数组)实现的。另外,Hash表底层依赖的两个方法hashcode与equals方法,想存入HashSet的元素需要复写这两个方法。

为什么说其最底层呢?请先看下这段源码:

public class HashSet
    extends AbstractSet
    implements Set, Cloneable, java.io.Serializable {
    
    private transient HashMap map;
    
    public HashSet() {
        map = new HashMap<>();
    }
}

显而易见,HashSet的内部是基于HashMap实现的,我们都知道在HashMap中的key是不允许重复的,你换个角度看看,那不就是说Set集合吗?

我们只需要用一个固定值值代替Map中的value,

    private static final Object PRESENT = new Object();    

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

jdk中用了一个静态的Object对象代替了value。那Set中的元素做key便可以保证元素不重复。

下面讲解一下HashSet使用和理解中容易出现的误区:

1、HashSet中存放null值

HashSet中时允许出入null值的,但是在HashSet中仅仅能够存入一个null值哦。

2、 HashSet中存储元素的位置是固定的

HashSet中存储的元素的是无序的,这个没什么好说的,但是由于HashSet底层是基于Hash算法实现的,使用了hashcode,所以HashSet中相应的元素的位置是固定的哦。

3、 遍历HashSet的几种方法

        HashSet hashSet = new HashSet();
        hashSet.add("hello,");
        hashSet.add("hello,");
        hashSet.add("world");

        // 第一种遍历方式,使用foreach遍历
        for (String str : hashSet) {
            System.out.print(str);
        }
        System.out.println();

        // 第二种遍历方式,使用迭代器遍历
        Iterator iterator = hashSet.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next());
        }

LinkedHashSet

LinkedHashSet不仅是Set接口的子接口而且还是上面HashSet接口的子接口,和HashSet由HashMap实现一样,LinkedHashSet的底部由LinkedHashMap实现。

        Set set = new HashSet();
        set.add("hello");
        set.add("world");

        for (String str : set) {
            System.out.print(str);
        }
        System.out.println();

        set = new LinkedHashSet();
        set.add("hello");
        set.add("world");
        for (String str : set) {
            System.out.print(str);
        }

上面的程序输出结果如下:

Java集合框架之Collections接口及实现类_第3张图片

可见,LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。

因为它底层由LinkedHashMap实现,所以更多细节参加LinkedHashMap。

TreeSet

TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。

TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。

向TreeSet中加入的应该是同一个类的对象。TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0

自然排序

自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。

Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是 负数,则表明obj1小于obj2。

定制排序

自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法

由于水平和时间有限,Map相关内容暂不总结

https://www.cnblogs.com/xiohao/p/4309462.html

https://zhuanlan.zhihu.com/p/24338517?utm_source=wechat_session&utm_medium=social

转载于:https://my.oschina.net/u/2930289/blog/1587498

你可能感兴趣的:(Java集合框架之Collections接口及实现类)