Java--集合

集合框架结构图

这里写图片描述

你要把握集合的两个大的方向,就是Collection和Map

补充一下:HashTable也实现了Map接口。

补充一下:Vector也是在List下面的。

List

List 集合是有序的,并且可以插入重复的值,也可以为空值。是否允许为空具体还要看子类的实现。

Vector

底层数据结构是通过数组来维护的,它的操作是线程安全,你可以自己查看源码,它的操作都添加了synchronized关键字

ArrayList

它实现了List,有序的集合,并且它允许添加空值元素。它的底层是通过数组来实现,可以实现可变数组长度,它比较类似于 Vector 类,除了此类是不同步的。

ArrayList是线程不安全的,如果有多个线程操作,一定要有同步操作。在集合中,也给出了集合同步操作。

 Collections.synchronizedList//该方法可以返回一个同步的ArrayList。

ArrayList在实现可变长度,实际上有一个加载因子系数,它会自动增长,你创建时,你可以给一个初始容量,这个初始容量,应该大于等于你的元素个数,另外,集合中元素个数超过容量,会按照加载因子扩充你的容量。

ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间

ListIterator

它是继承于Iterator,是它的增强版,它是双向迭代器,允许程序员按任一方向遍历列表。并且在迭代过程中,你可以进行插入、删除,它不会进行报错,这点和之前Iterator不一样,修改之后,要在下一次迭代可以看到修改的结果。

ListIterator it = list.listIterator();
        while (it.hasNext()) {
            System.out.println(it.next());

        }
        while(it.hasPrevious()) {
            System.out.println(it.previous());
        }
remove方法理解

这里写图片描述

LinkList 是实现List接口,可以允许添加的元素为空,可以添加重复元素,此类也实现了Deque ,这个是双端队列,这个数据结构即拥有队列特性、也拥有栈的特性。所以里面提供栈和队列的操作方法。

LinkList是通过一个双向链表来维护的。

在遍历的时候可以通过双向链表的特性从前往后、或者从后往前,因为继承List,它也是可以随机访问。

linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好

LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间,来存放指向下一个的指针。

LinkList 不是线程同步的。

        while(ll.size()!=0) {
           System.out.println(ll.removeLast());
       }

这个方法可以实现LinkList的从后遍历,对应的removeFirst是从前遍历。但是这个方法不太好的就是它的元素都删除了。

        String a;
     while((a = ll.pollLast())!=null) {
         System.out.println(a);
     }

这种方式和上面的类似,但是它是只读的,不修改数据,对应的还有pollFirst是从前往后遍历。

其他方式的遍历,迭代器都和之前的类似。在这就不多说了。

Set

set集合是不允许相同的元素,最多只能存放一个null。set是无序的。编译时候不会有任何问题,但是运行出来,会自动去除重复的

HashSet

该类是实现了set接口,它不保证set迭代的顺序是一直不变的。可以接受一个为null的值。

TreeSet

基于 TreeMap 的 NavigableSet 实现。使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序

