Java集合框架

集合框架

本文根据 传智播客刘意Java基础班精华版以及自己的理解总结而来
此外还参考了http://www.cnblogs.com/xiohao/p/4309462.html

这里写图片描述

图片来自http://www.cnblogs.com/Qian123/archive/2016/07/19/5685155.html

上图是java中集合框架的继承表示,蓝色边框是接口,红色边框是实现类。

集合的由来

对象数组:数组既可以存储基本数据类型,又可以存储引用数据类型。存储引用数据类型的数组称作对象数组。数组是一种容器,可以存储对象,但是长度不可变,所以需要其他的长度可变或者更加灵活的容器来存储对象,这就有了集合(Collections,Map)。

我们学习的是面型对象语言,而面向对象对事物的描述是通过对象体现的,为了方便对多个对象进行管理操作,我们必须对多个对象进行存储,而要存储多个对象,不能是一个基本变量,而应该是容器类型变量。在我们所学的知识里,只有数组和StringBuffer是容器。

StringBuffer存储的是字符串,不一定能满足所有要求,有时存储的是一个构造数据类型

数组可以存放构造数据类型,但是长度不可变

这个时候,为了适应变化的需求,Java提供了集合

数组和集合的区别

  1. 长度区别:数组长度不可变,集合长度可变
  2. 存储类型:数组既可以存储基本数据类型,有可以存储构造数据类型。集合只能存储构造数据类型,如果要存储基本数据类型,需要进行封装
  3. 内容:数组存储的是同一种类型,集合可以存储不同类型的元素(通常情况下,利用泛型限制只能存储一种数据类型,而数组不支持泛型)

集合(Collections)可以存储多个数据,但是,存储的多个元素也是有不同需求的:比如所有元素是否相同,是否按照各自的顺序。针对不同的需求,Java提供了不同的集合类。不同的集合类实现的底层数据结构不同。但是这些集合类最终的目的是存储和操作,将共性的东西提取出来,称为接口(只有抽象方法,即每个方法没有方法体),得到一个集合的继承体系结构图,即上图。

Collection

public interface Collection<E> extends Iterable<E>

Collection是集合框架的顶级接口,主要功能如下:

函数作用
booleanadd(E e) 确保此 collection 包含指定的元素(可选操作)。
booleanaddAll(Collection<? extends E> c) 将指定 collection 中的所有元素都添加到此 collection 中(可选操作)。
voidclear() 移除此 collection 中的所有元素(可选操作)。
booleancontains(Object o) 如果此 collection 包含指定的元素,则返回 true
booleancontainsAll(Collection<?> c) 如果此 collection 包含指定 collection 中的所有元素,则返回 true
booleanequals(Object o) 比较此 collection 与指定对象是否相等。
inthashCode() 返回此 collection 的哈希码值。
Iterator<E>iterator() 返回在此 collection 的元素上进行迭代的迭代器。
booleanisEmpty() 如果此 collection 不包含元素,则返回 true
booleanremove(Object o) 从此 collection 中移除指定元素的单个实例,如果存在的话(可选操作)。
booleanremoveAll(Collection<?> c) 移除此 collection 中那些也包含在指定 collection 中的所有元素(可选操作)。
booleanretainAll(Collection<?> c) 仅保留此 collection 中那些也包含在指定 collection 的元素(可选操作)。
intsize() 返回此 collection 中的元素数。
Object[]toArray() 返回包含此 collection 中所有元素的数组。
<T> T[]toArray(T[] a) 返回包含此 collection 中所有元素的数组;返回数组的运行时类型与指定数组的运行时类型相同。

1. 几个特殊的函数:equals、hasCode,iterator,toArray
2. 数组、字符串和集合的长度如何表示:数组length,字符串length(),集合size()
3. add方法返回的是boolean类型,返回true,所以add永远为真
4. 集合可以直接打印,子类一般重写toString方法
5. clear和removeAll的区别:c1.clear(),移除c1中所有元素;c1.removeAll(c2),从c1中移除c1和c2共同的元素,只要移除一个就返回true
6. retainAll:假设有两个集合A,B,如果有交集,保存在A中,如果A中元素改变,则返回true,否则返回false
7. 集合的使用步骤:1、创建集合对象;2、创建元素对象;3、将元素加入集合中;4、遍历集合各种操作

List

List是Collection的子类,也是一个接口。List是有序(存入顺序和取出顺序一样)的 collection。此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。与 Set 不同,列表通常允许重复的元素。更确切地讲,列表通常允许满足 e1.equals(e2) 的元素对 e1e2,并且如果列表本身允许 null 元素的话,通常它们允许多个 null 元素。

List的特有功能:

booleanadd(E e) 向列表的尾部添加指定的元素(可选操作)。
voidadd(int index, E element) 在列表的指定位置插入指定元素(可选操作)。
booleanaddAll(int index, Collection<? extends E> c) 将指定 collection 中的所有元素都插入到列表中的指定位置(可选操作)。
Eget(int index) 返回列表中指定位置的元素。
intindexOf(Object o) 返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。
intlastIndexOf(Object o) 返回此列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1。
ListIterator<E>listIterator() 返回此列表元素的列表迭代器(按适当顺序)。
ListIterator<E>listIterator(int index) 返回列表中元素的列表迭代器(按适当顺序),从列表的指定位置开始。
Eremove(int index) 移除列表中指定位置的元素(可选操作)。
Eset(int index, E element) 用指定元素替换列表中指定位置的元素(可选操作)。
List<E>subList(int fromIndex, int toIndex) 返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之间的部分视图。

List除了用iterator遍历,还有其他特有的遍历方法:

  1. 用size()和get()通过for循环遍历

    for(int i=0;i<list.size();i++){
         System.out.println(list.get(i));
    }
  2. listIterator():List特有的迭代器,列表迭代器,继承了iterator接口,可以直接使用hasNext和next方法。

    下面是listIterator特有的方法:

void**add**(E e) 将指定的元素插入列表(可选操作)。
boolean**hasPrevious**() 如果以逆向遍历列表,列表迭代器有多个元素,则返回 true
int**nextIndex**() 返回对 next 的后续调用所返回元素的索引。
E**previous**() 返回列表中的前一个元素。
int**previousIndex**() 返回对 previous 的后续调用所返回元素的索引。
void**set**(E e) 用指定元素替换 nextprevious 返回的最后一个元素(可选操作)。
  • 可以逆向遍历,但是要先正向遍历,所以无意义,基本不使用。用hasPrevious和previous实现

  • 并发修改异常:对集合用遍历器遍历的过程中,如果对集合修改的话 ,会出现异常。原因是迭代器是依赖于集合的,在迭代的过程中集合发生的改变,迭代器却不知道,所以出现异常。

    Iterator it=list.iterator();
    while(it.hasNext()){
        if(...){
           c.add(...);
         }
    }

    解决方法:

    1、利用迭代器修改元素,Iterator只有remove方法,用listIterator,有add,set方法

    listIterator lit=list.listIterator();
    while(lit.hasNext()){
        String s=lit.next();
        if(s.equals("hello")){
          lit.add("javaEE"); //将JavaEES加到了hello的后面
        }
    }

    2、不用迭代器迭代集合,可以用for循环

    for(int i=0;i<list.size();i++){
    String s=(String)list.get(i);
    if(s.equals("world")){
      list.add("javaEE");  //将javaEE加到了最后
    }
    }

List有三个子类:ArrayList,LinkedList和Vector

ArrayList:底层数据结构是数组,查询快,增删慢。线程不安全,效率高。

Vector:底层数据结构是数组,查询快,增删慢。线程安全,效率低。

LinkedList:底层数据结构是链表,查询慢,增删快。线程不安全,效率高。

ArrayList

ArrayList的底层实现是数组,因此随机访问是O(1),但插入元素的时间复杂度是O(n)。和数组比较,长度可变,且允许null值。

和List相比,增加了以下特有的方法:

Objectclone() 返回此 ArrayList 实例的浅表副本。
voidensureCapacity(int minCapacity) 如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。
protected voidremoveRange(int fromIndex, int toIndex) 移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。
voidtrimToSize() 将此 ArrayList 实例的容量调整为列表的当前大小。

对于ArrayList而言,和List的操作基本一致,有以下几点需要注意:

  1. 在初始化ArrayList没有指定长度的话,默认是10

  2. ArrayList在增加新元素的时候如果超过了原始的容量的话,ArrayList扩容ensureCapacity的方案为原始容量3/2

    int newCapacity = oldCapacity + (oldCapacity >> 1);
  3. ArrayList是线程不安全的,在多线程的情况下不要使用。如果一定要在多线程使用List,可以使用Vector,因为Vector和ArrayList基本一致,区别在于Vector中的绝大部分方法都 使用了同步关键字修饰,这样在多线程的情况下不会出现并发错误。还有就是它们的扩容方案不同,ArrayList是通过原始容量*3/2,而Vector是允许设置默认的增长长度,Vector的默认扩容方式为原来的2倍。切记Vector是ArrayList的多线程的一个替代品。

  4. 三种遍历方式

Vector

Vector相当于ArrayList多线程的一个替代品。

特有方法:

void**addElement**(E obj) 将指定的组件添加到此向量的末尾,将其大小增加 1。,相当于add
E**elementAt**(int index) 返回指定索引处的组件。相当于get
Enumeration<E>**elements**() 返回此向量的组件的枚举。相当于iterator
Stack
public class Stack<E> extends Vector<E>{}

Stack继承自Vector,也是线程安全的。Stack 类表示后进先出(LIFO)的对象堆栈。它通过五个操作对类 Vector 进行了扩展 ,允许将向量视为堆栈。它提供了通常的 pushpop 操作,以及取堆栈顶点的 peek 方法、测试堆栈是否为空的 empty 方法、在堆栈中查找项并确定到堆栈顶距离的 search 方法。

也就是所,Stack是利用了Vector实现了关于栈的方法,是基于数组实现的。这里是质疑Stack的地方,因为栈的实现如果基于链表效率更高,既然只是为了实现栈,为什么不用链表来单独实现,只是为了复用简单的方法而迫使它继承Vector(Stack和Vector本来是毫无关系的)。这使得Stack在基于数组实现上效率受影响,另外因为继承Vector类,Stack可以复用Vector大量方法,这使得Stack在设计上不严谨。

通常如果要使用栈的话,用LinkedList实现。

LinkedList
public class LinkedList<E>extends implements List<E>, Deque<E>

LinkedList既实现了List接口,有实现了Deque接口(之后讲解)。

LinkedList的底层实现是双向循环链表,增删速度快,查询速度慢,并且允许null元素。除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾的 getremoveinsert 方法提供了统一的命名方法,如offer(),poll()。这些操作允许将LinkedList用作堆栈、队列或双端队列。

LinkedList的方法(和List相比增加的)

