集合
集合是Java中提供的一种容器,用来存储对象,并且对象的类型可以不一致。
集合和数组的区别:
- 数组的长度是固定的,集合长度是可变的
- 数组中存储的是同一类型的元素,一个集合可以存储不同类型的对象(利用了泛型,不同类型的对象都会被提升为Object类型)
- 数组可以存储基本数据类型值,集合只能存储对象
分类:
集合按照存储结构,分为单例集合java.util.Collection和双列集合java.util.Map
Collection:
Collection是单例集合的最顶层父接口,有两个重要的子接口:java.util.List和java.util.Set。
List(如ArrayList、LinkedList):元素有序、元素可重复
Set(如HashSet、TreeSet):元素无需,元素不可重复
在理解集合之前,需要先了解泛型,因为集合接口中广泛用到了泛型
泛型
泛型,即“参数化类型”。参数的概念在方法调用中用的最多,调用一个方法,传递必要的参数,并在方法中使用该参数。“参数化类型”就是将具体的类型参数化,本来定义一个变量的时候需要指定它的类型,利用泛型后,将变量的类型也可以定义为一个参数,在使用的时候传入具体的类型。
泛型可以定义在类、接口、方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型类
定义在类上的泛型是在创建对象时确定的
/*
* 定义具有泛型的类
* 修饰符 class 类名 <范型变量>{
* 范型变量一般用E,K,V,T;
* }
* */
public class MyClass1<E> {
}
使用:
MyClass1<String> c = new MyClass1<String>();
MyClass1<Integer> c = new MyClass1<Integer>();
泛型方法
定义在方法上的泛型是在调用方法时确定的
/*
泛型方法(方法上含有泛型)
格式:
修饰符 <泛型变量> 返回值类型 方法名称(参数列表...) {
//...
}
*/
public class MyClass02<T> {
//此方法上的泛型不是自己定义的,而是使用的类上的泛型
public void print(T t) {
System.out.println(t);
}
//泛型方法: 泛型E是在方法上自己定义的
public <E> void show(E e) {
System.out.println(e);
}
//返回值的类型也定义为泛型
public <E> E show(E e) {
System.out.println(e);
}
}
使用:
MyClass02<Integer> c = new MyClass02<Integer>();
c.print(1); //只能是整数
c.show("abc");
c.show(123);
c.show(1.2);
泛型接口
定义在接口上的泛型由实现类的对象确定,或者由实现类确定
/*
泛型接口(接口上含有泛型)
格式:
public abstract interface 接口名<泛型变量> {
//抽象方法
}
*/
public interface MyInter<T> {
//抽象方法
public abstract void print(T t);
}
//定义实现类时确定泛型的类型
public class MyImp1 implements MyInter<String> {
public void print(String t){
//...
}
}
//定义实现类是可以不用确定泛型的类型
public class MyImp2 implements MyInter<T> {
public void print(T t){
//...
}
}
使用:
MyImp2<String> my = new MyImp2<String>();
my.print("abc");
泛型通配符
当使用泛型类或者接口时(不是定义泛型)传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配 符后,里面的只能使用Object类中的共性方法,集合中元素自身方法无法使用。
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
test1(list1);
Collection<String> list2 = new ArrayList<String>();
test1(list2);
}
public static void test1(Collection<?> list) {
//不能使用add方法,因为元素类型在调用的时候才能确定
//list.add("abc"); 编译器会报错
System.out.println(list.size());
}
泛型的上限和下限
JAVA的泛型中可以指定一个泛型的上限和下限。
泛型的上限:
- 格式: 类型名称 <? extends 类 > 对象名称
只能接收该类型及其子类
泛型的下限:
- 格式: 类型名称 <? super 类 > 对象名称
只能接收该类型及其父类型
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Object> list3 = new ArrayList<Object>();
test1(list1);
test1(list2); //报错,String类型不是Number类型的子类
test2(list3);
}
//必须是Number类型或者是Number类型的子类
public static void test1(Collection<? extends Number> list) {
}
//必须是Number类型或者是Number类型的父类
public static void test2(Collection<? super Number> list) {
}
Collection接口中使用泛型的源码
//Collection接口中使用泛型
public interface Collection<E> extends Iterable<E> {
//...
}
//Iterable接口中使用泛型
public interface Iterable<T> {
//...
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
//增强for循环中,t的类型用泛型T代替
for (T t : this) {
action.accept(t);
}
}
//...
}
使用泛型的好处
- 可以统一数据类型,便于操作
- 将运行时的异常提前到了编译时,提高了效率
如Collection<String> coll = new ArrayList<String>();
定义了集合中只能存字符串对象,如果存其他的类型,则编译期间就会报错(编译器会有报错提示) - 避免了强制类型转换
- 实现代码的模板化,把数据类型当作参数传递,提高了可重用性
接口
Collection提供的接口
Collection提供了一些接口,供单列集合(List和Set)通用,常用接口如下:
public boolean add(E e)
:把给定的对象添加到集合中public void clear()
:清空集合中所有元素public boolean remove(E e)
:从集合中删除指定对象public boolean contains(E e)
:判断集合中是否存在指定对象public boolean isEmpty()
:判断集合是否为空public int size()
:返回集合的元素个数public Object[] toArray():
:把集合中的元素存储到数组中
用法:
public static void collectionApis() {
//利用多态
Collection<String> coll = new ArrayList<String>();
//1
coll.add("a");
coll.add("b");
coll.add("c");
System.out.println(coll);//[a, b, c]
//2
coll.remove("b"); //true
coll.remove("d"); //false
System.out.println(coll); //[a, c]
//3
System.out.println(coll.size()); //2
//4
System.out.println(coll.contains("e")); //false
//5
System.out.println(coll.isEmpty()); //false
//6
Object[] arr = coll.toArray();
System.out.println(arr[0]); //a
//7
coll.clear();
System.out.println(coll); //[]
}
Iterator提供的接口
public Iterator iterator()
:获取集合对应的迭代器public E next()
:取迭代器的下一个元素public boolean hasNext()
:判断是否有下一个元素
初始状态,迭代器索引位于第一个元素前,调用一次next(),迭代器向后移动一个位置,如果该位置有元素,则返回该元素,否则报错
java.util.NoSuchElementException
。当hasNext方法返回false,表示到达了集合末尾。
在遍历的过程中,不能对集合元素进行增删操作
用法:
public static void IteratorTest() {
Collection<String> coll = new ArrayList<String>();
coll.add("a");
coll.add("b");
coll.add("c");
//获取集合对应的迭代器
Iterator<String> it = coll.iterator();
//判断是否有下一个元素
while(it.hasNext()) {
String item = it.next(); //取迭代去的下一个元素
//不能在遍历的过程中修改元素,否则报错:java.util.ConcurrentModificationException
//coll.remove("b");
System.out.println(item);
}
String item = it.next(); //没有元素了再去取元素则报错:java.util.NoSuchElementException
System.out.println(item);
}
增强for循环
用来遍历集合和数组
在遍历的过程中,不能对集合元素进行增删操作
for(元素的数据类型 变量 : Collection集合/数组){
//
}
用法:
public static void forTest() {
Collection<String> coll = new ArrayList<String>();
coll.add("a");
coll.add("b");
coll.add("c");
for(String item: coll) {
System.out.println(item);
}
int[] arr = {1, 2, 3};
for(int item: arr){
System.out.println(item);
}
}
Collection的默认类型Object
public static void test() {
//没有指定类型,则默认是Object类型,可以存放各种类型的元素
Collection coll = new ArrayList();
coll.add(1);
coll.add("abc");
coll.add('e');
coll.add(1.2);
System.out.println(coll);
for(Object item: coll) {
if(item instanceof String) {
System.out.println("字符串");
//item.length(); //错误写法,因为此时item是Object类型,不能使用String的特有方法
System.out.println("字符串的长度:"+((String) item).length());
}
System.out.println(item);
}
}
List集合
java.util.List接口继承自Collection接口,实现了List接口的集合(如ArrayList和LinkedList)称为List集合,List接口的特点:
- 元素存取有序
- 带有索引,可以通过索引访问集合中的任意元素
- 集合中的元素可重复
List集合中的方法
public void add(int index, E element)
:向指定的位置添加指定的元素,索引从0开始public E get(int index)
:获取指定位置的元素,索引从0开始,public E remove(int index)
:移除指定位置的元素,索引从0开始,并返回被移除的元素public E set(int index, E element)
:将指定位置的元素替换为指定的值,索引从0开始,并返回旧值
用法:
public static void listApisTest(){
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
//1
list.add(1, "aa");
System.out.println(list); //[a, aa, b, c]
//2
System.out.println(list.get(2));//b
//System.out.println(list.get(5));//报错:java.lang.IndexOutOfBoundsException
//3
System.out.println(list.remove(1)); //aa
System.out.println(list); //[a, b, c]
//4
System.out.println(list.set(1, "1")); //b
System.out.println(list); //[a, 1, c]
}
ArrayList和LinkedList
ArrayList集合使用数组作为存储结构,因此元素的查找块,增加和删除慢;LinkedList集合使用链表作为存储结构,因此元素的查找慢,增加和删除快。
Set集合
java.util.Set接口继承自Collection接口,实现了Set接口的集合(如HashSet、LinkedHashSet)称为Set集合,Set接口的特点:
- 不带索引
- 集合中的元素不可重复
Set集合元素是唯一的前提是元素所属类重写hashCode和equals方法。
举个例子:
public class Person {
Integer id;
String name;
public Person(Integer id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(null == obj || getClass() != obj.getClass()) return false;
Person person = (Person)obj;
if(id != person.id) return false;
return name != null ? name.equals(person.name) : person.name == null;
}
}
public static void main(String[] args) {
HashSet set = new HashSet<>();
Person p1 = new Person(1,"a");
Person p2 = new Person(2,"b");
set.add(p1);
set.add(p2);
p1.name = "c";
set.remove(p1);
System.out.println(set);
set.add(new Person(1,"c"));
System.out.println(set);
set.add(new Person(1, "a"));
System.out.println(set);
}
结果:
[Person{id=1, name='c'}, Person{id=2, name='b'}]
[Person{id=1, name='c'}, Person{id=2, name='b'}, Person{id=1, name='c'}]
[Person{id=1, name='c'}, Person{id=2, name='b'}, Person{id=1, name='a'}, Person{id=1, name='c'}]
分析:
public static void main(String[] args) {
HashSet set = new HashSet<>();
Person p1 = new Person(1,"a");
Person p2 = new Person(2,"b");
set.add(p1);
set.add(p2);
p1.name = "c";
//删除p1={1, "c"},按照p1处理过的哈希值在数组中查找该位置是否有元素,
//这时候这个位置并没有元素,因为set集合中的p1虽然值变成了{1, "c"},
//但是哈希值没变,还是插入时的{1,"a"}计算出来的哈希值,因此,删除无效
set.remove(p1);
System.out.println(set);
//按照{1, "c"}处理过的哈希值在数组中查找该位置是否有元素,
//这时候这个位置并没有元素,因此添加成功
set.add(new Person(1,"c"));
System.out.println(set);
//按照{1, "c"}处理过的哈希值在数组中查找该位置是否有元素,
//这时候,这个位置有元素,该元素就是修改之前的p1,因此继续
//按照equals方法比较两个对象是否相等,结果不相等,因此添加成功
set.add(new Person(1, "a"));
System.out.println(set);
}
HashSet
特点:元素无序
HashSet根据对象的哈希值来确定元素在集合中的存储位置,查找和存取都需要计算元素的哈希值,而不是遍历查找,因此存取和查找的性能好。
每个类都会默认继承Object类,Object类中有hashCode和equals方法,当子类不重写这两个方法时:
//Person.java
public class Person {
String name;
int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
}
//Demo.java
public static void hashSetTest() {
HashSet<Person> set1 = new HashSet<Person>();
set1.add(new Person("张三", 12));
set1.add(new Person("李四", 18));
set1.add(new Person("张三", 12));
for(Person item: set1){
System.out.print(item.name);
System.out.println(item.age);
}
}
打印的结果为:
张三12
李四18
张三12
因为item中存储的是实例的地址值,地址值不一样,得到的哈希值也不一样。
Person类重写hashCode和equals方法后:
public class Person {
//...
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj == null || getClass() != obj.getClass()) return false;
Person per = (Person)obj;
return this.age == per.age && Objects.equals(this.name, per.name);
}
}
结果:
李四18
张三12
LinkedHashSet
特点:元素无序,但是能按照元素插入顺序遍历元素
LinkedHashSet继承了HashSet,在上面说的HashSet的存储结构上,加上了一个双向链表用来记录元素插入顺序。比如下面这段代码:
Set linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add(new String("AA"));
linkedHashSet.add(456);
linkedHashSet.add(456);
linkedHashSet.add(new String("刘"));
TreeSet
- 底层采用红黑色
- 可以按照添加对象的指定属性进行排序,方式有两种:自然排序和定制排序
- 自然排序:比如插入-1,2,4,3,遍历顺序为-1,2,3,4。如果元素是自定义类对象,则该类可以实现Comparable接口,并重写compareTo方法,在该方法中处理按照属性排序的逻辑
public class Student implements Comparable{
private int age;
Student(int age){
this.age=age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
@Override
public int compareTo(Object o) {
Student student =(Student)o;
//重写compareTo(),传入一个参数,然后拿当前的类的年纪跟传入参数的年纪作为对比,返回-1时,其实就是倒序排列,可以简化为
//Integer.compare(this.age, student.age);
return this.age> student.age?-1:this.age< student.age?1:0;
}
}
- 定制排序
public class TestM {
public static void main(String[] args) {
TreeSet<Student> objects = new TreeSet<>(((o1, o2) -> o1.age>o2.age?-1:o1.age<o2.age?1:0));//这个地方同样可以使用lambda表达式来代替Integer.compare(o2.age, o1.age)
objects.add(new Student(5));
objects.add(new Student(5));
objects.add(new Student(6));
objects.add(new Student(8));
System.out.println(objects);
}
}
- 添加的元素必须是同类的对象
Map
java.util.Map,Map集合用来存储键值对。Collection接口下的集合是单列集合,存储单个单元数据,而Map接口下的集合存储数据为key-value键值对,称为双列集合。Map集合不能包含重复的键,但是值可以重复。
Map集合和Set集合一样,要保证键的唯一性,需要重写键的hashCode和equals方法。
常用的Map集合有:
- HashMap:采用哈希表结构,元素无序,线程不安全,效率高低
- HashTable:和HashMap一样,但是线程安全,效率低,基本不使用
- LinkedHashMap:采用哈希表+链表结构,元素无序,但是能按照元素插入顺序遍历元素,原因同LinkedHashSet中的解释
Hashtable和HashMap的异同
相同点:
- 都实现了Map接口
不同点:
- 继承不同
public class Hashtable extends Dictionary implements Map
public class HashMap extends AbstractMap implements Map
- Hashtable中的方法是线程安全,同步的,效率低;HashMap中的方法默认情况下是线程不安全,非同步的,效率高
- Hashtable中的key和value都不允许出现null;HashMap中的key可以是null,且只有一个key是null,可以有多个value是null。因此,HashMap中不要使用get()方法来判断key是否存在,因为get()返回null,有可能是key本身就是null值,所以应该使用containsKey()方法
- 遍历方式的内部实现不同,Hashtable、HashMap都使用了 Iterator。而Hashtable还使用了Enumeration的方式
- 哈希值不同,Hashtable直接使用对象的hashCode()作为哈希值,HashMap将对象的hashCode()计算之后的值作为哈希值
- 数组初始大小和扩容方式不同,Hashtable中的数组默认大小是11,扩容是oldx2+1;HashMap中的数组默认大小是16,扩容是oldx2
Map接口采用方法
public V put(K key, V value)
:向集合中添加键值对public V remove(Object key)
:根据给定的键,移除元素,并返回值public V get(Object key)
:根据给定的键,获取值public Set<K> keySet()
:将Map集合中所有的键放到Set集合中public Set<Map.Entry<K, V>> entrySet()
: 获取到Map集合中所有的键值对对象的集合(Set集合)
Entry将Map中的键值对封装成了对象,通过entrySet可以获取这些键值对组成的对象,每个对象都有一个getKey()和getValue()方法获取对应的键和值。
用法:
public static void mapTest() {
HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
map.put("a", 4);
System.out.println(map);//{a=4, b=2, c=3}
map.remove("a");
System.out.println(map); //{b=2, c=3}
System.out.println(map.get("b")); //2
Set<String> set = map.keySet();
System.out.println(set); //[b, c]
for(String key: set) {
System.out.println(map.get(key)); //2 3
}
}
public static void entryTest() {
HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
System.out.println(entrySet); //[a=1, b=2, c=3]
/*
a1
b2
c3
*/
for(Map.Entry<String, Integer> entry: entrySet) {
System.out.print(entry.getKey());
System.out.println(entry.getValue());
}
}
LinkedHashMap支持按照访问顺序来遍历数据:
public static void linkedHashMapTest() {
//10:哈希表初始大小 0.75f:装载因子 true:按照访问顺序来遍历数据
HashMap<String, Integer> map = new LinkedHashMap<>(4, 0.75f, true);
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
System.out.println(map); //{a=1, b=2, c=3}
map.get("a");
System.out.println(map);//{b=2, c=3, a=1}
}
Collections工具类
Collections是一个操作Set、List、Map等集合的工具类,常用方法有:
public static <T> boolean addAll(Collection<T>c, T ... elements)
:往集合中批量添加元素public static void shuffle(List<?> list)
:打乱集合元素的顺序(注意,这个接口的参数只能是List类型的集合)public static <T> void sort(List<T> list)
:将集合中的元素按照默认规则排序(注意,这个接口的参数只能是List类型的集合)public static <T> void sort(List<T> list, Comparator<> super T>)
:将集合中的元素按照指定规则排序(注意,这个接口的参数只能是List类型的集合)
用法:
public static void collectionsApisTest() {
ArrayList<Integer> list = new ArrayList<Integer>();
Collections.addAll(list,3,2,5,7,4);
System.out.println(list);
Collections.sort(list);
System.out.println(list); //[2, 3, 4, 5, 7]
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//o1-o2,按照从小到大排序
//o2-o1,按照从大到小排序
return o2 - o1;
}
});
}
源码简析
ArrayList
- 底层采用数组存储数据 ;
- 在JDK7中,实例化ArrayList的时候,如果没有指定数组大小,底层会创建一个大小为10的Object类型的数组;每次插入元素的时候,检查是否需要扩容,当数组满了,扩容为原来的1.5倍;
- 在JDK8中,实例化ArrayList的时候,如果没有指定数组大小,底层会初始化对象数组为
{}
,并没有创建数组。当首次往数组中添加元素的时候,底层创建长度为10的数组,扩容同JDK7。 - 线程不安全,效率高
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
Vector
- Vector作为古老的List接口实现类,目前使用不多,代替者是ArrayList
- 在JDK7和JDK8中实现,同ArrayList在JDK7中
- 线程安全,效率低
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
LinkedList
- 底层采用双向链表存储数据
HashSet和HashMap
HashMap底层结构和HashSet类似,执行HashSet set = new HashSet<>();
的时候,调用的是public HashSet() { map = new HashMap<>(); }
方法,因此,这里重点介绍HashMap。
存储结构
HashMap底层使用哈希表结构存储,JDK8之前,哈希表采用数组和链表实现(即解决冲突的方法是链地址法);JDK8及其之后,哈希表采用数组、链表和红黑树实现,因为出现大量哈希冲突的时候,链表太长会导致查找效率降低,因此JDK8的做法是当链表元素个数超过8并且数组长度大于64的时候,将链表转换为红黑树。
初始化和扩容
JDK7中,实例化一个HashMap时,默认创建一个大小为16的数组(如果没有指定初始大小的话),数组为Entry[];在JDK8中,在添加第一个元素的时候才会创建一个大小为16的数组(如果没有指定初始大小的话),数组为Node[],Node继承了Entry;如果哈希表装载因子(数组中的元素包括链表上的元素除以数组长度)超过0.75(0.75*16=12)时,会扩容成原数组大小的两倍,扩容之后还需要重新hash。
下面用Node代表数组。
添加元素
比如要插入的键值对是k1->v1,会先计算k1的哈希值h1,然后根据h1计算对应的哈希表中的位置,这里用的是数组大小减1在和h1做位与运算,相当于h1和数组大小取模运算,但是位与运算效率要高一点。如果这个位置为空的话,那就把元素直接放到这个位置上。如果这个位置已经存在元素了,那么就会遍历这个桶上的链表,拿着h1去和这些元素的哈希值作对比,如果存在元素的哈希值和h1相同,并且key的equals方法返回的也是true,那么就会用新的v1去替换掉这个旧的已存在元素的value。如果遍历到链表的尾部,还不存在这样的元素的话,那新的元素就可以直接插入到链表尾部。这个时候,当链表长度大于8并且哈希表容量大于等于64的时候,链表会转成红黑树。
哈希冲突
- JDK7中,连地址法解决哈希冲突,是通过头插入法,如数组0号位置有元素A,此时新元素B的数组下标也是0,则将0号位置换成元素B,让B指向A
- 而在JDK8中,通过尾插法,将B元素链在A元素后面。当链表元素个数超过8并且数组长度大于64的时候,将链表转换为红黑树;当链表元素个数大于等于7并且数组长度大于等于64的时候,对数组进行扩容。
Hashtable、ConcurrentHashMap、SynchronizedMap
由于HashMap是线程不安全的,因此在要求线程安全的并发场景下,可以使用以下三种方式解决:
- Hashtable
- ConcurrentHashMap
- SynchronizedMap
Hashtable:
public synchronized V get(Object key) {
// ...
}
public synchronized V put(K key, V value) {
// ...
}
可以看到,Hashtable的put和get方法都加上了synchronized关键字,因此当一个线程对Hashtable的对象进行put操作时,另一个线程不但不可以put,而且也不能get。
ConcurrentHashMap:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
JDK8中,ConcurrentHashMap采用了transient volatile HashEntry<K,V>[] table
保存数据。这样对每个table数组元素加锁,见源代码中synchronized(f),因此可以减少并发冲突的概率,提高并发性能。对于get方法,ConcurrentHashMap采用了与HashMap一样的思路,并没有加锁。
SynchronizedMap:
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
SynchronizedMap的put和get方法都加上了synchronized关键字,调用了HashMap的put和get方法。
综上,ConcurrentHashMap的put操作是对数组元素加锁,get操作没有加锁;而SynchronizedMap的put和get方法都进行了加锁,因此ConcurrentHashMap的效率高于SynchronizedMap的效率。
总结
List集合
- 元素存取有序
- 带有索引,可以通过索引访问集合中的任意元素
- 集合中的元素可重复
- ArrayList
采用数组作为存储结构,因此元素的查找块,增加和删除慢 - LinkedList
采用链表作为存储结构,因此元素的查找慢,增加和删除快
Set集合
- 不带索引,无序性,但不代表输出顺序随机,每次输出的顺序一样,只是和插入的顺序不一样,因为底层数组是按照元素的哈希值作为索引存储的
- 集合中的元素不可重复,前提是元素对象所属类实现了hashCode和equals方法
- HashSet
采用数组+链表+红黑树实现,元素的增删改查性能好
元素无序 - LinkedHashSet
在HashSet实现的基础上增加了链表,元素的增删改查性能好
能够让元素按照存储的顺序取出
Map集合
- 用来存储键值对
- 键不重复(前提是键对象所属类实现了hashCode和equals方法)
- HashMap
采用哈希表结构,元素无序 - LinkedHashMap
采用哈希表+链表结构,能够让元素按照存储的顺序取出