Java:类集(List,Vector,Set,HashMap)

类集:就是一个动态的对象数组,是对一些实现好的数据结构的包装,这样在使用时会非常方便,而且最重要的是类集框架本身不受对象数组长度的限制。

类集的特性:(1)这种框架是高性能的,对基本类集(动态数组、链接表、树和散列表)的实现是高效率的。所以一般很少需要人工对这些"数据引擎"编写代码。(2)框架必须允许实现不同类型的类集以相同的方式和高度互操作方式工作。(3)类集必须是容易扩展和修改的。为了实现这一目标,类集框架被设计成包含了一组标准接口。

常用的接口有:Collection、List、Set、Map、Iterator、ListIterator、Enumeration、SortedSet、SortedMap、Queue、Map.Entry。

接口描述
Collection是存放一组单值的最大父接口,所谓的单值是指集合中的每个元素都是一个对象。在新的开发标准中已经很少直接使用此类接口进行操作。
List是Collection接口的子接口,也是最常用的接口。此接口对Collection接口进行了大量的扩展,里面的内容是允许重复的。
Set是Collection接口的子类,没有对Collection接口进行扩充,里面不允许存放重复的元素。
Map是存放一对值的最大父接口,即接口中的每个元素都是一对,以key——value的形式保存。
Iterator集合的输出接口,用于输出集合中的内容,只能进行从前到后的单项输出。
ListIterator是Iterator的子接口,可以进行由前向后或有后向前的双向输出。
Enumeration是最早的输出接口,用于输出指定集合中的内容。
SortedSet单值的排序接口,实现此接口的集合类,里面的内容可以使用比较器排序。
SortedMap存放一对值的排序接口,实现此接口的集合类,里面的内容按照key排序,使用比较器排序。
Queue队列接口,此接口的子类可以实现队列操作。
Map.EntryMap.Entry的内部接口,每个Map.Entry对象都保存着一对key——value的内容,每个Map接口中都保存有多个Map.Entry接口实例。

1,Collection接口和Collections类

1.1,Collection接口

public interface Collection<E> extends Iterable<E>

从接口中可以定义,此接口使用了泛型的定义,在操作时必须指定具体的操作类型。这样可以保证类集操作的安全性,避免ClassCastException异常。

Collection接口时单值存放的最大父接口,可以向其中保存多个单值数据。

方法描述
public boolean add(E o)向集合中插入对象
public boolean addAll(Collection<? extends E> e)将一个集合的内容插入进来
public boolean void clear()清除此集合中的所有元素
public boolean contains(Object o)判断某一个对象是否存在该集合中
public boolean containsAll(Collection<?> e)判断一组对象是否在集合中存在
public boolean equals(Object o)对象对比
public int hashCode()哈希码
public boolean isEmpty()集合是否为空
public Iterator<E> iterator()为Iterator接口实例化
public boolean remove(Object o)删除指定对象
public boolean removeAll(Collection<?> c)删除一组对象
public boolean retainAll(Collection<?> c)保存指定对象
public int size()求出集合的大小
public Object[] toArray()将一个集合变为对象数组
public <T> T[] toArray(T[] a)指定好返回的对象数组类型

Collection接口虽然是集合的最大接口,但是如果直接使用Collection接口进行操作,则表示的操作意义不明确,所以在Java开发中不提倡直接使用Collection接口。主要接口如下:

  • List:可以存放重复的内容。
  • Set:不能存放重复的内容,所有的重复内容靠hashCode()和equals()两个方法区分。
  • Queue:队列接口。

1.2,Collections类(集合操作类)

Collections类常用方法及类型:

方法类型描述
public static <T> Collection<T> synchronizedXxx(Collection<T> c) 普通将指定集合设置为线程同步。
public static final List EMPTY_LIST常量返回一个空的List集合
public static final Set EMPTY_SET常量返回空的Set集合
public static final Map EMPTY_MAP常量返回空的Map集合
public static <T> boolean addAll(Collection<? super T>c, T...a)普通为集合添加内容
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)普通找到最大的内容,按比较器排序
public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll)普通找到集合中的最小内容,按比较器排序
public static <T> boolean replaceAll(List<T> list,T oldVal, T newVal)普通用新的内容替换集合的指定内容
public static void reverse(List<?> list)普通集合反转
public static <T> int binarySearch(List<? extends Comparable<? super T>>list, T key)普通查找集合中的指定内容
public static final <T> List<T> emptyList()普通返回一个空的List集合
public static final <K,V> Map<K,V> emptyMap()普通返回一个空的Map集合
public static final <T> Set<T> emptySet()普通返回一个空的Set集合
public static final <T extends Comparable<? super T>> void sort(List<T> list)普通集合排序操作,根据Comparable接口进行排序
public static void swap(List<?> list,int i,int j)普通交换指定位置元素

1,创建线程同步集合

Collection c = Collections.synchronizedCollection(new ArrayList<>());
List list = Collections.synchronizedList(new ArrayList<>());
Set set = Collections.synchronizedSet(new HashSet<>());
Map map = Collections.synchronizedMap(new HashMap<>());

2,返回不可变的集合:因为在List、Set、Map集合中,返回的对象都是无法进行增加数据的,因为没有实现add()方法。

List<String> allList = Collections.EMPTY_LIST;
Set<String> allSet = Collections.EMPTY_SET;
allList.add("燕双嘤");
==========================================
报错

3,添加内容:addAll可以接收可变参数,然后可以传递任意多的参数作为集合的内容。

List<String> allList = new ArrayList<>();
Collections.addAll(allList,"燕双嘤","杜马","步鹰");
for (String s:allList){
    System.out.println(s);
}

4,反转集合中的内容

Collections.reverse(allList);

5,检索内容:直接通过Collection类中的binarySearch()方法完成内容的检索,中文在compareTo()方法下有BUG。

List<String> allList = new ArrayList<>();
Collections.addAll(allList,"燕双嘤","杜马","步鹰");
int name = Collections.binarySearch(allList,"燕双嘤");
System.out.println(name);

6,替换集合中的内容

Collections.replaceAll(allList,"步鹰","云建民");

7,集合排序

Collections.sort(allList);

8,交换指定位置的内容

Collections.swap(allList,0,2);

2,List接口

2.1,List接口的定义

public interface List<E> extends Collection<E>

List是Collection接口,其中可以保存各个重复的内容。但是与Collection不同的是,在List接口中大量地扩充了Collection接口,拥有了比Collection接口定义更多的方法定义。List接口比Collection接口扩充了更多的方法,而且此方法操作起来方便。但如果想要想使用此接口,则需要通过其子类进行实例化。

方法描述
public void add(int index, E element)在指定位置增加元素
public boolean addAll(int index, Collection<? extends E>) c在指定位置增加一组元素
E get(int index)返回指定位置的元素
public int indexOf(Object o)查找指定位置的元素
public int lastIndexOf(Object o)从后向前查找指定元素的位置
public ListIterator<E> listIterator()为ListIterator接口实例化
public E remove(int index)按指定位置删除元素
public List<E> subList(int fromIndex, int toIndex)取出集合中的子集合
public E set(int index, E element)替换指定位置的元素