返回值方法 作用
void**addFirst**(E e) 将指定元素插入此列表的开头。
void**addLast**(E e) 将指定元素添加到此列表的结尾。
Object**clone**() 返回此 LinkedList 的浅表副本。
Iterator<E>**descendingIterator**() 返回以逆向顺序在此双端队列的元素上进行迭代的迭代器。
E**element**() 获取但不移除此列表的头(第一个元素)。
E**getFirst**() 返回此列表的第一个元素。
E**getLast**() 返回此列表的最后一个元素。
int**lastIndexOf**(Object o) 返回此列表中最后出现指定元素的索引,如果此列表中不包含该元素,则返回 -1。
boolean**offer**(E e) 将指定元素添加到此列表的末尾(最后一个元素)。
boolean**offerFirst**(E e) 在此列表的开头插入指定的元素。
boolean**offerLast**(E e) 在此列表末尾插入指定的元素。
E**peek**() 获取但不移除此列表的头(第一个元素)。
E**peekFirst**() 获取但不移除此列表的第一个元素;如果此列表为空,则返回 null
E**peekLast**() 获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null
E**poll**() 获取并移除此列表的头(第一个元素)
E**pollFirst**() 获取并移除此列表的第一个元素;如果此列表为空,则返回 null
E**pollLast**() 获取并移除此列表的最后一个元素;如果此列表为空,则返回 null
E**pop**() 从此列表所表示的堆栈处弹出一个元素。
void**push**(E e) 将元素推入此列表所表示的堆栈。
E**remove**() 获取并移除此列表的头(第一个元素)。
E**removeFirst**() 移除并返回此列表的第一个元素。
boolean**removeFirstOccurrence**(Object o) 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。
E**removeLast**() 移除并返回此列表的最后一个元素。
boolean**removeLastOccurrence**(Object o) 从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。

以上方法可以分为5部分:

  1. 添加:addFirst、addLast
  2. 获取:getFirst、getLast
  3. 删除:remove、removeFirst、removeLast
  4. 栈:push、pop(popFirst、popLasr)、peek(peekFirst、peekLast)
  5. 队列:offer(offerFirst、offerLast)、pop(popFirst、popLasr)、peek(peekFirst、peekLast)

使用LinkedList需要注意一下几点:

  1. ArrayList和LinkedList的区别:区别就是线性表中顺序表和链表的区别
  2. LinkedList是基于双向循环链表的,其依赖于一个内部类Entry实现类似指针的功能
  3. LinkedList也是非线程安全的。
  4. LinkedList可以当做堆栈和队列来使用,比Stack使用更方便。为什么?看Stack节
  5. 遍历方式
  6. contains方法的底层方法是equals方法,对于构造类型来说,如果不重写equals方法的话,默认使用父类Object中的equals方法,比较地址是否相同。这里主要是equals的问题。

Queue

public interface Queue<E>extends Collection<E>

是Collection的直接子接口,主要实现的是队列的功能。队列是先进先出的数据结构,只能在对头和队尾进行处理。Queue 实现通常不允许插入 null 元素。所以只有下列的方法

1抛出异常返回特殊值
插入add(e)offer(e)
移除remove()poll()
检查element()peek()
PriorityQueue
public class PriorityQueue<E>extends AbstractQueue<E>implements Serializable

PriorityQueue是Queue的一个子类,底层数据结构是二叉堆,是线性结构,但是逻辑结构是非线性的,能够实现队列中按照一定规则有序。PriorityQueue是优先队列,和TreeSet一样,根据自然顺序进行排序或者根据构造的比较器进行排序。如何实现自然顺序排序和构造比较器具体见TreeSet。

Deque
public interface Deque<E>extends Queue<E>

Deque是一个接口,实现该接口的类是队列的一种实现,底层实现是双向循环链表,是一个线性结构。支持在两端插入和移除元素。下表总结了相关的12 种方法:

1第一个元素(头部)最后一个元素(尾部)
抛出异常特殊值抛出异常特殊值
插入addFirst(e)offerFirst(e)addLast(e)offerLast(e)
移除removeFirst()pollFirst()removeLast()pollLast()
检查getFirst()peekFirst()getLast()peekLast()

Deque是双向循环链表,上述12种方法可以实现栈和队列,所以实现Deque的类可以用作栈和队列,比如LinkedList,同时也是List的子类。

Set

public interface Set extends Collection{}

Set是一个不包含重复元素的Collection。更确切地讲,set 不包含满足 e1.equals(e2)的元素对 e1 和 e2,并且最多包含一个 null 元素。和数学上的集合定义一致。为什么Set中能保证元素的唯一性呢?HashSet和TreeSet有不同的实现方式。

Set中的元素是无序的(相比于存储顺序)。但是Set中有自己的顺序。
Set和Collection的方法相比,没有增加的。

HashSet
public class HashSet<E>extends AbstractSet<E>implements Set<E>

HashSet是Set的一个实现类,其中的元素不允许重复,且是无序的(和添加顺序相比)。HashSet是基于Hash算法和HashMap实现的,每个元素的位置在rehash之前是确定的。

HashSet中和Set相比没有特殊的方法。提供了add、remove、contains、size等方法进行操作。

使用HashSet需要注意以下几点:

  1. HashSet中允许有null,但只能有一个
  2. HashSet存储元素的位置在rehash之前是固定的
  3. 对HashSet的遍历与 HashSet 实例的大小(元素的数量)和底层 HashMap 实例(桶的数量)的“容量”的和成比例。因此,如果要求遍历效率很高,则不要将初始容量设置得太高(或将加载因子设置得太低)。
  4. 几种遍历方式

HashSet中最重要的一点是,什么保证了Set中元素的唯一性?下面解析HashSet的源码

public interface Collection<E> extends Iterable<E> {}
public interface Set<E> extends Collection<E>{}
public class HashSet<E> extends AbstractSet<E> implements Set<E>{
  private static final Object PRESENT = new Object(); //常量

  public HashSet() { //构造方法,构造了一个Map
        map = new HashMap<>();
  }

  public boolean add(E e) {
        return map.put(e, PRESENT)==null; //PRESENT是常量,HashSet的add方法和HashMap的put方法有关
  }

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true); //该方法通过判断hashCode和equals方法,来确定两个对象是否相等,如果相等,则用后面的替换前面的。如果不相等,则添加
    }
  }

}

