Java核心技术梳理-集合

编辑:Discuz论坛 发布于2019-04-03 20:39

一、前言

在日常开发中,我们经常会碰到需要在运行时才知道对象个数的情况,这种情况不能使用数组,因为数组是固定数量的,这个时候我们就会使用集合,因为集合可以存储数量不确定的对象。

集合类是特别有用的工具类,不仅可以存储数量不等的对象,还可以实现常用的数据结构,并且能够存储有映射关联的关联数组。

集合类和数组不一样,数据既可以存储基本类型,也可以存储对象,而集合只能存储对象(对象的引用变量)。

Java集合大致分为:

  • Set :无序,不可重复集合

  • List:有序,可重复集合

  • Map:具有映射关系集合

  • Queue:队列集合

Java的集合类主要是由两个接口派生出来:Collection 和Map 。

集合的框架可看此图:http://img.blog.csdn.net/20160124221843905

二、Collection 和 Iterator

2.1 Collection

Collection 接口是List、Set、Queue的父接口,其中定义了一些集合操作的通用方法,集合类似一个容器,而容器无非是添加对象、删除对象、清空容器、判断容器是否为空。

Collection collection = new ArrayList();
//添加
collection.add("晚安");
collection.add(9);
//返回长度
System.out.println(collection.size());
//移除
collection.remove(9);
//是否包含
System.out.println(collection.contains("晚安"));
//是否为空
System.out.println(collection.isEmpty());
Collection books = new HashSet();
books.add("晚安");
books.add("愿长夜无梦");
books.add("在所有夜晚安眠");
//去掉collection 包含的元素
books.removeAll(collection);
System.out.println(books);
books.add("晚安");
//保留两者都有的数据
books.retainAll(collection);
System.out.println(books);

Collection 继承了Iterable接口,Java 8为Iterable提供了forEach方法,且这个方法的参数是一个函数式接口,我们可以通过这个方法进行集合遍历,并且可以使用Lambda表达式。

books.forEach(p -> System.out.println(p));

2.2 Iterator

//获取迭代器
Iterator iterator = books.iterator();
//判断是否遍历完成
while (iterator.hasNext()){
    //获取集合中的下一个元素,返回的Object对象,需要强制转换
    String text = (String)iterator.next();
    System.out.println(text);
    //这里的删除对象是迭代器中删除,删除的是上一个next返回的方法,而且不会真正删除books中的内容
    iterator.remove();
    //会报错
    books.remove(text);
}

我们看到这里有一个删除方法,但是删除的并不是books的内容,而且如果修改了其中的内容,实际的内容也不会改变,这里我们就可以得出结论:集合并不是把本身传给了迭代器,而是将集合中的元素传递给了迭代器

迭代器采用的是快速失败机制,一旦在迭代过程中发现集合被改变,立即抛出错误,这样可以避免共享了资源而导致数据不一致问题。

我们也可以直接通过forEachRemaining 来遍历,这也是一个函数式接口

iterator.forEachRemaining(p-> System.out.println(p));

2.3 foreach

除了迭代器之外,我们也可以直接通过 foreach遍历集合,且这种写法更便捷

for (Object s : books) {
    System.out.println(s);
}

与迭代器相同,这里循环的也不是集合本身,而是元素,并且也不能修改。

2.4 Predicate

Java 8为Collection 提供了一个removeIf(Predicate<? super E> filter) 方法,这个方法是批量删除符合条件的元素,这也是一个函数式接口,我们可以使用Lambda表达式。

books.removeIf(p -> ((String) p).length() > 5);

这个Predicate 我们可以充分的利用,它可以充分简化集合运算,如:

public static int count(Predicate predicate, Collection collection) {
    int total = 0;
    for (Object object : collection) {
        //判断是否满足条件
        if (predicate.test(object)) {
            total++;
        }
    }
    return total;
}
System.out.println(count(p -> ((String) p).length() > 5, books));

2.4 Stream

Collection 还有一个Stream()流式API,流式API在JQuery中常常会用到,主要分为中间方法和末端方法,顾名思义,中间方法就是允许继续调用后续方法,而末端方法是最终的操作。Stream的引入极大的丰富了集合的操作。

常用的中间方法有

  • filter(Predicate<? super T> predicate) :过滤不符合条件的集合

  • sorted:排序

  • limit(long maxSize) :对数量进行控制,一般是排序之后的操作

  • distinct():去重

常用的末端方法有

  • forEach(Consumer<? super T> action):遍历

  • toArray():转换成数据

  • min(Comparator<? super T> comparator):获取最小值

  • max(Comparator<? super T> comparator) :获取最大值

  • count() :总数

我们可以很方便的组合这些API,而对集合进行操作,简单的例子如下:

System.out.println(books.stream().filter(p->((String) p).contains("夜")).count());

在平时的开发我们可以慢慢熟悉这些写法。

三、Set

Set不记住添加顺序,也就是并不会按照添加顺序进行排序,并且不允许包含重复元素,当添加了重复元素时,add方法会返回false,下面分别介绍其实现类HashSet,TreeSet,LinkedHashSet,EnumSet。

3.1 HashSet

顾名思义,HashSet是按照Hash算法来存储集合中的元素,因此具有很好的存储和查找性能,Hashset不是线程安全的,在多线程情况下,我们需要通过代码来保证其同步,HashSet元素值可以是null。

HashSet是通过判断两个对象equals()相等并且hashCode()的返回值也相等来决定这两个对象是否为同一对象的。

那这个时候就有些问题了,

  1. 如果两个对象的equals()为true,但是hashCode()的返回值不相等,那这个时候HashSet认为这两个对象不等,都会保存,但是其实与我们的期望就不一样了。

  2. 如果两个对象的hashCode()返回值相等,但是equals()为false,这个时候也会保存,但是会保存在同一个位置,并通过链式结构来保存,这样会对性能产生影响。

所以我们要将对象保存到HashSet中,我们就要尽量保证两个对象在equals()为true时,其返回的hashCode()的值也要相等。

3.2 LinkedHashSet

LinkedHashSet是HashSet的子类,LinkedHashSet也是通过hashCode来确定位置的,但是从名字中可以看出,它还通过链表进行了插入次序的维护,也就说是遍历的时候可以是有顺序的,但是加入了排序意味着性能的降低。

3.2 TreeSet

TreeSet是SortedSet的实现类,这就意味着TreeSet可以确保集合元素处于排序状态,既然需要排序,那就有排序规则,TreeSet有两个排序方法:自然排序和定制排序。

  • 自然排序:TreeSet是调用compareTo方法来比较元素之间的大小。

  • 定制排序:定制排序就是我们按照我们制定的规则来进行排序。

TreeSet treeSet=new TreeSet((o1,o2)->
{
   String m1 = (String)o1;
   String m2=(String)o2;
   return m1.length()>m2.length()?-1:0;
});

由于要进行排序,所以TreeSet添加的必须是同一个类元素,否则会报错。

因为增加了排序,所以相应的也增加了一些方法:

TreeSet<Integer> treeSet1 = new TreeSet<>();
treeSet1.add(1);
treeSet1.add(2);
treeSet1.add(3);
//之前的一个元素
System.out.println(treeSet1.lower(2));
//后一个元素
System.out.println(treeSet1.higher(2));
//第一个元素
System.out.println(treeSet1.first());
//最后一个元素
System.out.println(treeSet1.last());

3.4 EnumSet

EnumSet是专门存储枚举的集合,所有的元素都必须是指定枚举类型的枚举值,EnumSet也是有序的,排序规则与枚举定义的顺序相同。

EnumSet在内部以位向量方式存储,存储非常紧凑、高效,运行效率也很好,EnumSet不允许加null。