2.2,数组集合:ArrayList

ArrayList的底层是用数组来实现的,默认第一次插入元素时创建大小为10的数组,超出限制时会增加 50%的容量,并且数据以 System.arraycopy() 复制到新的数组,因此最好能给出数组大小的预估值。按数组下标访问元素的性能很高,这是数组的基本优势。直接在数组末尾加入元素的性能也高,但如果按下标插入、删除元素,则要用System.arraycopy()来移动部分受影响的元素,性能就变差了,这是基本劣势。

ArrayList是List子类,可以直接通过对象的多态性为List接口实例化。

public class ArrayList<E> extends AbstractList<E> implements List<E>,RandomAccess,Cloneable,Serializable

可以看出ArrayList类继承了AbstractList类。此接口实现了List接口,所以可以直接使用ArrayList为List接口实例化。

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E>

【问题】ArrayList 实现 RandomAccess 接口有何作用?为何 LinkedList 却没实现这个接口?

【答案】RandomAccess 是一个标记接口,用于表示该集合支持高效的随机访问。ArrayList 实现 RandomAccess 接口的主要作用是,当程序通过索引访问 ArrayList 的元素时,会优先选择使用基于索引的随机访问方式,这种访问方式的时间复杂度为 O(1)。这样就可以提高程序的运行效率,尤其是当集合元素数量很大时。相比之下,LinkedList 实现 RandomAccess 接口的作用并不大,因为它并不支持基于索引的随机访问方式。由于 LinkedList 中的元素是通过链表相连的,因此要访问其中的任意一个元素,需要先遍历它之前的所有元素,时间复杂度为 O(n)。因此,如果 LinkedList 实现了 RandomAccess 接口,也不会产生优化效果。

【问题】数组和列表的区别?

【答案】一般情况下,如果需要存储的元素数量固定,并且对于访问元素的性能要求较高,那么使用 Array 更为合适;如果需要动态扩展存储空间,并且对于插入、删除等操作的性能要求较高,那么使用 ArrayList 更为合适。

  • 数据类型:Array 可以存储基本数据类型和对象类型,而 ArrayList 只能存储对象类型。

  • 大小:Array 在创建时需要指定大小,且大小不可更改,而 ArrayList 大小可以动态扩展。

  • 访问方式:Array 可以通过索引直接访问元素,而 ArrayList 需要使用 get() 方法。

  • 性能:Array 在访问元素时性能更好,因为它是基于连续的内存空间存储的,而 ArrayList 是基于数组实现的,插入或删除元素时需要移动后面的元素,性能较低。

2.3,Vector

在List接口中还有一个子类Vector,Vector类属于一个挽救的子类,从整个Java集合发展历史来看,Vector算是一个元老级的类,在JDK1.0时就已经存在此类。到了JDK1.2之后强调了集合框架的概念,所以先后定义了很多的新接口,但是考虑到一大部分用户已经习惯使用Vector类,所以Java的设计者就让Vector类多实现了一个List接口,这才将其保留下来。Vector与ArrayList类一样也继承自AbstractList类,基本一样,但是也存在一些细小差别。

比较ArrayListVector
推出时间JDK1.2之后推出的,属于新的操作类JDK1.0时推出,属于旧的操作类
性能采用异步处理方式,性能更高采用同步处理方式,性能较低
线程安全属于非线程安全的操作类属于线程安全的操作类
输出只能使用Iterator、foreach输出可以使用Iterator、foreach、Enumeration输出

其他线程安全的List:

  • Collections.SynchronizedList:SynchronizedList是Collections的内部类,Collections提供了synchronizedList方法,可以将一个 线程不安全的List包装成线程安全的List,即SynchronizedList。它比Vector有更好的扩展性和兼 容性,但是它所有的方法都带有同步锁,也不是性能最优的List。
  • CopyOnWriteArrayList:CopyOnWriteArrayList是Java 1.5在java.util.concurrent包下增加的类,它采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。在所有线程安全的List中,它是性能最优的方案。

2.4,CopyOnWriteArrayList

CopyOnWriteArrayList是Java并发包里提供的并发类,简单来说它就是一个线程安全且读操作无锁的 ArrayList。正如其名字一样,在写操作时会复制一份新的List,在新的List上完成写操作,然后再将原 引用指向新的List。这样就保证了写操作的线程安全。

CopyOnWriteArrayList允许线程并发访问读操作,这个时候是没有加锁限制的,性能较高。而写操作的时候,则首先将容器复制一份,然后在新的副本上执行写操作,这个时候写操作是上锁的。结束之后 再将原容器的引用指向新容器。注意,在上锁执行写操作的过程中,如果有需要读操作,会作用在原容器上。因此上锁的写操作不会影响到并发访问的读操作。

  • 优点:读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。在遍历传统的List时,若中途有别的线程对其进行修改,则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的List容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了。
  • 缺点:一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC。二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读 和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

2.5,ConcurrentSkipListMap

ConcurrentSkipListMap 是 Java 中一个线程安全的有序映射表,底层基于 跳表 实现。它可以支持高并发的读写操作,并且具有较好的并发性能。在并发环境下,ConcurrentSkipListMap 的读操作是 lock-free 的,而写操作则是通过锁分段技术实现的,因此可以有效地减少锁的竞争。

ConcurrentSkipListMap 中的键值对是按照键的顺序进行排序的,因此可以用来实现按照键排序的数据结构,比如有序的映射表和集合。它支持高效的插入、删除和查找操作,时间复杂度均为 O(log n)。

  • 线程安全:ConcurrentSkipListMap 是线程安全的,多个线程可以同时访问它,而不会导致数据不一致或竞态条件。

  • 排序:ConcurrentSkipListMap 中的键值对是按照键的顺序进行排序的,可以用来实现有序的映射表和集合。

  • 并发性能:ConcurrentSkipListMap 的读操作是 lock-free 的,写操作则是通过锁分段技术实现的,因此可以有效地减少锁的竞争,具有较好的并发性能。

  • 适用场景:ConcurrentSkipListMap 适用于需要高并发读写、有序的键值存储和查询场景,比如缓存、计数器、排行榜等。

ConcurrentSkipListMap 的内部实现是比较复杂的,它涉及到跳表的构建和维护,因此在使用时需要考虑其性能和空间复杂度。此外,由于其键是有序的,因此需要实现 Comparable 接口或者传入自定义的 Comparator 对象来比较键的大小。

2.6,链表集合:LinkedList

LinkedList表示的是一个链表的操作类,该类实现了List接口,同时也实现了Queue接口。

ArrayList和LinkedList的区别:

  • ArrayList的实现是基于数组,LinkedList的实现是基于双向链表;
  • 对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随 机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的 时间复杂度是O(N);
  • 对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引;
  • LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
