集合类之间的关系如下:实线边框的是实现类,折线边框的是抽象类,而点线边框的是接口(图片来源)
有以下核心方法:
Collection接口下面有两个重要的子接口List和Set:
允许添加null,且允许添加多个null。
相对于Collation加入了以下两个常用方法:
get() --> 根据索引取得元素
set() --> 修改指定索引元素的内,返回修改前内容
底层为数组,线程同步不安全。
其本质为对象动态数组。
默认容量为10,当数据超过当前容量会进行自动扩容,扩容规则为:
newCapacity = oldCapacity + (oldCapacity >> 1);
即:每次扩容原数组的1/2 。
简单实例:
import java.util.ArrayList;
import java.util.List;
public class TestCollection {
public static void main(String[] args) {
List list = new ArrayList<>();// 在创建一个ArrayList对象时一般将其向上转型为List,
// 在定义时,建议加上泛型
list.add(9);
list.add(5);
list.add(2);
list.set(2, 7);// List 独有方法 ,将下标为2的元素修改为7.
System.out.println(list.get(1));
}
}
底层为双向链表,线程同步不安全。
简单实例:
import java.util.LinkedList;
import java.util.List;
public class TestCollection {
public static void main(String[] args) {
List list = new LinkedList<>();// 在创建一个LinkedList对象时一般将其向上转型为List,
// 在定义时,建议加上泛型
list.add(9);
list.add(5);
list.add(2);
list.set(2, 7);// List 独有方法 ,将下标为2的元素修改为7.
System.out.println(list.get(1));
}
}
底层为数组,线程同步安全。但是,但是效率低一般不常用。
简单实例:
import java.util.List;
import java.util.Vector;
public class TestCollection {
public static void main(String[] args) {
List list = new Vector<>();// 在创建一个Vector对象时一般将其向上转型为List,
// 在定义时,建议加上泛型
list.add(9);
list.add(5);
list.add(2);
list.set(2, 7);// List 独有方法 ,将下标为2的元素修改为7.
System.out.println(list.get(1));
}
}
可以看到,ArrayList , LinkedList , Vector 在使用上的差别并不大。因此我们根据自己的需要选择合适的存储结构,比如插入删除比较多的时候就选用LinkedList , 如果随机访问比较多就使用ArrayList , 如果要求线程安全就选用Vector 。
ArrayList和Vector的区别:
1.Vector 从JDK1.0开始,ArrayList从JDK1.2开始
2.加载方式:Vercor 底层基于数组,在实例化的时候会直接创建一个数组,ArrayList是懒加载机制,在第一次添加数据的时候在会创建数组;
3.扩容方式:Vector默认容量为10,扩容机制为newCapacity=2*oldCapacity,ArrayList的默认容量为10,扩容机制为newCapacity=oldCapacity+oldCapacity>>1;
4.输出方式:Vector多支持Enumeration输出方式。
3.Vector线程安全,ArrrayList线程不安全。
4.Vector同步处理,性能较低,ArrayList 异步处理,性能较高。
ArrayList和LinkedList的区别:
LinkedList和ArrayList的差别主要来自于Array和LinkedList数据结构的不同。
1) 因为Array是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大的,因为这需要重排数组中的所有数据。
2) 相对于ArrayList,LinkedList插入是更快的。因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是ArrayList最坏的一种情况,时间复杂度是O(n),而LinkedList中插入或删除的时间复杂度仅为O(1)。ArrayList在插入数据时还需要更新索引(除了插入数组的尾部)。
3) 类似于插入数据,删除数据时,LinkedList也优于ArrayList。
4) LinkedList需要更多的内存,因为ArrayList的每个索引的位置是实际的数据,而LinkedList中的每个节点中存储的是实际的数据和前后节点的位置。
如果List需要存储引用类型,并且使用到 .remove() , contains() 等方法,建议复写该引用类型的 .equals() 方法。
以下为复写 .equals() 的完整Student 实例
import java.util.ArrayList;
import java.util.List;
class Student {
public int id;
public String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "id : " + id + " name : " + name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;//如果传进来的对象就是当前对象就直接返回true
}
if (obj instanceof Student) {
Student stu = (Student) obj;
if (this.id == stu.id) {
if (this.name.equals(stu.name)) {
return true;
}
return false;
}
return false;
}
return false;
}
}
public class TestCollection {
public static void main(String[] args) {
List stu = new ArrayList<>();
stu.add(new Student(1, "andy"));
stu.add(new Student(2, "john"));
stu.add(new Student(3, "jack"));
Student black = new Student(4, "black");
stu.add(black);
System.out.println("stu是否包含这个对象:"+stu.contains(new Student(3, "jack")));
stu.remove(new Student(1, "andy"));
System.out.println(stu.get(0));//id=1 的 andy 被成功删除
}
}
打印结果:
stu是否包含这个对象:true
id : 2 name : john //原来的第一个Student对象andy被成功删除
那出现这个现象的原因是什么呢?
点击 此处 查看详细原因。
底层为数组(Hash表),实际上就是HashMap。
允许为null,不能重复,元素乱序存储。
Set不允许元素重复,那么在存储这个元素时会判断这个元素是否重复,因为他的底层就是用了一个HashMap来实现,因此他怎么判断重复,后面的map会详细说。
底层为红黑二叉树,实际上就是TreeMap.
不允许为null,不能重复,有序存储(顺序可以自定义)//存储空会报错
TreeSet的有序存储,存储元素时会判断他是否重复,并且自动排序,判断重复元素由compareTo()方法来实现。因此自定义类要使用TreeSet必须覆写Comparable接口。具体的排序规则可以由我们自己来定。
自定义类实现Comparable接口实例:
import java.util.HashSet;
import java.util.Set;
class Person implements Comparable {
public String name;
public String school;
private int age;
public Person(String name, String school, int age) {
this.name = name;
this.school = school;
this.age = age;
}
public String toString() {
return "[name: " + this.name + " school: " + this.school + " age: " + age + "]";
}
/*
* 复写 .compareTo() 的规定: 当前对象大于传入对象,返回一个正数 当前对象等于传入对象,返回一个0 当前对象小于传入对象,返回一个负数
*/
@Override
public int compareTo(Person o) {
if (this.age > o.age) {
return 1;
} else if (this.age < o.age) {
return -1;
} else {
int i = this.name.compareTo(o.name);
if (i != 0) {
return i;
}
return this.school.compareTo(school);
}
}
}
public class Test {
public static void main(String[] args) {
Set list = new HashSet<>();
Person per1 = new Person("andy", "XUST", 21);
list.add(per1);
list.add(per1);
list.add(new Person("hjj", "XUST", 22));
list.add(new Person("zgd", "XUST", 23));
for (Person person : list) {
System.out.println(person);
}
}
}
针对键值对,可以通过key值找到value。key不可重复,value可以重复。
常用方法:
put(k,v);存储元素
get(k);取出元素
keySet();返回所有key信息保存在set接口(key值不能重复)
Values();返回所有values信息保存在Collection集合中(value可以重复)
entrySet();将Map集合转变成Set集合
允许key,value为空,value允许多个空
内部实现:数组+链表
HashMap的初始化和扩容:
HashMap为懒加载机制,当创建HashMap的实例化对象的时候,HashMap并不会直接分配存储空间,当第一次添加元素的时候才会真正分配空间。默认容量为16,默认负载因子为0.75,当当前元素的个数大于或等于阈值(即当前数组大小*负载因子)的时候就会扩容为原大小的2倍。
HashMap一共有4个构造方法:
HashMap(); -->默认容量,默认负载因子
HashMap(int initalCapacity) -->指定容量,但是指定容量必须是2的倍数,如果不是2的倍数系统会自动将这个数转成大于这个数且最接近这个数且是二的倍数的一个数。
HashMap(int initalCapacity,float) -->指定容量,指定负载因子(一般情况下负载因子不建议指定)
HashMap(Map< ? extends Key,? extends Value>map ) 利用这个Map再构造一个HashMap对象
HashMap底层可以看作是数组(Node[] table)和链表结合组成的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组的寻址;哈希值相同的键值对,则以链表形式存储,这里需要注意的是,如果链表大小超过阈值
HashMap是以
底层实现为红黑树,Key不可重复,且key不为null,输入null会抛出NullPointerException。
运用此类需要复写Comparable接口。
HashTable 是最早实现这种二元偶对象数据结构,后期的设计只是将其实现了Map接口。
ConcurrentHashMap 特点:线程安全,并且效率高,相当于HashMap 和HashTable的结合改良版。
JDK 1.7和JDK1.8的ConcurretHashMap实现原理的对比:
分离锁,也就是将内部进行分段(Segment),里面则是HashEntry 的数组,和 HashMap类似,哈希相同的条目也是以链表形式存放。
HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的。
实现方式:其核心是利用分段设计,在进行并发操作的时候, 只需要锁定相应段,这样就有效避免了类似 Hashtable 整体同步的问题,大大提高了性能。
JDK1.8之后: