Java集合详解
---->集合是什么?
通常,我们的Java程序需要根据程序运行时才知道创建了多少个对象。但若非程序运行,程序开发阶段,我们根本不知道到底需要多少个数量的对象,甚至不知道它的准确类型。为了满足这些常规的编程需要,我们要求能在任何时候,任何地点创建任意数量的对象,而这些对象用什么来容纳呢?我们首先想到了数组,但是!数组只能存放同一类型的数据,而且其长度是固定的,那怎么办了?集合便应运而生了。集合实际上就是一个容器,可以来容纳其他类型的数据。
Java集合类存放在java.util包中,是一个用来存放对象的容器。
注意:
1.集合只能存放对象。比如你存入一个int型数据66放入集合中,其实它是自动转换成Integer类后存入的,Java中每一种基本数据类型都有对应的引用类型。
2.集合存放的都是对象的引用,而非对象本身。所以我们称集合中的对象就是集合中对象的引用(对象的内存地址)。对象本身还是放在堆内存中。
3.集合可以存放不同类型,不限数量的数据类型。集合也是一个对象,可以内嵌。集合套集合。

---->Java集合的框架关系图
-
Collection集合

-
Map集合
---->Java集合各接口和实现类常用方法
Collection接口
-
没有使用泛型时,可以存储Object的所有子类型,使用泛型后,只能存储某个具体的类型。不能直接存储基本数据类型,也不能存Java对象,只是存放Java对象的内存地址。存放基本数据类型会自动装箱。
-
Collection接口的常用方法
-
1 **boolean add(Object o)添加对象到集合** 2 **boolean remove(Object o)删除指定的对象** 3 **int size()返回当前集合中元素的数量** 4 **boolean contains(Object o)查找集合中是否有指定的对象** 5 6 **boolean isEmpty()判断集合是否为空** 7 **Iterator iterator()返回一个迭代器** 8 **boolean containsAll(Collection c)查找集合中是否有集合c中的元素** 9 **boolean addAll(Collection c)将集合c中所有的元素添加给该集合** 10 **void clear()删除集合中所有元素** 11 **void removeAll(Collection c)从集合中删除c集合中也有的元素** 12 **void retainAll(Collection c)从集合中删除集合c中不包含的元素**
contains(Object o)判断查找集合中是否有指定的对象时,需要了解对象类型是否重写了euqals方法,若没有重写,比较的是内存地址,若重写则查看重写方法里面的内容比较是否相同,存放在集合里的类型一定要重写equals方法。remove()方法底层也调用了equals()方法
-
迭代器Iterator
-
迭代器(Iterator)是一种设计模式、提供了一种方法,来对集合、容器进行遍历的方式,不需要关注底层数据结构和数据类型,来达到底层和上层遍历解耦的目的
-
Iterable 迭代器接口,这是Collection类的父接口。实现这个Iterable接口的对象允许使用foreach进行遍历,也就是说,所有的Collection集合对象都有"foreach可遍历性"。这个Iterable接口只有一个方法: iterator()。它返回一个代表当前集合对象的泛型< T >迭代器,用于之后的遍历操作
-
Iterator its = map.keySet().iterator();
-
Iterator its = list.iterator();
-
Iterator its = set.iterator();
Iterator里面有三个方法:
-
boolean hasNext() :判断集合是否还有元素; true表示还存在元素 ,false表示不存在元素
-
E next():返回当前数据,迭代器前进一位
-
void remove():删除元素,不能调用集合的remove()方法,迭代器获取集合状态后,相当于有了一个快照,若调用集合的remove()方法,集合中元素改变,但是迭代器获取的还是之前集合的元素,不知道集合已经变化了,就会出现异常。用迭代器的remove()方法相当于是录像,录像里的状态发生改变,录像会一起改变。自动更新迭代器,并且更新集合
-
注意*:集合结构只要发生改变,迭代器必须重新获取。若还是之前的迭代器,会出现异常。获取迭代器类似于快照状态,获取迭代器对象后,当前集合状态是一个快照,迭代器会按照这个快照进行迭代,若发生了数据改变,改变后的集合状态跟之前是不一样的,会抛出异常。
public static void main(String[] args) {
Collection list=new ArrayList();
list.add(1);
list.add("hhh");
list.add(true);
System.out.println(list1.size());
//方式一:迭代器遍历
Iterator it=list.iterator();
System.out.println("*************方式一*************");
while(it.hasNext()) { //若为while(true)//会抛出异常
Object object=it.next();//取出来都是Object,类型跟之前一样
System.out.println(object);//调用了toString();
}
//方式二:foreach遍历
System.out.println("*************方式二*************");
for(Object o:list) {
System.out.println(o);
}
}
}
List接口
-
List接口是Collection接口的子接口,List中的元素:有序可重复,所以被称为是序列。
-
实现类:
-
ArrayList:数组实现,查询快,增删慢,轻量级;(线程不安全)
-
LinkedList:双向链表实现,增删快,查询慢 (线程不安全)
-
Vector:数组实现,重量级 (线程安全、使用少)
-
-
List接口的常用方法
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
//比较常用
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index)
int indexOf(Object o);
int lastIndexOf(Object o);
-
ArrayList
-
底层是Object数组,所以ArrayList具有数组的查询速度快的优点以及增删速度慢的缺点,但是向数组末尾添加元素效率蛮高,因为程序中查找元素操作较多,所以用的较多,。
-
不是线程安全的,默认初始化容量为10(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量为10),可以自己指定初始容量。
-
扩容:int newCapacity = oldCapacity + (oldCapacity >> 1),其中oldCapacity是原来的容量大小,oldCapacity >> 1 为位运算的右移操作,右移一位相当于除以2,所以这句代码就等于int newCapacity = oldCapacity + oldCapacity / 2;即容量扩大为原来的1.5倍(注意我这里使用的是jdk1.8)
-
ArrayList扩容优化策略:建议给定一个预估计的初始化容量,减少数组的扩容次数。
-
Collections是集合框架中的一个工具类。该类中的方法都是静态的。提供的方法中有可以对list集合进行排序,二分查找等方法。通常常用的集合都是线程不安全的。因为要提高效率。如果多线程操作这些集合时,可以通过该工具类中的同步方法,将线程不安全的集合,转换成安全的。collections.synchronizedList(集合)
-
-
LinkedList
-
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
-
LinkedList 实现 List 接口,能对它进行队列操作。
-
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
-
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
-
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
-
没有初始化容量,最初链表中无任何元素,first和last都为null。
-
LinkedList 是非同步的。不是线程安全的
-
优点:随机增删元素效率较高(增删元素不涉及到大量元素位移)
-
缺点:查询效率较低,每一次查找元素都要从头节点开始往下遍历。
-
-
Vector
-
向量(Vector)是一个封装了动态大小数组的顺序容器(Sequence Container)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组。
-
初始化容量为10,扩容为原本容量的2倍。
-
线程同步的
-
泛型
-
所有集合类都位于java.util包中,集合中只能保存对象的引用。集合类把它所含有的元素看成是Object的实例,这样方便但是也有隐患,即多个类型不同的元素被放入一个集合中,会增加集合访问时类型转换的困难,甚至会产生错误。泛型的引入改善了这种情况,使用泛型来限制集合里元素的类型,并让集合记住元素的类型。可以允许编译器检查加入集合的元素类型,避免值类型不一致的错误。
-
优点:集合中存储的元素类型统一了,从集合中取出的元素是泛型指定的类型,不需要进行大量的“向下转型”
-
缺点:导致集合中存储的元素缺乏多样性。
-
只在程序编译阶段起作用,给编译器参考,运行阶段泛型不参与
-
自动类型推断机制(钻石表达式)
List<String> list = new ArrayList<>()//后面的<>中的类型会根据前面的自动推断 -
可以自定义泛型,自定义泛型<>中是一个标识符,可以随便写。在迭代器类型中指定泛型,可以在迭代中直接获取参数类型,若获取的是对象,则要转化。
Set接口
-
set接口是继承自Collection的子接口,特点是元素不重复,存储无序。 在set接口的实现类中添加重复元素是不会成功的,判断两个元素是否重复根据元素类重写的hashCode()和equals()方法。
-
HashSet
-
无序不可重复,实际为一个HashMap的实例,允许null.
//构造方法 public HashSet(){ map = new HashMap<>(); } //add方法 public boolean add(E e){ return map.put(e,PRESENT)==null;//其底层调用的是HashMap的添加元素方法 }-
底层是哈希表数据结构,放到HashSet集合中的元素实际上是放到HashMap集合的key部分了。HashSet保证元素唯一,哈希表依赖于哈希值存储
-
-
TreeSet
-
TreeSet基于 TreeMap 的 NavigableSet 实现。使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。排序方式完全取决于使用的构造方法。使用元素的自然顺序对元素进行排序(自然排序)根据创建 set 时提供的 Comparator 进行排序(比较器排序)TreeSet保证元素的排序和唯一性的 底层数据结构是红黑树(自平衡二叉树)
-
Map接口
-
Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。
-
它也没有继承Collection。
-
在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。key值有HashSet的特性,无序不可重复。key是主导,value是附属
-
key和value都是引用数据类型,存储的都是对象的内存地址。
-
常用方法
/*Map接口中定义的一些常用方法: * public V put (K key,V value):把键与值添加到Map集合中 * public V get(Object key):根据指定的键,获取对应的值 * public V clear() :清空Map集合 * boolean containKey(Object key):判断是否包含指定的key * boolean containValue(Object value):判断是否包含指定的value * boolean isEmpty() :判断Map集合中元素个数是否为空 * public V remove (Object key):删除key对应的值 * int size() :获取集合中键值对的个数 * Collection<V> values() :获取Map集合猴子那个所有的value,返回一个Collection * Set<K> keySet:获取Map集合中所有的Key,存储到set集合中 * Set<Map.entry<K,v>> entrySet() :当Map类集合创建的时候,会生成一个Entry对象 * Map.entry<K,v>是一个接口 * 他是用来存储key和value的,每一组一个对象。这个方法可以把这些所有的Entry对象放到Set集合中。Set集合中元素类型是Map.Entry<K,V> * Entry对象中有getKey()和getValue()两种方法可以提取出key和value的值*/ -
Map集合遍历
-
使用Set集合迭代器方法
1、先使用keySet()方法获取关键字集合的Set集合
-
2、再根据Set集合的iterator方法得到相应的迭代器
-
3、根据Set的迭代器进行遍历
-
方式一:keySet()迭代器while循环
-
System.out.println("==========keySet()迭代器while循环=========="); Set<可以添加泛型> keySet = map.keySet(); Iterator<> it = keySet.iterator();//这里添加泛型后,就可以在while循环里获取这种泛型类型的对象,不在只能是Object对象 while(it.hasNext()){ Object key = it.next(); System.out.println(key+"---"+map.get(key)); } -
方式二:foreach循环
-
System.out.println("==========keySet()foreach循环=========="); Set<可以添加泛型> keySet = map.keySet(); for (Object(可以为泛型类型) key1 : keySet) { System.out.println(key1+"---"+map.get(key1)); }
-
-
使用entrySet迭代器方法
-
1、先使用Map接口的entrySet( )方法得到一个Set集合,Set集合元素类型是Map.Entry
2、根据得到的Set集合得到新的iterator迭代器
3、根据迭代器进行遍历,每次取出一个Node
4、从Node中获取key和value
![]()
- 方式三:entrySet()迭代器while循环
-
System.out.println("=========entrySet()迭代器while循环=========="); Set entrySet = map.entrySet(); Iterator iterator = entrySet.iterator(); while(iterator.hasNext()){ Map.Entry node = (Map.Entry)iterator2.next(); System.out.println(node.getKey()+"---"+node.getValue()); } -
方式四:entrySet()foreach循环//效率较高,直接从Node对象中获取属性值,适合大容量数据
-
System.out.println("==========entrySet()foreach循环=========="); for (Object object : entrySet) { Map.Entry node2 = (Map.Entry)object; System.out.println(node2.getKey()+"---"+node2.getValue()); }
-
-
完整代码(使用泛型)
public class MapForeach { public static void main(String[] args) { //HashMap<Object, Object> map1 = new HashMap<>(); Map<Integer,String> map = new HashMap<>();// map.put(001,"一号"); map.put(002,"二号"); map.put(003,"三号"); map.put(004,"四号"); map.put(005,"五号"); System.out.println("=========keySet()迭代器while循环=========="); Set<Integer> keys = map.keySet(); Iterator<Integer> it = keys.iterator(); while(it.hasNext()){ Integer key = it.next(); System.out.println(key+"-->"+map.get(key)); } System.out.println("=========keySet()foreach循环=========="); for (Integer key1 : keys) { System.out.println(key1+"---"+map.get(key1)); } System.out.println("=========entrySet()迭代器while循环=========="); Set<Map.Entry<Integer, String>> entries = map.entrySet(); Iterator<Map.Entry<Integer, String>> iterator = entries.iterator(); while(iterator.hasNext()){ Map.Entry<Integer, String> node = iterator.next(); System.out.println(node.getKey()+"-->"+node.getValue()); } System.out.println("=========entrySet()foreach循环=========="); for(Map.Entry<Integer,String> node2 :entries){ System.out.println(node2.getKey()+"---"+node2.getValue()); } } }
1、HashMap
-
集合底层是哈希表(散列表)的数据结构
-
哈希表:是一个数组和单向链表的结合体,哈希表的主干就是数组,链表则是主要为了解决哈希冲突而存在的。比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。 这个函数可以简单描述为:存储位置 = f(关键字) ,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。
-
插入过程
![]()
-
哈希冲突
-
如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
-
-
部分成员变量如下://初始值,为16,必须为2的次幂 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //当容量被占满0.75时就需要reSize扩容 static final float DEFAULT_LOAD_FACTOR = 0.75f; //链表长度到8,就转为红黑树 static final int TREEIFY_THRESHOLD = 8; // 树大小为6,就转回链表 static final int UNTREEIFY_THRESHOLD = 6; -
底层分析
//HashMap集合底层源代码
public class HashMap{
//HashMap底层实际上就是一个数组,数组中每一个元素是一个单向链表
transient Node<K,V>[] table;
//静态内部类HashMap.Node
static class Node<K,V>{
final int hash;//哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希算法,可以转换成数组下标)
final K key;//存储到Map集合中的Key
V value;//存储到Map集合中的value
Node<K,V> next;//下一个节点的内存地址
}
}
-
HashMap总体结构:横向的为数组,纵向的为链表,一个Entry就是一个Node

-
在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组
-
put(K,V)操作实现原理
-
先将K,V封装到Node对象中
-
底层调用K的hashCode()方法得出hash值,通过哈希算法/函数,将hash值转换成数组下标,下标位置处如果没有任何元素,把Node添加到这个位置上。
-
如果已经有元素,就把K和链表上的节点进行equals比较,如果返回false,节点添加到链表末尾,如果返回true,把这个节点的value值覆盖
-
-
get(K,V)操作实现原理
-
先调用K的hashCode()方法得出哈希值,通过哈希算法转化成数组下表,通过数组下标快速定位到某个位置,如果这个位置什么也没有,返回null。
-
如果这个位置有Node,参数K和每个Node中的K进行equals比较,如果返回false,那么返回null,没有找到。如果,返回true,返回这个Node中的value值
-
-
可以看出无论是put(K,V)还是get(K,V),都会先调用K的hashCode方法获取hash值,再调用equals()方法进行元素值比较,放在HashMap集合Key部分其实就是放在HashSet集合中了。所以hashCode()与equals()方法都需要重写(HashMap集合key部分和HashSet)。

-
为什么要重写hashCode()与equals()方法
-
在object类中,hashcode()方法是本地方法,返回的是对象的地址值,而object类中的equals()方法比较的也是两个对象的地址值,如果equals()相等,说明两个对象地址值也相等,当然hashcode()也就相等了;在String类中,equals()返回的是两个对象内容的比较,当两个对象内容相等时,Hashcode()方法根据String类的重写代码的分析,也可知道hashcode()返回结果也会相等。以此类推,可以知道Integer、Double等封装类中经过重写的equals()和hashcode()方法也同样适合于这个原则。当然没有经过重写的类,在继承了object类的equals()和hashcode()方法后,也会遵守这个原则。
-
在定义类时,我们经常会希望两个不同对象的某些属性值相同时就认为他们相同,所以我们要重写equals()方法,按照原则,我们重写了equals()方法,也要重写hashCode()方法,
-
如果两个对象相同,那么它们的hashCode值一定要相同;如果两个对象的hashCode相同,它们并不一定相同(这里说的对象相同指的是用eqauls方法比较)。所以java中的很多类都重写了这两个方法,例如String类,包装类,在java应用程序运行时,无论何时多次调用同一个对象时的hashCode()方法,这个对象的hashCode()方法的返回值必须是相同的一个int值
-
equals()方法不相等的两个对象,hashcode()有可能相等。反过来,hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。
-
-
同一个单向链表上所有节点的hash基本相同(由于hash算法的不同,可能不同的hash可以计算出相同的数组下标),同一个链表上的K和K的equals方法返回值为false,不可重复。
-
[参考链接](https://blog.csdn.net/woshimaxiao1/article/details/83661464)
2、HashTable
-
(1)线程安全:HashMap是线程不安全的类,多线程下会造成并发冲突,但单线程下运行效率较高;HashTable是线程安全的类,很多方法都是用synchronized修饰,但同时因为加锁导致并发效率低下,单线程环境效率也十分低;
-
(2)插入null:HashMap允许有一个键为null,允许多个值为null;但HashTable不允许键或值为null;
-
(3)容量:HashMap底层数组长度必须为2的幂,这样做是为了hash准备,默认为16;而HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11;
-
(4)Hash映射:HashMap的hash算法通过非常规设计,将底层table长度设计为2的幂,使用位与运算代替取模运算,减少运算消耗;而HashTable的hash算法首先使得hash值小于整型数最大值,再通过取模进行散射运算;
-
(5)扩容机制:HashMap创建一个为原先2倍的数组,然后对原数组进行遍历以及rehash;HashTable扩容将创建一个原长度2倍的数组,再使用头插法将链表进行反序;
-
(6)结构区别:HashMap是由数组+链表形成,在JDK1.8之后链表长度大于8时转化为红黑树;而HashTable一直都是数组+链表;
-
(7)继承关系:HashTable继承自Dictionary类;而HashMap继承自AbstractMap类;
-
(8)迭代器:HashMap是fail-fast;而HashTable不是
-
properties
-
properties是一个Map集合,key和value都是String类型,被称为属性类对象,是线程安全的
-
setProperties(key,value);
getProperties(Key);
-
3、TreeMap
-
TreeSet集合底层实际上是一个TreeMap
-
TreeMap底层是一个二叉树
-
放到TreeSet集合中的元素,等同于放到TreeMap集合Key部分了
-
key无序不可重复,但可以按照元素大小顺序自动排序。不允许出现重复的key;可以插入null键,null值;
-




浙公网安备 33010602011771号