从上面的代码中可以看出,首先HashSet是基于HashMap实现的,而HashMap中通过hashCode和equals判断两个值是否相等

主要了解hashCode和equals方法。如果两个对象重写了hashCode和equals方法,使得他们的这两个值,先判断hashCode,再判断equals,如果都相等,则说明两个值相等。如果构造的类中不重写这两个方法,则使用的是Object的,一般来说是不相等的。

下面的一个例子说明hashCode和equals的使用

//构造一个学生类
public class Student {
    public String name;
    public int age;

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

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }
}

//构造学生对象,加入HashSet中
public class Demo {
    public static void main(String[] args) {
        Student s1=new Student("A",1);
        Student s2=new Student("A",1);
         Student s3=new Student("A",1);
        Student s4=new Student("A",1);

        HashSet<Student> hs=new HashSet<Student>();
        hs.add(s1);
        hs.add(s2);
         hs.add(s3);
        hs.add(s4);
        for(Student s:hs){
            System.out.println(s.toString());
        }
    }
}

上面程序的结果是

Student [name=A, age=1]
Student [name=A, age=1]
Student [name=A, age=1]
Student [name=A, age=1]

两个对象内容完全一样,却能同时加入HashSet中。原因是没有重写hashCode和equals方法,继承的是Object中的,一般判断不相同(参考 Object的hashCode和equals方法)。

所以需要重写hashCode和equals方法。

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }


    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
LinkedhashSet
public class LinkedHashSet<E> extends HashSet<E>implements Set<E>{}

LinkedHashSet是HashSet的子类,具有可预知迭代顺序,并且元素唯一。底层实现是哈希表和链表,哈希表保证元素的唯一性,链表保证元素的顺序,实现时基于LinkedHashMap。没有特殊的方法。注意没有get(i)方法。虽然LinkedHashSet中的元素顺序是已知的,但是只是相比于加入的顺序。

SortedSet
public interface SortedSet<E> extends Set<E>

SortedSet是一个接口,在Set的基础上进一步提供关于元素的总体排序的Set。这些元素使用其自然顺序进行升序排序,或者根据创建有序 set 时提供的Comparator接口 进行排序。

插入SortedSet 的所有元素都必须实现 Comparable 接口。而且所有的元素都是可以相互比较的,即对于SortedSet中的任意两个元素e1和e2,执行e1.compareTo(e2)或者comparator.compare(e1,e2)都不得抛出异常ClassCastException。

TreeSet
public class TreeSet<E> extends AbstractSet<E>implements NavigableSet<E>, 
public interface NavigableSet<E>extends SortedSet<E>{}

可以看到,TreeSet不是SortedSet的直接子类,是NavigableSet的直接子类。

TreeSet是基于红黑树实现的,是一种二叉排序树。具体的实现是用TreeMap实现的。是在Set的基础上对元素按照自然顺序或者构造的Comparator进行排序,自然顺序是降序排序。为基本操作(addremovecontains)提供 log(n) 的时间开销。

TreeSet中的元素必须实现Comparable 接口,因为在TreeMap中将元素向上转型称为Comparable,所以必须实现该接口,否则会出错。

下面是一个构造类,没有实现Comparable 接口,会报错cn.itcast_Collection.Student cannot be cast to java.lang.Comparable

public class Student {
    public String name;
    public int age;

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

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }
}

public class Demo {
    public static void main(String[] args) {
        Student s1=new Student("A",1);
        Student s2=new Student("B",2);
        Student s3=new Student("C",4);
        Student s4=new Student("A",3);
        Student s5=new Student("B",6);
        Student s6=new Student("A",0);

        TreeSet<Student> hs=new TreeSet<Student>();
        hs.add(s1);
        hs.add(s2);
        hs.add(s3);
        hs.add(s4);
        hs.add(s5);
        hs.add(s6);

        for(Student s:hs){
            System.out.println(s.toString());
        }
    }
}

以上的程序中构造类Student没有实现Comparable 接口,出现了两个问题:1、没有说明如何排序;2、没有说明什么情况是是唯一的元素

解决以上问题由两种方式:1、 使Student类实现Comparable 接口;2、 使用有参构造方法TreeSet(Comparator < ? super E > comparator),并构造比较器。

1、使Student类实现Comparable 接口.如果要求按照年龄降序排序,当age和name一样为同一个元素,应该如何构造Student类?

public class Student implements Comparable<Student>{
    public String name;
    public int age;

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

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }

    @Override
    public int compareTo(Student o) {
        // TODO Auto-generated method stub
        int num=this.age-o.age;
        int num2=num==0? this.name.compareTo(o.name):num;
        return num2;
    }
}

上面构造的Student类实现了Comparable接口中的compareTo方法。在重写compareTo方法时,需要分析主要条件和次要条件,以上面的年龄和姓名为例,首先根据年龄进行排序,如果年龄相等,再判断姓名,如果姓名相同,则说明两个对象相同,只能添加一个。如果还有性别特征,需要在年龄,姓名都一样的基础上,判断性别,如果一样,则说明两个对象一样。

2、使用有参构造方法TreeSet(Comparator < ? super E > comparator),并构造比较器。 这时Student类可以不用实现Comparable接口

构造比较器也有两种方法。第一种是构造一个比较器类,实现Comparator接口,并重写compare方法

 class Mycomparator implements Comparator< Student >{
    public int compare(Student s1, Student s2) {
        // TODO Auto-generated method stub
        int num=s1.age-s2.age;
        int num2=num==0? s1.name.compareTo(s2.name):num;
        return num2;
     }
   }

 TreeSet< Student > hs=new TreeSet< Student >(new Mycomparator());