操作数组:ArrayList链表:LinkedList
随机访问O(1)O(N)
随机操作O(N)O(N)
头部插入O(N)O(1)
头部删除O(N)O(1)
尾部插入O(1)O(1)
尾部删除O(1)O(1)

【问题】如何提高链表的查询速度?

  • 哈希表:把链表中的元素按照某个特定的规则或者指标建立哈希表,以达到快速查找目标元素的目的。

  • 二叉搜索树:将链表中的元素构成一棵二叉搜索树,这样就可以通过二分查找方式快速定位目标元素。

  • 快慢指针:通过增加一些辅助指针,例如快指针和慢指针的方法,可以更快地定位链表中的某些元素。

  • 缓存:增加一个缓存区来存储最近访问的元素,以便下次查询时可以直接从缓存中取得结果,加快查询速度。

  • 跳表:跳表是一种基于多级索引的数据结构,可以在有序链表上快速插入、删除和查找元素,其查询效率甚至可以高于二叉搜索树。但是实现复杂度比较高。

3,Queue接口

Queue用于模拟队列这种数据结构,队列通常是指先进先出的容器。队列的头部保存在队列中存放时间最长的元素,队列的尾部保存在队列中存放时间最短的元素。新元素插入到队列的尾部,访问元素操作会返回列头部的元素。队列不允许随机访问队列中的元素。

Queue接口中定义了如下几个方法:

  • void add(Object e):将指定元素加入此队列的尾部。
  • Object element():获取队列头部的元素,但是不删除该元素。
  • boolean offer(Object e):将指定元素加入到此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法好用。
  • Object peek():获取队列头部的元素,但是不删除该元素。如果此队列为空,则返回null。
  • Object poll():获取队列头部的元素,并删除该元素。如果此队列为空,则返回null。
  • Object remove():获取队列头部的元素,并删除该元素。

3.1,PriorityQueue,BlockingQueue

PriorityQueue实现类:PriorityQueue是一个比较标准的队列实现类。之所以说它是比较标准的队列实现,而不是绝对标准的队列实现,是因为PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列大小进行重新排序。从这个意义上来讲,Priority已经违背了先进后出的规则。因为需要排序,所以不允许出现null,类似于TreeSet类。

BlockingQueue 是 Java 中一个非常有用的并发工具,它提供了一个线程安全的队列,允许多个线程同时对其进行操作。当队列为空时,从队列中获取元素的线程会被阻塞,直到有元素被放入队列中;当队列已满时,往队列中添加元素的线程会被阻塞,直到有空间可以放入元素。

  • ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,按照先进先出的原则对元素进行排序。

  • LinkedBlockingQueue:一个基于链表结构的可选有界阻塞队列,按照先进先出的原则对元素进行排序。

  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列,元素按照自然顺序或者指定的 Comparator 进行排序。

  • DelayQueue:一个支持延时获取元素的无界阻塞队列,只有延迟期满的元素才能够被获取。

  • SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。

  • LinkedTransferQueue:一个基于链表结构的无界阻塞队列,其中的元素可以在生产者和消费者之间进行传递。

  • LinkedBlockingDeque:一个基于链表结构的双向阻塞队列,可以在队列的两端同时进行插入和删除操作,按照先进先出的原则对元素进行排序。

3.2,Deque接口

Deque接口是Queue接口的子接口,它代表一个双端队列,Deque接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素:

  • void addFirst(Object e):将指定元素插入该双端队列的开头。
  • void addLast(Object e):将指定元素插入该双端队列的结尾。
  • Iterator descendingIterator():返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素。
  • Object getFirst():获取但不删除双端队列的第一个元素。
  • Object getLast():获取但不删除双端队列的最后一个元素。
  • boolean offerFirst(Object e):将指定元素插入该双端队列的开头。
  • boolean offerLast(Object e):将指定元素插入该双端队列的末尾。
  • Object peekFirst():获取但不删除该双端队列的第一个元素;如果为空,则返回null。
  • Object peekLast():获取但不删除该双端队列的最后一个元素;如果为空,则返回null。
  • Object pollFirst():获取并删除该双端队列的第一个元素;如果为空,则返回null。
  • Object pollLast():获取并删除该双端队列的最后一个元素;如果为空,则返回null。
  • Object pop():pop出该双端队列所表示的栈的栈顶元素。等于removeFirst()。
  • void push(Object e):将一个元素push进该双端队列所表示的栈的栈顶。等于addFirst(e)。
  • Object removeFirst():获取并删除该双端队列的第一个元素。
  • Object removeFirstOccurrence(Object e):删除该双端队列的第一次出现的元素e。
  • Object removeLast():获取并删除该双端队列的最后一个元素。
  • Object removeLastOccurrence(Object e):删除该双端队列的最后一次出现的元素e。

从上面方法看出,Deque不仅可以当成双端队列使用,而且可以被当成栈来使用,因为包含了pop()和push()两个方法。

3.3, ArrayDeque实现类

Deque接口提供了一个典型的实现类:ArrayDeque,从该名称可以看出,是一个基于数组的双端队列,创建Deque时同样可指定一个numElement参数,用于指定Object[]的长度,如果不指定,默认为16。将ArrayDeque当成“栈”来使用:

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        ArrayDeque stack = new ArrayDeque<>();
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack);
        System.out.println(stack.peek());
        System.out.println(stack.pop());
        System.out.println(stack);
    }
}
====================================
[3, 2, 1]
3
3
[2, 1]

将ArrayDeque当成“队列”来使用:

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        ArrayDeque stack = new ArrayDeque<>();
        stack.offer(1);
        stack.offer(2);
        stack.add(3);
        System.out.println(stack);
        System.out.println(stack.peek());
        System.out.println(stack.pop());
        System.out.println(stack);
    }
}
================================
[1, 2, 3]
1
1
[2, 3]

4,Set接口

Set接口也是Collection接口的子接口,但是与Collection或List接口不同的是,Set接口中不能加入重复的元素。

Set接口的定义如下:public interface Set<E> extends Collection<E>。Set接口与List接口的定义并没有太大的区别,Set接口并没有对Collection接口进行扩充,只是比Collection接口的要求更加严格,不能增加重复元素。

Set接口的实例无法像List接口那样可以进行双向输出,因为此接口没有提供像List接口定义的get(int index)方法。

Set的三个实现类: HashSet、TreeSet和EnumSet都是线程不安全的。 通常可以通过Collections工具类的synchronizedSortedSet方法来“包装”该Set集合。此操作最好是在创建时进行,以防止对Set集合的意外非同步访问:

SortedSet s = Collections.synchronizedSortedSet(new TreeSet<>());

4.1,散列的存放:HashSet

HashSet是Set接口的典型实现,大多数时候使用Set集合时就使用者实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的查取和查找性能。

HashSet的特点:

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
  • HashSet不是同步的,如果多个线程同时访问一个HashSet,无法保证安全性。
  • 元素值可以为null。
Set<String> allSet = new HashSet<String>();
allSet.add("A");allSet.add("B");allSet.add("C");allSet.add("C");allSet.add("C");
System.out.println(allSet);
=========================================
[A, B, C]

