Java——集合框架

Java 集合类位于 java.util 包下,JDK1.5 之后还在 java.util.concurrent 包下提供了一些多线程支持的集合类。Java 集合主要由两个接口派生而出:Collection 和 Map。Collection 的父接口是 Iterable(迭代器),所以 Collection 的子接口全部可以使用 Iterable 遍历集合。

Collection 的子接口包括 List、Set 和 Queue 接口。

List集合

-线程安全底层实现特点扩容
ArrayList线程不安全数组有序可重复默认大小10,每次扩容容量为原来的1.5倍
LinkedList线程不安全双向循环链表,既可以作队列,可以作栈
Vector线程安全数组方法都加上了synchronized,保证了线程安全每次扩容容量为原来的2倍
ArrayList

ArrayList 的动态扩容原理:其实就是创建新的长度的数组,然后把老数组数据 copy 到新数据,再覆盖掉老数组的过程。

ArrayList 并不是线程安全的,其方法里既没用到锁,也没用到 CAS 操作。

LinkedList

有序可重复,既可以被当作栈(先进后出)使用,也可以当成队列(先进先出)使用。

LinkedList<Integer> linkedList = new LinkedList<Integer>();
linkedList.offer(1); // 将元素加入队列的尾部
linkedList.offerFirst(2); // 将元素添加到队列的头部
Integer element1 = linkedList.peekLast(); // 访问、并不删除队列的最后一个元素
Integer element2 = linkedList.pollLast(); // 访问、并删除队列的最后一个元素

linkedList.push(3); // 将元素加入栈的顶部
Integer element3 = linkedList.peekFirst(); // 访问、并不弹出栈顶的元素
Integer element4 = linkedList.pop(); // 将栈顶的元素弹出

// 以List的方式 (按索引访问的方式) 来遍历集合元素
for (int i = 0; i < linkedList.size(); i++) {
    Integer element = linkedList.get(i);
}

LinkedList 线程不安全的,底层是双向循环链表实现的,可以自增扩容。

public class LinkedList<E> extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    transient int size = 0;
    transient Node<E> first; // 头节点
    transient Node<E> last; // 尾节点
    public 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;
        }
    }
    // 省略部分代码
}

对于遍历 List 集合元素,ArrayList 最好使用随机访问方法(get)来遍历,这样性能最好;LinkedList 则最好用迭代器(Iterator)来遍历集合元素。

为啥要有迭代器(模式)? 优势是什么?

  • 迭代器模式封装集合内部的复杂数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可;
  • 迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一;
  • 迭代器模式让添加新的遍历算法更加容易,更符合开闭原则。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。

fail-fast机制?

在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。

有两种比较干脆利索的解决方案,来避免出现这种不可预期的运行结果。一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错。第一种解决方案比较难实现,因为很难确定迭代器使用结束的时间点。第二种解决方案更加合理。Java 语言就是采用的这种解决方案。增删元素之后,我们选择 fail-fast 解决方式,让遍历操作直接抛出运行时异常。

Java 语言,迭代器类中除了前面提到的几个最基本的方法之外,还定义了一个 remove() 方法,能够在遍历集合的同时,安全地删除集合中的元素。


Set集合

HashSet
Set<Integer> set = new HashSet<Integer>();
Iterator it = set.iterator(); // 使用Iterator遍历集合元素
while (it.hasNext()) {
    Integer i = (Integer) it.next();
}

HashSet 判断两个元素相等的标准:两个对象通过 equals() 方法比较相等;两个对象的 hashCode() 返回值也相等。

通过阅读 HashSet 源码,可以看到,HashSet 是通过 HashMap 实现的。

public class HashSet<E> extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable {
    private transient HashMap<E, Object> map;
    public HashSet() {
        map = new HashMap<>();
    }
    // 省略部分代码
}

简而言之,HashSet 就是一个 value 为空对象的 HashMap。

TreeSet

TreeSet 采用红黑树的数据结构来存储集合元素,支持两种排序方法:自然排序和定制排序,默认自然排序

TreeSet<Integer> treeSet = new TreeSet<Integer>();
Integer first = treeSet.first(); // 获取第一个元素
Integer last = treeSet.last(); // 获取最后一个元素
SortedSet headSet = treeSet.headSet(25); // 获取小于25的子集, 不包含25
SortedSet tailSet = treeSet.tailSet(10); // 获取大于10的子集, 如果Set中包含10,子集中还包含10
SortedSet subSet = treeSet.subSet(-10, 20); // 获取大于等于-10, 小于20的子集

通过阅读 TreeSet 源码,可以看到,TreeSet 是通过 TreeMap 实现的。