第二种直接在参数位置构造

 TreeSet< Student > hs=new TreeSet<Student>(new Comparator< Student >(){
    public int compare(Student s1, Student s2) {
        // TODO Auto-generated method stub
        int num=s1.age-s2.age;
        int num2=num==0? s1.name.compareTo(s2.name):num;
        return num2;
        }
  });

使用TreeSet需要注意的几点:

  1. TreeSet中的元素必须实现Comparable 接口,或者构造类实现了Comparable接口,或者使用有参构造,并构造比较器

  2. TreeSet如果是无参构造方法的话,是自然排序,如果需要自定义比较器,构造比较器

  3. TreeSet保证唯一性是根据返回值为0决定的

  4. TreeSet和HashSet的主要区别在于TreeSet中的元素会按照相关Comparator进行排序

至此,Set学习完毕,Set中最主要的两个类HashSet和TreeSet,它们的异同如下:

  1. 都是Set的子类,所以保证元素唯一性
  2. 都是根据Map实现的。HashSet是根据HashSet实现的,底层是Hash表,是由hashCode()和equals()判断元素是否形同;TreeSet是根据TreeMap实现的,底层的数据机构是红黑树,元素的唯一性是根据Compareable接口中compareTo方法的返回值或者构造器中compare的返回值是否为0决定的
  3. TreeSet比HashSet多了排序的功能,排序是根据构造类实现Compareable接口或者构造比较器实现的

至此,Collection学习完毕,对Collection做一个总结
这里写图片描述

上面是Collection框架的结构图。

Collection可以分成三部分:

  1. List:元素可以重复且元素有序(和add的顺序一样)。其子类根据底层的数据接口主要可以分成两类
    • 数组类:ArrayList,Vector,底层数据结构是数组,查询快,增删慢。其中Vector是线程安全的,是ArrayList的一个替代,其中的Stack是栈,但一般不用,主要用LinkedList实现栈。
    • 链表类:LinkedLis,底层数据结构是双向循环链表,查询慢,增删快,线程不安全
  2. Queue:队列,先进先出。具体实现类是PriorityQueue优先队列,底层数据结构是二叉堆,元素按照一定的规则有序。因为有序,所有和TreeSet一样,需要实现Compareable接口或者构造比较器实现元素的比较。其中LinkedList也实现了Deque接口,可以作为队列使用。
  3. Set:元素不可重复。根据功能不同主要分成两类:
    • HashSet:元素不可重复,且无序,底层数据结构是哈希表,元素需要根据hashCode()和equals()比较是否相同,所以构造类需要实现这两个方法
    • TreeSet:元素不可重读,但有序,底层数据结构是红黑树,需要实现Compareable接口或者构造比较器实现排序

Map

public interface Map<K,V>

Map对应于数学上的“映射”,将key(键)映射到value(值),存储的是二元元素(key,value),通过一个内部类Entry实现映射,当做一个节点,其中key是唯一标识的,每个key只能映射到一个value。Map数据结构只和key有关,和value无关。那学生为例,学号是唯一标识学生的,是key,每个学生的学号不能一样,而且每个学号只能对应一个学生。

Map不是Collection的子接口!!1、Collection中的元素是一元的,而Map的元素是二元的。2、Collection中的数据结构和元素有关,而Map 的数据结构只和key有关,和value无关;3、Map中的key是唯一的,和Collection中的Set要求一致,Set中的HashSet和TreeSet都是根据Map实现的,而不是Map是基于Set实现的。

Map的方法如下:

voidclear() 从此映射中移除所有映射关系(可选操作)。
booleancontainsKey(Object key) 如果此映射包含指定键的映射关系,则返回 true
booleancontainsValue(Object value) 如果此映射将一个或多个键映射到指定值,则返回 true
Set<Map.Entry<K,V>>entrySet() 返回此映射中包含的映射关系的Set视图。
booleanequals(Object o) 比较指定的对象与此映射是否相等。
Vget(Object key) 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null
inthashCode() 返回此映射的哈希值。
booleanisEmpty() 如果此映射未包含键-值映射关系,则返回 true
Set<K>keySet() 返回此映射中包含的键的 Set视图。
Vput(K key, V value) 将指定的值与此映射中的指定键关联(可选操作)。
voidputAll(Map<? extends K,? extends V> m) 从指定映射中将所有映射关系复制到此映射中(可选操作)。
Vremove(Object key) 如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。
intsize() 返回此映射中的键-值映射关系数。
Collection<V>values() 返回此映射中包含的值的Collection视图。

1. 注意没有set方法,因为Map是根据key实现的,key是无序的,和Collection中额Set一样没有set方法。
2. put方法返回的是value值。执行此方法put(key,value),如果map中有存在的相同的key,则用value代替原来的value,并返回原来的value
3. 获取功能:get(Object key) 根据key获取value;keySet() ,获取所有key;values(),获取所有value;entrySet(),获取映射关系
4. 遍历Map的两种方式:1、keyset()和get(Object key)方法,首先将key集合拿出来,根据key找value;2、用entrySet方法,找到所有的映射关系,根据映射关系找key和value

   public class Demo {
        public static void main(String[] args) {
        Map< String,Integer > hs=new HashMap< String,Integer >();
        hs.put("A",1);
        hs.put("B",2);
        hs.put("C",3);
        hs.put("D",4);
        hs.put("E",5);
        hs.put("F",6);
        Set<String> s=hs.keySet();
        for(String  x: s){ //第一种方式
            Integer i=hs.get(x);
            System.out.println(x+"----->"+i);
        }
        Set< Map.Entry< String, Integer > > ms=hs.entrySet();
        for(Map.Entry< String, Integer > m:ms){ //第二种方式
            System.out.println(m.getKey()+"----->"+m.getValue());
        }
    }
   }

