本篇文章的目的
梳理Java集合知识
简单介绍各个组成部分
如有错误,请一定指出,手动笑脸。
文章目录
集合结构图
![集合结构图](https://i-blog.csdnimg.cn/blog_migrate/9300ef23e6b145a27b4c1088962c7431.png#pic_center)
看到array,就要想到角标。
看到link,就要想到first,last。
看到hash,就要想到hashCode,equals.
看到tree,就要想到两个接口。Comparable(可比较),Comparator(比较器)。
List
ArrayList
ArrayList是List的子接口,List是Collection的子接口
List接口: 有序的、不唯一
ArrayList: 有序的、不唯一
数据结构: Object数组
ArrayList的基本用法和特点
特点:
有序的:按照添加的顺序排列
可重复:同一个元素可以装多次
1:如何创建集合泛型对象
ArrayList<泛型> list=new ArrayList<>();
2:如何添加元素:
一次添加一个元素:
list.add(元素);
一次添加多个元素:
Collections.addAll(集合,元素,元素,...);
3:得到集合元素的个数
list.size();
4:得到某一个元素
list.get(下标);
5:如何判断集合里面是否出现指定元素
list.contains();
6:遍历
for+下标
for(int x=0;x<list.size();x++){
//x->下标
//list.get(元素);
}
foreache
for(集合的泛型 x :list){
//x->元素
}
迭代器********(重点)
for(得到迭代器对象;判断迭代器上面是否还有下一个元素;){
取出下一个元素
}
实例
for(Iterator<泛型>car=list.iterator();car.hasNext;){
car.next();->元素
}
ArrayList 如何删除元素:
list.remove(int 下标);
下标指向谁就删除谁,如果下标不存在就抛出异常。
清空集合:list.clear();
ArrayList其他细节
1:ArrayList类里面的remove(元素)的方法底层需要遵从equals比较机制
当我们想要删除一个元素的时候 底层拿着这个元素和集合里面的每一个元素做equals比较
2:谁主张谁举证
要被删除的对象会主动调用他自己类的equals方法和集合里面的每一个元素做比较
3:当我们使用迭代器遍历集合的过程中,不允许对集合的整体进行添加、删除操作,否则出发CME异常
如果想要在遍历的过程中进行删除,只能使用迭代器的删除方法:car.remove();
4:构造方法:
ArrayList list = new ArrayList(int 数组空间大小);
ArrayList list = new ArrayList();//数组默认开辟10块空间
集合会自动扩容:
jdk6.0及之前 x * 3 / 2 + 1
10 -> 16 -> 25....
jdk7.0及之后 x + (x >> 1)
10 -> 15 -> 22....
在项目开发的时候 尽量避免扩容:
1:创建一个更大的新的数组对象
2:将老数组里面的元素复制到新数组里面
3:改变引用指向
4:回收老数组对象
5:继续添加元素
扩容:list.ensureCapacity(数组空间)
缩容:list.trimToSize()
**************************************
Vector语法和ArrayList一模一样的
**************************************
面试题:
ArrayList和Vector之间的区别?
1:同步特性不同
Vector同一时间允许一个线程进行访问,效率较低,但是不会发生并发错误。
ArrayList同一时间允许多个线程进行访问,效率高,但是可能会发生并发错误。
**************************************
jdk5.0开始 集合的工具类[Collections]里面提供一个方法synchronizedList
可以将线程不安全的ArrayList集合变成线程安全的集合对象
于是Vector渐渐被淘汰了
2:扩容不同
ArrayList:分版本
jdk6.0及之前 x * 3 / 2 + 1
jdk7.0及之后 x + (x >> 1)
Vector:分构造方法
Vector(10) -> 2倍扩容 10 -》 20 -》 40...
Vector(10,3) -> 定长扩容 10 -》 13 -》 16...
3:出现的版本不同
Vector:since jdk1.0
ArrayList:since jdk1.2
**************************************
LinkedList语法和ArrayList一模一样
面试题:
ArrayList和LinkedList之间的区别?
ArrayList和LinkedList底层数据结构不同 导致优劣势不同
ArrayList:底层是基于数组实现的
优点:随机访问 遍历查找效率高
缺点:添加删除元素的时候效率低
LinkedList:底层是基于链表实现的
优点:添加删除元素的时候效率高。
缺点:随机访问 遍历查找效率低[从下标0开始找起]
**************************
Stack: 用数组模拟栈结构
LinkedList
LinkedList是List的子接口,List是Collection的子接口,和ArrayList同一层
从底层来看:
1. Array底层是Object[]
2. LinkedList底层是双向链表
链表结构也就意味着LinkedList更适合数据更新频繁的环境,因为LinkedList不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,类似于插入数据,删除数据时,LinkedList也优于ArrayList,其时间复杂度仅为O(1)
而且ArrayList删除数据是开销很大的,因为这需要重排数组中的所有数据(除了最后一个元素)。
下面来看看LinkedList的一些常用方法。
基本特点:
- 非同步,线程不安全;
- 支持null元素,元素可以重复;
- 可以被当作堆栈、队列或双端队列进行操作。
- 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
- LinkedList包含两个重要的成员:header 和 size。
header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。
Entry中包含成员变量: previous, next, element。
previous是该节点的上一个节点,
next是该节点的下一个节点,
element是该节点所包含的值。
size是双向链表中节点的个数。
使用场景:
LinkedList适用于对数据进行频繁的修改操作的场景。
Set
Set:注重独一无二的性质,该体系集合可以知道元素是否已经存在于集合中,不会存储重复的元素
特点:
- 存储时元素无序(存入和取出的顺序不一定相同)
- 值不能重复。
对象的相等性
- 引用到堆上同一个对象的两个引用是相等的。如果对两个引用调用hashCode方法,会得到相同的结果,如果对象所属的类没有覆盖Object的hashCode方法的话,hashCode会返回每个对象特有的序号(java是依据对象的内存地址计算出的此序号),所以两个不同的对象的hashCode值是不可能相等的。
如果想要让两个不同的Person对象视为相等的,就必须覆盖Object继下来的hashCode方法和equals方法;
因为Object 下的hashCode方法返回的是该对象的内存地址,所以必须重写hashCode方法(即换一种与重写后的equals判断相等条件相关的条件,来生成新的hashCode),才能保证两个不同的对象具有相同的hashCode,同时也需要两个不同对象比较equals方法会返回true。
该集合中没有特有的方法,直接继承自Collection。
案例:set集合添加元素并使用迭代器迭代元素,测试无序性、不可重复性。
public class Demo4 {
public static void main(String[] args) {
//Set 集合存和取的顺序不一致。
Set hs = new HashSet();
hs.add("世界军事");
hs.add("兵器知识");
hs.add("舰船知识");
hs.add("兵器知识");
hs.add("历史知识");
hs.add("动物知识");
System.out.println(hs);
// [世界军事, 历史知识, 动物知识, 兵器知识, 舰船知识]
Iterator it = hs.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
结果
[世界军事, 历史知识, 动物知识, 兵器知识, 舰船知识]
世界军事
历史知识
动物知识
兵器知识
舰船知识
HashSet
哈希表边存放的是哈希值。HashSet存储元素的顺序并不是按照存入时的顺序(和List显然不同) 是按照哈希值来存的所以取数据也是按照哈希值取得。
由于Set集合是不能存入重复元素的集合,那么HashSet也是具备这一特性的。
HashSet如何检查重复?
HashSet会通过元素的hashcode()和equals方法进行判断元素师否重复。
当对象加入集合时,HashSet会使用对象的hashCode来判断对象加入的位置。同时也会与其他已经加入的对象的hashCode进行比较
- 如果没有相等的hashCode,HashSet就会假设对象没有重复出现;
- 如果元素hashCode值相同,是不是就无法存入HashSet中了? 当然不是,会继续使用equals 进行比较。如果 equals为true 那么HashSet认为新加入的对象重复了,所以加入失败。如果equals 为false那么HashSet 认为新加入的对象没有重复。新元素可以存入。
- 哈希值相同equals为false的元素是怎么存储呢?
- 就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。
图1:hashCode值不相同的情况
图2:hashCode值相同,但equals不相同的情况。
总结:
元素的哈希值是通过元素的hashcode方法 来获取的,放数据时,HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法 如果 equls结果为true ,HashSet就视为同一个元素。如果equals 为false就不是同一个元素。
扩展
HashSet 和ArrayList集合都有判断元素是否相同的方法,
boolean contains(Object o)
HashSet使用hashCode和equals方法,ArrayList使用了equals方法
例子:使用HashSet存储自定义对象,并尝试添加重复对象(对象不可重复的判定)
package ttest;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetDemo {
public static void main(String[] args) {
Set<Person> hs = new HashSet<Person>();
hs.add(new Person("tom",18));
hs.add(new Person("jerry",18));
hs.add(new Person("maks",19));
hs.add(new Person("maks",19));
hs.add(new Person("jimi",18));
Iterator it = hs.iterator();
while(it.hasNext()) {
Object per = it.next();
System.out.println(per);
}
}
}
class Person{
private String name;
private Integer age;
public Person() {
super();
}
public Person(String name, Integer age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
@Override
public int hashCode() {
System.out.println("hashCode:" + this.name);
return this.name.hashCode() + age * 13;
}
@Override
public boolean equals(Object obj) {
System.out.println(this + " equals " + obj);
if(obj instanceof Person) {
Person p = (Person)obj;
return this.name.equals(p.name) && this.age == p.age;
}else {
return false;
}
}
}
结果
hashCode:tom
hashCode:jerry
hashCode:maks
hashCode:maks
Person [name=maks, age=19] equals Person [name=maks, age=19]
hashCode:jimi
Person [name=maks, age=19]
Person [name=jimi, age=18]
Person [name=tom, age=18]
Person [name=jerry, age=18]
从结果来看,可以保证唯一,但不保证有序。
问题:现在有一批数据,要求不能重复存储元素,而且要排序。ArrayList 、 LinkedList不能去除重复数据。HashSet可以去除重复,但是是无序。
所以这时候就要使用TreeSet了
TreeSet
public class Demo5 {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add("ccc");
ts.add("aaa");
ts.add("ddd");
ts.add("bbb");
System.out.println(ts); // [aaa, bbb, ccc, ddd]
}
}
红黑树
红黑树是一种特定类型的二叉树
红黑树算法的规则: 左小右大。
既然TreeSet可以自然排序,那么TreeSet必定存在排序规则的
-
让存入的元素自定义比较规则。
-
给TreeSet指定排序规则。
实现排序的两种措施
方式一:元素自身具备比较性
元素自身具备比较性,需要元素实现Comparable接口,重写compareTo方法,(当compareTo()函数返回值为0时,说明两个对象相等,此时该对象不会添加进来)也就是让元素自身具备比较性,这种方式叫做元素的自然排序也叫做默认排序。
方式二:容器具备比较性
当元素自身不具备比较性,或者自身具备的比较性不是所需要的,那么此时可以让容器自身具备比较性。需要定义一个类实现接口Comparator,重写compare方法,并将该接口的子类实例对象作为参数传递给TreeMap集合的构造方法。
注意:当Comparable比较方式和Comparator比较方式同时存在时,以Comparator的比较方式为主;
注意:在重写compareTo或者compare方法时,必须要明确比较的主要条件相等时要比较次要条件。(假设姓名和年龄一直的人为相同的人,如果想要对人按照年龄的大小来排序,如果年龄相同的人,需要如何处理?不能直接return 0,因为可能姓名不同(年龄相同姓名不同的人是不同的人)。此时就需要进行次要条件判断(需要判断姓名),只有姓名和年龄同时相等的才可以返回0.)
通过return 0来判断唯一性。
比较器接口
----| Comparable
compareTo(Object o) 元素自身具备比较性
----| Comparator
compare( Object o1, Object o2 ) 给容器传入比较器
方式一
- 让元素自身具备比较性,也就是元素需要实现Comparable接口,覆盖compareTo 方法。
例子
package testset;
import java.util.Set;
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
Set<Person1> ts = new TreeSet<Person1>();
ts.add(new Person1("aa", 20, "男"));
ts.add(new Person1("bb", 18, "女"));
ts.add(new Person1("cc", 17, "男"));
ts.add(new Person1("dd", 17, "女"));
ts.add(new Person1("dd", 15, "女"));
ts.add(new Person1("dd", 15, "女"));
System.out.println("**********************************");
System.out.println(ts);
System.out.println("**********************************");
System.out.println(ts.size()); // 5
}
}
class Person1 implements Comparable{
private String name;
private Integer age;
private String gender;
public Person1(String name, Integer age, String gender) {
super();
this.name = name;
this.age = age;
this.gender = gender;
}
public Person1() {
super();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", gender=" + gender + "]";
}
@Override
public int hashCode() {
return this.name.hashCode() + age * 13;
}
@Override
public boolean equals(Object obj) {
System.err.println(this + "equals :" + obj);
if (!(obj instanceof Person)) {
return false;
}
Person1 p = (Person1) obj;
return this.name.equals(p.name) && this.age == p.age;
}
@Override
public int compareTo(Object obj) {
Person1 p = (Person1)obj;
System.out.println(this+" compareTo:"+p);
if (this.age > p.age) {
return 1;
}
if (this.age < p.age) {
return -1;
}
return this.name.compareTo(p.name);
}
}
结果(有手动调整间隙)
Person [name=aa, age=20, gender=男] compareTo:Person [name=aa, age=20, gender=男]
Person [name=bb, age=18, gender=女] compareTo:Person [name=aa, age=20, gender=男]
Person [name=cc, age=17, gender=男] compareTo:Person [name=aa, age=20, gender=男]
Person [name=cc, age=17, gender=男] compareTo:Person [name=bb, age=18, gender=女]
Person [name=dd, age=17, gender=女] compareTo:Person [name=bb, age=18, gender=女]
Person [name=dd, age=17, gender=女] compareTo:Person [name=cc, age=17, gender=男]
Person [name=dd, age=15, gender=女] compareTo:Person [name=bb, age=18, gender=女]
Person [name=dd, age=15, gender=女] compareTo:Person [name=cc, age=17, gender=男]
Person [name=dd, age=15, gender=女] compareTo:Person [name=bb, age=18, gender=女]
Person [name=dd, age=15, gender=女] compareTo:Person [name=cc, age=17, gender=男]
Person [name=dd, age=15, gender=女] compareTo:Person [name=dd, age=15, gender=女]
**********************************
[
Person [name=dd, age=15, gender=女],
Person [name=cc, age=17, gender=男],
Person [name=dd, age=17, gender=女],
Person [name=bb, age=18, gender=女],
Person [name=aa, age=20, gender=男]
]
**********************************
5
方式二
- 让容器自身具备比较性,自定义比较器。
- 当元素自身不具备比较性,或者元素自身具备的比较性不是所需的,那么这时只能让容器自身具备。
- 定义一个类实现Comparator 接口,覆盖compare方法,并将该接口的子类对象作为参数传递给TreeSet集合的构造函数。
例子
package testset;
import java.util.Comparator;
import java.util.TreeSet;
public class Demo5 {
public static void main(String[] args) {
TreeSet ts = new TreeSet(new MyComparator());
ts.add(new Book("think in java", 100));
ts.add(new Book("java 核心技术", 75));
ts.add(new Book("现代操作系统", 50));
ts.add(new Book("java就业教程", 35));
ts.add(new Book("think in java", 100));
ts.add(new Book("ccc in java", 100));
System.out.println(ts);
}
}
// 比较器
class MyComparator implements Comparator {
public int compare(Object o1, Object o2) {
Book b1 = (Book) o1;
Book b2 = (Book) o2;
System.out.println(b1+" comparator "+b2);
if (b1.getPrice() > b2.getPrice()) {
return 1;
}
if (b1.getPrice() < b2.getPrice()) {
return -1;
}
return b1.getName().compareTo(b2.getName());
}
}
class Book {
private String name;
private double price;
public Book() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public Book(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Book [name=" + name + ", price=" + price + "]";
}
}
结果(有手动调整间距)
Book [name=think in java, price=100.0] comparator Book [name=think in java, price=100.0]
Book [name=java 核心技术, price=75.0] comparator Book [name=think in java, price=100.0]
Book [name=现代操作系统, price=50.0] comparator Book [name=think in java, price=100.0]
Book [name=现代操作系统, price=50.0] comparator Book [name=java 核心技术, price=75.0]
Book [name=java就业教程, price=35.0] comparator Book [name=java 核心技术, price=75.0]
Book [name=java就业教程, price=35.0] comparator Book [name=现代操作系统, price=50.0]
Book [name=think in java, price=100.0] comparator Book [name=java 核心技术, price=75.0]
Book [name=think in java, price=100.0] comparator Book [name=think in java, price=100.0]
Book [name=ccc in java, price=100.0] comparator Book [name=java 核心技术, price=75.0]
Book [name=ccc in java, price=100.0] comparator Book [name=think in java, price=100.0]
[
Book [name=java就业教程, price=35.0],
Book [name=现代操作系统, price=50.0],
Book [name=java 核心技术, price=75.0],
Book [name=ccc in java, price=100.0],
Book [name=think in java, price=100.0]
]
LinkedHashSet
会保存插入的顺序
Map
什么是Map集合?
Map用于保存具有映射关系的数据,Map集合里保存着两组值,一组用于保存Map的ley,另一组保存着Map的value。
图解
Map集合的作用
和查字典类似,通过key找到对应的value,通过页数找到对应的信息。用学生类来说,key相当于学号,value对应name,age,sex等信息。用这种对应关系方便查找。
Map和Set的关系
可以说关系是很密切了,虽然Map中存放的是键值对,Set中存放的是单个对象,但如果把value看做key的附庸,key在哪里,value就在哪里,这样就可以像对待Set一样来对待Map了。事实上,Map提供了一个Entry内部类来封装key-value对,在计算Entry存储时则只考虑Entry封装的key。
如果把Map集合里的所有value放在一起来看,它们又类似于一个List,元素可以重复,每个元素可以根据索引来找,只是Map中的索引不再是整数值,而是以另一个对象作为索引。
Map用法
类型介绍
Java 自带了各种 Map 类,这些 Map 类可归为三种类型:
- 通用Map,用于在应用程序中管理映射,通常在 java.util 程序包中实现
HashMap、Hashtable、Properties、LinkedHashMap、IdentityHashMap、TreeMap、WeakHashMap、ConcurrentHashMap
- 专用Map,通常我们不必亲自创建此类Map,而是通过某些其他类对其进行访问
java.util.jar.Attributes、javax.print.attribute.standard.PrinterStateReasons、java.security.Provider、java.awt.RenderingHints、javax.swing.UIDefaults
- 一个用于帮助我们实现自己的Map类的抽象类
AbstractMap
类型区别
HashMap
- 最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为Null(多条会覆盖);允许多条记录的值为 Null。非同步的。
TreeMap
- 能够把它保存的记录根据键(key)排序,默认是按升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。TreeMap不允许key的值为null。非同步的。
Hashtable
- 与 HashMap类似,不同的是:key和value的值均不允许为null;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢。
LinkedHashMap
- 保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.在遍历的时候会比HashMap慢。key和value均允许为空,非同步的。
Map 初始化
Map<String, String> map = new HashMap<String, String>();
插入元素
map.put("key1", "value1");
获取元素
map.get("key1")
移除元素
map.remove("key1");
清空map
map.clear();
Map 遍历
初始化数据
Map<String, String> map = new HashMap<String, String>();
map.put("key1", "value1");
map.put("key2", "value2");
增强for循环遍历
使用keySet()遍历
for (String key : map.keySet()) {
System.out.println(key + " :" + map.get(key));
}
使用entrySet()遍历
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " :" + entry.getValue());
}
迭代器遍历
使用keySet()遍历
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
System.out.println(key + " :" + map.get(key));
}
使用entrySet()遍历
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println(entry.getKey() + " :" + entry.getValue());
}
直接给出结论
-
增强for循环使用方便,但性能较差,不适合处理超大量级的数据。
-
迭代器的遍历速度要比增强for循环快很多,是增强for循环的2倍左右。
-
使用entrySet遍历的速度要比keySet快很多,是keySet的1.5倍左右。
内部各类
HashMap和Hashtable的关系完全类似于ArrayList和Vector的关系
- Hashtable是线性安全的,HashMap是线性不安全的,所以后者效率更高。
- Hashtable不允许使用null作为key和value,否则会引发异常,而HashMap可以;
和HashSet的关系
- 与HashSet集合不能保证元素顺序一样,HashMap和Hashtable也不能保证键值对的顺序。他们判断两个key相等的标准也是:两个key通过equals方法比较返回true,两个key的hashCode值也相等。而判断value值相等的标准:只要两个对象通过equals方法比较返回true即可。
- 不能修改集合中的key,否则程序再也无法准确访问到Map中被修改过的key。
LinkedHashMap实现类
- 和HashSet中的LinkedHashSet一样,HashMap也有一个LinkedHashMap子类,使用双向链表来维护键值对的次序,迭代顺序和插入顺序保持一致。
SortedMap接口和TreeMap实现类
- 正如Set接口派生出SortedSet子接口,Sorted接口有一个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap接口也有一个TreeMap实现类。
TreeMap就是一个红黑树数据结构,每个键值对作为红黑树的一个节点。存储键值对时根据key对节点进行排序。可以保证所有键值对处于有序状态。
和TreeSet一样,TreeMap也有自然排序和定制排序两种排序方式。
public static void main(String[] args) {
TreeMap<String, Integer> stuTreeMap = new TreeMap<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
int num=o1.compareTo(o2);
return num;
}
});
stuTreeMap.put("LiMing",14);
stuTreeMap.put("LiMing",24);
stuTreeMap.put("Jenny",16);
stuTreeMap.put("Denny",24);
System.out.println(stuTreeMap);//{Denny=24, Jenny=16, LiMing=24}
System.out.println(stuTreeMap.firstEntry());//Denny=24
}
参考:java中的Map集合
参考:Java面试题–集合各实现类的底层实现原理
参考:这几道Java集合框架面试题在面试中几乎必问
参考:集合各实现类的底层实现原理
参考:HashMap底层实现原理及面试问题
参考:Arraylist
参考:LinkedList
参考:Java Set集合的详解