常用方法
    TreeSet<Person> ts = new TreeSet<Person>(new Comparator<Person>() {

            @Override
            public int compare(Person o1, Person o2) {
                // TODO Auto-generated method stub
                return o1.getAge()-o2.getAge();
            }
        });
        Person p1 = new Person("laohe", 13);
        Person p2 = new Person("laohe", 34);
        Person p3 = new Person("laohe", 70);
        Person p4 = new Person("laohe", 3);
        ts.add(p1);
        ts.add(p2);
        ts.add(p3);
        ts.add(p4);
         /*
          * 如果TreeSet中的元素是不可以比较,那么会报异常
         * ClassCastException - if the specified object 
         * cannot be compared with the elements currently
         *  in this set
         */
        Iterator<Person> it = ts.iterator();
        while(it.hasNext()) {
            System.out.println(it.next().getAge());
        }
        Person p5 = ts.ceiling(p4);//返回set大于给定元素的最小元素,如果不存在返回空
        Person p6 = ts.floor(p3);//返回set中小于等于给定元素的最大元素,如果不存在返回空
        System.out.println(p6.getAge());
        System.out.println(p5.getAge());
        Set set=ts.descendingSet();//返回该TreeSet的逆序视图
        Iterator<Person> it1 = set.iterator();
        while(it1.hasNext()) {
            System.out.println(it1.next().getAge());
        }
        SortedSet<Person> ss = ts.headSet(p3);//返回此 set 的部分视图,其元素严格小于 toElement
        SortedSet<Person> ss1 = ts.tailSet(p4);//返回此 set 的部分视图,其元素大于等于 fromElement。
        Iterator<Person> it2 = ss.iterator();
        while(it2.hasNext()) {
            System.out.println(it2.next().getAge());
        }

        Person p7 = ts.pollFirst();//获取并移除第一个(最低)元素;如果此 set 为空,则返回 null。
        Person p8 = ts.pollLast();//  获取并移除最后一个(最高)元素;如果此 set 为空,则返回 null

        Person p9 = ts.first();//获取Set的第一个元素
        Person p10 = ts.last();//获取Set的最后一个元素
Map

将键映射到值的对象。map如果你的键是一样的话,在后面添加的,会覆盖前面的,所以map中,不存在两个一样的键。map中键和值都可以为空。

某些映射实现可明确保证其顺序,如 TreeMap 类;另一些映射实现则不保证顺序,如 HashMap 类

HashMap

hashmap中key是不可以相同的,但是值是可以相同的,不同的键可以有相同的值,但是相同的键,相同的值会覆盖,

hashmap 键值与null关系

hashmap中的key是可以为空

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

key为null直接将hashcode取0。

对于value,肯定是可以为null的。这没有啥质疑的。

hashcode

提到HashMap,不得不说一下就是hashcode,hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的。

如果两个对象相同,就是适用于equals(java.lang.Object),object的方法是判断是不是同一个对象,那么这两个对象的hashCode一定要相同

如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面的。

两个对象的hashCode相同,并不一定表示两个对象就相同。

例如内存中有这样的位置
0 1 2 3 4 5 6 7

而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之如果不用hashcode而任意存放,那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。

但如果用hashcode那就会使效率提高很多。
我们这个类中有个字段叫ID,那么我们就定义我们的hashcode为ID%8,然后把我们的类存放在取得得余数那个位置。比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过ID除 8求余数直接找到存放的位置了。

但是如果两个类有相同的hashcode怎么办那(我们假设上面的类的ID不是唯一的),例如9除以8和17除以8的余数都是1,那么这是不是合法的,回答是:可以这样。那么如何判断呢?在这个时候就需要定义 equals(这个equals是判断是否同一个对象)。

也就是说,我们先通过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,那么我们就需要再通过 equals 来在这个桶里找到我们要的类。

那么。重写了equals(),为什么还要重写hashCode()呢?
想想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不通过重写hashcode()来找到桶。

如果没有hash,你要查找一个东西,比如在ArrayList中,看是不是等于某一个值,有多少个元素,就要有多少次equals方法调用,而hash,先计算出它的hash值,在这个hash值的位置,看有个几个元素,在调用equals方法,这样省了不少事。

经过上面的介绍,你已经对hash算法有了了解,下面就来详细的介绍hashmap了。

这里写图片描述

这是hashmap的数据结构。

hashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

    final int hash;
        final K key;
        V value;
        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;
        }

每一个Entry(条目)就是数组中的元素,其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

这里为啥会出现链表,实际上,这是因为如果某两个对象计算出来的hash值相同,但是通过equals,判断不是同一个对象,所以我们会把他们放在一起,形成链表。

HashMap的实现原理:

  • 利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
    存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
  • 获取时,直接找到hash值对应的下标,在进一步判断key是否相同对象(通过equals方法),从而找到对应值。
  • 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比