HashMap

public class HashMap< K,V >extends AbstractMap< K,V >implements Map< K,V >

HashMap实现了Map接口,是基于哈希表实现的,并允许使用null值和null键。HashMap是实现HashSet的基础,是根据hashCode和equals实现键的唯一性。首先比较hashCode,如果相等,采用链表的方式解决冲突。和Map相比没有特殊的方法。

无参构造方法默认生成大小为16,负载因子为0.75的空HashMap。要了解HashMap的实现,需要了解Hash表,如果构建Hash表,哈希函数,如何解决冲突等。

HashMap中主要应用一个内部类Entry实现键值映射,形成一个节点,存储在哈希表中。

HashMap和HashSet的相比主要是Map是存储的是二元映射,HashSet是有HashMap实现的,value是固定的。

LinkedHashMap
public class LinkedHashMap<K,V>extends HashMap<K,V>implements Map<K,V>

LinkedHashMap和HashMap的关系可以类比于LinkedHashSet和HashSet的关系,在哈希表的基础上加上链表,保证元素唯一的同时,实现元素的有序性。

Hashtable

public class Hashtable<K,V>extends Dictionary<K,V>implements Map<K,V>

这里首先说一下Dictionary,是JDK1.0版本中的字典,作用类似于Map,在之后就弃用了。现在主要用Map。

注意这里的Hashtable中的table是小写的!!

Hashtable是基于Hash表实现的,其实例化的时候有两个影响因子:初始容量和加载因子。容量是哈希表中桶的数量,在发生哈希冲突的时候,单个桶的链表会越来越长,这个链表必须按照顺序搜索。初始容量主要控制空间消耗与执行 rehash 操作所需要的时间损耗之间的平衡。如果初始容量大于 Hashtable 所包含的最大条目数除以加载因子,则永远 不会发生 rehash 操作。但是,将初始容量设置太高可能会浪费空间。加载因子是对哈希表在其容量自动增加之前可以达到饱和的一种程度默描述,加载因子过高减少空间开销,但查找时间增大,加载因子过小,可能不会产生冲突,但是空间开销大。当然HashMap也是基于哈希表实现的,也有以上的问题。

Hashtable和HashMap不同有以下3点:

  1. Hashtable是基于Dictionary实现的,而HashMap是基于JDK1.2才出现的Map实现的
  2. Hashtable中不能存放null值和null键,而HashMap可以
  3. Hashtable是线程安全的,而HashMap是非安全的

SortedMap

public interface SortedMap<K,V>extends Map<K,V>

SortedMapt的学习可以参照SortedSet,它是Map的子接口,在Map的基础上进一步提供关于键的总体排序的Map。这些键使用其自然顺序进行升序排序,或者根据创建有序 Map 时提供的Comparator接口 进行排序。

插入SortedMap 的所有键值映射中的键都必须实现 Comparable 接口。而且所有的键都是可以相互比较的,即对于SortedMap中的任意两个元素e1和e2的键e1.key和e2.key,执行e1.key.compareTo(e2.key)或者comparator.compare(e1.key,e2.key)都不得抛出异常ClassCastException。

TreeMap

TreeMap和Map的关系可以类比于TreeSet和Set的关系,TreeMap是Map的一种键有序实现,利用红黑树实现,在保证存储键值映射、键唯一的基础上,TreeMap可以按照键的自然顺序或者构造比较器进行排序

同理,实现自然顺序排序需要键所在的构造类实现Comparable接口,并实现compareTo方法;构造比较器的话,需要构造一个实现Comparator几口的比较器,实现compare方法。

在Map学习完毕之后,可以发现,Map的框架结构和Collection中的Set框架一致,只不过Map存储的是二元键值映射,利用一个内部类Entry实现,Set是在Map的基础上将value设置为常数实现的。Map和Collection.Set框架和需要注意的细节问题都一致,所以学习完Set之后,对于Map的理解只要转换到键值二元映射就可以了。

相关的问题

迭代器iterator

iterator:对 collection 进行迭代的迭代器,是依赖于集合而存在的。首先通过集合获取迭代器对象it,然后利用hasNext判断,最后利用next取对象。

Iterator是一个接口,为什么不定义成一个类呢?假设迭代器定义的是一个类,那么可以创建该类的对象,通过调用方法来实现功能。但是集合中有很多子类,元素存在的方式不一样,迭代器需要适应每一个子类才可以。所以将这些子类的迭代器有关的方法抽象出来,形成一个接口Iterator。所以Iterator是一个接口,里面有3个抽象方法,没有方法体,具体的实现在各个集合子类中以内部类的方式实现。可以看Iterator和ArrayList的源码。

下面根据源码看迭代器的原理:以ArrayList为例,父类是List,超父类是Collection,实现了方法iterator(),但是返回的是一个对象return new Itr(),而Itr()是ArrayList的一个内部类,实现了hastNext,next和remove方法。所以具体来说,Iterator it=c.iterator()返回的是对象c所在集合类的一个内部类的对象。c所在集合类的内部类Itr实现了从Iterable继承的iterator方法。

Collection c=new ArrayList(); 
Iterator it=c.iterator();  //多态,编译看c的左边,实现看c的右边

//c的左边是Collection的Iterator
//Collection继承了Iterable接口,Iterable中有抽象方法iterator
public interface Collection<E> extends Iterable<E> {}
//Iterable接口中只有一个方法返回Iterator类型
public interface Iterable<T> {
    Iterator<T> iterator();
}

