Java集合类之Collection

为什么会出现集合类?

我们都知道数组的弊端是长度固定。这样一来,数组就不能满足变化的要求。所以,Java就提供了集合供我们使用。

集合特点

  • 集合长度是可变的
  • 只能存储对象(在JDK1.5自动装箱拆箱特性后可以存储基本数据类型)
  • 可以存储多种类型对象(JDK1.5泛型,一般存储是一种)

集合和数组的区别

长度问题:

  • 数组固定
  • 集合可变

存储元素问题:

  • 数组可以是基本数据类型,也可以是引用类型
  • 集合在JDK1.5之前只能是引用类型

同一类型问题:

  • 数组中元素类型必须是一样的
  • 集合中元素可以是不一样的

功能问题:

  • 数组只能对数据进行存取操作。
  • 集合可以对数据进行增删存取操作。

元素数量判断问题:

  • 数组无法判断其中实际存有多少元素,length只告诉了数组的容量;
  • 而集合的size()可以确切知道元素的个数 。

集合体系的由来

集合是存储多个元素的容器,但是,由于数据结构不同,Java就提供了多种集合类。

为什么会出现这么多的容器呢?
因为每种容器对数据的存储方式都有不同,这个存储方式称之为:数据结构。

数据结构:数据存储的方式。

而这多种集合类有共性的功能,所以通过不断的向上抽取,最终形成集合体系结构(集合框架)。

集合类继承关系结构图

Java集合类之Collection_第1张图片
java collection集合体系图

集合类的共性方法

public interface Collection extends Iterable {
    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator iterator();

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection c);

    boolean removeAll(Collection c);

    void clear();

}

集合类的迭代器

理解迭代器

什么是迭代器?集合取出元素的方式(动作)。
由于每个容器的底层数据结构都不一样,它们对数据的存储操作实现也不一样。

集合都有取的操作,而取的操作不足以用一个方法来描述,比如说取之前判断是否有元素,此时就涉及到多个功能。那么此时我们把取出的操作封装成一个对象。此对象封装在集合的内部作为内部类,这样可以方便直接获取元素。而每一个容器的数据结构不同,所以取出的动作细节也不一样,但是都有共性内容 判断和取出,那么可以将写共性抽取形成一个接口Iterator。

那么这些内部类都符合一个规则,该规则是Iterator。如何获取集合的取出对象(iterator的子类对象)呢?
通过一个对外提供的方法iterator()。iterator()作用,获取容器中的内部类对象。

什么是Iterator?
定义了对集合元素操作的接口方法

Iterator主要方法:

public interface Iterator {
    
    boolean hasNext();

    E next();

}

迭代器的使用

代码示例:

public class CollectionDemo {
    public static void main(String[] args) {
        Collection collection = new ArrayList();
        collection.add(1);
        collection.add(2);
        collection.add(1);
        
        /*
         * 迭代器使用方式1
         */
        Iterator iterator = collection.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        
        /*
         * 迭代器使用方式2
         */
        for(Iterator i = collection.iterator();i.hasNext();){
            System.out.println(i.next());
        }
        /*
         * 两种方式特点:
         *  第二种方式 更加节省内存。因为iterator对象在for循环执行完后就会被回收。
         *  第一种 会一直停留在内存当中。
         */
    }
}

List类

List类共性方法

List:
特有方法。凡是可以操作角标的方法都是该体系特有的方法。


add(index,element);
addAll(index,Collection);


remove(index);


set(index,element);

get(index):
subList(from,to);
listIterator();
int indexOf(obj):获取指定元素的位置。
ListIterator listIterator();

ListIterator---列表迭代器

public class ListDemo {
    
    public static void main(String[] args) {
        ArrayList al = new ArrayList();
        al.add("java01");
        al.add("java02");
        al.add("java03");
        
        sop(al);
        
        //在迭代过程中,准备添加或者删除元素
        Iterator it = al.iterator();
        while(it.hasNext()){
            Object obj = it.next();
            
            if(obj.equals("java02")){
                al.add("java08");
                //it.remove();//将"java02"的引用从集合中删除了
            }
            sop(obj);
        }
        
        sop(al);
    }
    
    public static void sop(Object object){
        System.out.println(object);
    }
    
}

以上代码会产生以下异常:


Paste_Image.png

原因分析:
在迭代时,不可以通过集合对象的方法操作集合中的元素
解决方案:
使用List类提供的列表迭代器ListIterator。

引入列表迭代器ListIterator的原因是:
1.避免上方的异常
2.为迭代器添加更多的操作元素的方法,例如在迭代过程中添加元素,修改元素等新功能