【HashSet 怎么保证元素不重复的?】当向HashSet集合中存放一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不想等,HashSet会把它们存储在不同位置,依然可以添加成功。HashSet集合判断两个元素相等的标准是:equals()相等并且hashCode()相等。

当把一个对象放入HashSet中时,如果需要重写该对象对应类的equals()方法,则也应该重写其hashCode()方法。规则是:如果两个对象通过equals()方法比较返回true,这两个对象的hashCode值也应该相同。

  • 如果两个对象通过equals()方法比较返回true,但这两个对象的hashCode()方法返回不同的hashCode值时,这将导致HashSet会把这两个对象保存在Hash表的不同位置,从而使两个对象都可以添加成功,这就与Set集合规则冲突。
  • 如果两个对象的hashCode()方法返回的hashCode值相同,但它们通过equals()方法比较返回false时更麻烦:因为两个对象的hashCode相同,HashSet试图将它们保存在同一个位置,但又不能做到,所以会在这位置上使用链式结构来保存多个对象,将会导致性能的下降。

HashSet中每个能存储元素的槽位称为:桶,如果有多个元素的hasCode值相同,但是它们通过equals()方法比较返回false,就需要在一个桶里放多个元素,这样就导致了性能的下降。

重写hashCode()方法的基本规则:

  • 在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。
  • 当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法应该返回相等的值。
  • 对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值。 

HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表来维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按照元素的添加顺序来访问集合里的元素。

  • LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时,有很好的性能。
  • 虽然LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,因此它依然不允许集合元素重复。 

4.2,排序接口:SortedSet

SortedSet接口主要用于排序操作,即实现此接口的子类都属于排序的子类。

方法描述
public Comparator<? super E> comparator()返回与排序有关联的比较器
public E first()返回集合中的第一个元素
public SortedSet<E> headSet(E toElement)返回从开始到指定元素的集合
public E last()返回最后一个元素
public SortedSet<E> subSet(E fromElement,E toElement)返回指定对象间的元素
public SortedSet<E> tailSet(E fromElement)从指定元素到最后
SortedSet<String> allSet = new TreeSet<String>();
allSet.add("A");allSet.add("E");allSet.add("D");allSet.add("B");allSet.add("C");
System.out.println(allSet);
System.out.println("第一个元素:"+allSet.first());
System.out.println("最后一个元素:"+allSet.last());
System.out.println("headSet元素:"+allSet.headSet("C"));
System.out.println("tailSet元素:"+allSet.tailSet("C"));
System.out.println("subSet元素:"+allSet.subSet("B","D"));
==================================================
[A, B, C, D, E]
第一个元素:A
最后一个元素:E
headSet元素:[A, B]
tailSet元素:[C, D, E]
subSet元素:[B, C]

4.3,有序的存放:TreeSet

TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。与HashSet相比,TreeSet提供了如下几个额外的方法:

TreeSet通用用法:

Set<String> allSet = new TreeSet<String>();
allSet.add("C");allSet.add("C");allSet.add("E");allSet.add("A");allSet.add("B");
System.out.println(allSet);
======================================
[A, B, C, E]

HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列。

原理:Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法。也就是说,添加到TreeSet里的集合元素必须实现Comparable接口。另外,大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象obj强制类型转换成相同类型。如果希望TreeSet能正常运作,TreeSet只能添加同一种类型的对象。

5,集合的输出

Iterator迭代输出,是使用最多的输出方式
ListIterator是Iterator的子接口,专门用于输出List中的内容
Enumeration是一个旧的接口,功能与Iterator类似
foreachJDK1.5之后提供的新功能,可以输出数字或集合

5.1,迭代输出:Iterator

Iterator是专门的迭代输出接口,所谓的迭代输出就是将元素一个个进行判断,判断是否有内容,如果有则把内容输出。Iterator仅用于遍历集合,Iterator本身并不提供盛装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合(Collection对象)。

方法描述
public boolean hasNext()判断是否有下一个值
public E next()取出当前元素
public void remove()移除当前元素

1,输出Collection中的全部内容

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        List<String> allList = new ArrayList<String>();
        allList.add("A");allList.add("B");allList.add("C");allList.add("D");allList.add("E");
        Iterator<String> iterable = allList.iterator();
        while (iterable.hasNext()){
            System.out.print(iterable.next()+"、");
        }
    }
}
============================
A、B、C、D、E、

2,使用Iterator删除指定内容:当使用Iterator迭代访问Collection集合元素时,Collection集合里的元素不能被改变,只有通过Iterator的remove()方法删除上一次next()方法返回的集合元素才可以;否则将会引起java.util.ConcurrentModificationException异常。

while (iterable.hasNext()){
    String str = iterable.next();
    if ("C".equals(str)){
        allList.remove(str);
    }else{
        System.out.print(str+"、");
    }
}
================================
A、B、Exception in thread "main" java.util.ConcurrentModificationException

如果删除的是"D":

while (iterable.hasNext()){
    String str = iterable.next();
    if ("D".equals(str)){
        allList.remove(str);
    }else{
        System.out.print(str+"、");
    }
}
=============================
A、B、C、

对于HashSet、ArrayList等,迭代时删除元素都会导致异常——只有删除集合中某个特定元素时才不会抛出异常,这是由集合类的实现决定的,程序员不应该这么做。

正确的删除:

while (iterable.hasNext()){
    String str = iterable.next();
    if ("D".equals(str)){
        iterable.remove();
    }else{
        System.out.print(str+"、");
    }
}
===================
A、B、C、E、

5.2,双向迭代输出:ListIterator

Iterator接口的主要功能是由前向后单向输出,而此时如果想要实现有后向前或由前向后的双向输出,则必须使用Iterator的子接口。

方法描述
public boolean hasNext()判断是否有下一个值
public E next()取出当前元素
public void remove()移除当前元素
public void add(E o)将指定元素增加集合
public boolean hasPrevious()判断是否有上一个元素
public E previous()取出当前元素
public int nextIndex()返回下一个元素的索引号
public int previousIndex()返回上一个元素的索引号
public void set(E o)替换元素

1,进行双向迭代

List<String> allList = new ArrayList<String>();
allList.add("A");allList.add("B");allList.add("C");allList.add("D");allList.add("E");
ListIterator<String> iterator = allList.listIterator();
System.out.print("由前向后输出:");
while (iterator.hasNext()){
    String str = iterator.next();
    System.out.print(str+"、");
}
System.out.println();
System.out.print("由后向前输出:");
while (iterator.hasPrevious()){
    String str = iterator.previous();
    System.out.print(str+"、");
}
===================================
由前向后输出:A、B、C、D、E、
由后向前输出:E、D、C、B、A、

2,增加及替换元素

