Java 常用的集合有哪些?哪些是线程安全的?

线程安全的:Hashtable,ConcurrentHashMap,Vector ,CopyOnWriteArrayList ,CopyOnWriteArraySet

线程不安全的:HashMap,ArrayList,LinkedList,HashSet,TreeSet,TreeMap

常用到的集合有 Set、List、Map。其中set和list继承了collection接口。

一、List

常用操作

  • add(数据):添加数据
  • remove(数据):删除数据
  • indexOf(数据):返回数据在集合中第一次出现式的索引位置的值
  • contains():用于判断集合中是否包含指定的元素,有返回true。没有返回false。
  • clear():将list中的对象变成垃圾清空。

remove,contains,indexOf 三个方法会默认去调用equals方法。

1.1 ArrayList

  • ArrayList 是最常用的集合,它实现了 List 接口,继承了 AbstractList 类,由一个 Object[] 实例实现,即底层是一个数组,每个元素之间不能有间隔。
  • 默认初始化长度为 10,扩容规则为 0.5倍的原容量加1,即一次扩容后的长度为 16
  • 相对于LinkedList,它查询速度快,添加和删除较慢。它适合查找,不适合增删,从中间位置增删时,要对数组进行移动、复制、所费的代价比较高。当它的大小不满足时时会创建一个新数组,然后将旧数组的数据复制到新数组
  • 线程不同步(不安全)

1.2 LinkedList:

LinkedList底层是一个双向链表,所以很适合做插入、删除的操作。LinkedList继承于AbstractSequentialList,。它还提供了List接口中没有的方法,专门用于操作表头和表尾的元素,可以当堆栈、队列、双向链表使用 但不适合读 
 

  • 实现了 List 接口,继承了 AbstractSequentialList 类,同时也实现了Deque,Queue接口
  • 由一个 Node 节点链表实现,底层是一个双向链表,所以很适合做插入、删除的操作
  • 由于其数据结构为链表,无预扩容机制;
  •  特点:添加、删除速度快,查询相对于ArrayList较慢
  • 线程不同步(不安全)。

LinkindeList 特有的方法

  •     public void addFirst(E e) 将指定元素插入到次列表的开头
  •     public void addLast(E e) 将指定元素添加到此列表的结尾
  •     public E getFirst() 返回此列表的第一个元素
  •     public E getLast() 返回此列表的最后一个元素
  •     public E removeFirst() 移除并返回此列表的第一个元素
  •     public E removeLast() 移除并返回此列表的最后一个元素
  •     public E pop() 从此列表所表示的堆栈处弹出一个元素
  •     public void push(E e) 将元素推入此列表所表示的堆栈

1.3 Vector:

  • Vector实现了 List 接口,继承了 AbstractList 类,由一个 Object[] 实例实现,即底层为数组结构;
  • 默认初始化长度为 10,扩容加载因子为 1,当元素长度大于原容量时进行扩容,默认为 10,一次扩容后容量为 20;
  • 特点:线程安全,但是速度慢;在实现的方法上,用 synchronized 关键字进行了修饰,即在方法上实现了同步锁。
List删除需要注意的点

在实际开发中有时候会碰到这样的场景,需要将一个list集合中的某些特定的元素给删除掉,这个时候用可以用List提供的remove方法来实现需求。

List中的remove方法传入的参数可以是集合的下标,也可以是集合中一个元素,也可以是一个集合。
错误使用示例一:

@Data
@AllArgsConstructor
public class Person {
    private String id;
    private String name;
 
    public static void main(String[] args) {
        List<Person> lists = new ArrayList<>();
        lists.add(new Person("1", "张三"));
        lists.add(new Person("2", "王五"));
        lists.add(new Person("3", "李六"));
        lists.add(new Person("4", "哈哈"));
 
        for (Person person : lists) {
            if (person.getId().equals("2")) {
                lists.remove(person);
            }
        }
    }
}

这里我使用的是增强for循环,会发现直接报错。

List集合的一个特点是它其中的元素时有序的,也就是说元素的下标是根据插入的顺序来的,在删除头部或者中间的一个元素后,后面的元素下标会往前移动。循环的时候就会出现问题。

解决方案一:不要用for-each遍历,换成迭代器遍历,并且不要用list.remove()方法移除对象,用迭代器的方法iterator.remove()移除对象。 

@Data
@AllArgsConstructor
public class Person {
    private String id;
    private String name;
 
    public static void main(String[] args) {
        List<Person> lists = new ArrayList<>();
        lists.add(new Person("1", "张三"));
        lists.add(new Person("2", "王五"));
        lists.add(new Person("3", "李六"));
        lists.add(new Person("4", "哈哈"));
 
        Iterator<Person> iterator = lists.iterator();
        while (iterator.hasNext()){
            Person next = iterator.next();
            if(next.getId().equals("2")){
                iterator.remove();
            }
        }
 
        lists.forEach(item-> System.out.println(item));
    }
}

