本系列博客是韩顺平老师java基础课的课程笔记,B站:课程链接,吐血推荐的一套全网最细java教程,获益匪浅!
1. 集合框架体系
-
集合
- 可以动态地保存多个对象,使用比较方便
- 提供了一系列方便的操作对象的方法:add、remove、set、get等
- 使用添加删除元素比较简单。
-
集合类有很多,主要分为两大类:单列集合和双列集合
-
collection接口实现类的特点
- 可以存放多个元素,每个元素可以是object
- 有些collection的实现类,可以存放重复的元素,有些不可以
- 有些collection的实现类,是有序的(list),有些是无序的(set)
- collection接口没有直接的实现子类,是通过它的接口Set和List来实现的
-
collection接口实现类的遍历,以ArrayList为例:
- 使用iterator()迭代器进行遍历
- 所有实现collection接口的集合类都有一个iterator()方法,用以返回iterator接口的对象
- iterator仅用于遍历集合,本身并不存放对象
- 循环退出时,iterator指向最后一个元素。如果需要重新遍历,要重置迭代器
- 使用增强for循环进行遍历
- 增强for循环底层也是用到了迭代器,可以将其理解为简化版本的迭代器
- 数组也可以用增强for循环进行遍历
- 使用iterator()迭代器进行遍历
// 方法1
ArrayList arrayList = new ArrayList();
Iterator iterator=arrayList.iterator();while (iterator.hasNext())
{
//返回类型是object
Object obj=iterator.next();
}
Iterator iterator=arrayList.iterator()
// 方法2
for(Object book : arrayList){
System.out.println(book);
}
2. Collection(单列集合)
2.1 List
- List接口的方法
- List接口的特点
- List中元素是有顺序的(添加的顺序和取出的顺序一致),且有重复的元素
- List中每个元素都有其对应的顺序索引,取元素:list.get(3)
以下是🍓List接口的三个常用实现类
1️⃣ArrayList
- 可以加入null,并且可以加入多个
- ArrayList基本等同于Vector,除了ArrayList是线程不安全的(但是执行效率高),在多线程的情况下,不建议使用ArrayList
- 🍓ArrayList的扩容机制
- ArrayList中维护了一个Object类型的数组elementData
- 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次添加元素后扩容为10,如果还需要扩容则扩容elementData为原来的1.5倍(0-10-15-22-33…)
- 如果使用的是指定大小的构造器,则初始elementData为指定大小,如果需要扩容则直接扩容elementData为原来的1.5倍
2️⃣Vector
- vector的底层也是一个对象数组
- vector是线程同步的,即线程安全,vector类的操作方法带有synchronized
- 在开发中,如果需要线程同步安全时,考虑使用vector
- vector的扩容机制与ArrayList的扩容机制对比
3️⃣ LinkedList
-
LinkedList实现了双向链表和双端队列的特点
-
可以条件任意元素(包括null),且元素可重复
-
线程不安全,没有实现同步
-
底层操作机制
- 底层维护一个双向链表
- 维护了两个属性first 和 last 分别指向 首结点和尾结点
- 每个结点维护了prev next item 三个属性,其中通过prev指向前一个,next指向后一个及诶单那,最终实现双向链表
- 所以LinkedList的元素添加和删除,不是通过数组完成的,相对来说效率更高
-
ArrayList和LinkedList的比较
- 如果改查操作较多,选择ArrayList
- 如果增删的操作较多,选择LinkedList
- 一般来说,在程序中80%-90%的操作都是查询,因此大部分请鲁昂下会选择ArrayList
- 在一个项目中,根据业务灵活原则,一个模块使用ArrayList,另一个模块使用LinkedList,也就是说,要根据业务来选择
2.2 Set
- Set接口的基本介绍
- 无序(添加和去除的顺序不一致),没有索引
- 不允许重复元素,所以最多包含一个null
- Set接口的常用方法
- 和List一样,Set接口也是Collection的子接口,因此常用方法和Collection接口一样
- Set接口的遍历方式
- 可以使用迭代器和增强for循环,不能使用索引的方式来获取
以下是🍓Set接口的常用实现类
1️⃣HashSet
-
基本介绍
- HashSet实现了Set接口
- HashSet的底层实际上是HashMap,底层维护的是一个数组table+单向链表
- 可以存放null值,但是只能有一个null
- HashSet不保证元素是有序的,取决于hash后,再确定索引的结果。也就是说,不保证存放元素的顺序和取出的顺序一致,有可能一样,有可能不一样
- 不能有重复的元素
- 面试题:理解HashSet不能加入重复元素的真正含义需要看源码
-
HashSet添加元素的机制
- HashSet的底层实际上是HashMap
- 添加一个元素,先得到hash值,然后转成索引值
- 找到存储数据表table,看这个索引位置是已经存放了元素
- 如果有,则调用equals进行比较(注意具体比较什么是由程序员定的,不一定是比较值的大小),如果相同则放弃添加,如果不同的,则添加到最后
-
HashSet的扩容机制
-
第一次添加时,table扩容到16,临界值(threshold)是16*加载因子(loadFactor)0.75=12
-
也就是说,不是到达16才进行第二次扩容,而是在12时就开始扩容
-
如果table数组到临界值12,就会扩容到162=32,新的临界值是320.75=24,以此类推
-
注意:新加入的结点无论是连接在链表之后,还是直接放入在table中,都是要占用一个size的
-
在java8中,如果一条链表的个数到达TREEIFY_THRESHOLD(默认为8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认为64),就会进行树化(红黑树),否则仍采用数组扩容机制(比如说table大小为16,但是某条链表上的元素已经有8个,该链表再加入新元素时,table会扩容为32,该链表长度变为9)
-
在没有树化之前,其实是数据结构中拉链法的hashmap
-
-
树化之后table没有什么变化,只要是长度>=8的单链表会变成一棵红黑树(Node结点变成TreeNode结点)
-
-
练习题
- 两个元素的hashcode相同只能说明两个元素存入的位置是一样的,接下来比较equal,equals返回true不再添加进去,equals返回false的话就会挂在在单链表后面
- hashcode和equals都需要重写,直快捷键alt+insert就可以快捷地重写
package com.leetcode;
import java.util.HashSet;
import java.util.Objects;
public class test {
public static void main(String[] args) {
HashSet hashSet=new HashSet();
hashSet.add(new Employee("a",18));
hashSet.add(new Employee("b",17));
hashSet.add(new Employee("a",18));
System.out.println(hashSet); }
}
class Employee{
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age; }
@Override public String toString() {
return "Employee{"+
"name:"+name+
",age:"+age+"}"; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == employee.age && Objects.equals(name, employee.name); }
@Override
public int hashCode() {
return Objects.hash(name, age); }
}
- 思考题
package com.leetcode;
import java.util.HashSet;
import java.util.Objects;public class test {
public static void main(String[] args) {
HashSet hashSet=new HashSet();
hashSet.add(new Employee("linda",1,new MyDate(1,2,3)));
hashSet.add(new Employee("linda",4,new MyDate(1,2,3)));//前两个属于相同的员工
hashSet.add(new Employee("linda",1,new MyDate(1,3,3)));
//碰撞时不能添加进入HashSet必须满足两个条件:1. hashcode相同(代表存入到table中的相同位置)2.equals相同(代表存入的内容一样,不准存)
System.out.println(hashSet); }
}
class Employee{
private String name;
private int sal;
private MyDate birthday;
public Employee(String name, int sal, MyDate birthday) {
this.name = name;
this.sal = sal;
this.birthday = birthday; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday); }
@Override
public int hashCode() {
return Objects.hash(name, birthday); }
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", sal=" + sal +
", birthday=" + birthday +
'}'; }
}
class MyDate{
private int year;
private int month;
private int day;
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyDate myDate = (MyDate) o;
return year == myDate.year && month == myDate.month && day == myDate.day; }
@Override
public int hashCode() {
return Objects.hash(year, month, day); }
@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}'; }
}
2️⃣LinkedHashSet
-
基本介绍
- LinkedHashSet是HashSet的子类
- LinkedHashSet的底层是LinkedHashMap,底层维护了一个table+双向链表
- LinkedHashSet根据元素的hashcode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入的顺序保存的
- LinkedHashSet不允添加重复的元素
-
底层机制
-
双向链表使得遍历LinkedHashSet时,也能确保插入顺序和遍历顺序一致(和HashSet不一样)
-
-
每一个结点都有before和after属性,这样形成一个双向链表
-
添加元素时,首先求hash值,再求索引,确定其在table中的位置,然后将添加的元素加入到双向链表中(如果已经存在则不再添加)
-
-
扩容机制
- 第一次添加时,table扩容到16,存放的结点类型是LinkedHashMap$Entry
- 数组的类型是HashMap N o d e [ ] ,存放的元素类型是 L i n k e d H a s h M a p Node[] ,存放的元素类型是LinkedHashMap Node[],存放的元素类型是LinkedHashMapEntry
- 其他和HashSet相同
3️⃣TreeSet
- 最大的特点是可以排序
- 当使用无参构造器时,默认是无序的,但是可以使用有参构造器传入Comparetor接口
- 不允许加入Comparetor比较结果相同的元素!(比如说compator按照字符串长度进行排序,如果两个字符串长度相同,那么后面进来的字符串无法插入)
- TreeSet的底层还是TreeMap
3. Map(双列集合)
- map接口的特点
-
特点一
- map与collection并列存在,用于保存具有映射关系的数据(key-value)
- map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
- Map中的key不允许重复,原因和HashSet一样(当有相同的key时,会将原来的结点替换掉)
- map中的value可以重复
- map中的key和value都可以为null,但是key为null只能有一个,value为null可以有多个
- 常用String作为map中的key,key是object类型的,因此只要是对象就可以放进去
- key和value之间存在单向一对一关系,即通过key总能找到对应的value
- HashSet中存的就是key,所有结点的value都填充了一个叫做present的对象
-
特点二
- map存放数据的key-value示意图,一对k-v是放在HashMap的Node结点中的(node结点是HashMap的一个内部类),又因为Node实现了Entry接口,有些书上也说一对k-v就是一个Entry
-
- k-v 最后是 HashMap$Node node=newNode(hash,key,value,null)
- 2.k-v 为了方便程序员的遍历,还会创建EntrySet集合,该集合存放的类型Entry,每一个Entry有对象kv,也就是EntrySet<Entry<K,V>> 即 transient Set<Map.Entry<K,V>> entrySet
-
- EntrySet中定义的类型是Map.Entry,但是实际上存放的还是HashMap N o d e ,这是因为 H a s h M a p Node ,这是因为HashMap Node,这是因为HashMapNode实现了接口Map.Entry
-
- 当把HashMap$Node对象存到entrySet中就可以方便遍历,因为Map.Entry提供了两个重要的方法: getKey() getValue()
-
《这一节着实没听懂》
-
map接口的常用方法
-
map遍历的六种方式
- 相关方法
- containKey:查找键是否存在
- keySet:获取所有的键值
- entrySet:获取所有关系k-v
- values:获取所有的值
- 第一组:通过keySet先取出所有的key,再通过key取出对应的Value
- 第二组:通过collection把所有的value取出
- 第三组:通过EntrySet来获取k-v
- 相关方法
Set keySet = map.keySet();
//第一组
for(Object key:keySet){
System.out.println(key+"-"+map.get(key));}
Iterator iterator = keySet.iterator();while (iterator.hasNext()){
Object key=iterator.next(); System.out.println(key+"-"+map.get(key));}
//第二组:
Collection values = map.values();
for(Object value :values){
System.out.println(value);}
Iterator iterator1 = values.iterator();while (iterator1.hasNext()){
Object o=iterator1.next(); System.out.println(o);}
//第三组
Set set = map.entrySet();
// entrySet<Map.Entry<k,v>>
for(Object entry : set){
Map.Entry node=(Map.Entry) entry; System.out.println(node.getKey()+"-"+node.getValue());}
Iterator iterator2 = set.iterator();while (iterator2.hasNext()){
Map.Entry node=(Map.Entry) iterator2.next(); System.out.println(node.getKey()+"-"+node.getValue());}
- map小结
- HashMap是以k-v的方式来存储数据的,是Map接口使用频率最高的实现类
- key不能重复,但是value是可以重复的,如果添加相同的key,会覆盖原来的k-v,等同于修改
- 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的
- HashMap没有实现同步,因此是线程不安全的
🍓以下是Map接口的常用实现类
1️⃣HashMap
- (k,v)是一个Node类,它实现了接口Map.Entry<K,V>
- jdk7.0的HashMap的底层实现是【数组+链表】,jdk8底层的实现是【数组+链表+红黑树】
- HashMap的扩容机制和HashSet完全一致(因为HashSet的底层就是用HashMap实现的)
- 链表长度到达8,并且table长度到达64时才会树化。假如树化后,不停地删除这棵树上的结点,就会进行剪枝操作:将这棵树转为链表
2️⃣Hashtable
-
基本介绍
- 存放键值对
- 键值都不允许为null
- 使用方法和HashMap一样
- HashTable是线程安全的,HashMap是线程不安全的
-
底层机制
- 底层有数组HashTable$Entry[] 初始化大小为11
- 临界值 threshold 8 = 11 * 0.75 (即大小大于8时开始扩容)
- 扩容机制: 211+1=23 即 newCapacity=oldCapacity2+1
-
hashMap与hashtable的对比
2️⃣Properties
- Properties继承自HashTable类并实现了Map接口,也是使用一种键值对的形式来保存数据的
- 使用特点和hashtable类似
- Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
- 说明:工作后xxx.properties文件通常作为配置文件,这个知识点在IO流举例
3️⃣TreeMap
- 使用默认的构造器创建TreeMap是无序的(如果是字符串是默认按照字符串的大小进行排序的)
- 如果两个k-v的compator结果相同,会将原来的v进行替换,k不变(比如compare按照k的字符串长度进行排序,有了【abc,123】后再加入h【sp,567】结果会变成【abc,567 】)
4. 总结
- 开发中应该如何选择集合实现类?
- 判断存储的类型:是一组对象还是一组键值对?
-
一组对象:Collection接口
-
允许重复:List
- 增删多:LinkedList(底层维护了一个双向链表)
- 改查多:ArrayList(底层维护Object类型的可变数组)
-
不允许重复:Set
- 无序:HashSet(底层是HashMap,维护一个哈希表,即数组+链表+红黑树)
- 排序:TreeSet
- 插入和取出顺序一致:LinkedHashSet,维护一个数组+双向链表
-
-
一组键值对:Map接口
- 键无序:HashMap(底层是jdk7:数组+链表,jdk8:数组+链表+红黑树)
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap
- 读取文件:Properties
-
- 判断存储的类型:是一组对象还是一组键值对?
5. Collections工具类
-
基本介绍
- Collections是一个操作Set、List、Map等集合的工具类
- Collections中提供了一系列静态的方法对集合元素进行排序,查询和修改等操作
-
顺序操作
- Collections.shuffle(list)
-
查找替换
内容好多呀~ 不过不要害怕,继续往下面学!