List<String> allList = new ArrayList<String>();
allList.add("A");allList.add("B");allList.add("C");allList.add("D");allList.add("E");
ListIterator<String> iterator = allList.listIterator();
while (iterator.hasNext()){
    String str = iterator.next();
    System.out.print(str+"、");
}
iterator.add("F");
System.out.println();
while (iterator.hasPrevious()){
    String str = iterator.previous();
    System.out.print(str+"、");
}
System.out.println();
System.out.print(allList);
=====================================
A、B、C、D、E、
F、E、D、C、B、A、
[A, B, C, D, E, F]

5.3,fail-fast和fail-safe

fail-fast机制:指的是在集合对象遍历的过程中,如果遇到并发修改操作(例如在迭代过程中修改了集合中的元素),则会抛出ConcurrentModificationException异常,停止迭代(默认)。

fail-safe机制:指的是在集合对象遍历的过程中,不会抛出ConcurrentModificationException异常,因为迭代器在遍历过程中不是直接对原始集合对象进行操作,而是对集合对象的副本进行操作。

6,Map接口

与Collection类似,如果要想使用Map接口必须依靠其子类实例化。Map接口中常用的子类如下:

  • HashMap:无序存放,是新的操作类,key不允许重复。
  • Hashtable:无序存放,是旧的操作类,key不允许重复。
  • TreeMap:可以排序的Map集合,按集合中的key排序,key不允许重复。
  • WeakHashMap:弱引用的Map集合,当集合中的某些内容不再使用时清除掉无用的数据,使用gc进行回收。
  • IdentityHashMap:key可以重复的Map集合。

6.1,新的子类:HashMap

HashMap的特点:HashMap是线程不安全的实现、HashMap可以使用null作为key或value。

HashMap底层实现原理:它基于hash算法,通过put方法和get方法存储和获取对象。HashMap为什么线程不安全(不支持并发):

  • Hash冲突导致链表形成环形,死循环:当两个线程同时对HashMap进行扩容或者添加节点操作时,可能会在同一个位置发生Hash冲突,导致链表形成环形,形成死循环,从而使CPU无法释放资源。

  • 并发修改导致链表结构被破坏,导致数据丢失:当两个线程同时对HashMap进行修改操作时,比如同时添加节点、删除节点等,可能会导致链表结构被破坏,从而导致数据丢失。

HashMap如何实现线程安全:直接使用Hashtable类;直接使用ConcurrentHashMap;使用Collections将HashMap包装成线程安全的Map。

HashMap中的循环链表是如何产生的?:在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。

HashMap如何解决哈希冲突?:为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时,会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时,又会将红黑树转换回单向链表提高性能。

还有其他解决哈希冲突的方式吗?:

  • 开放定址法:当发生冲突时,使用一个探测序列来寻找下一个可用的哈希桶。常见的探测序列包括线性探测、二次探测和双重哈希等。

  • 链接法:将哈希表的每个桶都设置为链表头节点,当有冲突发生时,将新的键值对插入到链表中。这样相同哈希值的键值对可以通过链表存储在同一个桶内。

  • 建立更好的哈希函数:使用一个更好的哈希函数可以减少哈希冲突的概率。好的哈希函数能够均匀地将键值映射到桶中,降低冲突的可能性。

  • 拉链法:类似于链接法,但是每个键值对有多个哈希函数,可以选择另一个哈希桶进行插入,以避免冲突。

  • 完美哈希(Perfect Hashing):对于已知的键集,可以使用完美哈希函数,确保没有冲突发生。这通常需要在构建哈希表之前进行预处理。

HashMap的扩容机制(resize 方法的执行过程):

  • 数组的初始容量为16,而容量是以2的次方扩充的。
  • 数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
  • 为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。
  • 对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组。所以上面也说了链表长度的阈值是7或8,因为会有一次放弃转换的操作。

JDK8的改变:

  • JDK7中的HashMap,是基于数组+链表来实现的,它的底层维护一个Entry数组。它会根据计算的hashCode将对应的KV键值对存储到该数组中,一旦发生hashCode冲突,那么就会将该KV键值对放到对应的已有元素的后面, 此时便形成了一个链表式的存储结构。
  • JDK7中HashMap的实现方案有一个明显的缺点,即当Hash冲突严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低,其时间复杂度为O(N)。
  • JDK8中的HashMap,是基于数组+链表+红黑树来实现的,它的底层维护一个Node数组。(1)当链表的存储的数据个数大于等于8(TREEIFY_THRESHOLD=8)的时候,不再采用链表存储,而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化,链表为O(N),而红黑树一直是O(logN),可以大大的提高查找性能。(2)当链表的存储的数据个数小于6个(UNTREEIFY_THRESHOLD = 6)该红黑树就会被转换成链表。

【问题】HashMap为什么用红黑树而不用B/B+树?B/B+树多用于外存上时,B/B+也被成为一个磁盘友好的数据结构。HashMap本来是数组+链表的形式,链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面,这个时候遍历效 率就退化成了链表。

【问题】自定义类作为HashMap的key需要做什么?需要重写该类的hashCode()和equals()方法。这是因为HashMap底层使用哈希表实现,哈希表中的每个元素都是一个键值对,其中键的值是通过哈希函数计算得到的一个整数,该整数用于确定键值对在哈希表中的位置。为了保证哈希表的正确性,需要保证具有相同键的对象在哈希表中的hashCode值相等,同时equals()方法也需要正确实现,以保证具有相同键的对象在哈希表中可以正确比较相等。

【问题】hashmap如何初始化才能使得put效率最高?在使用 HashMap 时,我们应该根据实际情况设置合适的初始容量和负载因子以提高 put 方法的效率。

  • HashMap 的初始容量指的是底层数组的大小,如果数组过小,就会导致哈希冲突的概率增大,进而影响性能;反之,如果数组过大,会浪费内存。因此,我们可以通过自行计算或基于经验选择一个初始容量。
  • 而负载因子则是当哈希表大小达到总容量乘以负载因子时,便会触发扩容操作。因此,负载因子设置不合理会对性能产生影响。一般来说,我们希望负载因子尽可能的小,这样可以减少哈希冲突的概率,但同时也会增加空间使用的开销。根据 JDK1.8 版本的文档,建议默认负载因子 0.75 是比较合适的设置。

【put 方法的执行过程】

  • 首先,根据key的hashCode值和HashMap的长度计算该键值对在HashMap中的索引位置,如果该位置为空,则直接将该键值对插入到该位置,并返回null。

  • 如果该位置已经存在键值对,则需要进行以下步骤:(1)遍历该位置的链表,查找是否已经存在相同的key。如果存在,则用新的value替换旧的value,并返回旧的value。(2)如果不存在相同的key,则将该键值对插入到链表的末尾,并返回null。

  • 如果HashMap中的元素个数超过了负载因子(load factor)阈值(默认为0.75),则需要进行扩容操作。扩容操作会创建一个新的数组,并将原来的键值对重新计算索引位置并插入到新的数组中。

当多个键值对的hashCode值相同时,它们会被插入到同一个索引位置的链表中,这就需要在查找和插入时遍历该链表,以保证HashMap的正确性。同时,为了保证HashMap的性能,在插入时需要尽可能保持哈希冲突的概率较低,这就需要合理选择哈希函数和负载因子的值。

