JAVA集合
Java集合类主要由两个接口派生而出:Collection和Map,Collection存储着对象的集合,而Map存储着键值对(两个对象)的映射表。
Collection体系
1. List
-
ArrayList:基于动态数组实现,支持随机访问。数组的默认大小为10,扩容为原来的1.5倍,只序列化数组中有元素填充那部分内容,保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
构造方法
//使用无参构造方法初始化一个ArrayList对象时,该数组的大小为0,只有当调用add()方法添加元素时,才会使得数组的大小变为默认大小10. ArrayList() ArrayList(int initialCapacity) ArrayList(Collection<? extends E> c)
其他方法
add(E e) add(int index, E element) addAll(Collection<? extends E> c) addAll(int index, Collection<? extends E> c) clear() //从集合中删除所有元素 //返回此ArrayList实例的浅表副本,元素本身不会被复制,复制的是地址,需要强转,对于复制品的操作相当于对于原集合的操作,取别名。注:浅克隆与深克隆的区别。 public Object clone() ArrayList<Integer> arrayList2 = (ArrayList<Integer>) arrayList1.clone(); boolean contains(Object o) //如果集合中包含指定的元素,则返回 true get(int index) //返回集合中指定位置的元素 indexOf(Object o) //返回此列表中第一次出现的指定元素的索引,如果此列表不包含该元素,则返回-1。 lastIndexOf(Object o) //返回此列表中指定元素最后一次出现的索引,如果此列表不包含该元素,则返回-1。 isEmpty() //集合为空吗 iterator() //返回该集合元素的迭代器 listIterator() listIterator(int index) //删除指定位置的元素,删除指定元素的第一个匹配项,删除指定集合中包含的所有元素 E remove(int index) boolean remove(Object o) boolean removeAll(Collection<?> c) boolean retainAll(Collection<?> c) //仅保留此列表中包含在指定集合中的元素。 set(int index, E element) size() //返回此列表中的元素数 subList(int fromIndex, int toIndex) //返回指定的 fromIndex (包含)和 toIndex (不包括)之间的此列表部分的视图。 Object[] toArray() trimToSize() //将此 ArrayList实例的容量调整为列表的当前大小。
如何解决ArrayList线程不安全的情况?
可以使用
Collections.synchronizedList();
得到一个线程安全的 ArrayList。List<String> list = new ArrayList<>(); List<String> synList = Collections.synchronizedList(list);
也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
List<String> list = new CopyOnWriteArrayList<>();
-
Vector:和ArrayList类似,但它是线程安全的。它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。Vector进行扩容时可以指定增量大小,当增量大小为0时,每次扩容都会增加当前容量的一倍,即翻一番。
构造方法
Vector()//构造一个空向量,使其内部数据数组的大小为 10 ,其标准容量增量为零。 Vector(int initialCapacity)//构造一个具有指定初始容量且容量增量等于零的空向量。 Vector(int initialCapacity, int capacityIncrement)//构造具有指定初始容量和容量增量的空向量。 Vector(Collection<? extends E> c)//按照集合的迭代器返回的顺序构造一个包含指定集合元素的向量。
其他方法
capacity() //返回此向量的当前容量, Vector有多大 size() //有效数据量 ...
-
LinkedList:基于双向链表实现,只能顺序访问,但可以快速地进行插入和删除元素操作。其还可以用作栈、队列和双端队列。
基于双向链表实现,使用 Node 存储链表节点信息。
private static class Node<E> { E item; Node<E> next; Node<E> prev; }
每个链表存储了 first 和 last 指针:
transient Node<E> first; transient Node<E> last;
构造方法
LinkedList()
LinkedList(Collection<? extends E> c)
模拟栈
LinkList<Integer> linkList = new LinkList<>();
linkList.push(1);
linkList.push(2);
linkList.pop();
模拟队列
LinkList<Integer> linkList = new LinkList<>();
//队尾插入元素,队头删除元素
linkedList.addLast(1);
linkedList.removeFirst();
//队头插入元素,队尾删除元素
linkedList.addFirst(2);
linkedList.removeLast();
模拟双端队列
LinkList<Integer> linkList = new LinkList<>();
//队头插入和删除元素
linkedList.addFirst(2);
linkedList.removeFirst();
//队尾插入和删除元素
linkedList.addLast(1);
linkedList.removeLast();
2. Set
-
HashSet:基于哈希表实现,底层为HashMap。支持快速查找,但不支持有序操作。并且失去了元素的插入顺序信息,也就是说使用迭代器遍历HashSet得到的结果是不确定的。
HashSet<String> hashSet = new HashSet<>(); hashSet.add("锄禾日当午"); hashSet.add("汗滴禾下土"); hashSet.add("谁知盘中餐"); hashSet.add("粒粒皆辛苦"); Iterator iterator = hashSet.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); //打印结果的顺序不是添加顺序 } //汗滴禾下土 //谁知盘中餐 //锄禾日当午 //粒粒皆辛苦
方法
//构造方法:由于HashSet的底层为HashMap,所以采用HashMap的默认初始容量16和负载因子0.75 HashSet() HashSet(int initialCapacity) HashSet(int initialCapacity, float loadFactor) HashSet(Collection<? extends E> c)
-
LinkedHashSet:具有HashSet的查找效率,并且内部使用双向链表维护元素的插入操作,插入顺序即为迭代器的遍历顺序。
-
TreeSet:基于红黑树实现,底层为TreeMap。支持有序操作,例如根据一个范围查找元素的操作。但是查找效率不如HashSet,HashSet的查找效率为O(1),TreeSet则为O(logN)。
当TreeSet中的E为自定义数据类型时,其需要实现Comparable接口,即重写CompareTo()方法!(也可以通过Comparator接口来实现比较方法)
为什么需要实现Comparable接口?
这是因为TreeSet是有序的,这种有序是按照自然顺序排列的,JVM需要知道这些数据如何排序以进行存储,这就需要我们制定排序的规则了,而重写compareTo(T o)方法就是我们重写排序规则。当E为String时,因为String类已经实现了Comparable接口,所以我们可以直接使用。
public class ComparableTest {
public static void main(String[] args) {
Student student1 = new Student("贾宝玉", 14, 88.5);
Student student2 = new Student("林黛玉", 13, 90.5);
Student student3 = new Student("史湘云", 13, 85);
Student student4 = new Student("薛宝钗", 15, 91);
TreeSet<Student> treeSet = new TreeSet<>();
treeSet.add(student1);
treeSet.add(student2);
treeSet.add(student3);
treeSet.add(student4);
Iterator<Student> iterator = treeSet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
class Student implements Comparable<Student>{
private String name;
private int age;
private double score;
public Student() {
}
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
@Override
//成绩从大到小,年龄从小到大
public int compareTo(Student stu) {
if(this.score == stu.score){
return this.age - stu.age;
}else if(this.score < stu.score){
return 1;
}
return -1;
}
}
//通过Comparator接口实现比较方法
//参数为Comparator实现类对象的TreeSet构造方法:TreeSet(Comparator<? super E> comparator)
public class ComparatorTest {
public static void main(String[] args) {
//通过匿名类的方式实现Comparator接口
TreeSet<Stu> treeSet = new TreeSet<>(new Comparator<Stu>() {
@Override
public int compare(Stu o1, Stu o2) {
if(o1.getScore() == o2.getScore()){
return o1.getAge() - o2.getAge();
}else if(o1.getScore() > o2.getScore()){
return -1;
}
return 1;
}
});
Stu stu1 = new Stu("贾宝玉", 14, 88.5);
Stu stu2 = new Stu("林黛玉", 13, 90.5);
Stu stu3 = new Stu("史湘云", 13, 85);
Stu stu4 = new Stu("薛宝钗", 15, 91);
treeSet.add(stu1);
treeSet.add(stu2);
treeSet.add(stu3);
treeSet.add(stu4);
Iterator<Stu> iterator = treeSet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
class Stu{
private String name;
private int age;
private double score;
public Stu() {
}
public Stu(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
3. Queue
- LinkedList:可以用它实现双端队列。
- PriorityQueue:基于堆结构实现,可以用它来实现队列优先。
Iterator 和 ListIterator迭代器
Iterator迭代器可以作为Collection的迭代器遍历其中的元素,而ListIterator只能遍历List集合的元素,并且提供了更多的方法,具体请查看API文档!!!
/**
* Iterator的主要方法:
* hasNext(), next(), remove()
*/
ArrayList<Integer> arrayList1 = new ArrayList<>();
arrayList1.add(1);
arrayList1.add(2);
arrayList1.add(3);
arrayList1.add(4);
arrayList1.add(5);
//iterator()方法返回一个迭代器对象,
//hasNext()方法的返回值是Boolean类型
//next()方法返回数据并使得指针+1
//remove()方法,只能在调用next()后调用该方法一次
Iterator<Integer> iterator = arrayList1.iterator();
iterator.next();
iterator.remove();
//输出2,3,4,5,1被删除了
while(iterator.hasNext()){ //如果存在下一个元素
Integer i = iterator.next(); //指针后移一位并返回下一个元素的值
System.out.println(i);
}
/**
* ListIterator的主要方法:
* void add(E e):在next()返回的元素之前插入,后指针指向插入元素的位置。
* Boolean hasNext()
* E next()
* int nextIndex()
* Boolean hasPrevious()
* E previous()
* int previousIndex()
* void remove(E e) :将当前指针所指向的元素删除,指针上移一次
* void set(E e):将当前指针所指向的元素改为e
*/
ArrayList<Integer> arrayList2 = new ArrayList<>();
arrayList2.add(1);
arrayList2.add(2);
ListIterator<Integer> listIterator = arrayList1.listIterator();
//验证add()方法
listIterator.next(); //先让指针指向1
listIterator.add(99); //添加99, 1,99,2 指针指向了99
listIterator.previous(); //需要回退两步,才能到达初始状态,空的头指针
listIterator.previous();
while(listIterator.hasNext()){ //用迭代器进行遍历:1,99,2
System.out.println(listIterator.next());
}
//假设指针在初始位置
listIterator.next();
listIterator.next();
listIterator.remove(); //1,2 指针移动2的位置,移除2后,指针指向1
listIterator.set(99); //将1变为99
Map体系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7hOeWnz-1618228084281)(C:\Users\Administrator\Desktop\笔记\Java集合\map.png)]
-
HashMap:基于哈希表实现。
- 采用数组+链表的存储方式(数组默认大小为16);
-
将key进行哈希运算得到哈希值之后与16进行取模运算,得到几就存储在下标为几的哈希桶中。HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中数据量大于等于8时,链表转换成树结构;若桶中数据量小于等于6时,树结构还原成链表。
-
为什么大于等于8时要将链表结构转化为红黑树结构?而小于等于6时又将树结构重新转化为链表结构?那为什么不是小于等于7时又将树结构重新转化为链表结构呢?
答:因为红黑树的平均查找时间为log(n),而链表的平均查找时间为n/2。当数据量大于等于8时,红黑树的查找效率将超过链表的查找效率。而哈希桶中从8及以上的数据量变为6及以下时,两者的查找效率基本上没有区别,但是构建和调整树使得其成为红黑树的时间成本却比较大。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
-
为什么哈希桶的初始大小为16,其他数值不行吗?
首先,哈希桶的大小需设置成2的k次幂,因为再对一个key的哈希值进行取模运算时,HashMap是按照等价的(n-1)& hash位运算进行的,因为位运算的效率比较高。当n为2的k次幂,那么(n-1)& hash的结果与hash % n的运算结果是等价的,而且n-1的二进制补码是每个二进制位上全为1的数,比如2^4 - 1 = 15,转化成二进制为1111。15与0-15进行位与运算还是该数本身,但是如果map的长度不是2的幂次,比如为15,那长度-1就是14,二进制为1110,无论与谁相与最后一位一定是0,0001,0011,0101,1001,1011,0111,1101这几个位置就永远都不能存放元素了,空间浪费相当大,也增加了添加元素发生碰撞的概率。
其次,桶的初始化大小设置成2,4,8,那么可能会因为初始容量不足造成很多hash冲突;设置成32,64的话又显得浪费空间了。
-
为什么负载因子是0.75?
答:负载因子为0.75在时间和空间成本之间提供了良好的折中。较低的负载因子会提高查询效率,但是会造成空间浪费;较高的负载因子虽然可以有效利用空间,但是查询效率低下。
-
HashMap,HashTable与ConcurrentHashTable的区别?
HashMap:线程不安全,效率高。
HashTable:线程安全,效率低。当一个线程对该集合进行操作时,其他线程不能操作。
ConcurrentHashTable:采用分段锁机制,保证线程安全,效率又较高。当一个线程对一个哈希桶进行操作时,其他线程不能操作这个哈希桶,但可以操作其他哈希桶。
-
方法
//构造方法
HashMap(); //使用默认初始容量16和负载因子0.75
HashMap(int initialCapacity); //使用指定初始容量和负载因子0.75
HashMap(int initialCapacity, float loadFactor); //使用指定初始容量和负载因子
HashMap(Map<? extends K, ?extends V> m);
//其他方法
put(K key, V value) //将指定的值与此映射中的指定键相关联。
containsKey(Object key) //如果此映射包含指定键的映射,则返回 true 。
containsValue(Object value) //如果此映射将一个或多个键映射到指定值,则返回 true 。
get(Object key) //返回指定键映射到的值,如果此映射不包含键的映射,则返回 null 。
keySet() //返回此映射中包含的键的Set视图。
//遍历HashMap
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("小米", 1);
hashMap.put("华为", 2);
hashMap.put("OPPO", 3);
hashMap.put("vivo", 4);
Set<String> set = hashMap.keySet(); //方式一
for (String key:set) {
System.out.println(key + "-->" + hashMap.get(key));
}
Set<Map.Entry<String, Integer>> set1 = hashMap.entrySet(); //方式二
for(Map.Entry<String, Integer> entry:set1){
System.out.println(entry);
}
-
TreeMap:基于红黑树实现。
-
HashTable:和HashMap类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
-
LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。