集合
集合
为什么要有集合?
数组的缺陷:长度指定就无法更改,只能存一个类型的数据,增加和删除麻烦(灵活性不够)
所以就有了集合:可以动态保存多个对象,提供了一系列操作对象的API,add,remove,set,get
集合框架体系

单列集合:
两个子接口:list,set

双列集合(键值对)
Map接口

Collection接口
特点:单列集合
迭代器Iterator:
实现Collection接口的集合类都能使用迭代器Iterator

原理:会有一个游标使用next()方法向下走,走过一个返回一个数
常用方法:
next()返回下一个元素
hasnext()判断下一个元素是否为空
注意:
调用next()方法前,必须调用hasnext()判断,不然会抛异常
public static void main(String[] args) {
Collection c=new ArrayList();
c.add(1);
c.add("你好");
c.add(true);
c.add('e');
// 1.拿到迭代器
Iterator iterator = c.iterator();
// 这里有一个快捷键===>itit(快速生成迭代器循环)
// 快捷键Ctrl+J显示有哪些快捷键
while (iterator.hasNext()){//2.判断是否还有数据
Object next = iterator.next();//3.返回下一个数据,类型为Object
System.out.println(next);
}
// 当循环完,这里在获取迭代器的值会报错
// 要想,继续遍历,就得重置
iterator=c.iterator();
iterator.next();
}
增强for循环:
1.增强for循环,底层也是用的迭代器
2.增强for循环可以说简化版本的迭代器
Collection c=new ArrayList();
c.add(1);
c.add("你好");
c.add(true);
c.add('e');
// 1.增强for循环,底层也是用的迭代器
// public Iterator<E> iterator() {
// return new Itr();
// }
// 2.增强for循环可以说简化版本的迭代器
// 3.快捷键: I
for (Object o:c){
System.out.println(o);
}
List接口:
特点(重复,有序,有索引):
1.List集合类中的元素有序(添加和取出顺序一致),且元素可重复
2.每个元素都有其顺序的索引
常用方法:
- add(int index,Object ele):插入到指定位置
- addall(int index,Collection ele),讲ele中的所有元素插入进来
- get(int index)获取指定位置元素
- int indexof(Object):返回obj在当前集合首次出现的位置
- int lastIndexOf(Object):返回obj在当前集合尾次出现的位置
- Objcet remove(int index):移除指定位置元素
- object set(int index,Object obj):设置指定位置的元素为obj,相当于替换
- List subList(int fromindex,int toIndex):这里的范围是左闭右开
// 常用方法:
// 1.add(int index,Object ele):插入到指定位置
l.add(1,"插到1位置");
System.out.println(l);
// 2.addall(int index,Collection ele),讲ele中的所有元素插入进来
Collection c = new ArrayList();
c.add(1);
c.add("插");
l.add(1,c);
System.out.println(l);
// 3.get(int index)获取指定位置元素
// 4.int indexof(Object):返回obj在当前集合首次出现的位置
System.out.println(l.indexOf("nihao"));
// 5.int lastIndexOf(Object):返回obj在当前集合尾次出现的位置
System.out.println(l.lastIndexOf("nihao"));
// 6.Objcet remove(int index):移除指定位置元素
l.remove(4);
System.out.println(l);
// 7.object set(int index,Object obj):设置指定位置的元素为obj,相当于替换
l.set(4,2);
System.out.println(l);
// 8.List subList(int fromindex,int toIndex):这里的范围是左闭右开
List list = l.subList(1, 2);
System.out.println(list);
}
ArrayList
特点(有序,高效,重复,但是插入删除等效率低):
1、可以存放多个null值
2、底层是由数组实现的
3、ArrayList基本等同于Vactor,除了线程不安全(执行效率高),在多线程的情况下不要用ArrayList
线程为什么不安全?
ArrayList list = new ArrayList();
// 线程是不安全的,没有synchronized
// public boolean add(E e) {
// ensureCapacityInternal(size + 1); // Increments modCount!!
// elementData[size++] = e;
// return true;
// }
底层操作源码分析:
-
ArrayList底层维护了一个Object类型(意味着什么都能往里面丢)的数组elementDate
transient Object [] elementDate //transient短暂的,表示该属性不会被序列化
-
当创建ArrayList对象时,使用的是无参构造函器,则elementDate容量为0,第一次添加,则扩容为10,如继续扩容,则会10+位运算>>1,也就是扩容1.5倍
-
如果使用的是指定大小的构造器创建对象,则初始elementDate容量就为你指定的大小,如需要继续扩容,则直接扩容为你指定大小的1.5倍
创建无参构造器:
1.其实就是创建了一个空数组