【get 方法的执行过程】

  • 首先,根据key的hash值和HashMap的容量(table数组的长度),通过哈希函数计算出key在table数组中的位置(也叫桶),通常使用hash & (table.length - 1)的方式计算,其中hash为key的哈希值。

  • 如果该桶的元素为空,直接返回null;否则进入下一步。

  • 遍历该桶中所有的元素,找到key值相同的元素,如果找到则返回该元素的值;否则返回null。

【resize 为啥是2的n次方】一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模运算(据说提升了5~8倍)。

  • 如果容量为 2 的 n 次方,那么扩容时只需要将容量翻倍即可,得到足够大的数组,而不用考虑其他复杂的调整方式,这也可以提高 HashMap 的性能。
  • 使用位运算代替取模运算:如果散列表的容量为 2 的 n 次方,那么在计算索引位置时,就可以使用位运算(& 操作)代替取模操作。而计算机处理二进制比其他进制更高效,所以使用 2 的 n 次方作为 HashMap 的容量,可以确保计算索引时只需进行位运算,这比对其他数字取模或者除法等操作更高效。

 【equals&hashCode】

import java.util.HashMap;
import java.util.Random;

public class Main {
    public static void main(String[] args) {
        Test a = new Test(1);
        Test b = new Test(1);
        HashMap<Test, String> hashMap = new HashMap<Test, String>();
        hashMap.put(a, "1");
        hashMap.put(b, "1");
        System.out.println(hashMap.size());
        hashMap.clear();

        hashMap.put(a, "1");
        hashMap.put(a, "2");
        System.out.println(hashMap.size());
        hashMap.clear();

        hashMap.put(a, "1");
        hashMap.put(a, "1");
        System.out.print(hashMap.size());
        hashMap.clear();

    }

    static class Test {
        private int key;

        public Test(int k) {
            this.key = k;
        }

        public int hashCode() {
            return new Random().nextInt(101);
        }

        public boolean equals(Object object) {
            return true;
        }
    }
}

 【HashMap】 

import java.util.LinkedList;

class Entry<K, V> {
    K key;
    V value;

    public Entry(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

public class MyHashMap<K, V> {
    private static final int DEFAULT_CAPACITY = 16;
    private LinkedList<Entry<K, V>>[] buckets;

    public MyHashMap() {
        this(DEFAULT_CAPACITY);
    }

    public MyHashMap(int capacity) {
        buckets = new LinkedList[capacity];
        for (int i = 0; i < capacity; i++) {
            buckets[i] = new LinkedList<>();
        }
    }

    private int hash(K key) {
        return Math.abs(key.hashCode()) % buckets.length;
    }

    public void put(K key, V value) {
        int index = hash(key);
        LinkedList<Entry<K, V>> bucket = buckets[index];

        for (Entry<K, V> entry : bucket) {
            if (entry.key.equals(key)) {
                entry.value = value;
                return;
            }
        }

        bucket.add(new Entry<>(key, value));
    }

    public V get(K key) {
        int index = hash(key);
        LinkedList<Entry<K, V>> bucket = buckets[index];

        for (Entry<K, V> entry : bucket) {
            if (entry.key.equals(key)) {
                return entry.value;
            }
        }

        return null;
    }

    public void remove(K key) {
        int index = hash(key);
        LinkedList<Entry<K, V>> bucket = buckets[index];

        for (Entry<K, V> entry : bucket) {
            if (entry.key.equals(key)) {
                bucket.remove(entry);
                return;
            }
        }
    }
}

 【输出全部的key和value】 

Map<String,String> map = new HashMap<>();
map.put("name","燕双嘤");map.put("age","22");
Set<String> keys = map.keySet();
Iterator<String> iterator = keys.iterator();
System.out.print("全部的key:");
while(iterator.hasNext()){
    System.out.print(iterator.next()+"、");
}
System.out.println();
Collection<String> value = map.values();
Iterator<String> iterator2 = value.iterator();
System.out.print("全部的value:");
while (iterator2.hasNext()){
    System.out.print(iterator2.next()+"、");
}

6.2,ConcurrentHashMap

HashMap和ConcurrentHashMap:

  • HashMap是非线程安全的(当程序要从 HashMap 中获取和放置 key-value 对时,需要先计算 key 对应的 hashCode,然后根据 hashCode 去计算该 key-value 所在的 bucket 位置),这意味着不应该在多线程中对这些Map进行修改操作,否则会产生数据不 一致的问题,甚至还会因为并发插入元素而导致链表成环,这样在查找时就会发生死循环,影响到整个 应用程序。
  • Collections工具类可以将一个Map转换成线程安全的实现,其实也就是通过一个包装类,然后把所有功能都委托给传入的Map,而包装类是基于synchronized关键字来保证线程安全的(Hashtable也是基于 synchronized关键字),底层使用的是互斥锁,Collections.synchronizedMap() 只是 HashMap 简单的同步版本,因此它使用的也是最简单的同步行为,因此在这种方式下,每次最多只允许一个线程来 Collections.synchronizedMap()进行读、写,其他 线程处于阻塞、等待状态。性能与吞吐量比较低。
  • 在并发情况下,ConcurrentHashMap的效率通常比HashTable高,这是因为ConcurrentHashMap在内部实现上采用了分段锁(Segment),而HashTable使用的是全局锁(synchronized)。分段锁能够使得多线程并发访问时,只要访问的元素不在同一个段(Segment)内,就可以实现真正的并行处理,提高了并发性能。而全局锁会在任意一个线程访问HashTable时将整个HashTable锁住,导致其他线程无法同时进行操作,降低了并发性能。
  • ConcurrentHashMap在扩容时只需要对某个Segment进行扩容,不会像HashTable一样将整个数据结构进行扩容,因此在并发情况下,ConcurrentHashMap的性能也更优。

ConcurrentHashMap 增加了Segment(分段锁)的概念。基本上你可以把每个Segment(分段锁)近似地当成一个独立的 HashMap,ConcurrentHashMap 在初始化时被划分为若干个 Segment(默认是 16 个),这样 ConcurrentHashMap 最多允许与 Segment 相同数量(16) 的线程同时访问这些 Segment,以便在高并发性期间每个线程在特定的 Segment 上工作。在这种方式下,假如某个线程要访问 key-value 对保存在 Segment 10th 上,程序只需要对该 Segment 加锁,无需锁定剩余的15个 Segment。ConcurrentHashMap使用多个锁,每个锁控制一个Map的一个Segment,当在特定的 Segment中设置key-value时,只需对该Segment进行锁定,段中设置数据时,将获得该段的锁定。所以基本上更新操作都是同步的。 在获取数据时,只需要使用volatile读取,而不需要使用任何同步,如果volatile读取导致未命中,则获得该Segment 的锁,并在 synchronized代码块中再次搜索该key-value对(允许null)

【JDK1.8之后的改进】

  • 数据结构改进:JDK1.8中的ConcurrentHashMap采用了与HashMap类似的数组+链表+红黑树的数据结构,取代了之前的分段锁设计。这种设计使得ConcurrentHashMap在并发读写时能够更好地利用多核CPU的计算能力,从而提高并发性能。

  • 红黑树优化:当一个桶(bucket)中的元素超过一定数量(默认为8)时,ConcurrentHashMap会将链表转换为红黑树,以提高查找效率。同时,在JDK1.8中还优化了红黑树的插入、删除、查找等操作,进一步提高了性能。

  • 计数器统计:在JDK1.8中,ConcurrentHashMap新增了一个计数器统计机制,用于在进行并发操作时更加准确地统计元素的数量。这个机制能够避免由于多线程操作导致的数据不一致问题,同时也提高了并发性能。

  • 扩容优化:在扩容时,JDK1.8中的ConcurrentHashMap会将数组分成更小的部分进行扩容,从而降低扩容的开销,提高了并发性能。

6.3,LinkedHashMap

LinkedHashMap继承了HashMap类(key可以为null),并实现了Map接口。与HashMap类似,不同之处在于,LinkedHashMap中维护着一个按照插入顺序排序的双向链表。具体来说,LinkedHashMap将每个键值对都存储为一个Entry对象,并且在Entry对象内部维护了前驱、后继和哈希碰撞链表(即HashMap中的桶)指针。同时,LinkedHashMap还维护着一个boolean类型的accessOrder属性,用于表示遍历顺序是否按照缓存访问顺序排序。如果accessOrder被设置为true,那么在每次访问一个Entry时,该Entry会被移到链表的尾部,以保证最近访问的元素总是排在最后面。

LinkedHashMap的主要优点是提高了元素的查找速度和遍历速度,因为它利用了双向链表来维护元素的插入顺序或者缓存访问顺序,而缺点是占用的空间稍大于HashMap,因为需要额外维护链表指针的信息。

  • 记录最新访问的数据:可以使用LinkedHashMap来记录最新访问的数据,即把最近访问的数据放在链表尾部。这种情况通常出现在缓存中,例如缓存一些热门文章或商品信息,在用户访问时使用。

  • LRU缓存淘汰算法:LRU(Least Recently Used)是一种缓存淘汰算法,即根据最近访问时间将一些缓存中的元素淘汰出去。LinkedHashMap提供了accessOrder构造方法,当accessOrder设置为true时,可以按照缓存访问顺序排序,从而方便实现LRU缓存淘汰算法。

  • 实现基于FIFO(First In First Out)的缓存功能:通过重写LinkedHashMap中的removeEldestEntry方法,可以实现基于FIFO的缓存功能。

6.4,旧的子类:Hashtable

Hashtable也是Map接口的一个子类,与Vector类的推出时间一样,都属于旧的操作类,与HashMap使用没有太大的区别。甚至类名都不符合Java命名规范,过于老旧。

HashMap与Hashtable的区别:

比较HashMapHashtable
推出时间JDK1.2之后,属于新的操作类JDK1.0时推出,属于旧的操作类
性能采用异步处理方式采用同步处理方式,性能较低
线程安全属于非线程安全的操作类,不支持并发属于线程安全的操作类,支持并发
空键允许将key设置为Null不允许将key设置为null
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        HashMap ht = new HashMap();
        ht.put(null, null);
        ht.put(null, null);
        ht.put("a", null);
        System.out.println(ht);
    }
}
==========================
{null=null, a=null}

