前言
在上一篇中回顾了 Java 的三大特性: 封装, 继承和多态. 本篇则来介绍下集合.
集合介绍
我们在进行 Java 程序开发的时候, 除了最常用的基础数据类型和 String 对象外, 也经常会用到集合相关类.
集合类存放的都是对象的引用, 而非对象本身, 出于表达上的便利, 我们称集合中的对象就是指集合中对象的引用.
集合类型主要有 3 种: List,Set, 和 Map.
它们之间的关系可用下图来表示:
注: Map 不是 collections 的子类, 但是它们完全整合在集合中了!
List
List 接口是继承于 Collection 接口并定义 一个允许重复项的有序集合. 该接口不但能够对列表的一部分进行处理, 还添加了面向位置的操作.
一般来说, 我们在单线程中主要使用的 List 是 ArrayList 和 LinkedList 来实现, 多线程则是使用 Vector 或者使用 Collections.sychronizedList 来装饰一个集合.
这三个的解释如下:
ArrayList: 内部是通过数组实现的, 它允许对元素进行快随机访问. 当从 ArrayList 的中间位置插入或者删除元素时, 需要对数组进行复制, 移动, 代价比较高. 因此, 它适合随机查找和遍历, 不适合插入和删除.
LinkedList: 则是链表结构存储数据的, 很适合数据的动态插入和删除, 随机访问和遍历速度比较慢. 另外, 他还提供了 List 接口中没有定义的方法, 专门用于操作表头和表尾元素, 可以当作堆栈, 队列和双向队列使用.
Vector: 通过数组实现的, 不同的是它支持线程的同步. 访问速度 ArrayList 慢.
它们的用法如下:
- List list1 = new ArrayList();
- List list2 = new LinkedList();
- List list3 = new Vector();
- List list4=Collections.synchronizedList(new ArrayList())
在了解了它们的用法之后, 我们来看看为什么说使用 ArrayList 比 LinkedList 查询快, 使用 LinkedList 比 ArrayList 新增和删除快!
这里也用以下代码来进行说明, 顺便也将 Vector 进行比较.
代码示例:
- private final static int count=50000;
- private static ArrayList arrayList = new ArrayList<>();
- private static LinkedList linkedList = new LinkedList<>();
- private static Vector vector = new Vector<>();
- public static void main(String[] args) {
- insertList(arrayList);
- insertList(linkedList);
- insertList(vector);
- System.out.println("--------------------");
- readList(arrayList);
- readList(linkedList);
- readList(vector);
- System.out.println("--------------------");
- delList(arrayList);
- delList(linkedList);
- delList(vector);
- }
- private static void insertList(List list){
- long start=System.currentTimeMillis();
- Object o = new Object();
- for(int i=0;i<count;i++){
- list.add(0, o);
- }
- System.out.println(getName(list)+"插入"+count+"条数据, 耗时:"+(System.currentTimeMillis()-start)+"ms");
- }
- private static void readList(List list){
- long start=System.currentTimeMillis();
- Object o = new Object();
- for(int i = 0 ; i <count ; i++){
- list.get(i);
- }
- System.out.println(getName(list)+"查询"+count+"条数据, 耗时:"+(System.currentTimeMillis()-start)+"ms");
- }
- private static void delList(List list){
- long start=System.currentTimeMillis();
- Object o = new Object();
- for(int i = 0 ; i < count ; i++){
- list.remove(0);
- }
- System.out.println(getName(list)+"删除"+count+"条数据, 耗时:"+(System.currentTimeMillis()-start)+"ms");
- }
- private static String getName(List list) {
- String name = "";
- if(list instanceof ArrayList){
- name = "ArrayList";
- }
- else if(list instanceof LinkedList){
- name = "LinkedList";
- }
- else if(list instanceof Vector){
- name = "Vector";
- }
- return name;
- }
输出结果:
ArrayList 插入 50000 条数据, 耗时: 281ms
LinkedList 插入 50000 条数据, 耗时: 2ms
Vector 插入 50000 条数据, 耗时: 274ms
--------------------
ArrayList 查询 50000 条数据, 耗时: 1ms
LinkedList 查询 50000 条数据, 耗时: 1060ms
Vector 查询 50000 条数据, 耗时: 2ms
--------------------
ArrayList 删除 50000 条数据, 耗时: 143ms
LinkedList 删除 50000 条数据, 耗时: 1ms
Vector 删除 50000 条数据, 耗时: 137ms
从上述结果中, 可以明显看出 ArrayList 和 LinkedList 在新增, 删除和查询性能上的区别.
在集合中, 我们一般用于存储数据. 不过有时在有多个集合的时候, 我们想将这几个集合做合集, 交集, 差集和并集的操作. 在 List 中, 这些方法已经封装好了, 我们无需在进行编写相应的代码, 直接拿来使用就行.
代码示例如下:
- /**
- * 合集
- * @param ls1
- * @param ls2
- * @return
- */
- private static List<String> addAll(List<String> ls1,List<String>ls2){
- ls1.addAll(ls2);
- return ls1;
- }
- /**
- * 交集 (retainAll 会删除 ls1 在 ls2 中没有的元素)
- * @param ls1
- * @param ls2
- * @return
- */
- private static List<String> retainAll(List<String> ls1,List<String>ls2){
- ls1.retainAll(ls2);
- return ls1;
- }
- /**
- * 差集 (删除 ls2 中没有 ls1 中的元素)
- * @param ls1
- * @param ls2
- * @return
- */
- private static List<String> removeAll(List<String> ls1,List<String>ls2){
- ls1.removeAll(ls2);
- return ls1;
- }
- /**
- * 无重复的并集 (ls1 和 ls2 中并集, 并无重复)
- * @param ls1
- * @param ls2
- * @return
- */
- private static List<String> andAll(List<String> ls1,List<String>ls2){
- // 删除在 ls1 中出现的元素
- ls2.removeAll(ls1);
- // 将剩余的 ls2 中的元素添加到 ls1 中
- ls1.addAll(ls2);
- return ls1;
- }
当然, 经常用到的还有对 List 进行遍历.
List 数组遍历主要有这三种方法, 普通的 for 循环, 增强 for 循环(jdk1.5 之后出现), 和 Iterator(迭代器).
代码示例:
- List<String> list=new ArrayList<String>();
- list.add("a");
- list.add("b");
- list.add("c");
- for(int i=0;i<list.size();i++){
- System.out.println(list.get(i));
- }
- for (String str : list) {
- System.out.println(str);
- }
- Iterator<String> iterator=list.iterator();
- while(iterator.hasNext())
- {
- System.out.println(iterator.next());
- }
说明: 普通的 for 循环和增强 for 循环区别不大, 主要区别在于普通的 for 循环可以获取集合的下标, 而增强 for 循环则不可以. 但增强 for 循环写起来方法, 如果不需要获取具体集合的下标, 推荐使用增强 for 循环. 至于 Iterator(迭代器)这种也是无法获取数据下标, 但是该方法可以不用担心在遍历的过程中会集合的长度发生改变. 也就是在遍历的时候对集合进行增加和删除.
在 <阿里巴巴 Java 开发手册> 中, 对于集合操作也有这种说明.
不要在 foreach 循环里进行元素的 remove / add 操作. remove 元素请使用 Iterator 方式, 如果并发操作, 需要对 Iterator 对象加锁.
那么为什么不要使用 foreach 循环进行元素的 remove / add 操作呢?
我们这里可以简单的做下验证.
代码示例:
- List<String> list = new ArrayList<String>();
- list.add("1");
- list.add("2");
- System.out.println("list 遍历之前:"+list);
- for (String item : list) {
- if ("2".equals(item)) {
- list.remove(item);
- // 如果这里不适用 break 的话, 会直接报错的
- break;
- }
- }
- System.out.println("list 遍历之后:"+list);
- List<String> list1 = new ArrayList<String>();
- list1.add("1");
- list1.add("2");
- System.out.println("list1 遍历之前:"+list1);
- Iterator<String> iterator = list1.iterator();
- while (iterator.hasNext()) {
- String item = iterator.next();
- if ("2".equals(item)) {
- iterator.remove();
- }
- }
- System.out.println("list1 遍历之后:"+list1);
输出结果:
list 遍历之前:[1, 2]
list 遍历之后:[1]
list1 遍历之前:[1, 2]
list1 遍历之后:[1]
注意: 上述代码中, 在对 list 进行 for 循环遍历的时候, 加了 break,
上述示例中, 都正确的打印我们想要的数据, 不过在 foreach 循环中, 我在其中是加上了 break. 如果不加 break, 就会直接抛出 ConcurrentModificationException 异常!
Map
Map 接口并不是 Collection 接口的继承. Map 提供 key 到 value 的映射. 一个 Map 中不能包含相同的 key, 每个 key 只能映射一个 value.Map 接口提供 3 种集合的视图, Map 的内容可以被当作一组 key 集合, 一组 value 集合, 或者一组 key-value 映射.
Map 接口主要由 HashMap,TreeMap,LinkedHashMap,Hashtable 和 ConcurrentHashMap 这几个类实现.
它们的解释如下:
HashMap: HashMap 的键是根据 HashCode 来获取, 所以根据键可以很快的获取相应的值. 不过它的键对象是不可以重复的, 它允许键为 Null, 但是最多只能有一条记录, 不过却是可以允许多条记录的值为 Null. 因为 HashMap 是非线程安全的, 所以它的效率很高.
TreeMap: 可以将保存的记录根据键进行排序, 默认是按键值的升序排序(自然顺序). 也可以指定排序的比较器, 当用 Iterator 遍历 TreeMap 时, 得到的记录是排过序的. 它也是不允许 key 值为空, 并且不是线程安全的.
LinkedHashMap:LinkedHashMap 基本和 HashMap 一致. 不过区别在与 LinkedHashMap 是维护一个双链表, 可以将里面的数据按写入 的顺序读出. 可以认为 LinkedHashMap 是 HashMap+LinkedList. 即它既使用 HashMap 操作数据结构, 又使用 LinkedList 维护插入元素的先后顺序. 它也不是线程安全的.
Hashtable:Hashtable 与 HashMap 类似, 可以说是 HashMap 的线程安全版. 不过它是不允许记录的键或者值为 null. 因为它支持线程的同步, 是线程安全的, 所以也导致了 Hashtale 在效率较低.
ConcurrentHashMap: ConcurrentHashMap 在 Java 1.5 作为 Hashtable 的替代选择新引入的. 使用锁分段技术技术来保证线程安全的, 可以看作是 Hashtable 的升级版.
在工作中, 我们使用得最多的 Map 应该是 HashMap. 不过有时在使用 Map 的时候, 需要进行自然顺序排序. 这里我们就可以使用 TreeMap, 而不必自己实现这个功能. TreeMap 的使用和 HashMap 差不多. 不过需要注意的是 TreeMap 是不允许 key 为 null. 这里简单的介绍下 TreeMap 的使用.
代码示例:
- Map<String,Object> hashMap=new HashMap<String,Object>();
- hashMap.put("a", 1);
- hashMap.put("c", 3);
- hashMap.put("b", 2);
- System.out.println("HashMap:"+hashMap);
- Map<String,Object> treeMap=new TreeMap<String,Object>();
- treeMap.put("a", 1);
- treeMap.put("c", 3);
- treeMap.put("b", 2);
- System.out.println("TreeMap:"+treeMap);
输出结果:
- HashMap:{b=2, c=3, a=1}
- TreeMap:{a=1, b=2, c=3}
上述中可以看出 HashMap 是无序的, TreeMap 是有序的.
在使用 Map 的时候, 也会对 Map 进行遍历. 一般遍历 Map 的 key 和 value 有三种方式:
第一种通过 Map.keySet 遍历;
第二种通过 Map.entrySet 使用 iterator 遍历;
第三种是通过 Map.entrySet 进行遍历.
使用如下:
- Map<String, String> map = new HashMap<String, String>();
- for (String key : map.keySet()) {
- System.out.println("key="+ key + "and value=" + map.get(key));
- }
- Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
- while (it.hasNext()) {
- Map.Entry<String, String> entry = it.next();
- System.out.println("key=" + entry.getKey() + "and value=" + entry.getValue());
- }
- for (Map.Entry<String, String> entry : map.entrySet()) {
- System.out.println("key=" + entry.getKey() + "and value=" + entry.getValue());
- }
如果只想获取 Map 中 value 的话, 可以使用 foreach 对 Map.values()进行遍历.
- for (String v : map.values()) {
- System.out.println("value=" + v);
- }
在上述遍历中, 我们最多使用的是第一种 Map.keySet, 因为写起来比较简单. 不过在容量大的时候, 推荐使用第三种, 效率会更高!
Set
Set 是一种不包含重复的元素的 Collection, 即任意的两个元素 e1 和 e2 都有 e1.equals(e2)=false,Set 最多有一个 null 元素. 因为 Set 是一个抽象的接口, 所以是不能直接实例化一个 set 对象. Set s = new Set() 这种写法是错误的.
Set 接口主要是由 HashSet,TreeSet 和 LinkedHashSet 来实现.
它们简单的使用如下:
- Set hashSet = new HashSet();
- Set treeSet = new TreeSet();
- Set linkedSet = new LinkedHashSet();
因为 Set 是无法拥有重复元素的, 所以也经常用它来去重. 例如在一个 list 集合中有两条相同的数据, 想去掉一条, 这时便可以使用 Set 的机制来去重.
代码示例:
- public static void set(){
- List<String> list = new ArrayList<String>();
- list.add("Java");
- list.add("C");
- list.add("C++");
- list.add("JavaScript");
- list.add("Java");
- Set<String> set = new HashSet<String>();
- for (int i = 0; i <list.size(); i++) {
- String items = list.get(i);
- System.out.println("items:"+items);
- if (!set.add(items)) {
- System.out.println("重复的数据:" + items);
- }
- }
- System.out.println("list:"+list);
- }
输出结果:
- items:Java
- items:C
- items:C++
- items:JavaScript
- items:Java
重复的数据: Java
list:[Java, C, C++, JavaScript, Java]
注意: 如果是将对象进行去重的话, 是需要重写 set 中的 equals 和 hashcode 方法的.
总结
关于集合中 List,Map,Set 这三个的总结如下:
List:List 和数组类似, 可以动态增长, 根据实际存储的数据的长度自动增长 List 的长度. 查找元素效率高, 插入删除效率低, 因为会引起其他元素位置改变 <实现类有 ArrayList,LinkedList,Vector>
ArrayList: 非线程安全, 适合随机查找和遍历, 不适合插入和删除.
LinkedList : 非线程安全, 适合插入和删除, 不适合查找.
Vector : 线程安全. 不过不推荐.
Map: 一个 key 到 value 的映射的类 .
HashMap: 非线程安全, 键和值都允许有 null 值存在.
TreeMap: 非线程安全, 按自然顺序或自定义顺序遍历键(key).
LinkedHashMap: 非线程安全, 维护一个双链表, 可以将里面的数据按写入的顺序读出. 写入比 HashMap 强, 新增和删除比 HashMap 差.
Hashtable: 线程安全, 键和值不允许有 null 值存在. 不推荐使用.
ConcurrentHashMap: 线程安全, Hashtable 的升级版. 推荐多线程使用.
Set: 不允许重复的数据 . 检索效率低下, 删除和插入效率高.
HashSet: 非线程安全, 无序, 数据可为空.
TreeSet: 非线程安全, 有序, 数据不可为空.
LinkedHashSet: 非线程安全, 无序, 数据可为空. 写入比 HashSet 强, 新增和删除比 HashSet 差.
到此, 本文结束, 谢谢阅读.
来源: https://www.cnblogs.com/xuwujing/p/8886821.html