解决方案二:在循环中(比如for循环)使用remove方法时,注意要从List集合的最后一个元素开始遍历。

@Data
@AllArgsConstructor
public class Person {
    private String id;
    private String name;
 
    public static void main(String[] args) {
        List<Person> lists = new ArrayList<>();
        lists.add(new Person("1", "张三"));
        lists.add(new Person("2", "王五"));
        lists.add(new Person("3", "李六"));
        lists.add(new Person("4", "哈哈"));
 
        for (int i = lists.size() - 1; i >= 0; i--) {
            if (lists.get(i).getId().equals("2")) {
                lists.remove(i);
            }
        }
 
        lists.forEach(item-> System.out.println(item));
    }
}

二、 Map

Map是一个接口,存储的是键值对。Map存储的键如果重复则会覆盖值。重复的意思是hashcode和equals方法做比较,只有两个都一致则会认为是重复。

  • HashMap在JDK1.8的底层是(数组+链表+红黑树)
  • 根据键的hashcode存储数据,大多是情况下可以直接定位到它的值,因而具有很快的访问速度,
  • 遍历的顺序不确确定的。
  • HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
  • HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。
     

常用操作方法

  • put():添加数据。
  • get(key):获取单个数据。
  • keySet():获得键的集合。
  • values():获得所有值的集合。
  • entrySet():获得所有的entry对象(键值的集合)。
  • isEmpty():判断集合是否为空
  • size():获得数据的个数。
  • remove(key):删除某个值。


Map包含:HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap

其中Hashtable和ConcurrentHashMap是线程安全的。

2.1 HashMap:

  1. HashMap实现了 Map接口,继承了 AbstractMap类,数据结构采用的位桶数组,底层采用链表或红黑树进行存储;链表成红黑树。
  2.  默认初始化长度为 16,扩容加载因子为 0.75,一旦大于 0.75*16之后,就会调用resize()进行扩容,扩容2倍,即32;
  3. JDK1.7中,数据结构采用:位桶数组+链表结构;
  4. JDK1.8中,数据结构采用:位桶数组+(链表/红黑树);
  5. 支持克隆,无序,线程不同步,非安全。

2.2 LinkedHashMap:

  1. LinkedHashMap 实现了 Map 接口,继承了 HashMap 类;
  2.  迭代顺序由 accessOrder 属性的决定,默认为 false,以插入顺序访问;设置为 true 则按上次读取顺序访问(每次访问元素时会把元素移动到链表末尾方便下次访问,结构会时刻变化即get后会移动到末尾)。
  3.  默认初始化长度为 16,扩容加载因子为 0.75,一旦>0.75*16之后,就会调用resize()进行扩容,与HashMap一致;
  4.  支持克隆,有序,线程不同步,非安全。

 2.3 TreeMap

TreeMap可以对集合中的键进行排序,首先元素自身具有比较性。如果元素不具备比较性的时候就需要使容器具备比较性。

需要定义一个类实现接口Comparator,重写compare方法,并将该接口的子类实例对象作为参数传递给TreeMap集合的构造方法。

注意:当Comparable比较方式和Comparator比较方式同时存在时,以Comparator的比较方式为主

  1. TreeMap实现了 NavigableMap接口,继承了 AbstractMap 类;
  2.  数据结构基于红黑树实现;
  3. 该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法;
  4. 无初始化长度。
  5. 支持克隆,有序,线程不同步,非安全。

2.4  Hashtable

  1. Hashtable实现了 Map 接口,继承了 Dictionary类;
  2. 数据结构:也是一个散列表,数据结构与HashMap相同,key、value都不可以为 null;
  3. 该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法;
  4. 默认初始化长度为 11,扩容加载因子为 0.75,一旦>0.75*11之后,就会进行扩容,扩容到2n+1,即23;
  5. 支持克隆,无序,线程同步,安全。在实现的方法上,用 synchronized 关键字进行了修饰,即在方法上实现了同步锁
  6. 支持 Enumeration 遍历方式。

2.5  ConcurrentHashMap

  • ConcurrentHashMap实现了 ConcurrentMap接口,继承了 AbstractMap类;
  • 默认初始化长度为 16,扩容加载因子为 0.75,一旦大于 0.75*16之后,就会调用resize()进行扩容,扩容2倍,即32;
  • 数据结构:由Segment数组结构和HashEntry数组结构组成,一个ConcurrentHashMap中包含一个Segment数组,
  • Segment的结构和HashMap类似,是一种数组和链表结构。
  • 使用了锁分段技术,Segment是一种可重入锁ReentrantLock,每个Segment拥有一个锁,当对HashEntry数组的数据进行修改时,必须先获得对应的Segment锁
  • 不支持克隆,无序,线程同步,安全。

