Java基础(六)单列集合—Collection

本文详细介绍了Java中的单列集合—Collection,包括List接口(ArrayList、LinkedList、Vector的特点与操作)、Set接口(HashSet、TreeSet、LinkedHashSet的实现与排序原理)以及常用的迭代器和Collections工具类。重点讨论了集合的扩容、增删查改操作、迭代器的使用、以及集合与数组的转换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单列集合—Collection

在这里插入图片描述

1)List : 存取有序, 有索引, 可以存储重复的

①ArrayList : 底层数组结构 -> 查询快, 修改快;增删慢

  • 优点:查询效率高,因为数组中的元素在内存中是连续的,可以快速的根据下标获取
    集合中的元素
  • 缺点:增删效率低,因为在对数组中元素进行增删操作的时候,涉及移位

②LinkedList : 底层是双向链表结构 -> 增删快;查询慢, 修改慢

  • 优点:增删效率高,因为对链表上的元素进行增删操作的时候,不需要移位,只需
    要改变链表中节点的指向即可
  • 缺点:查询效率低,查询集合中的元素的时候,需要进行全链表的扫描

③Vector:底层数组结构 -> 只不过是线程安全的

2)Set : 存取无序, 无索引, 不可以存储重复的

①HashSet:底层是HashMap

②SortedSet:让集合中的元素具备了排序的能力

  • TreeSet:底层是TreeMap

在这里插入图片描述

3)集合与数组对比

集合特点:

  • 只能存储引用数据类型(基础数据类型——包装类)
  • 长度可变
  • 提供很多操作集合的方法

注意:

  • add()添加基本数据类型时,可以自动装箱:why? how?

    • JVM会自动调用对应包装类的valueOf()方法:返回一个包装类
  • 可以存储null值:how(联想到StringBuild添加null值)?

    • null也是一个Object类

1.Collection[接口]具体方法

1)Collection的“toString()方法”

首先:因为Collection是最顶层的父接口, 内部具备的功能, 所有子类都有一份.

细节1: 如果一个类没有找到我们使用的方法, 可以去父类中查询

问题:Collection 并没有toString()方法,如何实现打印输出的呢?

代码:
    public class Demo1_Collection {
        public static void main(String[] args) {
            // 1. 创建集合对象
            // 在jdk1.7版本之后, 右侧的尖括号中可以不写类型, 菱形泛型
            // <> : 泛型 -> 用于限制集合中存储的数据类型
            Collection<String> c = new ArrayList<>();
            // 2. 向集合中添加元素
            c.add("abc");
            c.add("bbb");
            // 3. 展示集合中的元素
            System.out.println(c.toString()); //谁调用的toString()
        }
    }

那么问题:Collection接口的toString方法从哪来的呢???

原因:接口虽然没有继承Object类,但是底层会存在Object类所有方法的签名(声明),

这样设计的原因在于:避免调用方法时,出现编译错误

2)Collection的成员方法

1)增

add(Object obj);   添加数据
addAll(Collection coll);  将参数中的所有数据添加到指定集合中

2)删

remove(Object obj);  删除数据
removeAll(Collection coll);  将参数集合中的数据从指定集合中移除

3)改:没有提供修改的方法

4)查

contains(12)   是否包含
containsAll(Collection coll)  返回指定集合中是否包含参数集合的全部内容
 
size()         获取数据的个数
isEmpty()      判断是否为空
clear()        清空

5)集合转数组

  • Collection --> Array

    toArray();

  • Array —> Collection

    Arrays.asList(arrays); 基本数据类型数组中数据不能转为集合

2.迭代器:Iterator 接口

Iterator iterator() ; 返回在此 collection 的元素上进行迭代的迭代器。

1.集合遍历:采用Iterator迭代器

  • Iterator iter = coll.iterator(); ——将一个指针指向集合的最上方

  • iter.next(); ——将指针指向下一个数据并返回该数据

    • next() : [每一次调用], 内部的指针都会向后移动一位.**
    • 在循环体中,推荐只调用一次next()方法;
  • iter.hasNext();//判断下一个是否有值

  • iter.remove();//移除迭代器此刻指向的对象

2.问题:Iterator 是个接口,它的实现类的实例化对象从哪来的?

//ArrayList中的源码:
public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
	int cursor;       // index of next element to return 游标
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount; //复制当前集合的版本号,发生修改异常的原因就是版本号的判断

    Itr() {}

    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();
        }
    }
	……
}

iterator()方法返回一个实现Iterator接口的成员内部类对象,

