Java笔记10
Java集合大致分为四大类,Set,List,Queue,Map。Set代表不记录插入顺序,不可重复的集合;List代表记录顺序,可重复的集合;Map代表具有映射关系的集合;Queue代表了队列的集合。集合可以存储数量不等的对象,实现常用的数据结构。集合就像是一种容器,在Java5之前,将对象的引用放入后,集合会丢失所有对象的数据类型,都当作Object类处理。Java5之后增加泛型,Java集合可以记住容器中对象的数据类型。
集合概述
在初期阶段,要保存数据,通常使用数组。但是这种结构的灵活性很差比如,保存数量不确定的数据和具有映射关系的数据。Java的集合类也称为容器类,位于java.util下,java.util.concurrent下还提供了一些多线程支持的集合类。集合中只能保存对象(引用变量)。集合类主要由两个接口派生:Collection和Map。
Set和List接口分别代表了不记录插入顺序和记录插入顺序的集合,Queue时Java提供的队列实现,与List相似。
Map接口的实现类非常多,主要的特点都是保存数据要用key-value对,key是数据的唯一标识,不可重复。对于粗线框起来的四个接口,大致可以看成三类,List的可以记住顺序,List长度可变,根据下表访问;Map的访问需要通过key值;Set无法记住顺序,所以是通过元素本身访问,不能重复。Queue有点特殊,不在此列。
Collection和Iterator接口
Collection接口是List,Set和Queue接口的父接口,它自生定义了一些方法:
- boolean add(Object o):该方法用于向集合添加一个元素,如果集合对象被添加操作改变了,返回true
- boolean addAll(Collection c):该方法用于将集合c中的所有元素添加到指定集合,返回值同上
- void clear():清除集合内的所有元素,将长度变为0
- boolean contains(Object o):返回集合里是否包含指定元素
- boolean containsAll(Collection c):返回集合里是否包含集合c里的所有元素
- boolean isEmpty():返回集合是否为空
- Iterator iterator():返回一个Iterator对象(迭代器),用于遍历集合里的元素
- boolean remove(Object o):删除集合中的指定元素o,当包含一个或多个元素o时,将删除第一个,返回true
- boolean removeAll(Collection c):从集合中删除集合c里包含的所有元素,如果删除了一个或以上的元素,返回true
- boolean retain(Collection c):从集合中删除集合c里不包含的元素,如果该操作改变了调用方法的集合,则返回true
- int size():返回集合中元素的个数
- Object[] toArray():把集合转换为一个数组
所有的Collection实现类都重写了toString()方法,System.out.println(c)
将把集合中全部元素打印出来。
使用Lambda表达式遍历集合
查看Collection接口的源码,发现它继承了Iterable接口。Java8为Iterable接口增加了一个forEach(Consumer action)的默认方法,该方法所需参数的类型是一个函数式接口。因此Collection集合也可以调用该方法。Iterable接口中的该方法的具体代码如下,Consumer是另一个接口,这里的语法涉及到泛型,先不做解释。
1 | default void forEach(Consumer<? super T> action) { |
Consumer接口中相关代码如下:
1 |
|
当调用forEach(Consumer action)方法时,程序会将集合元素传给Consumer的accept(T t)方法,因为该接口只有这一个抽象方法,所以这是个函数式接口,所以可以用Lambda表达式。
1 | import java.util.Collection; |
上面的代码会有warning,因为没用泛型,可以忽略。
使用Java8增强的Iterator遍历元素
Iterator接口也是Java集合框架的成员,但不用与容器,只作为迭代器。注意与Iterable接口的区别。Iterator接口中定义了如下四个方法:
- boolean hasNext():如果被迭代集合元素还没有被遍历完,则返回true
- Object next():返回集合里的下一个元素
- void remove():删除集合里上一次next方法返回的元素
- void forEachRemaining(Consumer action):Java8新增的默认方法,该方法可以使用Lambda表达式来遍历集合元素
前面的Iterable接口就是包含了Iterator<T> iterator()
方法,所以实现了迭代。那么为什么要这样用一个接口包含迭代相关的方法,再用另一个接口包含呢?参看链接:https://blog.csdn.net/github_39424631/article/details/75048955
1 | import java.util.Iterator; |
Iterator对象必须依靠一个集合对象。这段代码由两个地方值得注意,一是book = "测试字符串";
这句并不会修改集合中的值,因为是值传递。二是it.remove();
删除的是上一次的next返回值。如果改books.remove(book);
修改当前的next返回值就会出java.util.ModificationException异常。因为是不允许在迭代过程中集合的,后面的HashSet,ArrayList都是如此。因为如果可以修改可能会产生因为资源共享而引发的潜在问题。而且,Iterator采用的是快速失败(fail-fast)机制,一旦检测到集合修改就立即引发异常。
使用Lambda表达式遍历Iterator
与使用Iterable类似。forEachRemaining(Consumer action)的代码:
1 | default void forEachRemaining(Consumer<? super E> action) { |
使用foreach循环遍历集合元素
相较于用Iterator接口迭代访问,foreach反而更加方便。使用类似for(Object obj : books)
的语句就可以,但是任然不可以在迭代中修改集合的元素。参考链接:https://segmentfault.com/q/1010000003775035?_ea=362485
用Java8新增的Predicate操作集合
Java8为Collection集合新增了一个removeIf(Predicate filter)方法,代码如下:
1 | default boolean removeIf(Predicate<? super E> filter) { |
1 | books.removeIf(ele -> ((String)ele).lenght() < 3); |
这句代码的意思是删除集合中符合条件的元素
1 | import java.util.function.Predicate; |
ele -> ((String) ele).length() > 10)
这句产生了一个Predicate接口的实例,这个对象被传入calAll方法。然后迭代books,将里面的元素传入这个实例的test()方法,具体形参就是ele,test()方法的实现过程就是Lambda表达式的过程。
使用Java8新增的Stream操作集合
关于Stream的详细内容,单开一篇写,书上写的没头没尾非常糟糕。
Set集合
Set集合的最大特点就是不能重复,Set集合通常不能记住元素的添加顺序,Set集合与Collection基本相同,没有提供任何额外的方法。当试图添加一个重复的元素,add()方法会返回false。这些事Set集合的通用知识,HashSet,TreeSet,EnumSet三个实现类又各有不同。
HashSet类
HashSet是Set的典型接口,大多数就是使用这个类HashSet按Hash算法来存储集合中的元素。具有如下特点:
- 不能保证元素的排列顺序
- HashSet不是同步的,如果由多个线程同时访问一个HashSet,必须通过代码来保证同步
- 集合元素值可以是null
当向HashSet中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该元素的hashCode值。HashSet判断元素相等的条件是:equals()方法结果为true,hashCode相同。只有这样才算做两个一样的元素。在重写hashCode()方法和equals()方法时,应注意,equals()返回true时,hashCode也应相同。因为如果不同,那么就会添加成功,使得存在两个相同的元素。(这里的相同指按照自定义规则的equals方法的结果,本来Object中要求同一个对象才能返回true,但是重写后规则改变,可能是不同对象内容相同。)
上面说如果equals方法返回true,hashCode却不同会出现两个一样的值;反过来,如果hashCode相同,equals却不同,HashSet会试着将两个元素保存在一个地方,实际就会用一种链式结构(?)来保存多个对象。HashSet访问元素是根据hashCode值快速定位的,如果具有两个相同的hashCode值,就会导致性能下降。
总之不论为什么,重写equals和重写hashCode必须同时进行。
hash被译为哈希,散列。hash算法的价值在于速度,该算法可以直接根据hashCode值算出元素的存储位置,从而快速定位该元素。类似于数组的索引,只是hashCode的值不是连续的。
因为默认的hashCode方法就是返回当前对象的地址。所以一般需要重写,规则如下:
- 程序中多次调用同一个对象的hashCode方法应该返回相同值
- 当equals返回了true,那么hashCode应该相同(equals返回false,hashCode也可以相同,但是应竭力避免)
- 对象中用于equals方法作比较标准的实例变量,都应该被用于计算hashCode值
具体步骤:
1.把对象内每个有意义的实例变量(即参与equals()方法比较标准的实例变量)计算出一个int类型的hashCode值,计算方式如下表:
实例变量类型 | 计算方式 |
---|---|
boolean | hashCode=(f?0:1); |
int,char,short,byte | hashCode=(int)f; |
long | hashCode=(int)(f^(f>>>32)); |
float | hashCode=Float.floatToIntBits(f); |
double | long L=Double.doubleToLongBits(f); hashCode=(int)(L^(L>>>32)); |
引用类型 | (null==f?0:hashCode=f.hashCode()); |
2.用第一步计算出来的多个hashCode值组成一个hashCode值,代码如下:
return f1.hashCode() + (int)f2;
这句代码的意义不明,Effective Java一书中也不是这么写的,书上的代码是:result 31 * result + c
,完整的代码如下:
1 | public class User { |
3.当向集合中添加可变对象时必须十分小心,如果修改了这个对象的内容,有可能会导致算出来的hashCode与其他的一样,这样就会出现无法正确访问到该对象。
LinkedHashSet类
HashSet类还有一个子类LinkedHashSet,与HashSet基本类似,但是它是有顺序的,是通过链表来实现记住添加顺序。在性能上略低于HashSet,但是在迭代时会有很好的性能。
TreeSet
TreeSet是SortedSet接口的实现类,正如名字一样,它可以确保元素处于排序状态。与HashSet相比,该类还提供了其他几个额外方法:
- Comparator comparator():如果TreeSet采用了定制排序,则返回定制排序使用的comparator,如果是自然排序,则返回null
- Object first():返回集合第一个元素
- Object last():返回集合最后一个元素
- Object lower(Object e):返回指定元素的前一个元素,指定元素可以不是集合中的
- Object higher(Object e):返回指定元素的后一个元素,指定元素可以不是集合中的
- SortedSet subSet(Object fromElement,Object toElement):返回此Set中的子集,范围是fromElement(包含)到toElement(不包含)
- SortedSet headSet(Object toElement):返回子集,由从头到toElement的元素组成
- SortSet tailSet(Object fromElement):返回子集,有fromElement到尾组成
TreeSet的插入顺序又元素大小决定。HashSet采用hash算法决定存储位置,TreeeSet则是采用红黑树的数据结构来存储元素,具体的存储规则有两种。
自然排序
TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素大小,然后按升序排列。这就是自然排序。Java提供了一个Compareable接口,接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,0代表相等,正数代表调用者大于被比较者,负数代表小于。一些常用的类已经实现了这个接口:
- BigDecimal,BigInteger以及所有的数值型对应的包装类:按数据大小比较
- Character:按字符的unicode值比较
- Boolean:true对应的大于false
- String:按字符的unicode值比较
- Data,Time:比较日期时间
当将一个对象添加到这种集合时,必须实现了Comparable()接口,不然无法比较的话,也没法正确的加入元素,会引发ClassCastException异常。但是第一个可以不实现这个接口,因为它没什么可以比较的。不过将这唯一一个元素取出来时,仍就会引发上面的异常。
另一点需要注意的是,只有对象类型相同才有比较的意义。若类型不相同,就会强制转换。也就是说,TreeSet中的元素都是一个类型的,当类型不同时,comparaTo方法没法正确执行。会抛出ClassCastException异常。
对于这种集合来说,判断是否相同的唯一标准是compareTo判断是否相等。如果相等就没法添加到集合中。也就是说,哪怕equals方法返回的是true,只要compareTo返回的不是0,该元素仍旧可以添加进去。所以equals和compareTo方法应该保持一致性,避免这种不合理情况。反过来,equals判断不相等,compareTo判断相等的情况也要避免。
对于添加可变对象也要非常注意。假定集合{1,2,3,4},1被改为4(这里简写,实际集合中加入的必须是对象)。这就导致了有两个一样的元素,对这两个一样的,你都没法删除,只能等待重新索引,例如删掉2之后。然后就会是采用链式结构将两个一样的元素特殊处理。这种情况也要竭力避免。
定制排序
如果需要实现降序排列等特别的排序,则需要借助Comparator接口的int compare(T o1,T o2)抽象方法。代码如下:
1 | import java.util.TreeSet; |
在new一个ts时,调用了另一个与上面方式不同的构造器,代码如下:
1 | public TreeSet(Comparator<? super E> comparator) { |
该构造器接受一个Comparator接口的实现。而Comparator接口只有一个int compare()方法,所以是一个函数式接口。而且还new了一个TreeMap,再追更溯源到TreeMap中,代码如下:
1 | public TreeMap(Comparator<? super K> comparator) { |
由此可见 compatator这个东西最终被内含在了TreeSet中。但是这里如何实现了add时将add的参数传递过去,我还是不明白。
EnumSet类
看名字就能知道这是什么集合,该集合中的所有元素都是指定枚举类型的枚举值。该枚举类型在创建EnumSet时显式或隐式指定。EnumSet类的顺序由枚举值在枚举类中的定义顺序来决定集合元素的顺序。
EnumSet内部以位向量形式存储,这种形式非常经凑,高效。尤其是在批量操作时。该集合不允许加入null,但是可以查找null和删除null。
该类不提供构造器,需通过类方法创建EnumSet对象。常用方法如下:
- EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的集合
- EnumSet complemetOf(EnumSet s):创建一个枚举类中的所有枚举值与指定集合的元素的差集
- EnumSet copyOf(Collection c):使用一个普通集合创建一个EnumSet集合
- EnumSet copyOf(EnumSet s):复制一个EnumSet集合
- EnumSet noneOf(Class elementType):创建一个元素类型为指定枚举类型的空EnumSet
- EnumSet of(E first,E…rest):创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须是同一枚举类
- EnumSet range(E from,E to):创建一个从from到to枚举值范围内所有枚举值的EnumSet集合
各Set实现类的性能分析
Hashset的性能要优于TreeSet,特别是常用的添加,查询等,因为TreeSet需要额外的红黑树。Hashset的子类LinkedHahSet有一个链表,所以遍历会很快。EnumSet是性能最好的,但是元素的类型受限。
List集合
List代表有序,可重复的集合,每个元素都有类似于数组下标的索引。
Java8改进的List接口和ListIterator接口
List接口作为Collection接口的子接口,可以使用Collecion中的全部方法,同时,由于是有序集合,所以有一些根据索引操作集合的方法。
- void add(int index,Object element):将元素element插入到List集合的index处
- boolean addAll(int index,Collection c):将集合c的所有元素都插入到list的index处
- Object get(int index):返回index处的元素
- int indexOf(Object o):返回对象o在集合中第一次出现的索引
- int lastIndexOf(Object o):同上,返回最后一次出现的索引
- Object remove(int index):删除并返回index处的元素
- Object set(int index,Object element):将index处的元素替换成element对象,返回被替换的旧元素
- List subList(int fromIndex,int toIndex):返回从fromIndex(包含)到toIndex(不包含)的子集
除此之外,Java8还为List接口增加了两个默认方法:
- void replaceAll(UnaryOperator operator):根据operator指定的计算规则重新设置List集合的所有元素
- void sort(Comparator c):根据Comparator参数对集合重新排序
1 | import java.util.List; |
1 | [天国之秋, 追忆似水年华, 果壳中的宇宙, 罗马人的故事, 天朝的崩溃] |
注意在代码(1)处,new了一个新的对象,这个新的“编译原理”与集合中的是不同的,但是从返回值1可以看出:List判断两个对象相等只要通过equals()就行。
Java8新增的两个方法的代码如下:
1 | default void sort(Comparator<? super E> c) { |
1 | default void replaceAll(UnaryOperator<E> operator) { |
Comparator和UnaryOperator都是函数式接口。所以都可以使用Lambda表达式。
1 | import java.util.List; |
与Set只有提供了iterator()方法不同,List还提供了一个listIterator()方法,该方法返回一个ListIterator对象,ListIterator接口继承了Iterator接口,提供了专门操作List的方法。ListIterator接口除了继承Iterator接口的方法,还增加了一些方法:
- boolean hashPrevious():返回该迭代器关联的集合是否还有上一个元素
- Object previous():返回该迭代器的上一个元素
- void add(Object o):在制定位置插入一个元素
1 | import java.util.ArrayList; |
ArrayList和Vector实现类
ArrayList和Vector作为List的两个典型实现,完全支持前面介绍的List接口的全部功能。
ArrayList和Vector是基于数组实现的List类,所以内部封装了一个动态的,允许再分配的Object[]数组。ArrayList和Vector对象使用initialCapacity
参数设置数组的长度,当添加元素超过数组长度时,initialCapacity
自动增加。
由于这种自动的行为,所以大多数时候程序员不需要管这个initialCapacity
的。但是一旦多次重复的添加过大的数组,造成多次修改initialCapacity
值,这时就应该使用ensureCapacity(int minCapacity)方法一次性增加initialCapacity
值。
initialCapacity
可以在创建集合时制定,如果不指定,默认值为10.
除此之外,这两个集合还提供如下两个方法来重新分配Object[]数组:
- void ensureCapacity(int minCapacity):上面提到的,让数组长度增加大于或等于minCapacity值
- void trimToSize():将数组长度调整为当前元素的个数,减少浪费的空间
ArrayList和Vector的显著区别是,ArrayList是线程不安全的,当多个线程访问ArrayList集合时,如果有修改,必须手动同步。此外,Vector是一个非常古老的类,早在集合之前就有了,在集合出现后,Vector被重写,出现了一些方法上的重复,命名不同但功能一样的情况(可能已经有所改进),不建议使用Vector类,即使它有线程安全的优点。
Vector类还有个Stack子类,但是同样也是非常古老的,所以也不建议使用。要使用栈则应该使用List的实现类ArrayDeque,这个类也实现了Deque接口。
固定长度的List
在以前的文章中曾经提到过Arrays这个类,专门用于对数组的一些操作。其中有一个asList(Object… a)方法,它可以把一个数组或若干个对象转换为一个List集合。这个转换而来的特殊Lsit既不是Vector也不是ArrayList,而是Arrays中的内部类ArrayList实例(与ArrayList类不是一回事)。
对于这种特殊的List,不可以增加或删除,尽管在写代码时可以写List.add()或List.remove(),但是在运行时将产生UnsupportedOperationException异常。
Queue集合
Queue接口中定义了如下几个方法:
- void add(Object e):将指定元素加入队列的尾部
- Object element():获取队列的头部,不删除
- boolean offer(Object e):将指定元素加入队列的队尾,当使用容量有限的队列时,此方法比add方法好
- Object peek():获取队列的头部元素,若队列为空,返回null
- Object poll():返回并删除队列的头部元素,队列为空时,返回null
- Object remove():获取队列的头部元素并删除
PriorityQueue实现类
PriorityQueue是一个比较标准的队列实现类,其保存顺序是按照元素大小决定的。所以调用peek和poll方法的结果不是先进先出的(FIFO)。从这个意义来说,PriorityQueue已经违反了一个队列最基本的规则。
PriorityQueue队列不允许插入null元素。同时,由于需要排序,与TreeSet类似有自然排序和定制排序两种。
Deque接口和ArrayDeque实现类
Deque接口是Queue接口的子接口,它代表一个双端队列,即两端都可出队或入队。该接口该出了以下方法从两端操作队列(不完全):
- void addFirst(Object e):将指定元素插入头部
- void addLast(Object e):将指定元素插入尾部
- Iterator descendingIterator():返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素
- Object getFirst():获取但不删除第一个
- Object getLast():获取但不删除最后一个
- boolean offerGirst(Object e):将制定元素插入开头,返回boolean值
- boolean offerLast(Object e):将指定元素插入末尾,返回boolean值
- Object peekFirst():获取但不删除该队列的第一个,如果队列为空,返回null
- Object peekLast():获取但不删除该队列的最后一个,如果队列为空,返回null
- Object pollFirst():获取并删除该队列的第一个,如果队列为空,返回null
- Object pollLast():获取并删除该队列的最后一个,如果队列为空,返回null
- Object pop():(栈方法)pop出该双端队列表示的栈的栈顶元素,相当于removeFirst()
- void push(Object e):(栈方法)将一个元素插入栈顶,相当于addFirst(Object e)
- Object removeFirst():获取并删除该双端队列的第一个元素
- Object removeFirstOccurrence(Object o):删除队列中第一次出现的元素o
- Object removeLast():获取并删除该双端队列的最后一个元素
- boolean removeLastOccurrence(Object o):删除队列中最后一次出现的元素o
从上面的方法可以看出,Deque接口的方法具有栈的“特质”。具体当做栈来使用则是ArrayDeque实现类。他是一个基于数组实现的双端队列。创建Deque时同样可以指定numElements参数,该参数用于指定数组长度。如果不指定,默认为16。
下面代码示范了把ArrayDeque当做“栈”用:
1 | import java.util.ArrayDeque; |
当然,ArrayDeque仍然可以作为一个队列使用。
LinkedList实现类
LinkedList实现了Deque接口,也实现了List接口,可以根据索引来访问集合中的元素。而由于实现了Deque接口,所以上面提到的用与双端队列,用于栈,用于队列都是可以的。所有的基于数组的集合,例如ArrayList,ArrayDeque内部都是以数组保存元素的,所以随机访问的性能很好。而LinkedList是用链表实现的,随机访问的效率略差,但插入,删除时性能出色。
各种线性表的性能分析
List就是一个线性表接口,ArrayList,LinkedList有是线性表的两种典型实现:基于数组和基于链表。Queue代表队列,Deque代表双端队列(也可用于栈)。
尽管上一节写到了ArrayList,LinkedList之间有着性能差异,但是它的影响对于初级应用来说是微乎其微的。而且总体来说还是ArrayList更胜一筹,只在插入等操作有劣势,所以大部分时候用ArrayList就可以了。
关于List集合,有如下建议:
- 如果需要遍历集合元素,对于ArrayList,Vector集合,应该采用随机访问方法(get)来遍历集合;对于LinkedList,则应该采用迭代器(Iterator)来遍历集合。
- 如果需要经常执行插入,删除操作则应考虑用LinkedList集合
Java8增强的Map集合
Map用于保存具有映射关系的数据,Map集合里保存着两组值,一组用于保存key,另一组保存了value。key和value可以是任何引用类型的数据。key是不可以重复的。value是可以重复的。注意,value可以重复不是说一个key对应多个value,key和value是单项一对一关系,总能根据一个key找到唯一的,确定的value。
把key和value分开来看,key构成了一个set,没有顺序,不能重复。实际上Map里面确实包含一个keySet()方法,用于返回key组成的set集合。
Set和Map的关系非常的紧密,key的存储形式和Set里的元素存储形式很像,Map的子类的名字和Set子类的名字也非常相似,例如Set接口下有HashSet,LinkedSet,SortedSet(接口),TreeSet,EnumSet等子接口和实现类,而Map中也有HashMap,LinkedHashMap,SortedMap(接口),TreeMap,EnumMap等子接口和实现类。正如名字所暗示的,Map的这些实现类和子接口中的key值的存储形式与对应Set集合中的元素的存储形式完全相同。Map提供了一个Entry内部类来封装key-value对,key和value组成了Entry,多个Entry构成了Map。如果把value当做key的附属,Map可以视为一个Set。事实上,在Java中实现Set是实现了一个value值全为null的Map。
再看value。把所有的value放在一起非常类似List:元素之间可以重复,每个元素可以根据索引来查找。只是Map中不使用整数值做索引值,而是使用key。
Map可以视作两种集合的映射。
下面列出了Map接口中定义的常用方法:
- void clear():删除该Map对象中的所有key-value对
- boolean containsKey(Object key):查询Map中是否包含指定key
- boolean containsValue(Object value):查询Map中是否包含一个或多个指定value
- Set entrySet():返回Map中包含的key-value对组成的Set集合,每个集合元素都是Map.Entry对象
- Object get(Object key):返回指定key对应的value,如果不包含,则返回null
- boolean isEmpty():检查Map是否为空,是则返回true
- Set keySet():返回Map中所有的key组成的Set集合,注意与entrySet区别
- Object put(Object key,Object value):添加一个key-value对,如果已经存在相同的key,新的value会覆盖旧的
- void putAll(Map m):将指定Map中的key-value复制到本Map中
- Object remove(Object key):删除指定key的key-value对,返回指定key的value值
- boolean remove(Object key,Object value):Java8新增方法,删除指定key和vaule对应的key-value对。如果删除成功返回true
- int size():返回Map中key-value对的个数
- Collection values():返回Map里所有的value组成的Collection
Map中包含一个内部类Entry,该类封装了一个key-value对。Entry包含了如下三个方法:
- Object getKey():返回该Entry里包含的key值
- Object getValue():返回该Entry里包含的value值
- Object setValue(V value):设置该Entry对中的value值,并返回新的value值
下面的代码示范了一部分方法:
1 | import java.util.HashMap; |
Java为Map新增的方法
Java8除了增加了remove(Object key,Object value)默认方法外,还增加了如下方法(只列出方法声明):
- Object compute(Object key, Bi Function remappingFunction):该方法使用remappingFunction根据原key-value对计算一个新value。只要新value不为null,就使用新value覆盖原value;如果原value不为null,但新value为null,则删除原key-value对;如果原value、新value同时为null,那么该方法不改变任何key-value对,直接返回null。
- Object computelfAbsent(Object key, Function mappingFunction):如果传给该方法的key参数在Map中对应的value为null,则使用mappingFunction根据key计算一个新的结果,如果计算结果不为null,则用计算结果覆盖原有的value。如果原Map原来不包括该key,那么该方法可能会添加一组key-value对。
- Object computeIfPresent(Object key, BiFunction remappingFunction):如果传给该方法的key参数在Map中对应的value不为null,该方法将使用remappingFunction根据原key、value计算一个新的结果,如果计算结果不为null,则使用该结果覆盖原来的value;如果计算结果为null,则删除原key-value对。
- void forEach(BiConsumer action):该方法是Java8为Map新增的一个遍历key-value对的方法,通过该方法可以更简洁地遍历Map的key-value对。
- Object getOrDefault(Object key, V defaultValue):获取指定key对应的value。如果该key不存在,则返回 defaultValue。
- Object merge(Object key, Object value, BiFunction remappingFunction):该方法会先根据key参数获取该Map中对应的value。如果获取的value为null,则直接用传入的value覆盖原有的value(在这种情况下,可能要添加一组key-value对);如果获取的value不为null,则使用remappingFunction函数根据原value、新value计算一个新的结果,并用得到的结果去覆盖原有的 value。
- Object putIfAbsent(Object key, Object value):该方法会自动检测指定key对应的value是否为null,如果该key对应的value为null,该方法将会用新value代替原来的null值。
- Object replace(Object key, Object value):将Map中指定key对应的value替换成新value。与传统put()方法不同的是,该方法不可能添加新的key-value对。如果尝试替换的key在原Map中不存在,该方法不会添加key-value对,而是返回null。
- boolean replace(K key, V oldValue, V newValue):将Map中指定key-value对的原value替换成新value。如果在Map中找到指定的key-value对,则执行替换并返回true ,否则返回false。
- replaceAll(BiFunction function):该方法使用BiFunction对原key-value对执行计算,并将计算结果作为该key-value对的value值。
Java8改进的HashMap和Hashtable实现类
HashMap和Hashtable实现类是Map接口的典型实现类,它们的关系类似ArrayList和Vector的关系。同样的Hashtable也是非常古老的实现类,有两个繁琐的方法elements()和keys()方法,前者类似于Map接口中定义的values方法,后者类似与Map中的keySet方法。
Java8改进了HashMap的实现,使得在使用HashMap时,出现了key冲突仍就具有较好的性能。HashMap和Hashtable的典型区别在于:
- Hashtable是线程安全的,HashMap不是,所以后者性能稍好,但是即使是为了做到线程安全,也不应使用Hashtable
- Hashtable不允许使用null作为key和value,会引发NullPointerException异常,而HashMap可以,但是因为key是不能重复的,所以只能有一个key为null,value无此限制
用作key的对象必须实现hashCode()方法和equals()方法。与HashSet方法类似,HashMap和Hashtable不能保证元素的存储顺序。而equals方法和hashCode方法的规则与之类似,equals返回true,hashCode也应返回true。而判断是否包含某个value的方法(containsValue())是通过equals方法。
LinkedHashMap实现类
LinkedHashMap内部使用双向链表来维护key-value的顺序,该顺序即插入顺序。
1 | import java.util.LinkedHashMap; |
使用Properties读写属性文件
Properties类是Hashtable的子类,用于处理属性文件,例如ini文件。这个类用于将属性文件的“属性名=属性值”对读取到Map中,或者将MapEntry对写入到属性文件。
- String getProperty(String key):获取属性文件中指定属性名的属性值
- String getProperty(String key,String defaultValue):与上面的相似,如果不存在指定属性值,则指定默认值
- Object setProperty(String key,String value):设置属性值
除此之外,还有两个读写属性文件的方法:
- void load(InputStream inStream):从属性文件(以输入流表示)中加载key-value对,把加载的key-value追加到Properties里
- void store(OutputStream out,String comments):上面的反过来
SortedMap接口和TreeMap实现类
与Set接口有个子接口SortedSet,SortedSet接口有个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap也有一个TreeMap实现类。
TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点TreeMap存储key-value对时,需要根据key对节点进行排序,TreeMap可以保证所有的key-value处于有序状态。同样也有两种排序方式,自然排序和定制排序:
- 自然排序:所有key必须实现Comparable接口,并且所有key应该是同一类,否则抛出ClassCastException
- 定制排序:创建TreeMap时,传入一个Comparator对象,负责对key排序,key不要求实现Comparable接口
类似TreeSet中判断两个元素相等的标准,TreeMap的标准是:两个key通过compareTo()方法判断。equals方法的结果应和compareTo的结果相匹配。
- Map.Entry firstEntry():返回该Map中最小key所对应的key-value对,如果该Map为空,则返 回 null。
- Object firstKey():返回该Map中的最小key值,如果该Map为空,则返回null。
- Map.Entry lastEntry():返回该Map中最大key所对应的key-value对,如果该Map为空或不存 在这样的key-value对,则都返回null。
- Object lastKey():返回该Map中的最大key值,如果该Map为空或不存在这样的key,则都返 回 null-
- Map.Entry higherEntry(Object key):返回该 Map 中位于 key 后一位的 key-value 对(即大于指定 key的最小key所对应的key-value对)。如果该Map为空,则返回null。
- Object higherKey(Object key):返回该Map中位于key后一位的key值(即大于指定key的最小 key值)。如果该Map为空或不存在这样的key-value对,则都返回null。
- Map.Entry lowerEntry(Object key):返回该 Map 中位于 key 前一位的 key-value 对(即小于指定 key的最大key所对应的key-value对)。如果该Map为空或不存在这样的key-value对,则都返 回 null。
- Object lowerKey(Object key):返回该Map中位于key前一位的key值(即小于指定key的最大 key值)。如果该Map为空或不存在这样的key,则都返回null。
- NavigableMap subMap(Object fromKey, boolean fromlnclusive, Object toKey, boolean tolnclusive): 返回该Map的子Map,其key的范围是从fromKey (是否包括取决于第二个参数)到toKey (是 否包括取决于第四个参数)。
- SortedMap subMap(Object fromKey, Object toKey):返回该 Map 的子 Map,其 key 的范围是从 fromKey (包括)到toKey (不包括)。
- SortedMap tailMap(Object fromKey):返回该 Map 的子 Map,其 key 的范围是大于 fromKey (包 括)的所有key。
- NavigableMap tailMap(Object fromKey, boolean inclusive):返回该 Map 的子 Map,其 key 的范围 是大于fromKey (是否包括取决于第二个参数)的所有key。
- SortedMap headMap(Object toKey):返回该 Map 的子 Map,其 key 的范围是小于 toKey (不包括) 的所有key。
- NavigableMap headMap(Object toKey, boolean inclusive):返回该 Map 的子 Map,其 key 的范围 是小于toKey (是否包括取决于第二个参数)的所有key。
1 | import java.util.TreeMap; |
WeakHashMap实现类
WeakHashMap与HashMap的用法基本相似。与HashMap的区别在于,HashMap的key保留了对实际对象的强引用,而WeakHashMap保留的是弱引用。前者内部的key引用的所有对象不会被垃圾回收机制回收,HashMap也不会删除key对应的key-value;而后者不同,key引用的对象如果没有被其他强引用变量引用,则key引用的对象就有可能被回收,WeakHashMap也有可能删除对应的key-value。
IdentityHashMap实现类
IdentityHashMap提供了与HashMpa类似的基本相似的方法,也允许用null,也不保证顺序,唯一明显不同的就是,IdentityHashMap比HashMap更严格的比较是否相同,它使用“==”。
EnumMap实现类
EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类。EnumMap具有如下特征:
- EnumMap在内部以数组形式保存,所以这种实现形式非常紧凑、高效。
- EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对的顺序。 当程序通过keySet()、entrySet()、 values()等方法遍历EnumMap时可以看到这种顺序。
- EnumMap不允许使用null作为key,但允许使用null作为value。如果试图使用null作为key时将抛出NullPointerException异常。如果只是查询是否包含值为null的key,或只是删除值为null的key,都不会抛出异常。
在创建EnumMap时必须指定一个枚举类,从而将EnumMap和指定枚举类关联起来:
1 | import java.util.EnumMap; |
各Map实现类的性能分析
虽然HashMap和Hashtable实现机制几乎一样,但由于Hashtable是一个古老的,线程安全的集合,所以通常它要慢一点。
TreeMap通常要比HashMap,Hashtable要慢,尤其在插入,删除时。因为它是使用红黑树管理key-value对的。
使用TreeMap有一个好处:TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作。当TreeMap被填充之后,就可以调用keySet(),取得由key组成的Set,然后使用toArrayQ方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速地查询对象。
对于一般的应用场景,程序应该多考虑使用HashMap,因为HashMap正是为快速查询设计的 (HashMap底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则
可以考虑使用TreeMap。
LinkedHashMap比HashMap慢一点,因为它需要维护链表来保持Map中key-value时的添加顺序。IdentityHashMap性能没有特别出色之处,因为它采用与HashMap基本相似的实现,只是它使用”==” 而不是equalsO方法来判断元素相等。EnumMap的性能最好,但它只能使用同一个枚举类的枚举值作为key。
HashSet和HashMap的性能选项
对于HashSet及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;对于HashMap、Hashtable及其子类而言,它们采用hash算法来决定Map中key 的存储,并通过hash算法来增加key集合的大小。
hash表里可以存储元素的位置被称为“桶(bucket)”,在通常情况下,单个“桶”里存储一个元素, 此时有最好的性能:hash算法可以根据hashCode值计 算出“桶”的存储位置,接着从“桶”中取出元素。但hash表的状态是open的:在发生“hash冲突”的情况下,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索。
因为HashSet和HashMap、Hashtable都使用hash算法决定其元素(HashMap则只考虑key)的存储,因此HashSet、HashMap的hash表包含如下属性:
- 容量(capacity):hash表中桶的数量。
- 初始化容量(initial capacity):创建hash表时桶的数量。HashMap和HashSet都允许在构造器中指定初始化容量。
- 尺寸(size):当前hash表中记录的数量。
- 负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的hash表,依此类推。轻负载的hash表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)。
除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。
HashSet和HashMap、Hashtable的构造器允许指定一个负载极限,HashSet和HashMap、Hashtable默认的“负载极限”为 0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询);较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销,程序员可以根据实际情况来调整HashSet和HashMap的“负载极限”值。
操作结合的工具类Collections
Java提供了一个操作Set,List和Map等集合的工具类:Collections.该工具类提供了大量对集合元素进行排序,查询,修改等操作,还提供了讲集合对象设置为不可变,集合对象实现同步控制等方法。
排序操作
Collections提供了如下常用的类方法用于对List集合元素进行排序:
- void reverse(List list):反转指定List集合中元素的顺序。
- void shuffle(List list):对 List集合元素进行随机排序(shuffle方法模拟了“洗牌”动作)。* void sort(List list):根据元素的自然顺序对指定List集合的元素按升序进行排序。
- void sort(List list, Comparator c):根据指定Comparator产生的顺序对List集合元素进行排序。
- void swap(List list, int i, int j):将指定List集合中的i处元素和j处元素进行交换。
- void rotate(List list, int distance):当distance为正数时,将list集合的后distance个元素“整体”移到前面;当distance为负数时,将list集合的前distance个元素“整体”移到后面。该方法不会改变集合的长度。
查找、替换操作
Collections还提供了如下常用的用于查找、替换集合元素的类方法:
int binarySearch(List list, Object key):使用二分搜索法搜索指定的List集合,以获得指定对象在List集合中的索引。如果要使该方法可以正常工作,则必须保证List中的元素已经处于有序状态。
- Object max(Collection coll):根据元素的自然顺序,返回给定集合中的最大元素。
- Object max(Collection coll, Comparator comp):根据Comparator指定的顺序,返回给定集合中的最大元素。
- Object min(Collection coll):根据元素的自然顺序,返回给定集合中的最小元素。
- Object min(Collection coll, Comparator comp):根据Comparator指定的顺序,返回给定集合中的最小元素。
- void fill(List list, Object obj):使用指定元素obj替换指定List集合中的所有元素。
- int frequency(Collection c, Object o):返回指定集合中指定元素的出现次数。
- int indexOfSubList(List source, List target):返回子List对象在父List对象中第一次出现的位置索引 ;如果父List中没有出现这样的子List,则返回-1。
- int lastIndexOfSubList(List source, List target):返回子List对象在父List对象中最后一次出现的位置索引;如果父List中没有出现这样的子List,则返回-1。
- boolean replaceAll(List list, Object oldVal, Object newVal):使用一个新值newVal替换List对象的所有旧值oldVal。
同步控制
Collections类中提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
Java中常用的集合框架中的实现类HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap和TreeMap都是线程不安全的。如果有多个线程访问它们,而且有超过一个的线程试图修改它们,则存在线程安全的问题。Collections提供了多个类方法可以把它们包装成线程同步的集合。
1 | import java.util.*; |
设置不可变集合
Collections提供了如下三类方法来返回一个不可变的集合:
- emptyXxx():返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是SortedSet、Set,还可以是Map、SortedMap等。
- singletonXxx():返回一个只包含指定对象(只有一个或一项元素)的、不可变的集合对象,此处的集合既可以是List,还可以是Map。
- unmodifiableXxx():返回指定集合对象的不可变视图,此处的集合既可以是List,也可以是Set、SortedSet,还可以是 Map、SorteMap 等。
1 | import java.util.*; |
Enumeration接口
Enumeration接口是Iterator迭代器的“古老版本”,从JDK1.0开始,Enumeration接口就已经存在了(Iterator从JDK1.2才出现)。Enumeration接口只有两个名字很长的方法。
- boolean hasMoreElements():如果此迭代器还有剩下的元素,则返回true。
- Object nextElement():返回该迭代器的下一个元素,如果还有的话(否则抛出异常)。
总结
给出一篇文章作为总结:https://www.cnblogs.com/ysocean/p/6555373.html。
这张图也很不错,单独放出来:
全文完,共三万五千余字。