concurrentHashmap在各个版本都是线程安全的,只是1.8版本实现时做了比较大的调整,具体为:ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构采用数组+链表/红黑二叉树的方式实现。当链表中(bucket)的节点个数超过8个时,会转换成红黑树的数据结构来存储,这样设计的目的是为了减少同一个链表冲突过大情况下的读取效率。synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

三、 Set

  1. Set集合是一个无序集合
  2. Set中重复的数据只能添加一遍,JVM用hashcode和equals方法来判断是否重复,只有两个方法返回一致则认为是重复,先调用hashcode方法如果返回不一致则不调用equals方法,如果返回一致则调用equals方法再来判断是否一致。 所以使用时如果加的是实体对象那么实体对象要实现无重复数据要把equals和hashcode重写
  3. Set 集合的遍历与List集合的遍历类似,只是它是无序的不能用普通for循环遍历,采用

Iterator迭代器
Iterator不是一个集合,它是一个访问List 和Set集合的方法

Iterator的基本操作有

  • hasNext():如果有还元素可以迭代则返回true
  • next():返回迭代的下一个元素。
  • remove():将迭代器返回的元素删除。

3.1 HashSet

  • HashSet存储元素的顺序并不是按照存入时的顺序而是按照哈希值来存取的。
  • HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。
  • HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。底层数据结构是哈希表(一个元素为链表的数组,综合了数组与链表的优点)。
  • HashSet不是同步的;
  • 集合元素值可以是null;


内部存储机制

  当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals方法比较true,但它们的hashCode方法返回的值不相等,HashSet将会把它们存储在不同位置,依然可以添加成功。
也就是说。HashSet集合判断两个元素的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode方法返回值也相等。

靠元素重写hashCode方法和equals方法来判断两个元素是否相等,如果相等则覆盖原来的元素,依此来确保元素的唯一性

3.2 TreeSet

一个有序的Set集合,对新添加的元素按照指定的顺序排序。Integer和String对象都可以进行默认的排序,而自定义对象必须实现Comparable并重写相应的ComapreTo方法。

实现排序的两种方式:

1.Student类中实现  Comparable<T>接口


import java.util.Objects;
 
public class Student {
    private  String name;
    private Integer age;
 
    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
 
    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;
    }
 
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(name, student.name) && Objects.equals(age, student.age);
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

如果想倒序那么把compareTo方法改为

 @Override
    public int compareTo(Student o) {
        //判断如果两个年龄都一样排序 如果不判断会缺失数据 如果年龄相同数据会直接丢失 感兴趣可以注释下面代码测试
        if(this.age -o.age==0){
            //要么返回正数要么返回负数 如果有id也可以根据id排序
            return  -1;
        }
        
        //按照年龄进行排序
        return o.age -this.age;
    }

2.重写Comparable接口中的Compareto方法  此方法会覆盖(重写)实体中实现的排序对象Comparator的compareTo方法


 
import java.util.Comparator;
import java.util.TreeSet;
 
public class TreeSetDemo02 {
	public static void main(String[] args) {
		TreeSet<Student> ts=new TreeSet<Student>(new Comparator<Student>() {
			@Override
			public int compare(Student o1, Student o2) {
				//判断如果两个年龄都一样排序 如果不判断会缺失数据 如果年龄相同数据会直接丢失 感兴趣可以注释下面代码测试
				if(o2.getAge() -o1.getAge()==0){
					//要么返回正数要么返回负数 如果有id也可以根据id排序
					return  -1;
				}
 
				//按照年龄进行排序
				return o2.getAge() -o1.getAge();
			}
		});
		//创建元素对象
		Student s1=new Student("zhangsan",20);
		Student s2=new Student("lis",22);
		Student s3=new Student("wangwu",24);
		Student s4=new Student("chenliu",26);
		Student s5=new Student("zhangsan",22);
		Student s6=new Student("qianqi",24);
		
		//将元素对象添加到集合对象中
		ts.add(s1);
		ts.add(s2);
		ts.add(s3);
		ts.add(s4);
		ts.add(s5);
		ts.add(s6);
		
		//遍历
		for(Student s:ts){
			System.out.println(s.getName()+"-----------"+s.getAge());
		}
	}
}

四、高并发场景你是如何使用集合的?

高并发线程安全集合主要使用CAS(实现自旋锁的基础),底层大多时间使用AQS 实现线程安全同步。

AQS:全称为AbstractQueuedSynchronized,它是JUC包中Lock锁的底层实现,可以用AQS来实现多线程的同步器。

  • List 集合使用JUC下的类去实现高并发情况下的集合安全  CopyOnWriteArrayList
  • Set 集合使用JUC下的类去实现高并发情况下的集合安全  CopyOnWriteArraySet
  • Map 集合使用使用JUC下的类去实现高并发情况下的集合安全 ConcurrenthashMap     例如: Map<String , String > map = new ConcurrenthashMap<>() 

猜你喜欢

转载自blog.csdn.net/ddwangbin520/article/details/131495795