//c的右边是ArrayList的iterator方法
//因为ArrayList继承List,先看List,List中有iterator方法
public interface List<E> extends Collection<E> {
   Iterator<E> iterator(); //抽象方法
}
//ArrayList继承了List,并实现了iterator方法,但是返回的是Itr一个内部类
public class ArrayList<E> extends AbstractList<E> implements List<E>{
   public Iterator<E> iterator() {
        return new Itr();
   }  
  //内部类
   private class Itr implements Iterator<E> {
     public boolean hasNext() {
         .......
     }
     public E next() {
       ........
     }
     public void remove() {
       ........
     }
   }
}


//其中Iterator接口如下
public interface Iterator<E> {
  boolean hasNext();
  E next();
  default void remove();
}

以下是使用iterator方法的具体步骤:

import java.util.Iterator;
Iterator it=c.iterator();
while(it.hasNext()){    
  System.out.println(c.next());
}
//也可以写成for循环,it对象执行完就释放,效率高
for(Iterator it=c.iterator();it.hasNext())
{    
  System.out.println(c.next());
}

泛型

什么是泛型

Collection中可以存放基本数据类型和构造数据类型,和数组、StringBuffer不一样,Collection中可以存放不同的类型。如果一个集合中存放了Integer和String和Student类型,在处理时首先将构造数据类型用Object接受,然后再强制转换成所需类型。如果转换正确,可以正常运行,如果转换错误,编译能通过,但是运行出错。所以需要在编译期间就使得这个问题暴露,一个Collection中只存放一种类型,而泛型是用来声明Collection存放数据的类型的,如果存入的数据和泛型声明的不一样,则编译不能通过。

集合模仿数组的做法,在创建对象的时候明确存放数据的类型,这样就不会出现以上问题。

泛型即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在调用时传入具体的类型(类型实参)。

泛型的格式<类型>,此处的数据类型是引用数据类型。

泛型的好处:

  1. 把运行使其的问题提前到了编译期间。早期的时候用Object代替任何类型

    //构建一个Object类型,包含get和set方法
    public class ObjectTool {
        Object obj;
        public Object getObj() {
            return obj;
        }
        public void setObj(Object obj) {
            this.obj = obj;
        }
    }
    
    //利用上述类讨论泛型的必要性
    public class Demo {
    public static void main(String[] args) {
        ObjectTool obj=new ObjectTool();
    
        obj.setObj(new Integer(10)); //用Object类型接受所有构造类型
        Integer i=(Integer)obj.getObj();//将Object强制转换成需要的构造类型
    
        obj.setObj(new String("Lee"));
        Integer j=(Integer)obj.getObj();//如果转换错误,编译能通过,运行出错。所以说泛型能将问题提前到编译期间。   
    }   
    }
  2. 避免了强制类型转化

  3. 优化了程序设计

泛型类,泛型方法和泛型接口
  1. 泛型类:public class 类名< 构造数据类型1,构造数据类型2,… >,最常用的

    public class ObjectTool<T> { //泛型类
    public void show(T t){   
        System.out.println(t);
    }
    }
    
    public class Demo {
    public static void main(String[] args) {
        ObjectTool<String> obj=new ObjectTool<String>();
        obj.show("Lee");
    
        ObjectTool<Integer> obj1=new ObjectTool<Integer>();
        obj1.show(2);
    }   
    }
  2. 泛型方法:public class <构造数据类型1,构造数据类型2,…> 返回类型 方法名

    public class ObjectTool{
        public <T> void show(T t){ //泛型方法
            System.out.println(t);
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            ObjectTool obj=new ObjectTool();
            obj.show("Lee");
            obj.show(10);
            obj.show(true);
        }   
    }
  3. 泛型接口,所有Collection中的接口和继承都是这样实现的

    //首先定义一个接口
    public interface Iter <T>{
        public abstract void show(T t);
    }
    
    //一个实现接口的类。1、已经知道该类的类型;2、不知道该类的类型
    
    //1、已经知道该类的类型;
    public class IterImpl implements Iter<String>{
        public void show(String s){
            System.out.println(s);
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            Iter< String > it=new IterImpl();
            it.show("Lee");
        }   
    }
    
    //2、不知道该类的类型
    public class IterImpl<T> implements Iter<T>{
        public void show(T t){
            System.out.println(t);
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            Iter< String > it=new IterImpl<String>();
            it.show("Lee");
        }   
    }
泛型通配符
//首先定义3个有继承关系的类
class Animal{}
class Cat extends Animal{}
class Dog extends Animal{}

在上面的继承关系中,Cat是Animal的子类,那么ArrayList< Cat >是不是ArrayList< Animal >的子类呢?答案是否定的。来看下面的例子

public class Demo {
    public static void main(String[] args) {
        ArrayList<Animal> list1=new ArrayList<Animal>();
        list1.add(new Animal());
        ArrayList<Cat> list2=new ArrayList<Cat>();
        list2.add(new Cat());

        getData(list1);
        getData(list2); // 1 编译出错
    }

    public static void getData(ArrayList<Animal> data){
        System.out.println(data.get(0));
    }
}