总之一句话,ListIterator可以在迭代的过程中,集合中的元素进行增删改查。

代码体现:

public class ListIteratorDemo {
    
    public static void main(String[] args) {
        ArrayList al = new ArrayList();
        al.add("java01");
        al.add("java02");
        al.add("java03");
        
        sop(al);
        
        
        ListIterator it = al.listIterator();
        while(it.hasNext()){
            Object obj = it.next();
            
            if(obj.equals("java02")){
                it.add("java08");
            }
        }
        sop(al);

        //反向迭代
        while(it.hasPrevious()){
            Object obj = it.previous();
            if(obj.equals("java03")){
                sop(obj);
                it.set("javaee");
            }
        }
        sop(al);
    }
    public static void sop(Object object){
        System.out.println(object);
    }
}

List的子类介绍

List:元素是有序的,可以重复。

  • Vector:
    • 底层是数组数据结构。
    • 线程同步。
    • 因为效率低,被ArrayList替代了。
    • 它还可以通过枚举来取出元素。
    LinkedList在内存中是以链表形式组织的,链表中的数据在内存中是松散的,每一个节点都有一个指针指向下一个节点,这样查找起来就比较慢了。而插入删除的时候就是断开一个节点,然后插入删除之后再接起来。

ArrayList类

  • ArrayList:
    • 底层的数据结构使用的是动态数组结构;
    • 特点:查询速度很快,但是增删稍慢;
    • 线程不同步。

可变长度数组(动态数组)其实就是当数组不够时,创建一个新的长度更长的数组,然后把旧的数组内容复制到新的数组中。

  • 数组的特点是其中的元素在内存中的地址是连续的,所以ArrayList的优点在于get、set的效率高,时间复杂度为常数。
  • ArrayList中插入和删除效率较低,由于每插入/删除一项,都需要移动后续所有项的位置,时间复杂度为O(N)。

List集合判断元素是否相同,依据是元素的equals方法。

contains、indexOf等方法在执行其核心逻辑时,都要对集合中元素进行判断是否有此元素,判断的原理都是equals()。
如果不重写equals方法,则默认比较地址,这会导致contains将永远返回false,indexOf、将永远返回-1。所以,我们需要重写equals方法来自己判断,例如可以通过比较对象中一个特征字段的值来比较两个对象是否相等。

LinkedList类

LinkedList特有方法

addFirst();
addLast();

getFirst();
getLast();
获取元素,但不删除元素。如果集合中没有元素,会出现NoSuchElementException

removeFirst();
removeLast();
获取元素,但是元素被删除。如果集合中没有元素,会出现NoSuchElementException

在JDK1.6出现了替代方法。

offerFirst();
offerLast();

peekFirst();
peekLast();
获取元素,但不删除元素。如果集合中没有元素,会返回null。

pollFirst();
pollLast();
获取元素,但是元素被删除。如果集合中没有元素,会返回null。

  • LinkedList
    • 底层使用的双向链表结构。
    • 特点:增删速度很快,查询稍慢。
    • 线程不同步。

双向链表的特点,元素(结点)之间的地址不连续,通过引用找到当前结点的上一个结点和下一个结点,即插入和删除效率较高,只需要常数时间,而get和set则较为低效,需要O(n)的时间。

LinkedList与ArrayList比较

LinkedList的方法和使用和ArrayList大致相同,由于LinkedList是链表实现的,所以额外提供了在头部和尾部添加/删除元素的方法,并且没有ArrayList扩容的问题了。另外,ArrayList和LinkedList都可以实现栈、队列等数据结构,但LinkedList本身实现了队列的接口,所以更推荐用LinkedList来实现队列和栈。

一种数据结构可能会基于另一种数据结构实现,比如说队列基于链表。

Vector类

通过以下代码介绍

/*
枚举就是Vector特有的取出方式。
发现枚举和迭代器很像。
其实枚举和迭代是一样的。

因为枚举的名称以及方法的名称都过长。
所以被迭代器取代了。
枚举郁郁而终了。

*/
class VectorDemo 
{
    public static void main(String[] args) 
    {
        Vector v = new Vector();

        v.add("java01");
        v.add("java02");
        v.add("java03");
        v.add("java04");

        Enumeration en = v.elements();

        while(en.hasMoreElements())
        {
            System.out.println(en.nextElement());
        }
    }
}

Vector类与ArrayList类比较

  • Vector和ArrayList的底层数据结构都是数组,在使用上相似。
  • Vector的方法都是同步的(Synchronized),是线程安全的,而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。(建议使用ArrayList,因为高效,就算是多线程可以自己加锁)
  • 当容器空间不足时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样,ArrayList就有利于节约内存空间。

总结

在不同的应用场景下,选择合适的集合。比如说在需要频繁读取集合中的元素时,使用ArrayList效率较高,而在插入和删除操作较多时,使用LinkedList效率较高。

Set类

Set体系的类特点:

  • 不保证放入元素的顺序(存入和取出的顺序不一定一致)
  • 元素不可以重复。

HashSet类

HashSet底层数据结构是哈希表,线程不同步。

下面是HashSet的构造函数以及主要方法的代码:

public HashSet() {
        map = new HashMap<>();
}

public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
}

public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
 }

private transient HashMap map;

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}

public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

public int size() {
    return map.size();
}

从以上代码可以知道:

  • HashSet基于HashMap实现,底层使用HashMap保存数据。
  • 其实HashSet就是操作了HashMap的key,并且同时具有Set接口的特点。

HashSet是如何保证元素唯一性的呢?
是通过两个方法,hashCode和equals来完成。
如果元素hashCode值相同,才会判断equals是否为true。
如果元素的hashCode不同,不会调用equals方法。

注意:对于判断元素是否存在以及删除等操作,依赖的方法是元素的hashCode和equals方法。

/**
 * 往HashSet集合中存入自定对象 姓名和年龄相同为同一个人,重复元素。
 * 
 */
public class HashSetDemo {

    public static void print(Object obj) {
        System.out.println(obj);
    }

    public static void main(String[] args) {
        HashSet hashSet = new HashSet<>();

        hashSet.add(new Person("a1", 11));
        hashSet.add(new Person("a2", 12));
        hashSet.add(new Person("a3", 13));
        hashSet.add(new Person("a2", 12));
        // hashSet.add(new Person("a4",14));

        // print("a1:"+hashSet.contains(new Person("a2",12)));

        // hashSet.remove(new Person("a4",13));

        Iterator it = hashSet.iterator();

        while (it.hasNext()) {
            Person p = (Person) it.next();
            print(p.getName() + "::" + p.getAge());
        }
    }

}

class Person {
    private String name;
    private int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int hashCode() {
        System.out.println(this.name + "....hashCode");
        return name.hashCode() + age * 37;
    }

    public boolean equals(Object obj) {

        if (!(obj instanceof Person))
            return false;

        Person p = (Person) obj;
        System.out.println(this.name + "...equals.." + p.name);

        return this.name.equals(p.name) && this.age == p.age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

LinkedHashSet

LinkedHashSet是HashSet的子类,它和HashSet的区别就在于LinkedHashSet的元素严格按照放入顺序排列。LinkedHashSet内部使用LinkedHashMap实现,所以它和HashSet的关系就相当于HashMap和LinkedHashMap的关系。如果你想让取出元素的顺序和插入元素的顺序完全相同,那么就使用LinkedHashSet代替HashSet。

TreeSet

上面提到HashSet是用HashMap实现的,其实这里的TreeSet也是用Map接口的另一个实现类TreeMap实现的。TreeMap是一个有序的二叉树。TreeSet实现了SortedSet接口,其特点是会对放入其中的元素进行排序,和LinkedHashSet不同的是,LinkedHashSet是根据元素的放入顺序进行排序,而TreeSet是根据元素的自然顺序进行排序。

保证元素唯一性的依据:
compareTo方法return 0.

Java集合类之Collection_第2张图片
二叉树原理图

TreeSet排序的第一种方式:让元素自身具备比较性。
元素需要实现Comparable接口,覆盖compareTo方法。
这种方式也称为元素的自然顺序,或者叫做默认顺序。

TreeSet能对Integer和String类型的数据进行排序,是因为Integer和String都实现Comparable接口并实现了compareTo方法。

TreeSet的第二种排序方式。
当元素自身不具备比较性时,或者具备的比较性不是所需要的。
这时就需要让集合自身具备比较性。
在集合初始化时,就有了比较方式。

实际开发中,TreeSet的使用频率较低,是因为TreeSet每插入一个数据,就会排一次序,导致性能降低,而一般我们都是放入一堆数据后再一起排序,所以用不到TreeSet。但如果刚好碰到需要进行插入后即时排序的需求,那这时候就可以用上TreeSet了。

总结

在Set接口的实现类中,HashSet是一种元素不重复且无序的存储容器,可以存储一个null元素,放入HashSet的对象需要重写equals和hashCode方法以保证存入对象唯一;LinkedHashSet是HashSet的子类,具有HashSet的性质,且它保存了元素放入的顺序;TreeSet可以将存入的元素按照一定的规则排序,但是对象和集合其中一个必须要比较性,TreeSet中不能存在null元素。

你可能感兴趣的:(Java集合类之Collection)