Java集合框架之Collection

集合

集合框架概述

  1. 集合、数组都是对多个数据进行存储的结构,简称Java容器。此时的存储指的是内存层面的,不涉及到持久化的存储
  2. 数组在存储多个数据方面的特点:
    • 一旦初始化,长度确定
    • 需要指明数组的元素类型,只能操作指定类型的数据:String[] arr;
  3. 数组在存储多个数据方面的缺点:
    • 初始化后长度不可修改
    • 数组中提供的方法有限,对于添加、删除、插入数据等操作非常不便,效率不高
    • 获取数组中实际元素个数的需求,数组没有现成的属性或方法可用
    • 数组有序、可重复。对于无序、不可重复的需求不能满足。

Java集合可分为Collection和Map两种体系

  • Collection接口:单列数据,定义了存取一组对象的方法的集合
    • List:元素有序、可重复的集合(“动态”数组)
      • ArrayList、LinkedList、Vector
    • Set:元素无序、不可重复的集合
      • HashSet、LinkedHashSet、TreeSet
  • Map接口:双列数据,保存具有映射关系“key-value对”的集合
    • HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

Collection接口中的方法的使用

contains(Object obj)

containsAll(Collection coll)同上

实现的机制:当前元素与ArrayList中的每个元素逐一对比内容(注意不是对比地址,调用的是equals()方法),找到返回true,未找到返回false。

因为是调用equals()方法,因此向Collections()接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()方法。