2.这个时候add()会先确定是否扩容,然后执行赋值添加

2.1 确定是否扩容

2.2 扩容的长度

2.3 实现扩容

2.4 实现扩容的真正操作

创建有参构造器:
跟无参大体相同



总结:
1.首先进入创建ArrayList对象的构造器,会判断你是否有参
2.无参构造,调用add()方法的时候,ensureCapacityInternal()方法会先判断是否需要扩容,进入这个判断方法,会有一个方法calculateCapacity()判断你是不是空数组(也就是是不是无参构造器),如果是无参会给你容量minCapacity赋值为10,在进入下一个方法ensureExplicitCapacity()里有一个判断,会判断当前容量(你值的个数)是否大于数组长度,如果大于进入真正扩容的方法grow(),然后这个方法里如果你是空参就会把为10的容量minCapacity扩容数组
3.有参:跟无参一样,不过就是进入创建构造器时,有参会给你直接赋值你参数大小的长度的数组
Vector
特点(安全):
- 底层也是Object elementDate[]对象数组
- 线程是同步的,安全的,效率低
方法都是带锁的

源码分析:

无参构造器会直接跳到有参构造器,并且扩容为10

然后添加数据add(),进入下面这个方法

判断容量和数组大小

然后扩容

它这里是直接扩容两倍

LinkedList
特点(有序,重复且高效,无索引查找不便)
- 底层实现双向链表和双端队列的特点
- 两个属性frist和last,首尾节点
- 添加和删除不是用数组实现的,效率高,数组插入和删除,要整个移动,而链表只要改变指向
源码解析:
-
无参LinkedList构造器,此时first和last都为null
-
执行add()添加,进入Linklast()尾插
![1641278641492]()
3.尾插创建新结点

4.删除源码:
进入remove(),真正执行删除的是unlik()

在看看unlik()的源码:

图解一波:

ArrayList和linkedList的对比

增删用LinkedList,查询用ArrayList
Set接口
底层都是用的Map接口的不过[key,value]:value一直是一个定值PRESENT,没有用它,存的都是key
特点(无序不重复):
-
无序(添加和取出的数据不一致),没有索引
-
不允许重复,最多包含一个null
跟List接口一样都实现了Collection接口,所以基础方法相同,也可以迭代器和增强for遍历,但是不能通过索引获取了
常用方法:
public static void main(String[] args) {
Set set = new HashSet();
// 特点:
// 1.不能存重复元素,不能存多个null
// 2.存放和取出顺序不一致
// 3.但是它取出的顺序是固定的
set.add(1);
set.add(1);
set.add(null);
set.add(null);
set.add("n");
System.out.println(set);
System.out.println(set);
// 遍历:
// 1.迭代器遍历
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
// 2.增强for循环
for (Object o:set) {
System.out.println(o);
}
// 删除
set.remove(1);
System.out.println(set);
}
源码:
底层是HashMap

HashSet:

特点(不重复且高效插入):
- 实现了Set接口所以,同样不能有重复元素,只能存一个null
- 不保证元素的存放和取出顺序
疑问?
public static void main(String[] args) {
// 这里是不同对象,所以也满足不重复
HashSet hashSet = new HashSet();
hashSet.add(new Dog(10));
hashSet.add(new Dog(10));
System.out.println(hashSet);
// 这里?为什么只存进一个
hashSet.add(new String("1"));
hashSet.add(new String("1"));
System.out.println(hashSet);
}
}
底层机制说明(数组+链表+红黑树):
- Hashset的底层是HashMap
- 添加一个元素时,会先得到一个hash值,再转成索引
- 找到存储的数据表table,看这个索引位置是否已经存放了元素
- 如果没有则直接添加
- 如果有,则调用equals判断内容是否相等,如果相同放弃添加,如果不同添加到最后
- 在java8中,如果链表的元素个数达到了****(默认值为8),并且table的大小>=64就会转换成红黑树
源码:
添加时进入add(),发现真正用到的方法是HashMap的put():
PRESEENT是一个固定对象,为了符合Map键值对的设计

进入Put():

进入hash(key):会有一个算法帮你求出hash值(不是单纯的hashcode)
右移16位降低冲突
可以看到当key=null,hash为0,也解释了为什么null添加时会在第一个