public class TreeSet<E> extends AbstractSet<E>
        implements NavigableSet<E>, Cloneable, java.io.Serializable {
    private transient NavigableMap<E, Object> m;
    TreeSet(NavigableMap<E, Object> m) {
        this.m = m;
    }
    public TreeSet() {
        this(new TreeMap<E, Object>());
    }
    // 省略部分代码
}

简而言之,TreeSet 就是一个 value 为空对象的 TreeMap。

Map集合

-线程安全底层实现特点扩容
HashMap线程不安全hash 表无序,key 不允许null,value 可以null,为快速查询而设计的默认大小16,如果超过当前大小 * 扩容因子 0.75 就会扩容,扩容 2 倍后重排
LinkedHashMap线程不安全hash 表继承 HashMap
TreeMap线程不安全红黑树自然有序,key 不允许null,value 可以null,速度略慢于 HashMap
Hashtable线程安全hash 表

Map 用于保存具有映射关系的数据(key-value),key 不允许重复,value 可以重复。

速度对比:HashMap(最快) > Hashtable > TreeMap

HashMap

hash(哈希、散列)算法的功能:能保证快速查找被检索的对象,hash 算法的价值在于速度,当查询某个元素时,hash 算法可以直接根据该元素的 hashCode 值计算出该元素的存储位置,从而快速定位。

HashMap、HashTable、ConcurrentHashMap 的区别?

HashMap JDK1.8 前采用了数组 + 链表实现的,数组的特点是查询快增删慢,链表的特点是查询慢增删快,HashMap 结合了两者的优势,同时 HashMap 的操作是非 synchronized 的,因此效率比较高。

在这里插入图片描述
最坏情况下,hash() 计算后总是会命中同一个数组元素,那么 HashMap 的性能将会从原先的 O(1) 变成 O(n)。

针对这个问题,JDK1.8 及以后变成了数组 + 链表 + 红黑树实现,使用了一个 TREEIFY_THRESHOLD 常量来控制是否将链表转换为红黑树来存储它们,这意味着可以将最坏情况下的性能从 O(n) 提高到 O(logn)。

在这里插入图片描述
HashMap put() 方法的逻辑:

  1. 若 table 数组未被初始化,则进行初始化操作;
  2. 对 key 计算 hash 值,依据 hash 值计算 table 数组下标;
  3. 若未发生碰撞,即该下标的 “桶” 还没有存储头节点,则直接放入 “桶” 的头节点中;
  4. 若发生碰撞,即该下标的 “桶” 已经存储了头节点,则执行链表的添加操作;
  5. 若链表的长度超过树化阈值 (TREEIFY_THRESHOLD,默认 8)且数组长度
    大于等于64 则被改造成红黑树,而当删除操作导致低于最低树化阈值(默认 6)的时候,红黑树则转回成链表,保证更高的性能;
  6. 若键值对已经存在,则用新值替换旧值;
  7. 若某个 “桶” 满了(默认容量 16 * 扩容因子 0.75 = 12,则默认超过 12 就会扩容),就需要 resize(扩容 2 倍后重排)。

ConcurrentHashMap

早期的 ConcurrentHashMap 是通过分段锁技术来实现的,将 HashMap 的 table 数组逻辑上拆分成多个子数组,默认会分成 16 段,每个段配一把锁,这样多个线程如果操作的是不同的段,则不会被阻塞,理论上会比 HashTable 的效率提升 16 倍。

JDK 1.8 ConcurrentHashMap 的实现取消了分段锁,而 CAS + synchronized 使锁更细化。同时对结构也做了进一步优化,跟 JDK 1.8 的 HashMap 一样,由数据 + 链表 + 红黑树实现,synchronized 只锁定当前链表或者红黑树的首节点,这样只要 hash 不冲突,就不会产品线程安全问题,效率得到了进一步的提高。

ConcurrentHashMap put() 方法的逻辑:

  1. 若 table 数组未被初始化,则进行初始化操作;
  2. 对 key 计算 hash 值,依据 hash 值计算 table 数组下标;
  3. 若未发生碰撞,即该下标的 “桶” 还没有存储头节点,则使用 CAS 操作放入 “桶” 的头节点中,添加失败则循环重试;
  4. 检查到内部正在扩容,就帮助它一块扩容;
  5. 若发生碰撞,即该下标的 “桶” 已经存储了头节点,则使用 synchronized 锁住头节点(链表或者红黑树),如果是链表结构则执行链表的添加操作,如果是红黑树结构则执行树添加操作;
  6. 若链表的长度超过树化阈值 (TREEIFY_THRESHOLD,默认 8)且数组长度
    大于等于64 则被改造成红黑树,而当删除操作导致低于最低树化阈值(UNTREEIFY_THRESHOLD,默认 6)的时候,红黑树则转回成链表,保证更高的性能;
  7. 若键值对已经存在,则用新值替换旧值;
  8. 若某个 “桶” 满了(默认容量 16 * 扩容因子 0.75 = 12,则默认超过 12 就会扩容),就需要 resize(扩容 2 倍后重排)。
