集合类的使用

116 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情

集合类的使用

List列表

首先介绍ArrayList,它的底层是用数组实现的,内部维护的是一个可改变大小的数组,也就是我们之前所说的线性表!跟我们之前自己写的ArrayList相比,它更加的规范,同时继承自List接口。

先看看ArrayList的源码!

基本操作

List<String> list = new ArrayList<>();  //默认长度的列表
List<String> listInit = new ArrayList<>(100);  //初始长度为100的列表

向列表中添加元素:

List<String> list = new ArrayList<>();
list.add("lbwnb");
list.add("yyds");
list.contains("yyds"); //是否包含某个元素
System.out.println(list);

移除元素:

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("lbwnb");
    list.add("yyds");
    list.remove(0);   //按下标移除元素
    list.remove("yyds");    //移除指定元素
    System.out.println(list);
}

也支持批量操作:

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.addAll(new ArrayList<>());   //在尾部批量添加元素
    list.removeAll(new ArrayList<>());   //批量移除元素(只有给定集合中存在的元素才会被移除)
    list.retainAll(new ArrayList<>());   //只保留某些元素
    System.out.println(list);
}

我们再来看LinkedList,其实本质就是一个链表!我们来看看源码。

其实与我们之前编写的LinkedList不同之处在于,它内部使用的是一个双向链表:

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

当然,我们发现它还实现了Queue接口,所以LinkedList也能被当做一个队列或是栈来使用。

public static void main(String[] args) {
    LinkedList<String> list = new LinkedList<>();
    list.offer("A");   //入队
    System.out.println(list.poll());  //出队
    list.push("A");
    list.push("B");    //进栈
    list.push("C");
    System.out.println(list.pop());
    System.out.println(list.pop());    //出栈
    System.out.println(list.pop());
}

利用代码块来快速添加内容

前面我们学习了匿名内部类,我们就可以利用代码块,来快速生成一个自带元素的List

List<String> list = new LinkedList<String>(){{    //初始化时添加
  this.add("A");
  this.add("B");
}};

如果是需要快速生成一个只读的List,后面我们会讲解Arrays工具类。

集合的排序

List<Integer> list = new LinkedList<Integer>(){   //Java9才支持匿名内部类使用钻石运算符
    {
        this.add(10);
        this.add(2);
        this.add(5);
        this.add(8);
    }
};
list.sort((a, b) -> {    //排序已经由JDK实现,现在只需要填入自定义规则,完成Comparator接口实现
  return a - b;    //返回值小于0,表示a应该在b前面,返回值大于0,表示b应该在a后面,等于0则不进行交换
});
System.out.println(list);

迭代器

集合的遍历

所有的集合类,都支持foreach循环!

public static void main(String[] args) {
    List<Integer> list = new LinkedList<Integer>(){   //Java9才支持匿名内部类使用钻石运算符
        {
            this.add(10);
            this.add(2);
            this.add(5);
            this.add(8);
        }
    };
    for (Integer integer : list) {
        System.out.println(integer);
    }
}

当然,也可以使用JDK1.8新增的forEach方法,它接受一个Consumer接口实现:

list.forEach(i -> {
    System.out.println(i);
});

从JDK1.8开始,lambda表达式开始逐渐成为主流,我们需要去适应函数式编程的这种语法,包括批量替换,也是用到了函数式接口来完成的。

list.replaceAll((i) -> {
  if(i == 2) return 3;   //将所有的2替换为3
  else return i;   //不是2就不变
});
System.out.println(list);

Iterable和Iterator接口

我们之前学习数据结构时,已经得知,不同的线性表实现,在获取元素时的效率也不同,因此我们需要一种更好地方式来统一不同数据结构的遍历。

由于ArrayList对于随机访问的速度更快,而LinkedList对于顺序访问的速度更快,因此在上述的传统for循环遍历操作中,ArrayList的效率更胜一筹,因此我们要使得LinkedList遍历效率提升,就需要采用顺序访问的方式进行遍历,如果没有迭代器帮助我们统一标准,那么我们在应对多种集合类型的时候,就需要对应编写不同的遍历算法,很显然这样会降低我们的开发效率,而迭代器的出现就帮助我们解决了这个问题。

我们先来看看迭代器里面方法:

public interface Iterator<E> {
  //...
}

每个集合类都有自己的迭代器,通过iterator()方法来获取:

Iterator<Integer> iterator = list.iterator();   //生成一个新的迭代器
while (iterator.hasNext()){    //判断是否还有下一个元素
  Integer i = iterator.next();     //获取下一个元素(获取一个少一个)
  System.out.println(i);
}

迭代器生成后,默认指向第一个元素,每次调用next()方法,都会将指针后移,当指针移动到最后一个元素之后,调用hasNext()将会返回false,迭代器是一次性的,用完即止,如果需要再次使用,需要调用iterator()方法。

ListIterator<Integer> iterator = list.listIterator();   //List还有一个更好地迭代器实现ListIterator

ListIterator是List中独有的迭代器,在原有迭代器基础上新增了一些额外的操作。


Set集合

我们之前已经看过Set接口的定义了,我们发现接口中定义的方法都是Collection中直接继承的,因此,Set支持的功能其实也就和Collection中定义的差不多,只不过使用方法上稍有不同。

Set集合特点:

  • 不允许出现重复元素
  • 不支持随机访问(不允许通过下标访问)

首先认识一下HashSet,它的底层就是采用哈希表实现的(我们在这里先不去探讨实现原理,因为底层实质上维护的是一个HashMap,我们学习了Map之后再来讨论)

public static void main(String[] args) {
    HashSet<Integer> set = new HashSet<>();
    set.add(120);    //支持插入元素,但是不支持指定位置插入
    set.add(13);
    set.add(11);
    for (Integer integer : set) {
      System.out.println(integer);
    }
}

运行上面代码发现,最后Set集合中存在的元素顺序,并不是我们的插入顺序,这是因为HashSet底层是采用哈希表来实现的,实际的存放顺序是由Hash算法决定的。

那么我们希望数据按照我们插入的顺序进行保存该怎么办呢?我们可以使用LinkedHashSet:

public static void main(String[] args) {
    LinkedHashSet<Integer> set = new LinkedHashSet<>();  //会自动保存我们的插入顺序
    set.add(120);
    set.add(13);
    set.add(11);
    for (Integer integer : set) {
        System.out.println(integer);
    }
}

LinkedHashSet底层维护的不再是一个HashMap,而是LinkedHashMap,它能够在插入数据时利用链表自动维护顺序,因此这样就能够保证我们插入顺序和最后的迭代顺序一致了。

还有一种Set叫做TreeSet,它会在元素插入时进行排序:

public static void main(String[] args) {
    TreeSet<Integer> set = new TreeSet<>();
    set.add(1);
    set.add(3);
    set.add(2);
    System.out.println(set);
}

可以看到最后得到的结果并不是我们插入顺序,而是按照数字的大小进行排列。当然,我们也可以自定义排序规则:

public static void main(String[] args) {
    TreeSet<Integer> set = new TreeSet<>((a, b) -> b - a);   //在创建对象时指定规则即可
    set.add(1);
    set.add(3);
    set.add(2);
    System.out.println(set);
}

现在的结果就是我们自定义的排序规则了。

虽然Set集合只是粗略的进行了讲解,但是学习Map之后,我们还会回来看我们Set的底层实现,所以说最重要的还是Map。本节只需要记住Set的性质、使用即可。