进入putval():
第一个add()添加
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义一个Node数组 和辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
//第一次进入table肯定为null进入判断
if ((tab = table) == null || (n = tab.length) == 0)
//进入这个resize()方法,会把table长度扩容为16个空间
n = (tab = resize()).length;
//(1)根据key,得到的hash值,去计算该key应该存放在table表的那个索引位置
//并且把这个位置的对象,给P
//(2)如果p为空,表示没有存放这个元素,就创建一个Node
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果此时的数组长度不够
//threshold临界长度在扩容时设置成(LoadFactor加载因子)0.75*扩容长度(提前扩容)
if (++size > threshold)
//扩容
resize();
afterNodeInsertion(evict);
return null;
}
第二个add()添加:跟第一次,差不多不过此时table不为null,不用初始赋空间了
第三个add()我们添加一个相同的元素
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义一个Node数组 和辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
//第一次进入table肯定为null进入判断
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(1)根据key,得到的hash值,去计算该key应该存放在table表的那个索引位置
//并且把这个位置的对象,给P
//(2)如果p为空,表示没有存放这个元素,就创建一个Node
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
第三次肯定会进入这里
Node<K,V> e; K k;
//(3)如果当前table位置的链表的头元素p的hash与新元素的hash相等
// 并且满足下面两个条件之一
//准备加入的key和p指向的node结点的key相同
//key值不为空,并且加入的key值内容等于p指向的node结点的k值内容
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//(4)再判断是不是红黑树,如果是就调用putTreeVal()添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//(5) 如果也不是红黑树
else {
//遍历当前元素位置的链表
for (int binCount = 0; ; ++binCount) {
//如果遍历到最后为null,就退出循环,把新元素尾插进去
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//把新元素添加到链表后,立即判断链表长度是否有8个
if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st
//转成红黑树,这个方法treeifyBin里判断,table表的大小<64,只扩容
treeifyBin(tab, hash);
break;
}
//如果hash值或者对象地址或者值内容相同退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//p指向e(p的下一个结点),继续下一个循环
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果此时的数组长度不够
if (++size > threshold)
//扩容
resize();
afterNodeInsertion(evict);
return null;
}
总结:
首先,HashSet底层用的是HashMap(数组+链表+红黑树)
首次进入添加时:,首先会判断你是不是空表,如果表为空,会进入resize()进行扩容,扩容长度为16,此后每次扩容*2,但是还会根据(LoadFactor加载因子)0.75*16得到一个临界长度threshold,超过临界值就会扩容(提前扩容),然会根据hash值求出你在table表的索引位置的元素P,如果此时这个元素p为空,就添加元素
后面添加重复元素时:会进入p元素不为空的判断,此时会判断p元素的hash与新元素的hash是否相等,相等继续后面判断,(p元素的node结点的key的地址是否与新元素相等 || key值不为空,key的内容相等)满足其一就可进入判断,判断内容为e=p(e指向p),此时不能添加元素
如果hash相等,后面判断不成立,继续else if 判断是不是红黑树,如果是调用putTreeVal()添加
如果上面还不成立,继续else 遍历p的当前链表,遍历时判断hash,对象地址,对象值是否相等(如果相等就退出循环),不等就把p=e(e为p的下一个结点)然后继续循环,循环到最后把新结点元素添加到尾部,添加后,看当前链表长度是不是>8,如果成立进入treeifyBin()(红黑树方法,并不是直接转换成红黑树,它里面会看你表的个数是否达到64,如果达到64才会变成红黑树)此时退出循环链表
注意:添加时不是要把表的每个索引到占着到12才扩容,而是只要对象达到12就会扩容
LinkHashSet:
特点:(不重复,且有序可以查询)
- 是HashSet的子类
- 底层是一个LinkHashMap,底层维护了一个hash表+双向链表(head,tail,before,after),next维护数组中一行的单链表,before和after维护一个双链表保证有序
- 同HashSet一样也是通过hashcode确认在数组中的位置,同时使用了链表维护了插入次序(插入有序和取出顺序一致)
跟HashSet一样,添加元素时都是先求hash值,再求索引,确定在table表的位置,再把元素添加到双向链表,如果存在就不添加(跟HashSet一样)
分析:
- 第一次add()添加时,直接扩容为16(同HashSet),存放的类型是 LinkedHashMap$Entry
- 数组的类型是HashMapLinked$Node[]存放的类型却是LinkedHashMap$Entry,里面内部类继承
总结:
大体跟HashSet一样,效率不如HashSet,但是加了双向链表保证了有序
HashSet和LikHashSet都可以通过hashcode和equals来操作他们
Map:
特点:
- 与Collection并列存在,用于保存具有映射关系的数据:key-value
- Map中的kep和value可以是任何类型的数据,会封装到HashMap$Node对象中
- Map中的key不能重复(当key重复时,value会替换),原因跟HashSet一样,但是value可重复
- Map中key跟Hashset时一样可以为null,但是只能有一个null(null哈希值固定的,只能添加一次),但是value可以为很多null(value只是内容)
- 通过key能一对一找到value
- 为了方便遍历会有一个集合EntrySet存放我们的Node结点的引用,存储的类型是Entry(Node实现了Entry)通过,为什么要是Entry类型,因为接口Entry中有getKey()和getValue()