3.5 性能选择

如何选择HashSet和TreeSet呢?从性能方面来讲,HashSet要好,因为TreeSet需要额外的红黑树算法来排序,所以如果在不需要排序的情况下,我们都是选择HashSet。

四、List

List是有序的,可重复的集合,每个元素都可以通过对应的索引来进行访问,List继承了Collection,Collection中的方法List都能使用,而List作为有序集合,也就有一些与索引相关的方法。

List list = new ArrayList();
list.add("晚安");
list.add("愿路途遥远");
list.add("都有人陪在身边");
list.forEach(p-> System.out.println(p));
list.remove(1);
//在索引处添加数据
list.add(1, "愿路途遥远");
//获取指定索引位置元素
System.out.println(list.get(2));
System.out.println(list.size());
//设置索引位置的数据,index必须在现有的长度之内
list.set(2, "想要说的话还没说完");
//返回fromIndex(包含),到toIndex(不包含)集合至新集合
List list1 = list.subList(0, 2);
//排序,比较函数
list.sort((o1, o2) -> ((String) o1).length() - ((String) o2).length());
//将字符串长度作为新的集合元素替换原来的集合
list.replaceAll(p -> ((String) p).length());
list.forEach(p-> System.out.println(p));

4.1 ArrayList 、Vector、LinkedList

ArrayList 、Vector、LinkedList 是list的三个实现类,完全支持前面list接口实现的全部功能。

ArrayList 、Vector 是基于数组实现的,内部封装了一个动态的、允许再分配的Object[] 数组,初始化是通过initialCapacity参数确定初始长度,如果不指定的话默认是10,当我们能确定数组的长度时,我们可以给出,这样可以减少重新分配的次数,而提高性能。

ArrayList 、Vector在使用上完全相同,而Vector出现的较早,所有其中的一些方法名较长,而后改成List接口的方法后增加了一些方法,但是与其之前的方法有一些重复,我们一般都喜欢使用新东西的嘛,虽然Vector 线程安全,但如果我们使用Collections工具类同样可以使ArrayList 线程安全,所以总结就是使用ArrayList 就完事了。

LinkedList的内部实现与ArrayList 、Vector完全不同,它的内部实现是通过链表来存储的,并且它还继承了Deque接口,也即是可以当做双端队列来使用,由此可见它功能的强大。

LinkedList<String> linkedList = new LinkedList();
//将字符串放入队列尾部
linkedList.offer("队列尾部字符串");
//将字符放入栈顶部
linkedList.push("栈顶部字符串");
//将字符串放入到队列的头部
linkedList.offerFirst("队列头部字符串");
linkedList.forEach(p-> System.out.println(p));
//访问不删除栈顶元素
System.out.println(linkedList.peekFirst());
//访问不删除队列的最后一个元素
System.out.println(linkedList.peekLast());
//弹出栈顶元素
System.out.println(linkedList.pop());
//访问并删除队列的最后一个元素
System.out.println(linkedList.pollLast());

五、Queue

Queue 用于模拟队列这种数据结构,也就是先进先出的容器,队列简单理解就是排队打饭,先排队的人先吃饭,后来的就到队列尾部,队列通常不允许随机访问数据(这样就相当于插队了)。有以下方法:add(E e)

  • add(E e) :添加元素到尾部。

  • offer(E e):也是添加元素到尾部,不过在使用容量有限制的队列时,效率比add要高。

  • remove():获取头部元素并删除。

  • poll():获取尾部元素并删除。

  • element():获取头部元素,但不删除。

  • peek():获取头部元素,但不删除,队列为空返回null

Queue接口有PriorityQueue 实现类,除此之外,Queue 还有一个Deque 子接口,是一个双端队列,可以从两端来添加和删除元素,这样Deque实现类既可以当队列使用,也可以当栈使用,上面的LinkedList就是其实现子类,另外还有一个ArrayDeque。

