《Java编程思想》笔记 第十七章 容器深入研究

1 容器分类

  • 容器分为Collection集合类,和Map键值对类2种
  • 使用最多的就是第三层的容器类,其实在第三层之上还有一层Abstract 抽象类,如果要实现自己的集合类,可以继承Abstract类,而不必实现接口中的所有方法。
  1. Collection 接口
    1.  List 接口  (按插入顺序保存,元素可以重复)
      1.  ArrayList (相当于大小可变的数组,随机访问快,插入移除慢)
      2. LinkedList(插入移除快,随机访问慢,也实现了Queue接口)
    2.  set 接口(不能有重复元素)
      1.  HashSet(元素无序,查询速度非常快)
        1.  LinkedHashSet(按插入顺序保存,同时有HashSet的查询速度)
      2. TreeSet(按元素升序保存对象,或者根据元素实现的比较器排序)
    3. Queue接口(一头进一头出)

      1.  PriorityQueu(优先级高的先出,也实现了List接口)
  2. Map接口
    1.  HashMap (查找速度快,内部无规则排序)
      1.  LinkedHashMap(按插入顺序排序,查找速度快)
    2. TreeMap(按升序保存对象,或者根据元素实现的比较器排序)

2 填充容器

  • 所有Collection的构造器都可以接收另一个Collection(可以不同类型)来填充自己。

2.1 Collections

  • 数组有Arrays类填充,容器也有Collections类填充,这种工具类中一般都是静态方法不用创建它们的对象直接调用,所以很方便。
  1. fill(list, T obj)方法都只是复制一份对象的引用,并没有额外创建对象,并且只能填充List,它会将容器内的元素清空再添加元素。
  2. nCopies(int n, T o) 返回一个List 功能和fill一模一样。
  3. addAll( list, T ... obj) 将元素添加到集合,集合本身也有addAll()方法并且还可以指定位置开始添加

3 Collection

  • Collection中的方法在List和Set中都实现了,List还添加了额外的方法,如get(),这在Collection和Set中都没有,因为Set无序所以无法确定位置。

4 collection中的可选操作

  • 可选的方法就是该方法在父类中会抛出异常,如果子类不需要该方法就不必重写它,一但调用则抛出异常,如果需要就去重写它的功能。
  • Collection中的 各种添加 移除方法都是可选的。AbstractList ,AbstractSet,AbstractQueue中就是实现了可选功能,调用这些抽象类中的方法就会抛出异常。

5 List

  1. jdk1.8 中ArrayList的Add方法实现原理:  如果elementData中元素数大于10个则复制一份旧数组并扩容再将元素添加就去
      public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!!
    //关键代码:elementData = Arrays.copyOf(elementData, newCapacity);
    elementData[size++] = e;
    return true;
    }
  2. remove方法 使用本地方法直接操作内存改变,但这也属于浅复制多线程下可能复制出错导致删不掉元素, numMoved =  size - index - 1; 最后将数组最后一个元素设为null

         System.arraycopy(elementData, index+1, elementData, index,
    numMoved);
    elementData[--size] = null; // clear to let GC do its work
  3. 值得注意的是 size是ArrayList内元素个数,并不是数组长度,ArrayList内数组长度默认最小是10

  4. LinkedList 内部是由一个内部静态类实现的一个双向链表。
     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;
    }
    }

6 Set

  1. HashSet底层是由HashMap实现的,add进去的值就put在了map中作为key值,为了减小开销,所有value值为同一个new Object() 。
    private static final Object PRESENT = new Object();
    
     public boolean add(E e) {
    return map.put(e, PRESENT)==null;
    }
  2. 保证不重复:先检测有没有相同的hash值,如果没有就添加元素,如果有在equal比较key值,相同则不允许添加,不同则允许添加入map
  3. LinkedHashSet也是由 LinkedHashMap实现
  4. TreeSet底层由TreeMap实现。TreeSet 实现了NavigableSet接口,而NavigableSet接口继承了SortedSet接口 ,NavigableSet提供了搜索功能方法,SortedSet 提供了排序功能方法,
  5. TreeSet(Comparator<? super E> comparator) ,凡是带有比较排序功能容器都有一个能传入比较器Comparator对象的构造方法,使用这个构造器可以根据自己实现的Comparator排序。