Collections

Collections 工具类提供了大量方法对集合元素进行查询、排序、修改等操作:

// 对Collection集合进行查找
Collections.max(collection); // 获取集合最大元素
Collections.min(collection); // 获取集合最小元素
Collections.frequency(collection , 1); // 判断1在集合中出现的次数

// 同步控制, 创建线程安全的集合对象
Collection c = Collections.synchronizedCollection(new ArrayList());
List l = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());

// 对List集合元素进行排序
Collections.reverse(list); // 将元素的次序反转
Collections.sort(list); // 将元素的按自然顺序排序
Collections.shuffle(list); // 将元素的按随机顺序排序

// 对List集合元素进行替换
Collections.replaceAll(list , 0 , 1); // 将List中的0使用1来代替
Collections.binarySearch(list , 1); // 使用二分法搜索指定的List集合, 以获得List集合中的索引, 只有排序后的List集合才可用二分法查询

// 设置不可变集合, 如果试图改变, 将引发UnsupportedOperationException异常
List unList = Collections.emptyList(); // 创建一个空的、不可改变的List对象
Set unSet = Collections.singleton(2); // 创建一个只有一个元素, 且不可改变的Set对象
Map<String, Object> map = new HashMap<String, Object>();
map.put("C" , 100);
Map unMap = Collections.unmodifiableMap(map); // 返回普通Map对象对应的不可变版本

常见问题

1.hashmap的哈希算法?

主要分为三步:

  1. 取 hashCode 值: key.hashCode()
  2. 高位参与运算:h>>>16
  3. 取模运算:(n-1) & hash
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    i = (table.length - 1) & hash;//这一步是在后面添加元素putVal()方法中进行位置的确定

为了让数组元素分布均匀,我们首先想到的是把获得的 hash码对数组长度取模运算( hash%length),但是计算机都是二进制进行操作,取模运算相对开销还是很大的,那该如何优化呢?

HashMap 使用的方法很巧妙,它通过 hash & (table.length -1)来得到该对象的保存位,前面说过 HashMap 底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当 length 总是2的n次方时,hash & (length-1)运算等价于对 length 取模,也就是 hash%length,但是&比%具有更高的效率。比如 n % 32 = n & (32 -1)

2.HashMap扩容机制?

JDK1.7的代码:

void resize(int newCapacity) {   //传入新的容量
      Entry[] oldTable = table;    //引用扩容前的Entry数组
      int oldCapacity = oldTable.length;         
      if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了
          threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
          return;
      }
      Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组
     transfer(newTable);                         //!!将数据转移到新的Entry数组里
     table = newTable;                           //HashMap的table属性引用新的Entry数组
     threshold = (int)(newCapacity * loadFactor);//修改阈值
 }

这里就是使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。

 void transfer(Entry[] newTable) {
      Entry[] src = table;                   //src引用了旧的Entry数组
      int newCapacity = newTable.length;
      for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
          Entry<K,V> e = src[j];             //取得旧Entry数组的每个元素
          if (e != null) {
              src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
              do {
                  Entry<K,V> next = e.next;
                 int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
                 e.next = newTable[i]; //标记[1]
                 newTable[i] = e;      //将元素放在数组上
                 e = next;             //访问下一个Entry链上的元素
             } while (e != null);
         }
     }
 } 

newTable[i]的引用赋给了e.next,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到Entry链的尾部(如果发生了hash冲突的话)