5.1 PriorityQueue

PriorityQueue并不是一个标准的队列,因为它保存队列的顺序不是按照添加的顺序,而是按照大小去进行排序的,这样其实违反了队列的基本原则:先进先出,而排序的规则与之前说的TreeSet相同,这里就不赘述了。

5.2 ArrayDeque

ArrayDeque实现的是Deque,也就是说它是双端队列,简单理解就是既可以当队列使用,又可以当栈使用,当我们需要栈这种数据结构时,推荐使用ArrayDeque,Stack是古老的集合,不推荐使用。

我们分别将ArrayDeque 当做栈和队列来使用下:

栈:

ArrayDeque<String> stack = new ArrayDeque();
stack.push("晚安");
stack.push("愿路途遥远");
stack.push("都有人陪在身边");
System.out.println(stack);
//访问第一个元素,但不弹出
System.out.println(stack.peek());
//访问第一个元素,并且弹出
System.out.println(stack.pop());
System.out.println(stack);

队列:

ArrayDeque<String> queue=new ArrayDeque<>();
queue.offer("晚安");
queue.offer("愿长夜无梦");
queue.offer("在每个夜晚安眠");
System.out.println(queue);
//访问队列头部元素,但不删除
System.out.println(queue.peek());
//访问队列头部元素,并且删除
System.out.println(queue.poll());
System.out.println(queue);

六、Map

Map用于存储具有映射关系的数据,也就是键值对,Map集合保存着两组值,一组存key,另一组存value,这两组数据可以是任何应用类型的数据,key不允许重复,key和value存在单向的一对一关系。

Map中key 组合起来是一个Set集合,key没有顺序,也不能重复,Map中有个keySet()方法就是获取key集合。

Map的一些常用方法如下:

HashMap<Integer, String> map = new HashMap<>();
//放入数据
map.put(1,"宋江");
map.put(2,"卢俊义");
map.put(3,"吴用");
//如果原先位置存在数据时会返回原先的数据
System.out.println(map.put(3,"武松"));
//是否存在某key
System.out.println(map.containsKey(2));
//是否存在某value
System.out.println(map.containsValue("武松"));
//是否为空
System.out.println(map.isEmpty());
//获取长度
System.out.println(map.size());
//循环key值
for (Object key: map.keySet()) {
    //通过key值直接获取value
    System.out.println(map.get(key));
}
//根据key移除元素
System.out.println(map.remove(3));
//新的循环方式
map.forEach((key,value)-> System.out.println(key+":"+value));
//获取value,不存在则返回默认值
map.getOrDefault(8,"查无此人");
//只是替换,不会新增
map.replace(2,"林冲");
//清空数据
map.clear();

6.1 HashMap与Hashtable

HashMap与Hashtable都是Map接口的典型实现类,他们关系类似ArrayList与Vector,Hashtable早出现且线程安全,但是实现并不好,HashMap性能更好但线程不安全,Hashtable的key和value不允许为空,但是HashMap可以,我们一般也是推荐使用HashMap,即使需要线程安全也可以使用Collections工具类。

我们要正确的存储key,就要让作为key的对象必须实现hashCode()和equals()方法,那我们判断两个key值是否相等,也是和HashSet相同,必须hashCode()相等,equals()返回为true。

除了key值之外,我们有时候也要比较value值是否相等containsValue(),这里判断的话只需要equals()返回为true即可。

6.2 LinkedHashMap

 HashMap也有一个子类 LinkedHashMap,使用的双向链表来维护key-value的次序,链表维护了迭代顺序,迭代顺序与插入顺序相同。LinkedHashMap需要维护元素的插入顺序,那性能比HashMap要低,但因为其维护了顺序,迭代的时候就更快。

6.3 TreeMap