7 Queue

  • 除了并发应用外,Queue的实现只有LinkedList 和 PriorityQueue,这两个Queue的差异在于排序行为,而不是性能。队列行为在于add是添加到队头,remove移除队尾元素。
  1. PriorityQueue(Comparator<? super E> comparator) 可以使用自己的比较器比较,根据对象内某个属性设置在队列中的优先级。

8 Map

  1. HashMap的底层数据结构:

    1.    散列表(哈希表)即 数组+单链表  默认数组大小是16,数组大小一直保持是2n, 数组下标是 key的hash值进行扰乱  再与  数组大小减1  按位 做 & 运算得出。
    2.   扰乱的目的:为了使得到的值更加均匀。
    3.   减1的目的: 2n - 1 之后低位都是1,进行与运算后只要有一位不同那么就能的到不同的结果,减小了哈希冲突。同时结果范围也正好在数组大小相同。
  2. 链表的结构

    1.   jdk1.7 Enty
       static class Entry<K,V> implements Map.Entry<K,V> {
      final K key;
      V value;
      Entry<K,V> next;
      int hash;
    2.   jdk1.8 Node
      static class Node<K,V> implements Map.Entry<K,V> {
      final int hash;
      final K key;
      V value;
      Node<K,V> next;

      可以看出只是改了个名字而已。

  3. 单链表也称桶(bucket)

    1.   单链表是为了解决哈希冲突,不同的key可能计算出相同的hash值,如果哈希值冲突了那么就用头插法将新值插入链表的头,作为数组中的元素,不用尾插法能省去了遍历链表的开销。
    2.   如果key=null 那么key会一直在数组中作为链表的头。
  4. 负载因子

    1.   默认0.75 当map的桶位数(键值对数量)达到 数组容量*0.75 时就会进行扩容 ,扩容后的数组是原来的2倍。
  5. jdk1.7 扩容时的操作:

    1.   对旧链表正向遍历,遍历后插入新表,这样一来就是正向遍历,逆向插入,新表的链表顺序和旧表相反。
    2.   如果多个线程同时扩容容易循环链表,丢失一部分数据,所以多线程下put时是不安全的。
    3.   扩容后新表和旧表不一定有相同的链,也许某一部分会被拆到扩容增加的部分以减小原链长度,增加查询性能,这完全取决于Hash值。
  6. jdk1.8 扩容时的操作:

    1.   对旧链表正向遍历,检查hash值新增的高位(原来和1111做&运算,扩容后与11111做&运算,多了一位要比较的)如果是0那么还在原来数组位置,如果为1则在当前位置+扩容量 后的数组位置。
    2.   1.8 扩容后新增元素是用尾插法,所以不会出现循环,倒置链表,所以jdk1.8下put不会出现循环链表。
  7. jdk1.7 的HashMap存在的问题

    1.   有可能出现很多hash值相等的key那么数组还没有填满而,某一位置的链表非常长,put/get操作时要遍历整个链表,时间复杂度变为O(n)
  8. jdk1.8 对HashMap作出的改变:

    1.   底层数据结构变为 数组+链表+红黑树。解决了链表可能过长的问题,时间复杂度变为O(log n)
  9. jdk1.8 链表转化红黑树

    1. 限定了一些阀值,

      1.  桶的树化阀值: TREEIFY_THRESHOLD = 8 当链表长度大于8时将链表转为红黑树。
      2.  桶的链表还原阈值:UNTREEIFY_THRESHOLD = 6 当扩容后重新计算位置后若红黑树内结点少于6个则红黑树转化为链表
      3.  最小树化阀值 MIN_TREEIFY_CAPACITY = 64 只有当桶位数大于改64时才进行树化,并且以后树化不能小于 4*TREEIFY_THRESHOLD
      4.  查看红黑树
    2. 左子树右子树的判定

      1.  先通过comparableClassFor 反射获取key对象的所有接口查看有没有实现comparable接口
      2.  如果key值实现了comparable接口并且compareTo能正确执行并且key值和父结点是同一类型那么执行compareTo方法比较大小,如果大于父节点那么作为右孩子,如果小于父节点作为左孩子。
      3.  如果比较是相等的,或者没有实现接口则进入决胜局方法tieBreakOrder(),先比较他们的类名字符串如果是同一类型则类名比较就比较不出来,再调用本地方法identityHashCode()生成hash值,比较哈希值,如果哈希值相等返回-1,说明这两个对象在同一个对象,在同一块内存位置。
      4. 关于jdk1.8 的性能问题就在这里,如果Key 值没有实现comparable接口或者comparaTo方法不能的到正确结果,那么实现红黑树的性能没有只使用链表的高。
  10. get()取值:

    1.    计算出hash(key) 再通过key.equals()比较取出值
  11. 那些类做key值比较好?

    1.    String ,Integer 包装器类型,因为他们是final 型 key不会改变,不会出现放进map之后key被改变的情况,并且重写了hashCode()和equals()方法不会出现计算错误。
    2.   Object 对象做key值时要注意到上面的点。
  12. hashCode()和 equals()方法重写

    1. 重写equals方法也要重写hashCode方法这是因为 Object 中equals方法比较的是对象的地址,hashCode方法是根据对象地址生成的(字符串对象除外,相同的字符串有相同的hash值,哈希值是根据内容生成的。但字符串==仍然比较的是地址而不是哈希值)。
    2. 如果重写equals方法时是使用对象中某个条件判断他们相等,那么你再创建一个你认为相等的对象,但他两地址不一样,所以在没有重写hashCode方法后,他们的hashCode就不一样,这样存入map后使用后者取值就无法的到正确结果。
    3. 重写hashCode方法要保证 如果 equals 相同 那么hashCode一定相同,hashcode相同equals不一定相同,但这样会有哈希冲突,所以一个产生一个尽可能散列的hashCode方法非常重要。
    4. 内容相同的 字符串 内存中不止有一个,== 比较他们时 false, equals比较时为true ,哈希值相等。
  13. HashMap参考博客

    1. HashMap实现原理及源码分析
    2. HashMap在Java1.7与1.8中的区别
    3. JDK8:HashMap源码解析:comparableClassFor、compareComparables、tieBreakOrder方法
    4. JDK1.8源码阅读系列之四:HashMap (原创)
    5. 关于 HashMap 1.8 的重大更新

9 Stack和Vector

  • Vector 类可以实现可增长的对象数组。(和ArrayList相似)与数组一样,它包含可以使用整数索引进行访问的组件。但是,Vector 的大小可以根据需要增大或缩小,以适应创建 Vector 后进行添加或移除项的操作。
  • Stack是继承于Vector(矢量),由于Vector是通过数组实现的,这就意味着,Stack也是通过数组实现的而非链表
  1. Vector与ArrayList的最大区别就是Vector是线程安全的,而ArrayList不是线程安全的。另外区别还有:
    1. ArrayList不可以设置扩展的容量,默认1.5倍;Vector可以设置扩展的容量,如果没有设置,默认2倍
    2. ArrayList的无参构造方法中初始容量为0,而Vector的无参构造方法中初始容量为10。
    3. Vector线程安全,ArrayList线程不安全。
  2. Vector和它的子类Stack都是线程安全的集合。

10 接口的不同实现

  1. ArrayList底层是数组所以随机访问非常快,但添加删除时要复制数组,添加删除的代价较大。
  2. LinkedList 内部是双向链表所以插入删除代价低,对于随机访问会有顺着结点一个一个查找过程,所以速度较慢,但如果在2个端点插入或删除,会做特殊处理速度较快。
  3. TreeSet存在的唯一原因就是他可以维持元素的排序状态。

11 实用方法

  1. List的排序与查询所使用的方法与对象数组使用的方法有相同的名字和语法,Collections的static方法相当于代替了Arrays的静态方法而已。
  2. Collection 和 Map 可以设置为只读 。Collections的UnmodifiableXXX( )方法设置不同collection和Map为只读。
  3. Java容器类库的容错机制: 但遍历一个容器或者拿到了该容器的迭代器后改变容器状态如添加一个元素删除一个元素修改某个元素则会抛出异常。

12 持有引用

  • Java.lang.ref类库包含了一组类,这些类为垃圾回收提供了灵活性。
  • 三个继承Reference抽象类的类:SoftReference, WeakReference, PhantomReference,如果某个对象只能通过这三个对象才可以获得,那么GC会对这个对象作出不同的回收。
  • 这三个容器类用来保存对象的引用。
  1. 对象可获得:栈中有一个普通引用可以直接指向这个对象,或者通过不同的对象间接指向一个对象,那么这个对象就是可获得的或者可达的,可获得的对象是不能被回收的。
  2. 普通引用:也称强引用,没有被Reference包装的引用,通过普通引用可获得的对象不能被释放。
  3. 如果一个对象被普通引用指向,那么他就不能被释放,一直占据内存,如果没有引用指向那么就会被回收,如果有个对象希望以后还能访问到但是也希望内存不足时可以回收那么对这类对象的引用就可以放在Reference里
  4. Reference对象的可获得性由强到弱,越强越不容易被回收:
    1.  SoftReference 软引用 ,用来实现内存敏感的高速缓存,如果内存即将溢出时就回收对象。
        Object obj = new Object();
      SoftReference<Object> sf = new SoftReference<Object>(obj);
      obj = null;
      sf.get();//有时候会返回null
    2.  WeakReference 弱引用 用来“规范映射”而设计的,WeekHashMap中的key就是WeekReference。
    3. PhantomReference 虚引用  如果有个对象只有虚引用了那么他就会被回收。
  5. ReferenceQueue :GC时会在这种队列(可以自己创建,jvm也会自动创建)中查找虚引用,然后把虚引用的对象清理,softreference 和 weekreference 可以放也可以不放如ReferenceQueue中,但PhantomReference必须在ReferenceQueue中。
  6. WeakHashMap :key 保存弱引用,value 保存其他对象,当key弱引用指向的对象没有其他强引用引用那么key-value就会被回收。如果是普通HashMap那么key指向的对象除了多了一个HashMap的引用,还需要手动清理HashMap。
  7. 查看有关垃圾回收的知识点

知识点:

  1. 两种比较器Comparable和Comparator
    1. 一个类实现Comparable接口,重写compareTo()方法后就具有了比较能力。,至于什么跟什么比可以根据要求决定写在compareTo(Object a)方法里,
      • 如果指定的数与参数相等返回0。

      • 如果指定的数小于参数返回 -1。

      • 如果指定的数大于参数返回 1。

    2. Comparator比较器接口,可以创建自己需要的比较规则在compare(Object o1, Object o2)方法实现即可。可以对没有实现Comparable接口的类或者Comparable比较方式不符合要求的对象按自己需求比较.
      1. 1、o1大于o2,返回正整数

        2、o1等于o2,返回0

        3、o1小于o3,返回负整数

    3. Comparable.compareTo(Object a)也称自然排序,内比较器,自己内的元素排序。Comparator.compare(Object a, Object b) 外比较器,比较对象属性,无法对基本类型数组排序。
  2. 判断为空和为0非常重要,引用为空则没有指向一个实际对象,大小为0说明有对象没元素。
    Node<K,V>[] tab;
    if (tab == null || tab.length == 0)
上一篇:Android开发之重力传感器


下一篇:第一个thinkphp入口文件