hashmap的哈希冲突的解决方法
  1. 开放地址法
  2. 链地址法 (这时hashmap的解决办法)
  3. 二次哈希
  4. 建立一个公共溢出区
HashTable

此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值。键和值不可以为空

  if (value == null) {
            throw new NullPointerException();
        }

//hashtable在存放数据的时候,就判断它的value是否为空。

  int hash = key.hashCode();

可以看到hashtable存放是直接计算key的hashcode,如果为null,调用就会空指针异常,所以键也是不可以为空的。

为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode 方法和 equals 方法。

Hashtable 的实例有两个参数影响其性能:初始容量 和加载因子。通常,默认加载因子(.75)在时间和空间成本上寻求一种折衷。减少空间开销,必然查找速度就要慢。

            //ht.put(null, "sdas");
            //ht.put(1, null);
            //在hashtable中是不允许将空键或者是空的值放入,否则会引起空指针异常。
HashTable遍历
 Hashtable<Integer,String> ht = new Hashtable<>();
            Iterator<Entry<Integer, String>> it = null;
            ht.put(1, "one");
            ht.put(2,"two");
            ht.put(3,"three");
            ht.put(4,"four");
          Set<Entry<Integer, String>> set = ht.entrySet();
           it= set.iterator();
          while(it.hasNext()) {
             Entry<Integer, String> entry  = it.next();
               if(entry.getKey()==2) {
                  it.remove();
               }else {
               System.out.println(entry.getValue());
               }
            ht.remove(2);
          }

         Set<Integer> keyset =  ht.keySet();
          Iterator<Integer> it1 = keyset.iterator();     
         while(it1.hasNext()) {
             Integer name = it1.next();
            System.out.println("key"+name+"    value"+ht.get(name));
         }
        }
HashTable 方法

containsKey(Object key) 判断是否包含该key。

containsValue(Object value) 判断是否包含某个值。

在这里需要注意的是,在HashTable 很多方法中,都进行了同步操作,不需要你在另外操作。所以HashTable是线程安全的。

 public synchronized boolean replace(K key, V oldValue, V newValue) {
fail-fast

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常。

ConcurrentModificationException

当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。 例如,某个线程在 Collection 上进行迭代时,迭代器的结构已经建立起来,通常不允许另一个线性修改该 Collection。

需要注意的是产生这个异常单线程中也可能产生。还有虽然他会fail-fast这个事件处理机制,但是对于抛出异常,它只是进全力去抛出,所以我们不能完全依赖此机制。

参考:https://baike.baidu.com/item/fail-fast/16329854?fr=aladdin

测试用例

上面已经说明java错误机制和异常,下面演示代码:

    static Hashtable<Integer,String> ht = new Hashtable<>();
        public static void main(String[] args) {

            ht.put(1, "one");
            ht.put(2,"two");
            ht.put(3,"three");
            ht.put(4,"four");
          Set<Entry<Integer, String>> entry = ht.entrySet();
          ConcurrentModificationException
          Iterator it = entry.iterator();
          Thread t = new Thread(new Test131().new MyTask());
          t.start();
            try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
          while(it.hasNext()) {
            System.out.println(it.next());
          }
        }

        class MyTask implements Runnable{

            @Override
            public void run() {
                // TODO Auto-generated method stub
                ht.remove(3);
            }

        }
分析及解决办法

看源码:

 public E next() {
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }
 final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

主要看两块,你会发现在迭代器执行next方法前,会先进行check,而在check中,主要判断modCount expectedModCount是否相等,不想等则抛出异常,原来是这样集合操作中,它都会记录集合中元素的数量,这点我看源码put、remove都涉及到modCount 值的变化,而在迭代器创建的时候,就已经执行赋值:

 int expectedModCount = modCount;

这时,值都为0。比如,加入你集合添加了5个元素,这是你在创建好遍历对象的时候,expectedModCount = modCount = 5;

所以说,在迭代器创建好了,你在修改就会抛出异常,你的modCount 变化了,而expectedModCount 没有相应的变化。所以对于减少该异常,就有了一个解决办法,用迭代器自身的删除操作。

public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

你看它的源码,就多做了一部处理,即可解决这个问题。

解决方法

在删除元素的时候,可以采用迭代器的删除方法,这样就可以。

 Hashtable<Integer,String> ht = new Hashtable<>();
             Iterator<Entry<Integer, String>> it = null;
            ht.put(1, "one");
            ht.put(2,"two");
            ht.put(3,"three");
            ht.put(4,"four");
          Set<Entry<Integer, String>> set = ht.entrySet();
           it= set.iterator();
          while(it.hasNext()) {
             Entry<Integer, String> entry  = it.next();
             if(entry.getKey()==2) {
                  it.remove();
               }else {
               System.out.println(entry.getValue());
               }
            //ht.remove(2);产生异常
          }
        }