TreeMap是一个红黑树数据结构,每一个key-value即为红黑树的一个节点,存储时根据key进行节点排序,TreeMap保证key-value处于有序状态,也是两个排序机制,自然排序和定制排序,跟之前讲的类似。

因为TreeMap是有序的,那么就会提供一些访问前一个,后一个,第一个,最后一个这种方法,具体方法参考API文档。

6.4 WeakHashMap

从名字上就可以看出来WeakHashMap 是一个弱引用对象,HashMap的key保留了对对象的强引用,意味着只要HashMap对象不被销毁,那么HashMap所引用的对象就不会被销毁,HashMap也不会自动的删除这些key对应的key-value,而WeakHashMap则不行。

6.5 EnumMap

EnumMap 是与枚举类一起使用的,也就是说每个EnumMap 的key必须是一个枚举值,创建EnumMap时必须显示或隐式的指定对应的枚举类,EnumMap在内部已数组形式存储,紧凑而高效,并且按照枚举类定义的顺序进行排序,不允许key为null,但运行value为null。

七、Collections工具类

Collections工具类在上面已经提到过,就是用于操作集合的工具类,对集合的操作有排序、查找、同步控制、设置不可变。

7.1 排序

Collections提供了如下方法对list进行排序

ArrayList<Integer> list = new ArrayList<>();
list.add(2);
list.add(8);
list.add(5);
list.add(10);
list.add(7);
System.out.println("----自然排序----");
//自然排序
Collections.sort(list);
list.forEach(p-> System.out.println(p));
System.out.println("----反转----");
//反转
Collections.reverse(list);
list.forEach(p-> System.out.println(p));
System.out.println("----随机排序----");
//随机排序,相当于洗牌
Collections.shuffle(list);
list.forEach(p-> System.out.println(p));
System.out.println("----定制排序规则----");
//定制排序规则
Collections.sort(list,(o1,o2)->(o1-o2));
list.forEach(p-> System.out.println(p));
System.out.println("----定制排序规则----");
//调换list中指定位置的顺序
Collections.swap(list,2,4);
list.forEach(p-> System.out.println(p));
System.out.println("----将list最后的两个元素移到前面----");
//将list最后的两个元素移到前面
Collections.rotate(list,2);
list.forEach(p-> System.out.println(p));
System.out.println("----将list最后的两个元素移到前面----");
//将list中前面的两个元素移到后面
Collections.rotate(list,-2);
list.forEach(p-> System.out.println(p));

7.2 查找、替换操作

Collections提供了如下方法对list进行查找、替换操作

ArrayList<Integer> list = new ArrayList<>();
list.add(2);
list.add(8);
list.add(5);
list.add(10);
list.add(7);
list.add(7);
//自然排序
Collections.sort(list);
//二分法查找list,带入的参数为value,返回的为索引值(必须是排序之后)
System.out.println(Collections.binarySearch(list,10));
//最大值
System.out.println(Collections.max(list));
//最小值
System.out.println(Collections.min(list));
//出现的次数
System.out.println(Collections.frequency(list,8));
//新值替换所有的旧值
Collections.replaceAll(list,8,6);
list.forEach(p-> System.out.println(p));
//全部替换
Collections.fill(list,8);

7.3 同步控制

上面提过很多次可以使用Collections可以是集合变成线程安全,只要调用synchronizedXXX()便可以创建线程按照的集合

如:

Collection<Object> objects = Collections.synchronizedCollection(new ArrayList<>());

7.4 不可变集合

Collections提供了三类方法来获取不可变集合

  • emptyXXX():返回一个不可变的、空的集合对象

  • singletonXXX():返回一个只包含一个对象的,不可变的集合

  • unmodifiableXXX():返回指定集合的不可变视图

Collections.emptyList();
Collections.singletonList("原来是这样");
ArrayList<Integer> list = new ArrayList<>();
Collections.unmodifiableCollection(list);

集合的介绍和基本用法就是这样,当然这只是使用,后面还会进行源码的分析