6.5,排序接口:SortedMap接口

Sorted接口时排序接口,只要实现了此接口的子类,都属于排序的子类。

方法描述
public Comparator<? super K> comparator返回比较器对象
public K firstKey()返回第一个元素的key
public SortedMap<K,V> headMap(K toKey)返回小于等于指定key的部分集合
public K lastKey()返回最后一个元素的key
public SortedMap<K,V> subMap(K fromKey,K toKey)返回指定key范围的集合
public SortedMap<K,V> tailMap(K fromKey)返回大于指定key的部分集合
public class HelloWord {
    public static void main(String[] args) throws Exception {
        SortedMap<String,String> map = new TreeMap<>();
        map.put("A","ysy");
        map.put("B","by");
        map.put("C","dm");
        System.out.println(map.firstKey()+"--->"+map.get(map.firstKey()));
        System.out.println(map.lastKey()+"--->"+map.get(map.lastKey()));
        System.out.println("============================");
        for (Map.Entry<String,String> me:map.headMap("B").entrySet()){
            System.out.println(me.getKey()+"--->"+me.getValue());
        }
        System.out.println("============================");
        for (Map.Entry<String,String> me:map.tailMap("B").entrySet()){
            System.out.println(me.getKey()+"--->"+me.getValue());
        }
        System.out.println("============================");
        for (Map.Entry<String,String> me:map.subMap("A","C").entrySet()){
            System.out.println(me.getKey()+"--->"+me.getValue());
        }
    }
}
==========================================================
A--->ysy
C--->dm
============================
A--->ysy
============================
B--->by
C--->dm
============================
A--->ysy
B--->by

6.6,排序的子类:TreeMap

public class TreeMap<K,V>extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable

TreeeMap可以根据key进行排序,它是通过红黑树实现的。对Integer来说,其自然排序就是数字的升序;对String来说,其自然排序就是按照字母表排序。

Map<String,String> map = new TreeMap<>();
map.put("A","燕双嘤");
map.put("C","22");
map.put("B","第四野战军特务营营长燕双嘤");
Set<String> keys = map.keySet();
Iterator<String> iterator = keys.iterator();
while (iterator.hasNext()){
    String str = iterator.next();
    System.out.println(str+"-->"+map.get(str));
}
============================================
A-->燕双嘤
B-->第四野战军特务营营长燕双嘤
C-->22

String类本身已经实现了Comparable接口,如果使用自定义类作为key,必须实现Comparable接口。

6.7,Map接口的使用注意事项

1,不能直接使用迭代输出Map中的内容:Map集合的内容在开发中基本上作为查询的应用较多,全部输出的操作较少。而Collection接口在开发中的主要作用就是用来传递内容及输出。对于Map接口来说,其本身不能直接使用迭代(Iterator,foreach)进行输出的,因为Map接口中的每个位置存放的时一对值(key➡value),而Iterator中每次只能找到一个值。所以,如果非要使用迭代进行输出,则必须按照一定步骤进行。

  • 将Map接口的实例通过entrySet()变为Set接口对象。
  • 通过Set接口实例化为Iterator实例化。
  • 通过Iterator迭代输出,每个内容都是Map.Entry的对象。
  • 通过Map.Entry进行key➡value的分离。