public class CollectionTest {
    @Test
    public void test1()
    {
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(123123);
        coll.add(new String("asd"));
        coll.add(new Date());
        System.out.println(coll.contains(new String("asd")));//true
    }

remove(Object obj)`

返回值:成功为true,失败为false,也是按内容查找,因此也会调用equals()方法

removeAll(Collection coll1)

从当前集合中移除coll1中所有的元素,直接在当前集合修改

@Test
public void test2()
{
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(123123);
    coll.add(new String("asd"));
    coll.add(new Date());

    Collection coll1 = new ArrayList();
    coll1.add(123123);
    coll1.add(new String("asd"));

    coll.removeAll(coll1);
    System.out.println(coll);
    //[123, Thu Aug 27 15:15:57 CST 2020]
}

retainAll(Collection coll1)

求两个集合交集,与removeAll()类似,不再赘述

equals(Object obj)

判断当前集合和形参是否相等,关于是否考虑顺序,需要看obj的类型,如果是ArrayList则需要顺序相同,如果是Set则不需要。

hashCode()

返回当前对象的哈希值,哈希值根据内容确定。

toArray()

集合→数组

Object[]arr = coll.toArray();
for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}

Arrays.asList()

数组→集合,Arrays类的静态方法

List<Object> list = Arrays.asList(arr);
System.out.println(list);

迭代器

  • 集合元素的遍历,使用Iterator接口,Iterator称为迭代器

  • Collection接口继承了java.lang.Iterator接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象

  • 集合对象每次调用iterator()方法都会创建一个新的迭代器,默认游标在第一个元素之前

调用迭代器一定要先hasNext()next(),不然容易抛异常

@Test
public void test2()
{
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(123123);
    coll.add(new String("asd"));
    coll.add(new Date());

    Iterator iter = coll.iterator();
    while(iter.hasNext())
    {
        System.out.println(iter.next());
    }
}

每次调用next()方法执行两个动作:

  1. 指针下移
  2. 将下移以后集合位置上的元素返回

两种遍历集合的错误写法

错误方式一:

Iterator iter = coll.iterator();
while(iter.next()!=null)
{
    System.out.println(iter.next());
}

会跳着输出,while判断时已经往后移动了一个位置。

错误方式二:

Iterator iter = coll.iterator();
while(coll.iterator().next()!=null)
{
    System.out.println(coll.iterator().next());
}

每次调用iterator()都创造新的迭代器对象,因此每次都输出第一个元素。

remove()方法

内部定义了remove(),可以在遍历的时候删除对应位置的元素。注意与Collection中的remove()方法区别

while(iter.hasNext())
{
    Object obj = iter.next();
    if("asd".equals(obj))
    {
        iter.remove();
    }
}

注意:在第一次next()或者已经使用过remove()之后使用remove()会报错

foreach循环遍历集合元素

Java 5.0提供了foreach循环迭代访问集合、数组

@Test
public void test3()
{
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(123123);
    coll.add(new String("asd"));
    coll.add(new Date());

    //for(集合对象:集合)
    for (Object item:coll) {
        System.out.println(item);
    }
}

实际上内部调用的还是迭代器,用hasNext()和next()写成。

练习题

@Test
public void test3()
{
    String[] arr = new String[]{"MM","MM"};

//        for (int i = 0; i < arr.length; i++) {
//            arr[i]="GG";
//        }//"GG"

    for (String s:arr) {
        s="GG";
    }"MM"

    for (int i = 0; i < arr.length; i++) {
        System.out.println(arr[i]);
    }
}

foreach中相当于每次用迭代器获取了一个新的对象,类似于

Iterator iter = arr.iterator();
while(iter.hasNext())
{
    String s = iter.next()
    s = "GG";
}

这样看来很明显arr内的值并没有受到影响。

List接口

List:元素有序、可重复的集合(“动态”数组)

  • ArrayList、LinkedList、Vector

面试题:ArrayList、LinkedList、Vector三者的异同

:三个类都实现了List接口,存储数据的特点相同:有序、可重复。

  • ArrayList:List接口的主要实现类,线程不安全,效率高;底层使用Object[]存储
  • LinkedList:对于频繁拆入、删除操作,使用链表效率比ArrayList高;底层使用双向链表存储
  • Vector:作为List的古老实现类,线程安全,效率低;底层使用Object[]存储

ArrayList源码分析

jdk8.0中的版本

transient Object[] elementData;

public ArrayList() {
   this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 一开始是空的,默认为0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

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

private void ensureCapacityInternal(int minCapacity) {
 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
   modCount++;

   // overflow-conscious code
   if (minCapacity - elementData.length > 0)
       grow(minCapacity);
}

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时不会指定底层数组大小,第一次调用add时才创建好底层数组。第一次add时,length为0,minCapacity为1,进入grow()方法。扩容都是由grow()方法实现,每次扩容扩为1.5倍,若仍不够则直接以minCapacity为新的容量,若新的容量超过了ArrayList的最大容量,则进行处理。扩容后需要将原有数组中的数据复制到新数组。

结论:建议开发中使用构造器:ArrayList list = new ArrayList(int capacity),这样对于比较大的数组不会经常扩容,提高程序效率。

与JDK7.0的区别:JDK7.0中默认构造函数会将底层数组大小初始化为10。

LinkedList源码分析

//LinkedList的内部类Node
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;
    }
}
//两个成员属性
transient Node<E> first;
transient Node<E> last;

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

内部声明了Node类型的first和last属性,默认值为null

小面试题

@Test
public void test2()
{
    ArrayList coll = new ArrayList();
    coll.add(123);
    coll.add(123123);
    coll.add(new String("asd"));
    coll.add(new Date());

    updateList(coll);
    System.out.println(coll);
    //[asd, Fri Aug 28 14:53:26 CST 2020]
}

public void updateList(List list)
{
    list.remove(1);
    list.remove(new Integer(123));
}

在这里插入图片描述

注意区分List中remove(int index)remove(Object obj)

Set接口

Set:元素无序、不可重复的集合

  • HashSet、LinkedHashSet、TreeSet

面试题:ArrayList、LinkedList、Vector三者的异同

:三个类都实现了List接口,存储数据的特点相同:有序、可重复。

  • HashSet:Set接口的主要实现类,线程不安全,可以存储null
  • LinkedList:HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
  • TreeSet:可以按照添加对象指定属性进行排序

理解无序性和不可重复

以HashSet为例说明。

  1. 无序性

    不等于随机性 。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的Hash值决定。

  2. 不可重复性

    保证添加的元素按照equals()判断时,不能返回true。即:相同的元素只能添加一个。注: 自定义类要重写equals()函数。

添加元素过程

以HashSet为例说明。

HashSet底层存储机制采用数组+链表形式,数组的每一个位置可以存放一个链表。

向HashSet中添加元素a,首先调用a所在类的hashCode()方法,计算元素a的哈希值,此哈希值通过散列函数得到在底层数组中的存放位置,判断数组此位置上是否已经有元素:

  • 若没有其他元素,则a添加成功 →情况一
  • 若有元素b或以链表形式存在的多个元素,则比较元素a与元素b的hash值:
    • 如果hash值不同,则a添加成功**(hash不同则内容认为一定不同)** →情况二
    • 如果相同,则调用a所在类的equals()方法**(hash值相同不代表内容相同)**
      • 返回true则添加失败
      • 返回false,则a与b还是不一样的,添加成功 →情况三

对于情况二和情况三,元素a与已经存在指定索引位置上数据以链表的方式存储。

  • jdk 7:元素a放到数组中,作为链表的头
  • jdk 8:原来的元素放在数组中,a放到链表最后

底层数组初始容量是16,当使用率超过0.75(12个位置有数据)则扩容为2倍。
在这里插入图片描述

重写equals()hashCode()

在HashSet中添加自定义类的时候,不能忘记在自定义类中重写equals()hashCode()方法。这两个方法都是用于HashSet.add()中的添加成功的判定的,如果不进行重写,会调用Object类中的对应方法,比如Object.hashCode()采用了类似随机数的方式:

//未重写hashCode()
@Test
public void test2()
{
    HashSet coll = new HashSet();
    coll.add(123);
    coll.add(123123);
    coll.add(new User("Bill",10));
    coll.add(new User("Bill",10));

    System.out.println(coll);
    //[123123, User{name='Bill', age=10}, User{name='Bill', age=10}, 123]
}

随机数产生的两个hashCode自然是不相等的,因此调用add()方法时将其视作两个User对象。

注:重写时一定要保证相等的对象具有相等的散列码

不用自己写,Alt+Enter重载即可

LinkedHashSet的使用

在这里插入图片描述

LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个和后一个数据。

目的:对于频繁的遍历操作,LinkedHashSet的效率要高于HashSet

TreeSet

这部分了解即可,不需要花太多时间。

TreeSet是用于排序的,向TreeSet中添加的数据,要求是相同类的对象。不能添加不同类的对象。

两种排序方式:自然排序(Comparable)定制排序(Comparator)

自定义类要实现Comparable接口,先比较User类的姓名:

@Override
public int compareTo(Object o) {
    if(o instanceof User){
        User user = (User) o;
        return this.name.compareTo(user.name);
    }else{
        throw new RuntimeException("输入的类型不匹配");
    }
}

@Test
public void test1()
{
    TreeSet set = new TreeSet();
    set.add(new User("Bill",13));
    set.add(new User("Bill",134));
    set.add(new User("Adam",123));
    set.add(new User("Zick",31));

    Iterator it = set.iterator();
    while(it.hasNext())
        System.out.println(it.next());
}

在这里插入图片描述

从结果发现,输出确实是按照名字排序,但是发现少了个Bill,明明两个对象使用equals()并不相等,这是为什么呢?

**在TreeSet的自然排序中中,判断两个对象相等不是调用equals()方法,而是调用compareTo()方法!**所以两个名字为Bill的对象被视为相同。

实现二级排序:

@Override
public int compareTo(Object o) {
    if(o instanceof User){
        User user = (User) o;
        int result = this.name.compareTo(user.name);
        if (result!=0)
            return Integer.compare(this.age, user.age);
        else
            return result;
    }else{
        throw new RuntimeException("输入的类型不匹配");
    }
}

在这里插入图片描述

结果正常!

定制排序:比较两个对象是否相同的标准是compare()返回0,而不是equals()

@Test
    public void test2()
    {
        Comparator com = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                // 按照年龄从小到大排序
                if(o1 instanceof User && o2 instanceof User)
                {
                    User user1 = (User)o1;
                    User user2 = (User)o2;
                    return Integer.compare(user1.getAge(), user2.getAge());
                }else{
                    throw new RuntimeException("数据类型不匹配");
                }
            }
        };
        TreeSet set = new TreeSet(com);
        set.add(new User("Bill",13));
        set.add(new User("Bill",134));
        set.add(new User("Adam",123));
        set.add(new User("Zick",31));

        Iterator it= set.iterator();
        while(it.hasNext())
            System.out.println(it.next());
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值