实现:

public static void main(String[] args) {
// 1.为了遍历的方便,创建了一个EntrySet集合,存放的元素为Entry,但是实际上存放的是HashMap$Node ,因为Node实现了Entry这个接口
// Entryset集合里存放的是HashMap$Node的地址
// 2.当把HashMap$Node放到Entryset中就会方便我们的遍历,因为里面有getKey()和getValue()
Map map = new HashMap();
map.put("1",'a');
map.put("3",'a');
map.put("2",'a');
实现接口entrySet()
Set set = map.entrySet();
for (Object o: set){
转换Entry接口类型调用方法
Map.Entry e=(Map.Entry)o;
System.out.println(e.getKey()+" "+e.getValue());
}
}
常用方法:
- put:添加
- remove:删除
- get:获取值
- size:大小
- isEmpty:个数是否为0
- clear:清除所有
- containsKey:查找键是否存在
public static void main(String[] args) {
//1. put
Map map = new HashMap();
map.put("1","A");
map.put("1","B");
map.put("2","A");
map.put(null,"A");
map.put(null,null);
map.put("3",null);
System.out.println(map);
//2. remove
map.remove(null);
System.out.println(map);
System.out.println(map.remove("3", null));
System.out.println(map);
//3. get:获取值
System.out.println(map.get("1"));
//4. size
System.out.println(map.size());
//5. isEmpty:判断个数是否为0
System.out.println(map.isEmpty());
//6. clear:清除
map.clear();
//7. containsKey:查找键是否存在
System.out.println(map.containsKey("2"));
}
Map的三种遍历方式:
- 使用keyset(),取所有键
- 使用values(),取所有值
- 使用集合Entryset,使用getkey()和getvalues()分别取键值
public static void main(String[] args) {
Map map = new HashMap();
map.put(1,"a");
map.put(2,"b");
map.put(4,"d");
map.put(5,"e");
map.put(3,"c");
// 第一组通过keyset()拿到key
// 用增强for循环
Set set = map.keySet();
for (Object key:set){
System.out.println("第一种"+map.get(key));
}
// 迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println("第二种"+map.get(key));
}
// 第二组:用values()取出所有值
// for
Collection values = map.values();
for (Object v:values){
System.out.println(v);
}
// 迭代器
Iterator iterator1 = values.iterator();
while (iterator1.hasNext()) {
Object v = iterator1.next();
System.out.println(v);
}
// 第三组:通过Entryset集合
// for
Set set1 = map.entrySet();
for (Object key:set1){
Map.Entry K=(Map.Entry)key;
// System.out.println(K);//用等号连接的键值对
System.out.println(K.getKey()+""+K.getValue());//分开取
}
// 迭代器
Iterator iterator2 = set1.iterator();
while (iterator2.hasNext()) {
Map.Entry k = (Map.Entry) iterator2.next();
System.out.println(k.getKey()+""+k.getValue());
}
}
HashMap:
特点(键值对,key不能重复,无序):
跟Map里的一样
- 是Map最常用的类
- key不能重复,value可以重复,key和value都能为null,但key只能有一个null
- 添加相同的key值,会替换原来key的value
- 与HashSet一样存入和取出顺序是乱的,因为底层是由hash表的方式存在的
- 没有实现同步,线程不安全
替换的源码:

机制分析:

