Java学习记录9——集合
一、集合类
基本介绍
集合是一种容器,与数组类似,不同的是集合的大小是不固定的,可以动态保存任意多个对象。
通过创建集合的对象表示得到一个集合容器,同时集合提供了比数组更好用、更丰富的API功能来操作对象,如add、remove、set、get等,添加删除元素简洁
集合的框架体系
集合主要分为两组:
- 单列集合:单个存储元素——Collection接口
- 双列集合:存储键值对——Map接口
Collection接口有两个重要的子接口List和Set,它们的实现都是单列集合
Map接口的实现子类是双列集合,存放对应的Key-Value
总框架:
二、Collection接口
1.Collection接口实现类的特点
- Collection的实现子类可以存放多个元素,每个元素可以是Object
- 有些Collection的实现类可以存放重复元素,有些不可以
- Collection的实现类有的有序(List接口),有的无序(Set接口)
- Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现的
2.Collection接口的常用方法
方法名 | 描述 |
---|---|
add | 添加单个元素 |
remove | 删除指定元素 |
contains | 查找元素是否存在 |
size | 获取元素个数 |
isEmpty | 判断是否为空 |
clear | 清空 |
addAll | 添加多个元素 |
removeAll | 删除多个元素 |
containsAll | 查找多个元素是否存在 |
PS:clear()方法比removeAll()方法更高效
如果元素满足特定条件,我们也可以使用removeIf()方法将满足条件的删除
以ArrayList为例
List list = new ArrayList();
list.add("jack");
list.add(10);//自动装箱,相当于new Integer(10)
list.add(true);//自动装箱Boolean
list.remove(0);//按索引删除
list.remove(true);//指定删除元素
list.contains("jack"); //返回false
list.size();//1
List list1 = new ArrayList();
list1.addAll(list);//传入集合,把list中的元素添加到list1
//removeAll、containsAll也需要传入集合
3.Collection接口遍历元素的方式
1)使用Iterator(迭代器)
迭代器接口介绍:
- Iterator对象称为迭代器,主要用于遍历Collection集合中的元素
- 所有实现了Collection接口的集合类都有一个iterator()方法,用于返回一个实现了Iterator接口的对象(迭代器)
- Iterator仅用于遍历集合,本身不存放对象
迭代器的执行原理:
得到一个集合的迭代器,hasNext()方法:判断是否还有下一个元素,next()方法:游标后移,并将后移之后对应的集合位置上的元素返回
快捷键:itit
可快速生成如下while循环
Iterator iterator = coll.iterator();//得到一个集合的迭代器
while (iterator.hasNext()) {//有元素则继续遍历
Object obj = iterator.next();
System.out.println(obj);
}//循环结束时指向最后的元素
//再次遍历需要重置迭代器
iterator = coll.iterator();
PS:调用iterator.next()
方法之前必须要调用iterator.hasNext()
进行检测。
若不检测而下一条记录无效,直接调用iterator.next()会抛出NoSuchElementException异常
2)for循环增强
增强for循环就是简化版的iterator,本质一样,底层还是迭代器,只能用于遍历集合或数组
for(Object obj : col) { //此处要求为Object类型
}
三、List接口
1.基本介绍
List接口是Collection接口的子接口
- List集合中元素有序(添加和取出顺序一致)且可重复
- List集合中的每个元素都有其对应的顺序索引
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可根据序号存取容器中的元素
- List常见实现类:ArrayList、LinkedList、Vector
2.List接口的常用方法
ArrayList.remove(Integer.valueOf(1)); //删除值为1的元素
3.List的三种遍历方式
- 使用Iterator(迭代器)
- 使用增强for循环
- 使用普通for循环
1.ArrayList
从jdk1.2开始使用
允许任何元素(包括空元素),ArrayList由数组实现数据存储。
ArrayList基本等同于Vector,除了ArrayList线程不安全,执行效率高。多线程情况下不建议用ArrayList
底层操作机制源码分析
- ArrayList中维护了一个Object类型的数组
elementData.transient Object[] elementData
//transient意为瞬间,表示该序列不会被序列化 - 当创建ArrayList对象时,如果使用无参构造器,则初始elementDate容量为0,第一次添加扩容为10,如需再次扩容,则扩容为elementData的1.5倍
- 如果使用指定大小的构造器,则初始elementData容量为指定大小,如需扩容,则直接扩容为elementData的1.5倍
2.Vector
从jdk1.0开始使用
Vector底层也是一个对象数组protected Object[] elementData
Vector是线程同步的,线程安全,效率比ArrayList低
底层操作机制
- 当创建Vector对象时,如果使用无参构造器,则默认elementDate容量为10,如需再次扩容,则扩容为elementData的2倍
//new Vector()底层
public Vector() {
this(10);//本质上还是调用有参构造
}
如果使用指定大小的构造器,则初始elementData容量为指定大小,如需扩容,则直接扩容为elementData的2倍
//new Vector(int)底层
public Vector(int initialCapacity) {
this(initialCapacity,0);
}
3.LinkedList
LinkList底层实现了双向链表(jdk1.6之前为循环链表,jdk1.7取消了循环)和双端队列
可以添加任意元素(可重复),包括null,线程不安全,未实现线程同步
底层操作机制
- LinkList底层维护了一个双向链表
- 维护了两个属性
first
和last
分别指向首尾节点 - 每个节点(Node对象)中维护了prev、next、item三个属性
- LinkList元素的添加和删除通过双向链表实现,效率相对较高
LinkedList list = new LinkedList();
//底层:public LinkedList() {},此时first、last=null
add添加的底层源码:
l,e,null对应节点的prev,item,next
4.List集合的使用选择
多线程时使用Vector,ArrayList和LinkedList线程不安全,效率相对高
四、Set接口
1.基本介绍
无序(添加和取出顺序并不一样,但每次取出的顺序是固定的),无索引,不允许重复元素,最多包含一个null
Set接口常用实现类:HashSet、TreeSet、LinkedHashSet
常用方法和遍历方式同Collection接口,但没有索引,不能用索引方式通过普通for循环获取。
1.HashSet
- HashSet底层实际上是HashMap(HashMap底层是数组+链表+红黑树)
- HashSet不保证元素有序,取决于hash后,再确定索引的结果(添加和取出顺序并不一样,但每次取出的顺序是固定的)
- 不允许重复元素(本质是地址相同)
HashSet hashSet = new HashSet();
String a = "java";
String b = "java";
hashSet.add(a);//添加成功
hashSet.add(b);//添加失败,因为已有"java"
hashSet.add(new String("java"));//添加失败
hashSet.add(new Cat("Tom"));
hashSet.add(new Cat("Tom"));//不同对象,可以添加
System.out.println(hashSet);//1个java,两个Tom
原理:
- 添加一个元素时,先得到hash值,转换成索引值
找到存储数据表table,看这个索引位置是否已经有存放元素 - 如果没有,直接加入;如果有,调用equals比较(equals可以自己重写比较标准),相同就放弃添加,不同则添加到该索引的最后(以链表形式添加,如果该索引位置已经是链表,就使用for循环和链表所有元素比较)
- Java8中,如果一条链表的元素个数>=TREEIFY_THRESHOLD(默认为8),且table大小>=MIN_TREEIFY_CAPACITY(默认为64),就会进行树化(红黑树)
- 在把元素添加到链表后,立即判断该链表是否已达到8个节点,达到就调用treeifyBin(),判断若table大小<64,则先对table扩容,若>=64,则将当前链表转成红黑树
扩容机制:
table就是HashMap的一个数组,类型是Node[ ],若当前table为null或大小=0,就是第一次扩容,得到16个空间,临界值为160.75=12,0.75为加载因子(loadfactor)。若table使用到了临界值12(指其中元素数量达到临界值threshold,占用数组或链表的都算),就会扩容到162=32,新临界值为24
易错问题:
2.LinkedHashSet
LinkedHashSet 是 HashSet 的子类,底层是一个LinkedHashMap(HashMap的子类),维护了一个数组+双向链表
底层存储的数组是HashMap$Node []
,存放的数据元素是LinkedHashMap$Entry
类型(多态数组),因为LinkedHashMap的Entry中有可实现双向链表的before和after属性
LinkedHashSet根据元素的hashCode值来确定元素的存储位置,同时使用双向链表来维护元素的次序,这使得元素看起来是以插入顺序保存的
3.TreeSet
TreeSet存储数据有有序,不重复,底层为红黑树(自平衡的排序二叉树)
当使用无参构造器创建TreeSet时,元素无序
使用TreeSet提供的有参构造器,可传入一个比较器(匿名内部类),并指定排序规则。
构造器把传入的比较器对象(实现了Comparator接口的匿名内部类对象)赋给了TreeSet底层的TreeMap的属性comparator
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//调用String的compareTo方法比较字符串大小
return ((String) o1).compareTo((String) o2);//从小到大
//return ((String) o2).compareTo((String) o1);//从大到小
//按长度大小排序
return ((String) o1).length()-((String) o2).length();
}
});
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//按长度大小排序(会认为长度相等就是相同)
return ((String) o1).length()-((String) o2).length();
}
});
treeSet.add("tom");
treeSet.add("curry");
treeSet.add("abc");//添加失败,因为长度与"tom"相同
PS:
传入了Comparator匿名对象,使用实现的方法compare去重,如果方法返回0,就认为是相同的元素,不添加
若未传入Comparator匿名对象,则以所添加的对象实现的Comparable接口的compareTo去重
TreeSet若没有传入Comparator匿名对象,就需要把传入的元素转为Comparable类型,这就要求传入数据的类型必须实现Comparable接口。
TreeSet treeSet = new TreeSet();
treeSet.add(new Person());//ClassCastException异常
class Person {
}
//实现Comparable接口即可
class Person implements Comparable {
@Override
public int compareTo(Object o) {
return 0;//始终返回0,则只能添加一个
}
}
常用方法:
TreeSet<Integer> s = new TreeSet<Integer>();
//使用s.add(1);等把1-5都加进去,代码省略
System.out.println(s.ceiling(3)); //>=3的最小的数,输出3
System.out.println(s.floor(3)); //<=3的最大的数,输出3
System.out.println(s.higher(3)); //>3的最小的数,输出4
System.out.println(s.lower(3)); //<3的最大的数,输出2
System.out.println(s.headSet(3)); //<3的数组成的TreeSet,输出[1, 2]
System.out.println(s.tailSet(3)); //>=3的数组成的TreeSet,输出[3, 4, 5]
System.out.println(s.subSet(2,4)); //>=2且<4的数组成的TreeSet,输出[2, 3]
System.out.println(s.subSet(2,false,4,true)); //>2且<=4的数组成的TreeSet,输出[3, 4]
五、Map接口
1.Map基本介绍
- Map与Collection并列存在,用于保存有映射关系的数据Key—Value
- Map中key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
- Map中key 是无序的、不可重复的(同HashSet),value 是无序的、可重复的
- 如果用已用过的key存放value,那么这个新value会替换原有的value
- Map中key可为null,只能有一个;value也可为null,可以有多个
- 常用String类作为Map的key
- key和value之间存在单向的一对一关系,通过key找对应的value
- 一对key—value放在一个HashMap
N
o
d
e
中,
‘
H
a
s
h
M
a
p
Node中, `HashMap
Node中,‘HashMapNode node = newNode(hash,key,value,null);`
Node实现了Entry接口。为方便遍历,还会创建EntrySet集合,定义存放Entry类型,Entry对象(实际上就是Node对象)中有k,v指向newNode中的key和value。
将Node放到EntrySet方便遍历是因为Map.Entry提供了方法getKey()和getvalue()
遍历Node:
HashMap map = new HashMap();
map.add("k1","1");
map.add("k2","2");
Set set = map.entrySet();
for (Object obj : set) {//遍历
Map.Entry entry = (Map.Entry) obj;//向下转型
System.out.println(entry.getKey()+"-"+entry.getValue());
}
2.Map常用方法
方法名 | 描述 |
---|---|
put | 添加,若已有对应值则替换 |
get | 根据键获取值 |
remove | 根据键删除映射关系 |
containsKey | 查找键是否存在 |
size | 获取元素个数 |
isEmpty | 判断是否为空 |
clear | 清空 |
keySet | 获取所有的键 |
entrySet | 获取所有的关系 |
values | 获取所有的值 |
3.Map的遍历方式
-
先取出map所有的key,再通过key取出对应的value
Set keyset = map.keySet();
以Set类型获取所有键,可使用Set的两种遍历方式:
增强for循环、迭代器 -
把所有的value取出
Collection values = map.values();
以Collection类型获取所有键,可使用Collection的两种遍历方式:增强for循环、迭代器
3.通过EntrySet来获取k-v
增强for循环、迭代器
Set entrySet = map.entrySet();
//增强for循环
for (Object entry : entrySet) {
//将entry转成Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey()+"-"+m.getValue());
}
//迭代器
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()) {
Object entry = iterator.next();//entry为HashMap$Node类型,实现了Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey()+"-"+m.getValue());
}
六、Map实现类
1.HashMap
1)基本介绍
HashMap是Map接口使用频率最高的实现类,底层为数组+链表+红黑树。
HashMap没有实现同步,线程不安全
2)底层原理
(k,v)是一个Node,实现了Map.Entry<k,v>
jdk7.0的HashMap底层是数组+链表,jdk8.0底层是数组+链表+红黑树
3)扩容机制
2.LinkedHashMap
继承自 HashMap,所以它的底层仍然是数组和链表或红黑树组成。
在HashMap结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
3.HashTable
1)基本介绍
- 存放元素是键值对k-v
- HashTable的键和值都不能为null,否则抛出NullPointerException
- HashTable使用方法基本与HashMap一样
- HashTable线程安全,效率相对较低
2)底层原理(数组+链表)
- HashTable底层有数组
Hashtable$Entry[]
初始化大小为11 - 临界值threshold=11*0.75=8(向上取整)
3)扩容
执行方法 addEntry(hash,key,value,index); 添加k-v,封装到Entry。
元素数大于临界值时扩容,每次扩容为原容量的2倍+1,如11->23
4)与HashMap对比
4.Properties
Properties类继承自Hashtable类并实现了Map接口,使用键值对形式保存数据,key和value都不能为null。使用特点与Hashtable类似
Properties还可用于从.properties文件中,加载数据到Properties类对象,并进行读取和修改(.properties文件通常作为配置文件)
5.TreeMap
与TreeSet类似
使用默认构造器创建TreeMap对象,存储的键值对无序
使用有参构造器,可传入一个比较器(匿名内部类),并指定排序规则。
构造器把传入的比较器对象(实现了Comparator接口的匿名内部类对象)赋给了TreeMap的属性comparator
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//按长度大小排序(会认为长度相等就是相同)
return ((String) o2).length()-((String) o1).length();
}
});
treeSet.add("tom","TOM");
treeSet.add("curry","CURRY");
treeSet.add("abc","ABC");//添加失败,因为长度与"tom"相同,不会替换
七、集合类的选择
主要取决于操作特点,根据集合实现类的特性选择
1、先判断存储的类型:一组对象(单列)或 一组键值对(双列)
2、一组对象(单列):Collection接口
- 允许重复:List
- 增删多:LinkedList(底层双向链表)
- 改查多:ArrayList(底层Object类型的可变数组)
- 不允许重复:Set
- 无序:HashSet(底层HashMap,维护了一个哈希表)
- 排序:TreeSet
- 插入和取出顺序一致:LinkedHashSet(底层LinkedHashMap,维护了数组+双向链表)
3、一组键值对(双列):Map
- 键无序:HashMap(底层哈希表(数组+链表+红黑树))
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap(底层HashMap)
- 读取文件:Properties
八、Collections工具类
1.基本介绍
Collections 是一个操作Set、List和Map等集合的工具类
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,用Collections.方法名调用
2.排序操作
reverse(List)
:反转List中元素的顺序shuffle(List)
:对List集合元素进行随机排序sort(List)
:根据元素的自然顺序对指定的List集合元素按升序排序sort(List,comparator)
:根据指定的Comparator产生的顺序对List集合元素进行排序swap(List, int i, int j)
:将指定List集合中的 i 处元素和 j 处元素进行交换
3.查找、替换
Object max(Collection)
:根据元素的自然顺序,返回给定集合中的最大值Object max(Collection,Comparator)
:根据Comparator指定的顺序,返回给定集合中的最大值Object min(Collection)
:最小值Object min(Collection,Comparator)
int frequency(Collection,Object)
:返回指定集合中指定元素出现的次数void copy(List dest,List src)
:将src中的内容复制到dest中(dest空间大小需>=src)boolean replaceAll(List list, Object oldVal, Object newVal)
:使用新值替换List对象的所有旧值
九、集合转换为数组
1.集合转为对象数组
使用toArray()
方法即可,不传入参数只能转换为Object类型,不能强转;传入参数转换为对应类型:
ArrayList<Integer> list = new ArrayList<>();
Object[] objects = list.toArray();
Integer[] integers = list.toArray(new Integer[0]);
2.集合转为基本类型数组
toArray()
方法不能将集合直接转成基本数据类型的数组,需要使用流式编程和lambda表达式:
ArrayList<Integer> list = new ArrayList<>();
int[] ints = list.stream().mapToInt(x -> x).toArray();
stream()方法用于获取流对象,用mapToInt拿到int流对象,再使用toArray返回int数组即可。
x->x是lambda表达式,对应一个函数式接口,因为集合中泛型为Interger类型,可自动拆箱,所以直接返回x就行
如果这里式String泛型要转为int数组的话,就要调用Integer.parseInt()方法,lambda写法:mapToInt(Integer::parseInt)