上面1处编译出错,如果ArrayList< Cat >是ArrayList< Animal >的子类,在getData方法中可以将ArrayList< Cat >转成ArrayList< Animal >,便不会编译出错。那么从getData中取出的数据类型时什么呢?ArrayList< Cat >?ArrayList< Animal >?这又称为JDK1.5前没哟泛型时会出现的问题了,和泛型的理念违背。所以逻辑上不能说ArrayList< Cat >是ArrayList< Animal >的子类。那么上面的编译错误如何解决呢?可以使用泛型方法,也可以引入泛型通配符。

  • < ? >:?表示任意的泛型类型,可以认为ArrayList< ? >是ArrayList< Animal >,ArrayList< Cat >,ArrayList< Dog >的父类

    public class Demo {
    public static void main(String[] args) {
        ArrayList<Animal> list1=new ArrayList<Animal>();
        list1.add(new Animal());
        ArrayList<Cat> list2=new ArrayList<Cat>();
        list2.add(new Cat());
    
        getData(list1);
        getData(list2);
        System.out.println(list1.getClass()==list2.getClass());
    }
    
    public static  void getData(ArrayList<?> data){
        System.out.println(data.get(0));
        System.out.println(data.getClass());
        System.out.println(data.get(0).getClass());
    }
    }

    上面的例子解决了编译阶段的问题,运行结果是:

    cn.itcast_Collection.Animal@15db9742
    class java.util.ArrayList
    class cn.itcast_Collection.Animal
    cn.itcast_Collection.Cat@6d06d69c
    class java.util.ArrayList
    class cn.itcast_Collection.Cat
    true

    可以看出在运行阶段,编译器将list1和list2的类型都当做ArrayList,但是list1和list2中的元素仍然分别是Animal和Cat

  • < ? extends E >:类型通配符上限,表示对泛型的约束,所有继承自E的类型

    public class Demo {
    public static void main(String[] args) {
        ArrayList<? extends Animal> list1=new ArrayList<Animal>();
        ArrayList<? extends Animal> list2=new ArrayList<Cat>();
    
        list1.add(new Animal());  //1 编译错误
            list1.add(null);
        Animal a=(Animal)list1.get(0);
    }
    }

    上面的例子前半部分编译正确,List1和List2可以接收Animal的子类。但是1处编译错误,如果一个集合有< ? extends E >泛型,则不能往里面添加元素。为什么会出错呢?List< ? extends Frut > 表示 “具有任何从Animal继承类型的列表”,编译器无法确定list1所持有的类型,所以无法安全的向其中添加对象。可以添加null,因为null 可以表示任何类型。所以List 的add 方法不能添加任何有意义的元素,但是可以接受现有的子类型List< Apple > 赋值。

  • < ? super E >:类型通配符下限,表示对泛型的约束,E的所有的父类

    public class Demo {
    public static void main(String[] args) {
        ArrayList<Animal> list1=new ArrayList<Animal>();
        list1.add(new Animal());
        ArrayList<Cat> list2=new ArrayList<Cat>();
        list2.add(new Cat());
    
        getData(list1);
        getData(list2);  // 1  编译错误
        System.out.println(list1.getClass()==list2.getClass());
    }
    
    public static  void getData(ArrayList<? superT> data){
        System.out.println(data.get(0));
        System.out.println(data.getClass());
        System.out.println(data.get(0).getClass());
    }
    }

    和< ? extends E >一致。同理,当集合有< ? super E >泛型时,不能从中取出元素

    ???

Object类

java.lang.Object

Object 是类层次结构的根类。每个类都使用 Object 作为超类。所有对象(包括数组)都默认实现这个类的方法。

Object类只有一个无参构造方法,所有子类的构造方法都默认访问父类(Object)的无参构造方法

protected Object**clone**() 创建并返回此对象的一个副本。
boolean**equals**(Object obj) 指示其他某个对象是否与此对象“相等”。
protected void**finalize**() 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
Class<?>**getClass**() 返回此 Object 的运行时类。
int**hashCode**() 返回该对象的哈希码值。
void**notify**() 唤醒在此对象监视器上等待的单个线程。
void**notifyAll**() 唤醒在此对象监视器上等待的所有线程。
String**toString**() 返回该对象的字符串表示。
void**wait**() 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void**wait**(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
void**wait**(long timeout, int nanos) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。

主要了解hashCode,toString,equals

  1. hashCode():计算法对象的哈希值,一般是将内部地址值通过一个哈希函数计算出的整数,这个整数是该对象在哈希表中的位置索引。至于如何定义哈希函数可以自行定义,此时需要重写hashCode方法。

  2. toString:返回对象的字符串表示。通常toString 方法会返回一个“以文本方式表示”此对象的字符串。结果应是一个简明但易于读懂的信息表达式。建议所有子类都重写此方法。

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    Object中的toString的源码如上,表示”类名@地址值”,比如cn.itcast_Collection.Animal@15db9742。

  3. equals:判断某个对象和此对象相等。源码如下:

    public boolean equals(Object obj) {
       return (this == obj);
    }

    ==:表示的也是两个对象相等,但是当对象是基本数据类型时,比较的是值是否相等,如果是构造数据类型,比较的是地址值,即看两个值引用的是否是同一对象。所以如果是构造数据类型,直接使用equals的话,比较的是两个对象的地址值,但是意义不大,所以一般要重写equals方法,使得比较对象的值是否相等。注意,基本数据类型只有8种,4种整形,两种浮点型,一种char,一种boolean。字符串是引用数据类型,equals方法已经重写,比较的是字符串的内容。

    如何重写equals方法?对于Student类,其equals方法重写如下:

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    ==和equals的区别:==既可以比较基本数据类型,又可以比较构造数据类型,如果是构造数据类型的话,比较的是地址值是否相等;equals只能比较构造数据类型,如果不重写,继承自Object类,使用==,比较地址值,如果重写,按照重写的方式比较。

Collections和Arrays

Collections是一个类,区别与Collection是一个接口。是针对集合进行操作的工具类,该类中的方法都是静态方法(可以通过Collection是直接调用)。

Collection中有很多对集合进行操作的方法,比如排序,求最大值,二分查找,随机置换等,有需要直接根据API查找。

Collections和Collection的关系可以类比于Arrays和数组的关系。Arrays是针对数组的操作工具类,其中的方法都是静态方法。

Collections和Arrays都是util包下的,注意导包import

public class Collections extends Object  
public class Arrays extends Object

常用的方法:toString(….)将数组转成字符串表示的形式;sort(…)对数组进行排序;binarySearch(…)二分查找;asList(…)返回一个列表

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值