注意:

  • 1)如果集合中存储的类型,是自定义类型,没有重写toString()的话,打印的是自定义类对象的地址值
  • 2)contains()方法底层依赖于对应参数对象的equals()方法
  • 3)remove()方法底层依赖于对应参数对象的equals()方法
  • 4)remove()方法参数不会自动装箱,如果想要根据元素值来删除元素(如Integer类型数据),需要手动装箱
    • 因为:Collection的remove()方法 在 其实现类如ArrayList中会被重写[remove(int) 和 remove(Object)]
    • 使用多态形式调用 Collection 的 remove(int i) 调用的是 对应实现类的方法,并不会自动装箱
  • 5)使用迭代器迭代的同时,不能使用集合自身的remove()方法移除元素,一般会抛出ConcurrentModificationException()异常

建议:

  • 1)今后编写自定实体类(Student……),重写equals和toString方法
  • 2)使用remove方法时判断是否需要手动装箱
  • 3)使用迭代器迭代时删除元素:必须使用 迭代器对象自身的remove()方法;

示例代码:

public static void main(String[] args) {
	// 1. 创建集合对象, 容器中存储的是Person对象
	Collection<Person> c = new ArrayList<>();
	// 2. 向集合中添加Person对象.
	c.add(new Person("张三", 23)); 
	c.add(new Person("李四", 24));
	c.add(new Person("王五", 25));
	
	// 注意: 如果添加的元素是自定义对象的话, 那么没有重写toString, 将打印地址值
	System.out.println(c);
	
	// 3. 调用集合的删除方法, 删除张三
	// 注意 : remove方法底层依赖于equals方法.
	c.remove(new Person("张三", 23));
	System.out.println(c);
	
	// 4. 调用集合中判断是否包含的方法
	// 注意 : contains方法底层依赖于equals方法.
	System.out.println(c.contains(new Person("李四", 24)));
	
	// 5. 调用集合的清空方法
	c.clear();
	// 6. 调用集合判断是否为空的方法
	System.out.println(c.isEmpty());
	// 7. 打印集合的长度
	System.out.println(c.size());
}

3.List接口


List 元素存取有序(添加顺序),有索引, 可以存储重复元素

List接口方法:

1)增

add(Object obj);
addAll(Collection coll);
//增加索引后
add(int index,Object obj);在指定索引位置添加数据(数据的后移)
addAll(int index,Collection coll);

2)删

remove(Object obj);
removeAll(Collection coll);
remove(int index);移除指定索引位置的值
clear();

3)改

list.set(int index, Object obj); 修改

4)查

list.size()获得数据个数
list.contains(56.7)
list.containsAll(null)
list.isEmpty()
get(int index)根据索引值取值

List接口的遍历方式

  • a. Iterator
  • b. 增强for循环
  • c. 普通for循环

ArrayList


ArrayList是一个动态数组,实现了List接口以及list相关的所有方法,它允许所有元素的插入,包括null。另外,ArrayList和Vector除了线程不同步之外,大致相等。

1)构造方法

默认情况下,elementData是一个大小为0的空数组,当我们指定了初始大小的时候,elementData的初始大小就变成了我们所指定的初始大小了。

2)add()方法

在插入元素之前,它会先检查是否需要扩容,然后再把元素添加到数组中最后一个元素的后面。

检查扩容:ensureCapacityInternal()

在ensureCapacityInternal方法中,我们可以看见:

  • 1)当elementData为空数组时,它会使用默认的大小去扩容。所以说,通过无参构造方法来创建ArrayList时,它的大小其实是为0的,只有在使用到的时候,才会通过grow方法去创建一个大小为10的数组。
  • 2)当elementData为指定长度数组时[构造方法初始化时指定],需要扩容时,会通过 grow 方法去创建一个大小为原数组长度 1.5 被的新数组(注意:初始长度为1时,其0.5倍为0,但也会增到2)

3)grow()方法

grow方法是在数组进行扩容的时候用到的:

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);
}

从源码中,ArrayList每次扩容都是扩1.5倍(oldCapacity >> 1),然后调用Arrays类的copyOf方法,把元素重新拷贝到一个新的数组中去。

Vector

Vector很多方法都跟ArrayList一样,只是多加了个synchronized来保证线程安全,所以只把Vector与ArrayList的不同点提一下:

1)Vector比ArrayList多了一个属性:

protected int capacityIncrement;

这个属性是在扩容的时候用到的,它表示每次扩容只扩capacityIncrement个空间就足够了。该属性可以通过构造方法给它赋值。

2)构造方法初始化

从构造方法中可以看出Vector的默认大小也是10,而且它在初始化的时候就已经创建了数组了,这点跟ArrayList不一样。

3)grow方法:

从grow方法中我们可以发现,newCapacity[新容量]默认情况下是两倍的oldCapacity[旧容量],而当指定了capacityIncrement[Vector特有属性]的值之后,newCapacity变成了oldCapacity+capacityIncrement。

