java中的各集合
转自:
来源
- List , Set, Map都是接口,前两个继承至Collection接口,Map为独立接口
- Set下有HashSet,LinkedHashSet,TreeSet
- List下有ArrayList,Vector,LinkedList
- Map下有Hashtable,LinkedHashMap,HashMap,TreeMap
- Collection接口下还有个Queue接口,有PriorityQueue类
-
注意:
-
Queue接口与List、Set同一级别,都是继承了Collection接口。
看图你会发现,LinkedList既可以实现Queue接口,也可以实现List接口.只不过呢, LinkedList实现了Queue接口。Queue接口窄化了对LinkedList的方法的访问权限(即在方法中的参数类型如果是Queue时,就完全只能访问Queue接口所定义的方法 了,而不能直接访问 LinkedList的非Queue的方法),以使得只有恰当的方法才可以使用。 -
SortedSet是个接口,它里面的(只有TreeSet这一个实现可用)中的元素一定是有序的。
转载声明:原文转自
http://www.cnblogs.com/xiezie/p/5511840.html
这里要讨论这些常用的默认初始容量和扩容的原因是:
当底层实现涉及到扩容时,底层是数组的容器或重新
分配一段更大的连续内存
(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),
要将容器原来的数据全部复制到新的内存上
,这无疑使效率大大降低。
加载因子的系数小于等于1,意指 即当 元素个数 超过 容量长度*加载因子的系数 时,进行扩容。
另外,扩容也是有默认的倍数的,不同的容器扩容情况不同。
List
Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类:
分别是 ArrayList、Vector 和 LinkedList。
List是有序,可重复的。有序指的是存储顺序就是list的插入位置的顺序。
类型 | 安全 | 存储结构 | 特点 | 扩容 | 初始容量 |
ArrayList | 线程不安全 | 数组 | 查询快、增删慢 | 1.5倍 | 10 |
LinkedList | 线程不安全 | 双向链表 | 查询慢、增删快 | 无 | 无 |
Vector | 线程安全 | 数组 | 查询快、增删慢 | 2倍 | 10 |
1、ArrayList
线程不安全、非同步的
数组存储结构,查询快、增删慢
元素存储顺序就是数据插入顺序
ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数
组的缺点是每个元素之间不能有间隔,
当数组大小不满足时需要增加存储能力,就要将已经有数
组的数据复制到新的存储空间中
。
当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进
行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除
。
常用方法
import java.util.ArrayList;
public class F{
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
// 追加
list.add("1");
// 指定位置插入
list.add(1,"2");
list.add("3");
// toString方法
System.out.println(list.toString());
// contains方法,返回true,false
System.out.println(list.contains("1"));
// get方法 ,根据index获取元素
System.out.println(list.get(1));
// 返回与指定值第一个匹配的index
System.out.println(list.indexOf("1"));
// 根据value移除第一个值相同的元素
list.remove("1");
// 格局index移除元素
list.remove(0);
System.out.println(list.toString());
// 给指定index位置赋值
list.set(0,"1");
System.out.println(list.toString());
}
}
ArrayList中的元素必须是引用类型的对象,不能使用基本数据类型来代替泛型。
为啥呢?不确定具体答案。
可能和数据在内存中的位置相关,基本类型的数据存在栈中,而引用类型的数据存在对中,栈中的数据很容易被回收。
可能和使用方法相关,一般处理数据放到集合中时,可能会存在空值,引用类型允许null,而基础类型不允许。
经常配合Collections.sort()使用,举个例子,实现数组从大到小输出。
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class F{
public static void main(String[] args) {
String[] arr = {"a","f","b","d","c"};
ArrayList<String> list = new ArrayList<>();
for (String a:arr) {
list.add(a);
}
// 自定义compare实现倒叙
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return -1*(o1.compareTo(o2));
}
});
System.out.println(list.toString());
}
}
在ArrayList没有元素时,容量是0,向ArrayList中add第一个元素时,给出默认容量10(不是size),扩容时每次扩大为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1); >>右移可以理解为十进制数据/2,<<左移理解为乘2。
线程不安全解释:
【引用:
https://www.cnblogs.com/panbingqi/p/11041182.html
】
【引用:
https://blog.csdn.net/weixin_43407007/article/details/87901795
】
为什么ArrayList线程不安全?
举个栗子:
一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:
- 在 Items[Size] 的位置存放此元素;
- 增大 Size 的值。 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1; 而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。现在看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
既然ArrayList是线程不安全的,但如果需要在多线程中使用,3种方式来实现一个线程安全的ArrayList对象。
#1:自己手动同步
public static List<E> list = ... ;
lock.lock();
list.add();
lock.unlock();
#2:使用同步包装器
List<E> syncList = Collections.synchronizedList(new ArrayList<E>());
//迭代时,需要包含在同步块当中
synchronized(syncList){
while(Iterator<E> iter = syncList.iterator();iter.hasNext();){}
}
Collections.synchronizedList迭代时,需要包含在同步块当中
为什么遍历时要这样处理,为什么add()不需要synchronized,而遍历的时候需要加synchronized?
synchronizedList()方法对集合的add等操作都加了synchronized修饰,listIterator()以及iterator()方法没有加
List list = Collections.synchronizedList(new ArrayList());
...
synchronized (list) {
Iterator i = list.iterator(); // Must be in synchronized block
while (i.hasNext())
foo(i.next());
}
SynchronizedList和Vector最主要的区别:
【转自:
https://www.cnblogs.com/hongdada/p/10931891.html
】
- Vector扩容为原来的2倍长度,ArrayList扩容为原来1.5倍
- SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。
- 使用SynchronizedList的时候,进行遍历时要手动进行同步处理 。
- SynchronizedList可以指定锁定的对象。
2、Vector
【引用:
https://blog.csdn.net/weixin_43407007/article/details/87901795
】
线程安全、同步
底层是数组
数组存储结构,查询快、增删慢
效率低
元素存储顺序就是数据插入顺序
Vector扩容为原来的2倍长度
Vector 与 ArrayList 一样,也是通过数组实现的,不同的是
它支持线程的同步,即某一时刻只有一
个线程能够使用 Vector
,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,
访问它比访问 ArrayList 慢。
Synchronized是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保线程互斥的访问同步代码
Vector类中的capacity()/size()/isEmpty()/indexOf()/lastIndexOf()/removeElement()/addElement() 等方法均是 sychronized 的,所以,对Vector的操作均是线程安全的。对于Vector的操作均是线程安全这句话还需要注意一点是:如果是单个方法进行调用是线程安全的,但是如果是组合的方式进行调用则需要再次进行同步处理
所以在回答Vector与ArrayList的区别时,应该这样回答:
Vector 和 ArrayList 实现了同一接口 List, 但所有的 Vector 的方法都具有 synchronized 关键字修饰。但对于复合操作,Vector 仍然需要进行同步处理。
3、LinkedList
双向链表存储结构
增删快、查找慢
线程不安全,可以使用ArrayList中的方法来解决线程不安全的问题:1、自定义锁。2、同步包装器
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除
,随机访问和遍历速度比较
慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆
栈、队列和双向队列使用。
LinkedList中有size,first以及last全局变量,其作用分别是:
-
size — 存放当前链表有多少个节点。
-
first — 指向链表的第一个节点的引用
-
last — 指向链表的最后一个节点的引用
-
节点是内部类:包含节点的值,pre指针,next指针
-
Set
Set 注重独一无二的性质,该体系集合用于
存储无序
(存入和取出的顺序不一定相同)元素,
值不能重
复
。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断
的,
如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的
hashCode
方法和
equals
方
法。
Set接口的特点:
1、不允许存储重复元素
2、没有索引,没有带索引的方法,不能使用普通的for循环
名称 | 数据结构 | 线程安全? | 是否有序、唯一 | 允许null | 如何实现有序 | 如何实现唯一 |
HashSet | 哈希表 | 不安全,不同步 | 无序、唯一 | 允许 | 无序 | hashcode()+equals() |
LinkedHashSet | 链表+哈希表 | 不安全 | 有序、唯一 | 允许 | 链表FIFO | 由哈希表保证元素唯一 |
TreeSet | 红黑树 | 不安全 | 有序、唯一 | 不允许 |
自然排序、 比较器排序 |
根据比较的返回值是否是0来决定 |
1、
HashSet
(
Hash
表)
实现Set接口,底层是哈希表(实际上是HashMap实例),没有索引 也不能使用fori的方法遍历
哈希值:是一个十进制的整数,由系统随机给出(是一个模拟出来的逻辑地址,不是数据存储的物理地址)
public native int hashCode();没有方法体,native表示调用底层操作系统的方法
HashSet特点:
1、不允许存储重复元素
2、没有索引,没有带索引的方法,不能使用fori的方法遍历
3、是一个无序的集合,存储元素和取出元素可能不一致
4、底层是一个哈希表结构,查询速度非常快
哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(与 List 显然不
同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的
hashcode 方法来获取的,
HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较
equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是
同一个元素。
哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相
同的元素放在一个哈希桶中)。也就是哈希一样的存一列。如左图表示 hashCode 值不相同的情
况;右图 表示 hashCode 值相同,但 equals 不相同的情况。
HashSet 通过 hashCode 值来确定元素在内存中的位置。
一个 hashCode 位置上可以存放多个元 素
。
遍历方法:增强for、迭代器
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Main{
public static void main(String[] args) {
Set<Integer> myset = new HashSet<>();
myset.add(1);
myset.add(2);
myset.add(3);
//迭代器遍历
Iterator<Integer> it = myset.iterator();
while (it.hasNext()){
Integer item = it.next();
System.out.println(item);
}
//增强for循环
for (Integer item:myset) {
System.out.println(item);
}
}
}
2、
LinkHashSet
(
HashSet+LinkedHashMap
)
LinkHashSet实现了Set接口,继承了HashSet类,是有序的,与HashSet相比,他多了一个链表,用来记录元素的存储顺序,保证元素有序。
LinkHashSet是由HashSet和一个链表组成,HashSet本身就是由数组+链表可能还会有红黑树组成的,
对于 LinkedHashSet 而言,它继承与于HashSet、又基于 LinkedHashMap 来实现的。
LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与于HashSet,其所有的方法
操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并
通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操
作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。
3、
TreeSet
(二叉树)
引自:
https://blog.csdn.net/Regino/article/details/104549601
- TreeSet 类继承了 Set 接口和 SortedSet 接口,元素不能重复;
-
由于 SortedSet 接口能对元素升序排序,Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,
自
己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用。
(也叫自然排序),TreeSet 类也会对实现了
Comparable 接口
的类的对象自动排序,或者根据创建 TreeSet 时提供的 Comparator 进行排序(比较器排序); -
底层依赖于
TreeMap
,元素没有索引; -
通过
红黑树
实现,
不允许放入null值
; - 存储大量的需要进行快速检索的排序信息时,TreeSet 是一个很好的选择;
Map
Map特点:
1、map集合是一个双集合列,一个元素包含两个值(键,值)
2、map集合中的元素,可以和value的数据类型可以相同,可以不同
3、map集合中的元素,key不能重复,value可以重复
4、map集合中的元素,key和value一一对应
线程安全? | 底层实现 | 效率 | null允许? | 有序? | |
HashMap |
不安全(不同步) | 数组+单链表(红黑树) | 高 |
可以 (允许一条记录的键为 null,允许多条记 录的值为 null) |
有序 |
ConcurrentHashMap |
安全 | 允许,key,value都允许null | |||
HashTable |
安全(synchronized修饰public方法,同步) | 哈希表 | 低 |
键和值都 不允许 nul |
无序 |
TreeMap |
|||||
LinkHashMap |
不安全 | HashMap+链表 | 高 | 有序 |
1、
HashMap
(数组
+
链表+
红黑树)
HashMap
实现了Map接口
HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快
的访问速度,但遍历顺序却是不确定的。
HashMap 最多只允许一条记录的键为 null,允许多条记
录的值为 null
。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导
致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使
HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。我们用下面这张图来介绍
HashMap 的结构。
大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色
的实体是嵌套类 Entry 的实例,Entry 包含四个属性:
key, value, hash 值和用于单向链表
的 next。
1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
2. loadFactor:负载因子,默认为 0.75。
3. threshold:扩容的阈值,等于 capacity * loadFactor
2、
ConcurrentHashMap
3、HashTable
(线程安全)
实现了Map接口
HashTable
与HashMap区别
1、
HashMap底层是一个哈希表,是一个多线程的不安全的集合,速度快
HashTable底层是一个哈希表,是一个线程安全的集合,是单线程的集合,速度慢
2、HashMap集合可以存储null值,null键,可以存一个null键,多个null值
HashTable不可以存储NULL值,null键
3、HashTable和Vector一样在jdk1.2版本之后被更先进的集合给取代了
HashTable被HashMap取代,Vector被ArrayList取代
HashTable的子类Properties依然活跃
Properties集合是唯一一个和IO流相结合的集合
线程安全,单线程,慢
Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,
并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,
因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全
的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。
4、TreeMap
(可排序 )
TreeMap
继承了AbstractMap,并且使用一颗树。
TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,
也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。
如果使用排序的映射,建议使用 TreeMap。
在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的
Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。
5、LinkHashMap
(记录插入顺序)
LinkedHashMap 继承了HashMap ,是 HashMap 的一个子类,保存了记录的插入顺序,
具有可预知的迭代顺序。
遍历方式:
键找值 的方法:
1、通过Map集合的getKeys()方法,返回集合的所有键的集合keys
2、遍历集合keys中的所有key,使用迭代器或者增强for循环遍历
2、通过Map集合的get(key)方法,根据键返回对应的值
使用entrySet()方法遍历:
1、使用Map集合的entrySet()方法,将Map集合的Entry对象取出来,存在Set集合中。
2、遍历Set集合,获取每个Entry对象
3、使用Entry对象的getkey(),getValue()方法,获取键与值
Map集合存储自定义数据:
Map集合保证key是唯一的:
作为key的元素必须重写hashCode和equals方法。
小例子:Map中key是城市,value存person
package testmap;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
if (age != person.age) return false;
return name != null ? name.equals(person.name) : person.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
package testmap;
import java.util.HashMap;
public class MyTest {
public static void main(String[] args) {
show();
}
/**
* HashMap存储自定义的Person对象
* 地点String类型已经重写了hashCode和equals方法
* vallue可以重复
*/
public static void show(){
HashMap<String ,Person> hashMap = new HashMap<>();
hashMap.put("北京",new Person("张三",20));
hashMap.put("广州",new Person("张四",21));
hashMap.put("天津",new Person("张五",22));
hashMap.put("天津",new Person("张五",24));
for(String key :hashMap.keySet()){
Person person = hashMap.get(key);
System.out.println(key+" "+person);
}
}
}
小例子:统计字符串中每个字符出现的次数
package testmap;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
public class CountTest {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();
HashMap<Character,Integer> hashMap = new HashMap<>();
for (int i = 0; i < str.length(); i++) {
if(hashMap.containsKey(str.charAt(i))){
hashMap.put(str.charAt(i),hashMap.get(str.charAt(i))+1);
}
else{
hashMap.put(str.charAt(i),1);
}
}
for (Character c :hashMap.keySet() ) {
System.out.println(c+" "+hashMap.get(c));
}
}
}
斗地主小案例;
package testmap;
import java.util.*;
public class Doudizhu {
public static void main(String[] args) {
//1、准备牌
//创建Map集合,存储牌的索引和组装好的牌
HashMap<Integer,String> poker = new HashMap<>();
ArrayList<Integer> pokerIndex = new ArrayList<>();
List<String> pokercolor = new ArrayList<>();
pokercolor.add("♥");
pokercolor.add("♠");
pokercolor.add("♦");
pokercolor.add("♣");
String[] num = new String[] {"2","A","K","Q","J","10","9","8","7","6","5","4","3"};
ArrayList<String> nums = new ArrayList<>();
for (int i = 0; i < num.length; i++) {
nums.add(num[i]);
}
//手动存储大王小王
int index = 0;
poker.put(index,"大王");
pokerIndex.add(index);
index++;
poker.put(index,"小王");
pokerIndex.add(index);
index++;
for (int i = 0; i < nums.size(); i++) {
for (int j = 0; j < pokercolor.size(); j++) {
poker.put(index,pokercolor.get(j)+nums.get(i));
pokerIndex.add(index);
index++;
}
}
//洗牌 使用Collections.shuffle方法
Collections.shuffle(pokerIndex);
//发牌
ArrayList<Integer> player1 = new ArrayList<>();
ArrayList<Integer> player2 = new ArrayList<>();
ArrayList<Integer> player3 = new ArrayList<>();
ArrayList<Integer> dipai = new ArrayList<>();
//遍历牌索引集合
for (int i = 0; i < pokerIndex.size(); i++) {
Integer in = pokerIndex.get(i);
if(i>=51){
dipai.add(in);
}else if(i%3==0){
player1.add(in);
}else if(i%3==1){
player2.add(in);
}else if(i%3==2){
player3.add(in);
}
}
//给牌排序
Collections.sort(player1);
Collections.sort(player2);
Collections.sort(player3);
Collections.sort(dipai);
//看牌
kanpai("玩家1",poker,player1);
kanpai("玩家2",poker,player2);
kanpai("玩家3",poker,player3);
kanpai("底牌",poker,dipai);
}
public static void kanpai(String name,HashMap<Integer,String> poker,ArrayList<Integer> list){
String res = "";
for (int i = 0; i < list.size(); i++) {
res+=poker.get(list.get(i))+" ";
}
System.out.println(name+": "+res.trim());
}
}