(k,v)是一个Node对象实现了Entry接口,引用放在Entryset集合种
- HashMap底层维护的是一个Node类型数组table,初始为null
- 当添加key-value时,扩容机制看HashSet总结
- 扩容大于临界属性,就扩容
- 在树化方法里,进入树化这个方法看Node结点size是否>8(由于是只有找到相同结点才去链表一个一个判断,所以0-7有8个要加上已经有了的那个结点就是9个),进去后数组length>64要扩容
源码:
看HashSet
总结:
HashMap与HashSet的区别:
HashSet实现了set和Collction接口,所以有上面接口的方法和迭代器,但是根本上是一个HashMap,它的创建添加,跟HashMap一致,正如我们这么想,HashMap是一个双列键值对集合,键对值一一要对应,键不可重复很正常,而HashSet一个单列集合却也不能重复,原因就是因为也是由hash值确定table表索引,其实HashSet,也是一个键值对,但是HashSet的value值是一个已经固定的值
另外HashSet有一个子类LinkedHashSet,就是在HashSet的基础上底层有了个双链表,所以有序,HashMap也有个子类叫LinkedHashMap,同样有序
Hashtable:
特点(键值对,线程安全,键值都不能为null):
- 键值对双列集合
- 键值都不能为空
- 用法跟HashMap一致
- 线程是安全的,方法都带synchronized
源码上看:确实不能放null

机制分析:
-
底层数组为Hashtable$Entry[],初始为11
-
临界值为11*0.75=8
-
扩容(第二次):11*2+1=23( 源码部分:(oldCapacity << 1) + 1)
Properties类:
- 实现了Map接口所以也是键值对
- 继承于Hashtable
- 常用于配置文件
TreeSet:
底层就是TreeMap
特点:
- 当我们使用无参构造器的时候就会给我们传入的数据排序,但只能升序排序
- 自定义规则排序,使用TreeSet提供的构造器,可以传入一个比较器(匿名内部类),并制定排序规则
public static void main(String[] args) {
// 当我们使用无参构造器的时候就会给我们传入的数据排序,但只能升序排序
// 自定义规则排序,使用TreeSet提供的构造器,可以传入一个比较器(匿名内部类),并制定排序规则
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
// 调用字符串的compareto方法进行字符串大小比较
return ((String)o1).compareTo((String)o2);
}
});
treeSet.add("1");
treeSet.add("7");//当传入两个相同的元素时(当规则比较的是长度时,那么传入长度相等的数也传不进去),添加不进去
treeSet.add("0");
treeSet.add("6");
treeSet.add("1");
System.out.println(treeSet);
}
构造器底层:会把comparator给TreeMap
源码:
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
添加在调用add时,在底层会执行到TreeMap的put()方法:
源码:
Comparator<? super K> cpr = comparator;//这个就是我们传入的comparator
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);//如果相等,返回0,添加不进去
} while (t != null);
TreeMap:
特点:
跟TreeSet一致,就是当传入的key相同时,value会被覆盖
总结:
确认用什么?
先确认是用一个对象(单列集合)还是键值对(双列集合)
单列集合:实现Collecion接口
允许重复:List
增删多:LinkedList(底层双向链表)
改查多:AarryList(底层是一个Object类型数组)
不允许重复:Set
无序:HashSet(底层是HashMap==》数组+链表+红黑树)
排序:TreeSet
插入和取出顺序一致:LinkedHashSet(底层是HashMap==》在此基础+双向链表)
双列集合(键值对):实现Map接口
无序:HashMap(底层哈希表==》数组+链表+红黑树)
排序:TreeMap
插入和取出一致:LinkedHashMap
读取文件:Properties
Collections类:
-
是一个操作List,Set,Map等集合的工具类
常用方法:
- 反转reverse
- 随机排序shuffle
- sort自然排序,也可自定义
- max最大值,也可以用Comparator自定义
- min,也可以用Comparator自定义
- frequency(),出现次数
- replaceAll,替换
public static void main(String[] args) { ArrayList list = new ArrayList(); list.add(1); list.add(3); list.add(2); list.add(4); list.add(4); // 反转reverse Collections.reverse(list); System.out.println(list); // 随机排序 Collections.shuffle(list); System.out.println(list); // sort自然排序 Collections.sort(list); System.out.println(list); // sort自定义 // Collections.sort(list, new Comparator() { // @Override // public int compare(Object o1, Object o2) { // return ((String)o1).length()-((String)o1).length(); // } // }); // mxa最大值,也可以用Comparator自定义 System.out.println(Collections.max(list)); // min,也可以用Comparator自定义 System.out.println(Collections.min(list)); // frequency(),出现次数 System.out.println(Collections.frequency(list, 4)); // replaceAll,替换 Collections.replaceAll(list,4,1); System.out.println(list); }

浙公网安备 33010602011771号