Map<String,String> map = new HashMap<>();
map.put(new String("A"),"燕双嘤");
map.put(new String("B"),"第四野战军特务营营长燕双嘤");
map.put(new String("C"),"22");
Set<Map.Entry<String,String>> allSet = map.entrySet();
Iterator<Map.Entry<String,String>> iterator = allSet.iterator();
while (iterator.hasNext()){
    Map.Entry<String,String> me = iterator.next();
    System.out.println(me.getKey()+"-->"+me.getValue());
}
============================================
A-->燕双嘤
B-->第四野战军特务营营长燕双嘤
C-->22
Map<String,String> map = new HashMap<>();
map.put(new String("A"),"燕双嘤");
map.put(new String("B"),"第四野战军特务营营长燕双嘤");
map.put(new String("C"),"22");
for(Map.Entry<String,String> me:map.entrySet()){
    System.out.println(me.getKey()+"-->"+me.getValue());
}
===================================
A-->燕双嘤
B-->第四野战军特务营营长燕双嘤
C-->22

2,直接使用非系统类作为key:既然在Map接口中的内容都是以“key-->value”的形式出现的,而且在声明Map对象时,也是使用了泛型声明类型,那么现在就应该可以使用一个"字符串"表示key,用一个Person对象表示value。

class Person{
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}
public class HelloWord {
    public static void main(String[] args) throws Exception {
        Map<Person,String> map = new HashMap<>();
        map.put(new Person("张三",30),"zhangsan");
        System.out.println(map.get(new Person("张三",30)));
    }
}
==================================
null
Person person = new Person("张三",30);
map.put(person,"zhangsan");
System.out.println(map.get(person));
===================================
zhangsan

可以发现,根据key能够找到对应的value,但是为什么此时可以找到,而之前不可以找到?因为之前使用了匿名对象,每次在栈内存的地址都不同,所以无法找到。如果想要使用匿名类进行寻址,必须覆写equals()和hashCode()方法。

6.8,key可以重复的Map集合:IdentityHashMap

Map<String,String> map = new HashMap<>();
map.put("燕双嘤","ysy");map.put("燕双嘤","ysy");
map.put("杜马","dm");
System.out.println(map);
===============================
{燕双嘤=ysy, 杜马=dm}
Map<String,String> map = new IdentityHashMap<>();
map.put(new String("燕双嘤"),"ysy");map.put(new String("燕双嘤"),"ysy2");
map.put("杜马","dm");
System.out.println(map);
==============================
{燕双嘤=ysy2, 燕双嘤=ysy, 杜马=dm}

因为此时两个”燕双嘤“的栈内存地址一样,前面的会被覆盖。

7,Stack类和Properties类

7.1,Stack类

栈是采用先进后出的数据存储方式,每一个栈都包含一个栈顶,每次出栈是将栈顶的数据输出。在Java中使用Stack类进行栈的操作,Stack类是Vector类的子类。Stack类的定义如下:

public class Stack<E> extends Vector<E>
方法类型描述
public boolean empty()常量测试栈是否为空
public E peek()常量查看栈顶,但不删除
public E pop()常量出栈,同时删除
public E push()普通入栈
public int search(Obect o)普通在栈中查找
public class Main {
    public static void main(String[] args) {
        Stack<String> a = new Stack<>();
        a.push("A");
        a.push("B");
        a.push("C");
        System.out.print(a.pop() + "、");
        System.out.print(a.pop() + "、");
        System.out.print(a.peek() + "、");
        System.out.print(a.pop() + "、");
        System.out.print(a.peek());
    }
}
===========================================
C、B、A、A、Exception in thread "main" java.util.EmptyStackException
	at java.util.Stack.peek(Stack.java:102)
	at Main.main(Main.java:13)

7.2,属性类:Properties

Properties类简介:在Java中属性操作类是一个较为重要的类。而要想明白属性操作类,就必须先清楚什么叫属性文件,在一个属性文件中保存了多个属性,每一个属性就是直接用字符串表示出来的key=value对,而如果要想轻松操作这些属性文件中的属性,可以通过Properties类方便地完成。Properties类本身是Hashtable类的子类,既然是其子类,则肯定按照key和value的形式存储数据。

public class Properties extends Hashtable<Object,Object>
方法类型描述
public Properties()构造构造一个空的属性类
public Properties(Properties defaults)常量构造一个指定属性内容的属性类
public String getProperty(String key)常量根据属性的key取得属性的value,如果没有key则返回null
public String getProperty(String key,String defaultValue)普通根据属性的key取得属性的value,如果没有key则返回defaultValue
public Object setProperty(String key,String value)普通设置属性
public void list(PrintStream out)普通属性打印
public void load(InputStream inStream) throws IOException普通

从输入流中取出全部的属性内容

public void store(OutputStream out,String comments) throws IOException普通将属性内容通过输出输入流输出,同时声明属性的注释
public void storeToXML(OutputStream os,String comment)throws IOException普通以XML文件格式输出属性,默认编码
public void storeToXML(OutputStream os,String vomment,String encoding) throws IOException普通以XML文件格式输出属性,用户指定默认编码

虽然Properties类是Hashtable的子类,也可以像Map那样使用put()方法保存任意类型的数据,但是一般属性都是由字符串组成的。另外:loadFormXML()可以读取XML文件。

设置和取得属性:可以使用getProperty()和getProperty()方法设置和取得属性,操作时要以String为操作类型。

public class Main {
    public static void main(String[] args) {
        Properties pro = new Properties();
        pro.setProperty("ysy","燕双嘤");
        pro.setProperty("dm","杜马");
        System.out.println(pro.getProperty("ysy"));
        System.out.println(pro.getProperty("dm"));
    }
}

将属性保存在普通文件中:正常属性类操作完成之后,可以将其内容保存在文件中,那么直接使用store()方法即可,同时指定OutputStream类型,指明输出的位置。属性文件的后缀是任意的,但是最好按照标准,将属性文件的后缀统一设置成:*.properties。

public class Main {
    public static void main(String[] args) throws IOException {
        Properties pro = new Properties();
        pro.setProperty("ysy","燕双嘤");
        pro.setProperty("dm","杜马");
        File file = new File("D:"+File.separator+"ysy.properties");
        pro.store(new FileOutputStream(file),"Name");
    }
}
#Name
#Mon Mar 01 13:27:30 CST 2021
dm=\u675C\u9A6C
ysy=\u71D5\u53CC\u5624

从普通文件中读取属性内容:可以通过load()方法,从输入流中将所保存的所有属性内容读取出来。

Properties pro = new Properties();
File file = new File("D:" + File.separator + "ysy.properties");
pro.load(new FileInputStream(file));
System.out.println(pro.getProperty("ysy") + "、" + pro.getProperty("dm"));
=====================================================
燕双嘤、杜马

将属性保存在XML文件中:在Properties类中也可以把全部内容以XML格式的内容通过输出流输出,如果把属性保存在XML文件中,则文件后缀最好为:*.xml。

File file = new File("D:"+File.separator+"ysy.xml");
pro.storeToXML(new FileOutputStream(file),"Name");
===================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>Name</comment>
<entry key="dm">杜马</entry>
<entry key="ysy">燕双嘤</entry>
</properties>

从XML文件中读取属性:以XML文件格式读取全部属性之后,必须要使用loadFromXML()方法将内容读取进来。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

燕双嘤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值