迭代器的注意点

hasNext() 方法是如果有下一个元素返回true。
next 方法是返回下一个元素值。

源码分析:

     public E next() {
            checkForComodification();//这个是检查,在迭代器开始遍历,有没有外部在修改元素。
            int i = cursor;//这里cursor是游标的位置初始为0
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;//将List转化为数组操作
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;//游标向后移动一位
            return (E) elementData[lastRet = i];//返回下一个元素这里理解需要注意,因为它游标不是指向当前元素,而是前面的。
        }

结合图理解:

这里写图片描述

理解完上面,下面你看这个问题:

             System.out.println(it1.next());
             System.out.println(it1.next());
             //这个东西,对于新手还是要注意的,明明打印是两个一样的东西,为啥子就是不一样。原因就在上面执行完一次next(),游标cursor会加1,会变化,这两个值当然不一样,所以说,眼见的不一定为真,凡是还是要多看,多练。
Map.Entry (映射项)

我们在集合中常常见到这个,所以今天把这个拿出来说道说道。它是一个映射项,对应的就是键值对(键-值)。实现Map接口,Map.entrySet 方法返回映射的 collection 视图,然后遍历。

常用方法

这里写图片描述

获取对应的键和对应的值。

集合中线程不安全问题

在集合部分的api中,多处提到就是集合在多线程的操作下,会出现操作异常,而我们之前在多线程部分,知道用lock、synchronized关键字来解决。但是在集合部分,它有一个Collections类,给我们直接提供了包裹的同步集合操作。

这里写图片描述

上面只是截取几个常用的,还有很多自己可以参看api。

下面以HashSet为例,为大家演示集合在多线程操作下的问题。

用两个线程对同一个set集合进行操作,结束后返回set当中的size。在不同步的情况,返回size大于等于500,在同步情况下,返回500。

 static Set set = new HashSet<>();
      // static Set set  = Collections.synchronizedSet(set1);

        public static void main(String[] args) {    
            Test131 tt = new Test131();
            MyTask task = tt.new MyTask();
            Thread t = new Thread(task);
            Thread t1 = new Thread(task);
            t.start();
            t1.start();

            try {
                t.join();
                t1.join();//这两句代码,虽然和下面睡眠一样,都是等待线程任务执行完毕,但是采用方法是不同,主线程会等待子线程执行完,才执行。你也可以给线程时间,让它先一步一会,在同步。这样就形成main线程同步,但是还有两个线程操作。要学会这种方式。
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            /*try {
                Thread.sleep(5000);//在这里睡眠是为了让两个线程都执行好了,才打印数量。
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }*/


            System.out.println(set.size());

        }
        class MyTask implements Runnable {

            @Override
            public void run() {
                // TODO Auto-generated method stub
            for(int i =0;i<500;i++) {
            set.add(i); 
                 }
            }

            }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值