热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

Java集合教程之Collection实例详解

集合,或者叫容器,是一个包含多个元素的对象,下面这篇文章主要给大家介绍了关于Java集合教程之Collection的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下

前言

集合就是一组数的集合,就像是一个容器,但是我们应该清楚的是集合中存放的都是对象的引用,而不是真正的实体。而我们常说的集合中的对象其实指的就是对象的引用。

我们可以把集合理解为一个小型数据库,用于存放数据,我们对集合的操作也就是数据的增删改查,在 Java 中有两个顶层接口 Collection 和 Map 用于定义和规范集合的相关操作。这篇文章主要说一下集合框架中的 Collection 部分。

Collection 表示一组对象,这些对象可以是有序也可以是无序的,它提供了不同的子接口满足我们的需求。我们主要看看 List 和 Set 。

 

List 整体的特征就是有序可重复。我们需要研究的是上图中具体的实现类都有什么特性。底层的实现原理是什么,首先来看一看 List 的古老的实现类 Vector ,说是古老是因为在 JDK 1.0 的时候就出现了,都走开,我要开始看源码了!这些源码来自于 JDK 1.7。

public class Vector
 extends AbstractList
 implements List, RandomAccess, Cloneable, java.io.Serializable
{
 /**
  * The array buffer into which the components of the vector are
  * stored.
  */
 protected Object[] elementData;

 /**
  * The number of valid components in this {@code Vector} object.
  */
 protected int elementCount;

 /**
  * The amount by which the capacity of the vector is automatically
  * incremented when its size becomes greater than its capacity. If
  * the capacity increment is less than or equal to zero, the capacity
  * of the vector is doubled each time it needs to grow.
  */
 protected int capacityIncrement;

 public Vector() {
  this(10);
 }

 // 添加元素
 public synchronized boolean add(E e) {
  modCount++;
  ensureCapacityHelper(elementCount + 1);
  elementData[elementCount++] = e;
  return true;
 }

 // 删除元素
 public synchronized E remove(int index) {
  modCount++;
  if (index >= elementCount)
   throw new ArrayIndexOutOfBoundsException(index);
  E oldValue = elementData(index);

  int numMoved = elementCount - index - 1;
  if (numMoved > 0)
   System.arraycopy(elementData, index+1, elementData, index,
        numMoved);
  elementData[--elementCount] = null; // Let gc do its work

  return oldValue;
 }

 // 修改元素
 public synchronized E set(int index, E element) {
  if (index >= elementCount)
   throw new ArrayIndexOutOfBoundsException(index);

  E oldValue = elementData(index);
  elementData[index] = element;
  return oldValue;
 }
 // 查找元素
 public synchronized E get(int index) {
  if (index >= elementCount)
   throw new ArrayIndexOutOfBoundsException(index);

  return elementData(index);
 }
...

就以上源码分析就可以知道关于 Vector 的特征。1 底层实现是使用数组来存储数据,所以相应的查找元素和添加元素速度较快,删除和插入元素较慢。2 数组的初始长度为 10 ,当长度不够时,增长量也为 10 使用变量 capacityIncrement 来表示。3 方法的声明中都加入了 synchronized 关键字,线程安全的,所以效率相应降低了。 4 没分析出来的再看一遍。

下面开始看 ArrayList 的源码。

public class ArrayList extends AbstractList
  implements List, RandomAccess, Cloneable, java.io.Serializable
{
 private static final long serialVersiOnUID= 8683452581122892189L;

 /**
  * Default initial capacity.
  */
 private static final int DEFAULT_CAPACITY = 10;

 /**
  * Shared empty array instance used for empty instances.
  */
 private static final Object[] EMPTY_ELEMENTDATA = {};

 /**
  * The array buffer into which the elements of the ArrayList are stored.
  * DEFAULT_CAPACITY when the first element is added.
  */
 private transient Object[] elementData;

 /**
  * Constructs an empty list with an initial capacity of ten.
  */
 public ArrayList() {
  super();
  this.elementData = EMPTY_ELEMENTDATA;
 }

 // 添加元素
 public boolean add(E e) {
  ensureCapacityInternal(size + 1); // Increments modCount!!
  elementData[size++] = e;
  return true;
 }

 // 增加数组的长度
 private void grow(int minCapacity) {
  // overflow-conscious code
  int oldCapacity = elementData.length;
  int newCapacity = oldCapacity + (oldCapacity >> 1);
  if (newCapacity - minCapacity <0)
   newCapacity = minCapacity;
  if (newCapacity - MAX_ARRAY_SIZE > 0)
   newCapacity = hugeCapacity(minCapacity);
  // minCapacity is usually close to size, so this is a win:
  elementData = Arrays.copyOf(elementData, newCapacity);
 }
...

因为源码和 Vector 类似,所以有些就不贴了,但是不耽误我们继续分析 ArrayList 。1 底层存储数据使用的还是数组,长度依旧为 10 ,但是进步了,没有在刚开始创建的时候就初始化,而是在添加第一个元素的时候才初始化的。2 方法的声明少了 synchronized 关键字,线程不安全,但性能提高了。3 数组长度不够时,会自动增加为原长度的 1.5 倍。

以上分析也能体现出 Vector 和 ArrayList 的差别。主要就是想说 Vector 已经不用了。使用 ArrayList 即可,关于线程安全问题,后面再说。

接着看 LinkedList 的实现,上源码 ~

public class LinkedList
 extends AbstractSequentialList
 implements List, Deque, Cloneable, java.io.Serializable
{
 transient int size = 0;

 /**
  * Pointer to first node.
  */
 transient Node first;

 /**
  * Pointer to last node.
  */
 transient Node last;

 /**
  * Constructs an empty list.
  */
 public LinkedList() {
 }

 // 每一个元素即为一个节点,节点的结构如下(这是一个内部类啊)
 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;
  }
 }

 // 添加元素
 public boolean add(E e) {
  linkLast(e);
  return true;
 }

 void linkLast(E e) {
  final Node l = last;
  final Node newNode = new Node<>(l, e, null);
  last = newNode;
  if (l == null)
   first = newNode;
  else
   l.next = newNode;
  size++;
  modCount++;
 }

 // 删除某个节点的逻辑
 E unlink(Node x) {
  // assert x != null;
  final E element = x.item;
  final Node next = x.next;
  final Node prev = x.prev;

  if (prev == null) {
   first = next;
  } else {
   prev.next = next;
   x.prev = null;
  }

  if (next == null) {
   last = prev;
  } else {
   next.prev = prev;
   x.next = null;
  }

  x.item = null;
  size--;
  modCount++;
  return element;
 }
...

重点就是 LinkedList 的底层实现是双链表。这样就会有以下特性,1 查找元素较慢,但是添加和删除较快。2 占内存,因为每一个节点都要维护两个索引。3 线程不安全 。4 对集合长度没有限制。

以上,List 的几个实现已经分析完成,以后再谈到 Vector ,ArrayList ,LinkedList 之间的区别应该不会不知所云了吧!还要接着看 Collection 的另一个子接口 Set 。首先有个大前提,Set 中存储的元素是无序不可重复的。然后我们再来看实现类是如何实现的。下面开始 HashSet 的表演。

public class HashSet
 extends AbstractSet
 implements Set, Cloneable, java.io.Serializable
{
 private transient HashMap map;

 // Dummy value to associate with an Object in the backing Map
 private static final Object PRESENT = new Object();

 /**
  * Constructs a new, empty set; the backing HashMap instance has
  * default initial capacity (16) and load factor (0.75).
  */
 public HashSet() {
  map = new HashMap<>();
 }

 // 添加元素,其实就是像 Map 中添加主键,所以添加的元素不能重复
 public boolean add(E e) {
  return map.put(e, PRESENT)==null;
 }

 // 实现 Iterable 接口中的 Iterator 方法。
 public Iterator iterator() {
  return map.keySet().iterator();
 }
...

看了源码才发现,1 原来 HashSet 就是对 HashMap 的封装啊,底层实现是基于 hash 表的,回头有必要再好好的介绍一下 hash 相关的知识。2 Set 集合中的值,都会以 key 的形式存放在 Map 中,所以说 Set 中的元素不能重复。3 线程不安全。4 允许存放空值,因为 Map 的键允许为空。

今天要说的最后一个实现类终于出现啦,他就是 TreeSet ,这个实现类中的元素是有序的!注意这里说的有序是指按照一定的规则排序,而我们说 Set 集合中的元素无序是因为添加进集合的顺序和输出的顺序不保证一致。TreeSet 是怎么保证有序我们待会再说,还是一样的套路,是对 TreeMap 的封装,线程依旧不安全。

public class TreeSet extends AbstractSet
 implements NavigableSet, Cloneable, java.io.Serializable
{
 /**
  * The backing map.
  */
 private transient NavigableMap m;

 // Dummy value to associate with an Object in the backing Map
 private static final Object PRESENT = new Object();

 /**
  * Constructs a set backed by the specified navigable map.
  */
 TreeSet(NavigableMap m) {
  this.m = m;
 }

 /**
  * Constructs a new, empty tree set, sorted according to the
  * natural ordering of its elements. All elements inserted into
  * the set must implement the Comparable interface.
  */
 public TreeSet() {
  this(new TreeMap());
 }

 /**
  * Constructs a new, empty tree set, sorted according to the specified
  * comparator. All elements inserted into the set must be mutually
  * comparable by the specified comparator
  * If the Comparable natural ordering of the elements will be used.
  */
 public TreeSet(Comparator<&#63; super E> comparator) {
  this(new TreeMap<>(comparator));
 }
 
 // 添加元素方法
 public boolean add(E e) {
  return m.put(e, PRESENT)==null;
 } 
...

我们可以看到 TreeSet 中构造函数上方的注释,TreeSet 要保证元素有序,保证有序的思路是在添加元素的时候进行比较。

... // 这是 TreeMap 的 put 方法的节选,为了看到比较的过程。
 public V put(K key, V value) {
 ...
  // split comparator and comparable paths
  Comparator<&#63; super K> cpr = comparator;
  if (cpr != null) {
   do {
    parent = t;
    cmp = cpr.compare(key, t.key);
    if (cmp <0)
     t = t.left;
    else if (cmp > 0)
     t = t.right;
    else
     return t.setValue(value);
   } while (t != null);
  }
  else {
   if (key == null)
    throw new NullPointerException();
   Comparable<&#63; super K> k = (Comparable<&#63; super K>) key;
   do {
    parent = t;
    cmp = k.compareTo(t.key);
    if (cmp <0)
     t = t.left;
    else if (cmp > 0)
     t = t.right;
    else
     return t.setValue(value);
   } while (t != null);
  }
 ...
 }

Java 中提供了两种方式,第一种方法,需要我们所添加对象的类实现 Comparable 接口,进而实现 compareTo 方法,这种方式也叫自然排序,我们并没有传入什么排序规则。这种方式对应 TreeSet 的空参构造器。而另一种方式就是定制排序,即我们自己定义两个元素的排序规则,在实例化 TreeSet 的时候传入对应的排序规则即可,对应于 TreeSet 中带有 Comparator 接口的构造器,这里面需要实现 compare 方法 。有点迷糊了是吧,举个例子看看 ~

public class Person implements Comparable{

 public String name;
 public Integer age;

 public Person(String name,Integer age) {
  this.name = name;
  this.age = age;
 }
 /* 自定义的比较的逻辑:
  *  首先按照对象的 name 属性排序
  *  其次按照 age 属性排序
  * 方法的返回值为 0 ,大于 0,小于 0 ,分别对应于 相等,大于,和小于
  */
 @Override
 public int compareTo(Person o) {
  int i = this.name.compareTo(o.name);
  if(i == 0){
   return this.age.compareTo(o.age);
  }else {
   return i;
  }
 }
 @Override
 public String toString() {
  return "[Person] name:"+this.name+" age:"+this.age;
 }
}

 // 以下是测试代码
 public static void main(String[] args) {
  TreeSet set = new TreeSet<>();
  set.add(new Person("AJK923",20));
  set.add(new Person("BJK923",20));
  set.add(new Person("AJK923",21));
  set.add(new Person("BJK923",21));

  for (Person person : set) {
   System.out.println(person.toString());
  }
  /*
  [Person] name:AJK923 age:20
  [Person] name:AJK923 age:21
  [Person] name:BJK923 age:20
  [Person] name:BJK923 age:21
  */
    
  ----以下为定制排序的部分----匿名内部类实现 Comparator 接口----  
  TreeSet set2 = new TreeSet<>(new Comparator() {
   @Override
   public int compare(Person o1, Person o2) {
    int i = o1.age.compareTo(o2.age);
    if(i == 0){
     return o1.name.compareTo(o2.name);
    }else {
     return i;
    }
   }
  });

  set2.add(new Person("AJK923",20));
  set2.add(new Person("BJK923",20));
  set2.add(new Person("AJK923",21));
  set2.add(new Person("BJK923",21));

  for (Person person : set2) {
   System.out.println(person.toString());
  }
  /*
   [Person] name:AJK923 age:20
   [Person] name:BJK923 age:20
   [Person] name:AJK923 age:21
   [Person] name:BJK923 age:21
   */
 }

上面就是自然排序和定制排序的应用。要说明几点:

1 定制排序和自然排序同时存在的话,优先执行定制排序。可以看看上面的 put 方法的实现 。

2 自然排序对应 Comparable 接口,定制排序对应 Comparator 接口。

3 String ,包装类,日期类都已经实现了 Comparable 接口的 comparaTo 方法,所以我上面的例子中都偷懒了,没有自己实现具体比较,而是直接调用现成的。

看到这里,也算是对 Collection 有了整体的认识,但是这里我并没有说到具体的 API ,我现在日常也用不到几个方法,就放一张 Collection 接口的结构图吧,方法也比较简单,看个名字就大概知道什么意思了。

还是要重点说一下 iterator 方法。这个方法定义在 Iterable 接口中(Collection 继承了 Iterable 接口),方法返回的是 iterator 接口对象。iterator 中定义了迭代器的操作规则,而 Collection 的实现类中是具体的实现。Iterator 接口中定义了 next ,hasNext 和 remove 方法。看一下在ArrayList 中是如何实现的吧。

private class Itr implements Iterator {
  int cursor;  // index of next element to return
  int lastRet = -1; // index of last element returned; -1 if no such
  int expectedModCount = modCount;

  public boolean hasNext() {
   return cursor != size;
  }

  @SuppressWarnings("unchecked")
  public E next() {
   checkForComodification();
   int i = cursor;
   if (i >= size)
    throw new NoSuchElementException();
   Object[] elementData = ArrayList.this.elementData;
   if (i >= elementData.length)
    throw new ConcurrentModificationException();
   cursor = i + 1;
   return (E) elementData[lastRet = i];
  }

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

   try {
    ArrayList.this.remove(lastRet);
    cursor = lastRet;
    lastRet = -1;
    expectedModCount = modCount;
   } catch (IndexOutOfBoundsException ex) {
    throw new ConcurrentModificationException();
   }
  }

  final void checkForComodification() {
   if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
  }
 }

应用起来还是比较简单的,使用一个 while 循环即可。看下面这个例子。

  public static void main(String[] args) {

  List list = new ArrayList<>(); 
  list.add(1);
  list.add(2);
  list.add(3);

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

不知道你有没有发现,除了 Vector 以外,保存集合元素的那个变量都定义为 transient 不管你是数组,Node 或是 Map ,不由得我又要想一想为什么这样设计?

先看一下 transient 的作用吧!Java 的 serialization 提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用 serialization 机制来保存它。为了在一个特定对象的一个域上关闭 serialization,可以在这个域前加上关键字 transient 。当一个对象被序列化的时候,transient 型变量的值不包括在序列化的表示中,非 transient 型的变量是被包括进去的。

那么为什么要这么做呢,好好的标准序列化不用,原因如下:

对于 ArrayList 来说,底层实现是数组,而数组的长度和容量不一定相等,可能容量为 10 ,而我们的元素只有 5 。此时序列化的时候就有点浪费资源,序列化和反序列化还是要的,故 ArrayList 自己实现了两个方法,分别是 writeObject 和 readObject ,用于序列化和反序列化。

对于 HashSet 和  HashMap 来说,底层实现都是依赖于 hash 表,而不同的 JVM 可能算出的 hashCode 值不一致,这样在跨平台的时候就会导致序列化紊乱。故也重写了那两个方法。借用一句似懂非懂的话:

当一个对象的物理表示方法与它的逻辑数据内容有实质性差别时,使用默认序列化形式有 N 种缺陷,所以应该尽可能的根据实际情况重写序列化方法。

对应于 Collection ,有一个 Collections 工具类,其中提供很多方法,比如说集合的排序,求子集合,最大值,最小值,交换,填充,打乱集合等等,还记得上面说到的实现类中存在线程不安全的情况吧,这个工具类中提供很多对应的 synchronized 的方法。

后记 :不知不觉中扩展了这么多知识点,实话说,肯定还有遗漏的地方,就我现在能想到的依然还有很多,除去 Hash 表和 Hash 算法这一部分之外,我还没有对底层的数据结构进一步分析,数组,链表,二叉树等等,现在是分析不动了,本文已经很长了。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。


推荐阅读
  • 在Java中,抽象类无法直接实例化的原因在于其设计初衷是为了提供一种模板方法模式,其中包含未实现的方法。这些方法需要由子类来具体实现。因此,直接实例化抽象类没有实际意义,也无法满足编译器的要求。理解这一点有助于更好地利用抽象类进行面向对象编程。 ... [详细]
  • 第六章:枚举类型与switch结构的应用分析
    第六章深入探讨了枚举类型与 `switch` 结构在编程中的应用。枚举类型(`enum`)是一种将一组相关常量组织在一起的数据类型,广泛存在于多种编程语言中。例如,在 Cocoa 框架中,处理文本对齐时常用 `NSTextAlignment` 枚举来表示不同的对齐方式。通过结合 `switch` 结构,可以更清晰、高效地实现基于枚举值的逻辑分支,提高代码的可读性和维护性。 ... [详细]
  • 每日前端实战:148# 视频教程展示纯 CSS 实现按钮两侧滑入装饰元素的悬停效果
    通过点击页面右侧的“预览”按钮,您可以直接在当前页面查看效果,或点击链接进入全屏预览模式。该视频教程展示了如何使用纯 CSS 实现按钮两侧滑入装饰元素的悬停效果。视频内容具有互动性,观众可以实时调整代码并观察变化。访问以下链接体验完整效果:https://codepen.io/comehope/pen/yRyOZr。 ... [详细]
  • 初探性能优化:入门指南与实践技巧
    在编程领域,常有“尚未精通编码便急于优化”的声音。为了从性能优化的角度提升代码质量,本文将带领读者初步探索性能优化的基本概念与实践技巧。即使程序看似运行良好,数据处理效率仍有待提高,通过系统学习性能优化,能够帮助开发者编写更加高效、稳定的代码。文章不仅介绍了性能优化的基础知识,还提供了实用的调优方法和工具,帮助读者在实际项目中应用这些技术。 ... [详细]
  • 深入解析HTTPS:保障Web安全的加密协议
    本文详细探讨了HTTPS协议在保障Web安全中的重要作用。首先分析了HTTP协议的不足之处,包括数据传输过程中的安全性问题和内容加密的缺失。接着介绍了HTTPS如何通过使用公钥和私钥的非对称加密技术以及混合加密机制,确保数据的完整性和机密性。最后强调了HTTPS的安全性和可靠性,为现代网络通信提供了坚实的基础。 ... [详细]
  • 《Spring in Action 第4版:全面解析与实战指南》
    《Spring in Action 第4版:全面解析与实战指南》不仅详细介绍了Spring框架的核心优势,如简洁易测试、低耦合特性,还深入探讨了其轻量级和最小侵入性的设计原则。书中强调了声明式编程的优势,并通过基于约定的方法简化开发流程。此外,Spring的模板机制有效减少了重复代码,而依赖注入功能则由容器自动管理,确保了应用的灵活性和可维护性。 ... [详细]
  • 在日常的项目开发中,测试环境和生产环境通常采用HTTP协议访问服务。然而,从浏览器的角度来看,这种访问方式会被标记为不安全。为了提升安全性,当前大多数生产环境已经转向了HTTPS协议。本文将详细介绍如何在Spring Boot应用中配置SSL证书,以实现HTTPS安全访问。通过这一过程,不仅可以增强数据传输的安全性,还能提高用户对系统的信任度。 ... [详细]
  • 2012年9月12日优酷土豆校园招聘笔试题目解析与备考指南
    2012年9月12日,优酷土豆校园招聘笔试题目解析与备考指南。在选择题部分,有一道题目涉及中国人的血型分布情况,具体为A型30%、B型20%、O型40%、AB型10%。若需确保在随机选取的样本中,至少有一人为B型血的概率不低于90%,则需要选取的最少人数是多少?该问题不仅考察了概率统计的基本知识,还要求考生具备一定的逻辑推理能力。 ... [详细]
  • 链栈虽然通常以数组作为底层实现,但也可以采用链表来构建Stack类。在这种情况下,空堆栈通过NULL指针表示。当新元素被压入堆栈时,它会被添加到链表的头部,从而实现高效的入栈操作。此外,出栈操作则通过移除链表头部的节点来完成,确保了操作的时间复杂度为O(1)。这种设计不仅简化了内存管理,还提高了动态数据处理的灵活性。 ... [详细]
  • 每年,意甲、德甲、英超和西甲等各大足球联赛的赛程表都是球迷们关注的焦点。本文通过 Python 编程实现了一种生成赛程表的方法,该方法基于蛇形环算法。具体而言,将所有球队排列成两列的环形结构,左侧球队对阵右侧球队,首支队伍固定不动,其余队伍按顺时针方向循环移动,从而确保每场比赛不重复。此算法不仅高效,而且易于实现,为赛程安排提供了可靠的解决方案。 ... [详细]
  • 史丰收快速计算法在蓝桥杯竞赛中的应用与解析摘要:史丰收速算法通过从高位开始计算并预判进位,摒弃了传统的九九乘法表,彻底革新了手工计算方式。该方法的核心在于其独特的计算逻辑和高效的进位处理机制,使得复杂计算变得简便快捷。本文详细探讨了史丰收速算法在蓝桥杯竞赛中的具体应用,并对其原理进行了深入解析,旨在为参赛选手提供一种高效、准确的计算工具。 ... [详细]
  • 这篇文章 | 夕阳下的防火墙命令全解 ... [详细]
  • 在使用 jQuery 的 `html()` 方法时,我发现了一个奇怪的现象:该方法无法完整地插入指定的字符串内容。具体来说,当尝试插入较长或包含特殊字符的字符串时,部分内容可能会被截断或丢失。这一问题可能与 jQuery 对字符串的处理方式有关,建议在实际应用中进行充分测试以确保数据完整性。 ... [详细]
  • 家庭矿业:未来的投资前景依然可观?
    近年来,“家里有矿”这一说法常被用来形容家境殷实。结合2019年十大热词之一的“区块链”,“矿工”在区块链加密货币生态系统中占据着重要地位。随着技术的不断进步和市场的需求变化,家庭矿业作为一种新兴的投资方式,其未来发展前景依然值得期待。本文将探讨家庭矿业的投资潜力及其面临的挑战与机遇。 ... [详细]
  • 如何在系统设置中找到高级配置选项 ... [详细]
author-avatar
手机用户2502916905
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有