总结:

  1. ArrayList创建时的大小为0;当加入第一个元素时,进行第一次扩容时,默认容量大小为10。
  2. ArrayList每次扩容都以当前数组大小的1.5倍去扩容。
  3. Vector创建时的默认大小为10。
  4. Vector每次扩容都以当前数组大小的2倍去扩容。当指定了capacityIncrement之后,每次扩容仅在原先基础上增加capacityIncrement个单位空间。
  5. ArrayList和Vector的add、get、size方法的复杂度都为O(1),remove方法的复杂度为O(n)。
  6. ArrayList是非线程安全的,Vector是线程安全的。
-版本底层线程安全效率
ArrayListJDK1.2数组不安全较高
VectorJDK1.0数组安全较低

LinkedList


LinkedList 是通过一个双向链表来实现的,它允许插入所有元素,包括null,同时,它是线程不同步的。

1.基础知识:双向链表

双向链表每个结点除了数据域之外,还有一个前指针和后指针,分别指向前驱结点和后继结点(如果有前驱/后继的话)。

另外,双向链表还有一个first指针,指向头节点,和last指针,指向尾节点。

2.内部成员

1)属性:

链表的节点个数

transient int size = 0;

指向头节点的指针

transient Node<E> first;

指向尾节点的指针

transient Node<E> last;

2)结点结构:Node

Node 是在 LinkedList 里定义的一个静态内部类

Node 表示链表每个节点的结构,包括一个数据域item,一个后置指针next,一个前置指针prev。

  • E(Object) item;//真正存储当前节点的数据的
  • Node next;//存储下一个节点对象
  • Node prev;//存储上一个节点对象

3)常用方法

4)源码追踪

  1. 在实例化LinkedList对象时,构造器没有做任何事情(为LinkedList属性初始化)

  2. 在第一次添加时,创建一个节点对象(item=值,prev=null,next=null) 赋值给first和last

  3. 在第二次以及后面的每一次添加:

    先将原来的最后一个节点备份
    final Node l = last;

    创建一个新节点,设置新节点的前一个节点为之前的最后一个节点
    final Node newNode = new Node<>(l, e, null);

    将新节点设置为当前的最后一个节点
    last = newNode;

    将原来的最后一个节点的下一个节点设置为当前节点
    l.next = newNode;

5)总结

  1. LinkedList 的底层结构是一个带头/尾指针的双向链表,可以快速的对头/尾节点进行操作。

  2. 相比基于数组的ArrayList,链表结构的LinkedList的特点就是:在指定位置插入和删除元素的效率较高,但是查找指定元素的效率相对较慢

  3. ArrayList和LinkedList的优缺点对比

底层增删效率改查效率
ArrayList数组较低较高★
LinkedList双向链表较高较低

4.Set接口 - 集合


1.Set集合的特点

  • 无序(安插顺序):why
  • 不可重复:why?how?

HashSet


源码分析:

HashSet底层其实是维护了一个HashMap,HashMap底层维护了一个Node类型的数组(Node是一个单向的节点)

添加的原理:

  1. 会根据添加的数据计算出当前数据对应的hash值 —— hashCode()
  2. 根据当前数据的hash值数组(Node[])的长度计算出一个索引值,并作为当前数组下标
//1)Node hash值的计算: hashCode() 
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//2)Node对应的哈希数组索引值的计算:hash值 和 数组(Node[])的长度
tab[i = (n - 1) & hash])
  1. 判断当前下标是否有数据,如果没有,则直接添加;

  2. 如果当前下标有数据:

    • 判断这两个Node的hash值是否一致,如果hash值不一致,再判断当前结构是否是树结构
      如果是树结构就按照树结构的方式添加新数据
      如果不是树结构就按照单向链表的方式去添加(将新数据放在现有数据的next中 或者 进行覆盖)
    • 如果hash值是一样的,则判断内容是否一样((k = p.key) == key || (key != null && key.equals(k))),如果内容也一样则进行覆盖
//判断条件
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))

HashSet的不重复原理:由 HashCode() 和 equals() 来决定的

HashSet的无序的原理:因为添加数据时是通过数据的hash值计算出来的数组索引值,去存储到指定单向链表中;而取值时,是从头开始,
导致HashSet集合是无序的

2.Set接口并没有提供额外的方法(Collection)

3.遍历方式

  • a. Iterator
  • b. foreach

4.实现类

  • HashSet:如何实现的不重复?
  • TreeSet(排列大小有序):如何实现的不重复、如何实现的排序?
  • LinkedHashSet(存取有序) :如何实现的不重复、如何实现的存取有序?

思考:

往HashSet中添加若干个Person对象,我认为name和age是一样的两个人,不能同时添加到HashSet中
		Person per=new Person("张三", 18);
		Person per1=new Person("张三", 18);

TreeSet


1.特性:

  1. TreeSet 会有排序 不可重复
  2. 底层是一个红黑树
  3. 源码分析