JDK 1.8的resize源码:

 final Node<K,V>[] resize() {
     Node<K,V>[] oldTab = table;
     int oldCap = (oldTab == null) ? 0 : oldTab.length;
     int oldThr = threshold;
     int newCap, newThr = 0;
     if (oldCap > 0) {
         // 超过最大值就不再扩充了,就只好随你碰撞去吧
         if (oldCap >= MAXIMUM_CAPACITY) {
             threshold = Integer.MAX_VALUE;
             return oldTab;
         }
         // 没超过最大值,就扩充为原来的2倍
         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                  oldCap >= DEFAULT_INITIAL_CAPACITY)
             newThr = oldThr << 1; // double threshold
     }
     else if (oldThr > 0) // initial capacity was placed in threshold
         newCap = oldThr;
     else {               // zero initial threshold signifies using defaults
         newCap = DEFAULT_INITIAL_CAPACITY;
         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
     }
     // 计算新的resize上限
     if (newThr == 0) {
 
         float ft = (float)newCap * loadFactor;
         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                   (int)ft : Integer.MAX_VALUE);
     }
     threshold = newThr;
     @SuppressWarnings({"rawtypes""unchecked"})
         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
     table = newTab;
     if (oldTab != null) {
         // 把每个bucket都移动到新的buckets中
         for (int j = 0; j < oldCap; ++j) {
             Node<K,V> e;
             if ((e = oldTab[j]) != null) {
                 oldTab[j] = null;
                 if (e.next == null)
                     newTab[e.hash & (newCap - 1)] = e;
                 else if (e instanceof TreeNode)
                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                 else { // 链表优化重hash的代码块
                     Node<K,V> loHead = null, loTail = null;
                     Node<K,V> hiHead = null, hiTail = null;
                     Node<K,V> next;
                     do {
                         next = e.next;
                         // 原索引
                         if ((e.hash & oldCap) == 0) {
                             if (loTail == null)
                                 loHead = e;
                             else
                                 loTail.next = e;
                             loTail = e;
                         }
                         // 原索引+oldCap
                         else {
                             if (hiTail == null)
                                 hiHead = e;
                             else
                                 hiTail.next = e;
                             hiTail = e;
                         }
                     } while ((e = next) != null);
                     // 原索引放到bucket里
                     if (loTail != null) {
                         loTail.next = null;
                         newTab[j] = loHead;
                     }
                     // 原索引+oldCap放到bucket里
                     if (hiTail != null) {
                         hiTail.next = null;
                         newTab[j + oldCap] = hiHead;
                     }
                 }
             }
         }
     }
     return newTab;
 }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
水资源是人类社会的宝贵财富,在生活、工农业生产中是不可缺少的。随着世界人口的增长及工农业生产的发展,需水量也在日益增长,水已经变得比以往任何时候都要珍贵。但是,由于人类的生产和生活,导致水体的污染,水质恶化,使有限的水资源更加紧张。长期以来,油类物质(石油类物质和动植物油)一直是水和土壤中的重要污染源。它不仅对人的身体健康带来极大危害,而且使水质恶化,严重破坏水体生态平衡。因此各国都加强了油类物质对水体和土壤的污染的治理。对于水中油含量的检测,我国处于落后阶段,与国际先进水平存在差距,所以难以满足当今技术水平的要求。为了取得具有代表性的正确数据,使分析数据具有与现代测试技术水平相应的准确性和先进性,不断提高分析成果的可比性和应用效果,检测的方法和仪器是非常重要的。只有保证了这两方面才能保证快速和准确地测量出水中油类污染物含量,以达到保护和治理水污染的目的。开展水中油污染检测方法、技术和检测设备的研究,是提高水污染检测的一条重要措施。通过本课题的研究,探索出一套适合我国国情的水质污染现场检测技术和检测设备,具有广泛的应用前景和科学研究价值。 本课题针对我国水体的油污染,探索一套检测油污染的可行方案和方法,利用非分散红外光度法技术,开发研制具有自主知识产权的适合国情的适于野外便携式的测油仪。利用此仪器,可以检测出被测水样中亚甲基、甲基物质和动植物油脂的污染物含量,为我国众多的环境检测站点监测水体的油污染状况提供依据。
### 内容概要 《计算机试卷1》是一份综合性的计算机基础和应用测试卷,涵盖了计算机硬件、软件、操作系统、网络、多媒体技术等多个领域的知识点。试卷包括单选题和操作应用两大类,单选题部分测试学生对计算机基础知识的掌握,操作应用部分则评估学生对计算机应用软件的实际操作能力。 ### 适用人群 本试卷适用于: - 计算机专业或信息技术相关专业的学生,用于课程学习或考试复习。 - 准备计算机等级考试或职业资格认证的人士,作为实战演练材料。 - 对计算机操作有兴趣的自学者,用于提升个人计算机应用技能。 - 计算机基础教育工作者,作为教学资源或出题参考。 ### 使用场景及目标 1. **学习评估**:作为学校或教育机构对学生计算机基础知识和应用技能的评估工具。 2. **自学测试**:供个人自学者检验自己对计算机知识的掌握程度和操作熟练度。 3. **职业发展**:帮助职场人士通过实际操作练习,提升计算机应用能力,增强工作竞争力。 4. **教学资源**:教师可以用于课堂教学,作为教学内容的补充或学生的课后练习。 5. **竞赛准备**:适合准备计算机相关竞赛的学生,作为强化训练和技能检测的材料。 试卷的目标是通过系统性的题目设计,帮助学生全面复习和巩固计算机基础知识,同时通过实际操作题目,提高学生解决实际问题的能力。通过本试卷的学习与练习,学生将能够更加深入地理解计算机的工作原理,掌握常用软件的使用方法,为未来的学术或职业生涯打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值