TreeSet底层维护了一个TreeMap, 其排序在于 使用 compareTo() 来比较大小

  • 正数this大,负数参数大,如果是0则说明相同

2.问题:HashSet和TreeSet实现不重复的方式是一样的吗?各是什么?

  • HashSet:hashCode() + equals() = 哈希数组 + 链表/红黑树
  • TreeSet : 使用 [元素对象本身的compareTo()方法 或 自己声明的排序器 Comparator ] 进行大小判断 = 红黑树 [数据结构与相应算法]

3.注意事项:

  • TreeSet不能存 null 值:因为比较大小时会抛出空指针异常

  • 自定义的类型,要是没有指定排序方式,将会出现ClassCastException异常,

    • 因为当前的类型不具备可比性。
1)自然排序[内部比较器] --> 元素实现了 Comparable 接口

元素对应的类实现了 Comparable 接口 --> 重写compareTo() 方法

  • 1.添加的数据所属类型实现接口 implements Comparable
  • 2.实现接口中的抽象方法 int compareTo(Object obj)

Comparable接口API描述:

Comparable<T>
类型参数:
T - 可以与此对象进行比较的那些对象的类型

方法摘要:
int compareTo(T o) 
      比较此对象与指定对象的顺序。 
2)比较器排序[定制排序] --> 传入 Comparator接口的实现类的实例化对象

自定义接口实现类 或 使用接口的匿名内部类(JDK8会有优化)–> 采用TreeSet的有参构造方法

  • 在构造器中传入Comparator类型对象
  • 由于Comparator是一个接口,采用创建实现类或者匿名内部类去实现抽象方法int compare(Object o1,Object o2)

Comparator接口API描述:

接口 Comparator<T>
类型参数:
T - 此 Comparator 可以比较的对象类型

int compare(T o1, T o2) 
          比较用来排序的两个参数。 
boolean equals(Object obj) 
          指示某个其他对象是否“等于”此 Comparator。 
3)TreeMap源码
private final Comparator<? super K> comparator;


public TreeMap() {
    comparator = null;
}


public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? 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();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? 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);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

外部排序案例:

TreeSet b = new TreeSet( new Comparator(){
	@Override
	public int compare(Object o1, Object o2) {
		System.out.println("匿名的外部排序执行啦");
		return ((Person)o1).getAge() - ((Person)o2).getAge();
	}
});
b.add(new Person("张三", 18));

思考:往TreeSet中放入若干个Person对象,实现年龄的排序,如果都存在,谁优先级高?

4)Collecions工具类的两个 sore() 排序方法

1.sort(List list)

public static <T extends Comparable<? super T>> void sort(List<T> list)

根据元素的 自然顺序 对指定列表按升序进行排序。

列表中的所有元素都必须实现 Comparable 接口。

此外,列表中的所有元素都必须是可相互比较的。

  • (也就是说,对于列表中的任何 e1 和 e2 元素,e1.compareTo(e2) 不得抛出 ClassCastException)。

2.sort(List list, Comparator<? super T> c)

public static <T> void sort(List<T> list, Comparator<? super T> c)

根据指定比较器产生的顺序对指定列表进行排序。

此列表内的所有元素都必须可使用指定比较器相互比较

  • 也就是说,对于列表中的任意 e1 和 e2 元素,c.compare(e1, e2) 不得抛出 ClassCastException

LinkedHashSet


底层数据结构是?

  • 哈希表 + 单向链表 + 双向链表

怎么实现不重复? --> extends HashSet new 对象时使用的是LinkedHashMap(其底层依旧是 HashMap)

LinkedHashMap: extends HashMap<K,V>

//内部类 Entry 继承自 HashMap.Node : 为双向链表结构多了一个头,一个尾
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

	//指向双向链表的尾部
	transient LinkedHashMap.Entry<K,V> head;

	//指向双向链表的头部
    transient LinkedHashMap.Entry<K,V> tail;

如何实现存取有序? 底层结构实现了 双向链表

5.Collections工具类


Arrays 操作数组的工具类

Collections 操作集合(Collection)

reverse(List):反转 List 中元素的顺序[直接操作原List]
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src[source]中的内容复制到dest[destination 目的地]中(**注意条件**:目标列表的size >= 源列表的size )
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值	

6.问题


ArrayList 如何进行扩容操作的?以及与Vector的区别?

LinkedList 的底层存储结构,以及如何进行增删操作?

HashSet如何实现的不重复(HashMap)?

TreeSet如何实现的不重复、如何实现的排序?

HashMap源码解析:各成员变量的作用,与执行原理,以及如何保证key的唯一性,为何要转为红黑树? 如何转为红黑树? hash值的计算? 如何扩容?扩容之后如何保证hash值计算正确?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值