Java复习(九)----集合详解

什么是集合

集合就是由若干个确定的元素所构成的整体。

为什么要引入集合

在数学中,我们经常遇到集合的概念。如:

  • 一个班所有同学构成的集合
  • 一个网站的所有商品
  • 全体自然数
  • 有理数集合

那我们为什么要在计算机引入集合呢?这是因为便于处理一组类似的数据。如:

  • 计算所有同学的总成绩
  • 列举所有商品的价格和名称

在Java中,如果一个Java对象可以在nebula持有若干个其他Java对象,并对外提供访问接口,我们把这种Java对象称为集合。很显然,Java的数组就可以看作一种集合:

String[] s= new String[10]; // 可以持有10个String对象
s[0] = "Hello"; // 可以放入String对象
String first = s[0]; // 可以获取String对象

既然Java提供了数组这种数据类型,可以充当集合,那么,我们为什么还需要其他集合类?这是因为数组有如下限制:

  • 数组初始化后大小不可变;
  • 数组只能按索引顺序存取。

因此,我们需要各种不同类型的集合类来处理不同的数据,例如:

  • 可变大小的顺序链表;
  • 保证无重复元素的集合;

Collection

Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口。Java的java.util包主要提供了以下三种类型的集合:

  • List:一种有序列表的集合,例如,按索引排列的Student的List;
  • Set:一种保证没有重复元素的集合,例如,所有无重复名称的Student的Set;
  • Map:一种通过键值(key-value)查找的映射表集合,例如,根据Student的name查找对应Student的Map。

Java集合的设计有几个特点:一是实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类有ArrayList,LinkedList等,二是支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素。例如:

List<String> list = new ArrayList<>(); // 只能放入String类型

Java访问集合总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。

使用List

在集合类中,List是最基础的一种集合:它是一种有序列表。

List接口是Collection接口的子接口,用于定义线性表数据结构。可以将List理解为存放对象的数组,只不过其元素个数可以动态的增加或减少。List有一个重要的实现类–ArrayList类,List中的元素是有序排列的而且可重复,所以被称为是序列。

List可以精确的控制每个元素的插入位置,或删除某个位置元素,它的实现类ArrayList底层是由数组实现的。

List< E >接口,可以看到几个主要的接口方法:

  • 在末尾添加一个元素:boolean add(E e)
  • 在指定索引添加一个元素:boolean add(int index, E e)
  • 删除指定索引的元素:int remove(int index)
  • 删除某个元素:int remove(Object e)
  • 获取指定索引的元素:E get(int index)
  • 获取链表大小(包含元素的个数):int size()

实现List接口并非只能通过数组(即ArrayList的实现方式)来实现,另一种LinkedList通过“链表”也实现了List接口。在LinkedList中,它的内部每个元素都指向下一个元素:
在这里插入图片描述
ArrayList和LinkedList的区别:

ArrayListLinkedList
获取指定元素速度很快需要从头开始查找元素
添加元素到末尾速度很快速度很快
在指定位置添加/删除需要移动元素不需要移动元素
内存占用较大

通常情况下,我们优先使用ArrayList。ArrayList更适合于随机访问,而LinkedList更适合于插入和删除。

在这里插入图片描述
又因为List是列表类型,所以List接口还提供了一些适合于自身的常用方法,如表所示(此图借鉴此博客
在这里插入图片描述
这里介绍一下subList:

  • List的subList方法用于获取子List。
  • 需要注意的是,subList获取的List与原List占有相同的存储空间,对子List的操作会影响原List。
  • List < E >subList(int fromIndex, int toIndex);
  • fromIndex和toIndex是截取子List的首尾下标(前包括,后不包括)
    在这里插入图片描述

除了使用ArrayList和LinkedList,我们还可以通过List接口提供的of()方法,根据给定元素快速创建List:

List<Integer> list = List.of(1, 2, 5);

但是List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常。

List的特点

List接口允许我们添加重复的元素,即List内部的元素可以重复:

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("apple"); // size=1
        list.add("pear"); // size=2
        list.add("apple"); // 允许重复添加元素,size=3
        System.out.println(list.size());
    }
}
//结果为:3

List还允许添加null:

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("apple"); // size=1
        list.add(null); // size=2
        list.add("pear"); // size=3
        String second = list.get(1); // null
        System.out.println(second);
    }
}

遍历List

对于怎么遍历List,我们可以用for循环根据索引配合get(int)方法遍历:

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("apple", "pear", "banana");
        for (String s : list) {
            System.out.println(s);
        }
    }
}
/*结果为:
apple 
pear 
banana 
*/

我们除了使用for each来遍历List之外,还可以使用迭代器Iterator来访问List,虽然比较复杂,但总是具有最高的访问效率。

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("apple", "pear", "banana");
        for (int i=0; i<list.size(); i++) {
            String s = list.get(i);
            System.out.println(s);
        }
    }
}

对于Iterator与for each,大家可以去看看这篇文章,我感觉挺好的。

List和Array转换

把List变为Array有三种方法。

  • 第一种是调用toArray()方法直接返回一个Object[]数组:
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("apple", "pear", "banana");
        Object[] array = list.toArray();
        for (Object s : array) {
            System.out.println(s);
        }
    }
}
/*结果为:
apple 
pear 
banana 
*/

但是这种方法会丢失类型信息,所以实际应用很少。

  • 第二种方式是给toArray(T[])传入一个类型相同的Array,List内部自动把元素复制到传入的Array中:
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = List.of(12, 34, 56);
        Integer[] array = list.toArray(new Integer[3]);
        for (Integer n : array) {
            System.out.println(n);
        }
    }
}
/*
结果为:
12 34 56
*/

对于这个方法,如果我们传入类型不匹配的数组,例如,String[]类型的数组,由于List的元素是Integer,所以无法放入String数组,这个方法会抛出ArrayStoreException。

  • 最后一种更简洁的写法是通过List接口定义的T[] toArray(IntFunction<T[]> generator)方法:
Integer[] array = list.toArray(Integer[]::new);

Array转换为List:

  • Arrays类中提供了一个静态方法asList,使用该方法我们可以将一个数组转换为对应的List集合。
  • 其定义方法为:
    static < T >List< T > asList< T…a >
  • 返回的List集合元素类型由传入的数组的元素类型决定。
  • 并且要注意的是,返回的集合我们不能对其增删元素,否则会抛出异常。并且对集合的元素进行修改也会影响数组对应的元素。
public void testArrayToList(){
	String[] arr = {"a", "b", "c"};
	List<String> list = Arrays.asList(arr);
	System.out.println(list);//[a, b, c]
	//list.add("d");//会抛出UnsupportedOperationException
	
	System.out.println(list.getClass().getName());
	List<String> list1 = new ArrayList<String>();
	list1.addAll(Arrays.asList(arr));

编写equals方法

为什么要编写equals方法呢?
因为List内部并不是通过==判断两个元素是否相等,而是使用equals()方法判断两个元素是否相等,例如contains()方法可以实现如下:

public class ArrayList {
    Object[] s;
    public boolean contains(Object o) {
        for (int i = 0; i < size; i++) {
            if (o.equals(s[i])) {
                return true;
            }
        }
        return false;
    }
}

因此,要正确使用List的contains()、indexOf()这些方法,放入的实例必须正确覆写equals()方法,否则,放进去的实例,查找不到。我们之所以能正常放入String、Integer这些对象,是因为Java标准库定义的这些类已经正确实现了equals()方法。

那我们要如何正确编写equals()方法?equals()方法要求我们必须满足以下条件:

  • 自反性(Reflexive):对于非null的x来说,x.equals(x)必须返回true
  • 对称性(Symmetric):对于非null的x和y来说,如果x.equals(y)为true,则y.equals(x)也必须为true;
  • 传递性(Transitive):对于非null的x、y和z来说,如果x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)也必须为true;
  • 一致性(Consistent):对于非null的x和y来说,只要x和y状态不变,则x.equals(y)总是一致地返回true或者false;
  • 对null的比较:即x.equals(null)永远返回false。

首先,我们要定义“相等”的逻辑含义。对于Person类,如果name相等,并且age相等,我们就认为两个Person实例相等。

因此,编写equals()方法如下:

public boolean equals(Object o) {
    if (o instanceof Person) {
        Person p = (Person) o;
        return this.name.equals(p.name) && this.age == p.age;
    }
    return false;
}

对于引用字段比较,我们使用equals(),对于基本类型字段的比较,我们使用==。

如果this.name为null,那么equals()方法会报错,因此,需要继续改写如下:

public boolean equals(Object o) {
    if (o instanceof Person) {
        Person p = (Person) o;
        boolean nameEquals = false;
        if (this.name == null && p.name == null) {
            nameEquals = true;
        }
        if (this.name != null) {
            nameEquals = this.name.equals(p.name);
        }
        return nameEquals && this.age == p.age;
    }
    return false;
}

如果Person有好几个引用类型的字段,上面的写法就太复杂了。要简化引用类型的比较,我们使用Objects.equals()静态方法:

public boolean equals(Object o) {
    if (o instanceof Person) {
        Person p = (Person) o;
        return Objects.equals(this.name, p.name) && this.age == p.age;
    }
    return false;
}

我们总结一下equals()方法的正确编写方法:

  • 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
  • 用instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false;
  • 对引用类型用Objects.equals()比较,对基本类型直接用==比较。
  • 使用Objects.equals()比较两个引用类型是否相等的目的是省去了判断null的麻烦。两个引用类型都是null时它们也是相等的。
  • 如果不调用List的contains()、indexOf()这些方法,那么放入的元素就不需要实现equals()方法。

这里我们拓展一下equals和==的区别:
1、==用于比较变量的值,可以应用于任何类型,如果用于引用类型,比较的是两个引用变量中存储的值(地址信息),判断两个变量是否指向相同的对象;
2、equals是Object的方法,重写以后,可以用于比较两个对象的内容是否“相等”
3、需要注意的是,Object默认的equals方法的比较规则同 ==

使用Map

我们知道,List是一种顺序列表,如果有一个存储学生Student实例的List,要在List中根据name查找某个指定的Student的分数,应该怎么办?

最简单的方法是遍历List并判断name是否相等,然后返回指定元素。但是使用List来实现存在效率非常低的问题,因为平均需要扫描一半的元素才能确定。

所以我们这里使用Map这种键值(key-value)映射表的数据结构,作用就是能高效通过key快速查找value(元素)。

Map接口定义的集合又称查找表,用于存储所谓“Key-Value”映射对。Key可以看成是Value的索引,作为Key的对象在集合不可以重复。

用Map来实现根据name查询某个Student的代码如下:

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Student s = new Student("Xiao Ming", 99);
        Map<String, Student> map = new HashMap<>();
        map.put("Xiao Ming", s); // 将"Xiao Ming"和Student实例映射并关联
        Student target = map.get("Xiao Ming"); // 通过key查找并返回映射的Student实例
        System.out.println(target == s); // true,同一个实例
        System.out.println(target.score); // 99
        Student another = map.get("Bob"); // 通过另一个key查找
        System.out.println(another); // 未找到返回null
    }
}

class Student {
    public String name;
    public int score;
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
}
/*
结果为:
true 
99 
null
*/

通过上述代码可知:Map<K, V>是一种键-值映射表,当我们调用put(K key, V value)方法时,就把key和value做了映射并放入Map。当我们调用V get(K key)时,就可以通过key获取到对应的value。如果key不存在,则返回null。和List类似,Map也是一个接口,最常用的实现类是 HashMap

put()方法

  • Map接口中定义了向Map中存放元素的put方法:
    -V put (K key, V value)

  • 将Key-Value对存入Map, 如果在集合中已经包含改Key,则操作将替换该Key所对应的Value,返回值为该Key原来所对应的Value(如果没有则返回null)

public void TextPut(){
	//向Map中添加元素
	one.put("zhangsan",new Emp("zhangsan",25,"男"));
	one.put("lisi",new Emp("lisi",26,"男"));
}

始终牢记:Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。

get()方法

  • Map接口中定义了从Map中获取元素的get方法:
    -V get(Object key)
  • 返回参数key所对应的Value对象,如果不存在则返回null
public void TestGet(){
	//从Map中使用key获取value
	Emp emp = one.get("zhangsan");
	System.out.println(emp);

containsKey()方法

  • Map接口中定义了判断某个key是否在Map中存在:
    -boolean containsKey(Object key);
  • 若Map中包含给定的Key则返回true,否则返回false
public void Test(){
	boolen has = one.containsKey("zhangsan");
	System.out.println(has);

编写equals

HashMap之所以能根据key直接拿到value,原因是它内部通过空间换时间的方法,用一个大数组存储所有value,并根据key直接计算出value应该存储在哪个索引:
在这里插入图片描述
如果key的值为"a",计算得到的索引总是1,因此返回value为Person(“Xiao Ming”),如果key的值为"b",计算得到的索引总是5,因此返回value为Person(“Xiao Hong”),这样,就不必遍历整个数组,即可直接读取key对应的value。

当我们使用key存取value的时候,就会引出一个问题:

我们放入Map的key是字符串"a",但是,当我们获取Map的value时,传入的变量不一定就是放入的那个key对象。

换句话讲,两个key应该是内容相同,但不一定是同一个对象。测试代码如下:

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        String key1 = "a";
        Map<String, Integer> map = new HashMap<>();
        map.put(key1, 123);

        String key2 = new String("a");
        map.get(key2); // 123

        System.out.println(key1 == key2); // false
        System.out.println(key1.equals(key2)); // true
    }

因为在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。

我们经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。

我们再思考一下HashMap为什么能通过key直接计算出value存储的索引。相同的key对象(使用equals()判断时返回true)必须要计算出相同的索引,否则,相同的key每次取出的value就不一定对。

通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。

因此,正确使用Map必须保证:

  • 作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true;
  • 作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:
  • 如果两个对象相等,则两个对象的hashCode()必须相等;
  • 如果两个对象不相等,则两个对象的hashCode()尽量不要相等。

即对应两个实例a和b:

  • 如果a和b相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode();
  • 如果a和b不相等,那么a.equals(b)一定为false,则a.hashCode()和b.hashCode()尽量不要相等。

上述第一条规范是正确性,必须保证实现,否则HashMap不能正常工作。

而第二条如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的hashCode(),会造成Map内部存储冲突,使存取的效率下降。

正确编写equals()的方法在上面已经说过了,以Person类为例:

public class Person {
    String firstName;
    String lastName;
    int age;
}

把需要比较的字段找出来:

firstName
lastName
age
然后,引用类型使用Objects.equals()比较,基本类型使用==比较。

在正确实现equals()的基础上,我们还需要正确实现hashCode(),即上述3个字段分别相同的实例,hashCode()返回的int必须相同:

ublic class Person {
    String firstName;
    String lastName;
    int age;

    @Override
    int hashCode() {
        int h = 0;
        h = 31 * h + firstName.hashCode();
        h = 31 * h + lastName.hashCode();
        h = 31 * h + age;
        return h;
    }
}

注意到String类已经正确实现了hashCode()方法,我们在计算Person的hashCode()时,反复使用31*h,这样做的目的是为了尽量把不同的Person实例的hashCode()均匀分布到整个int范围。

和实现equals()方法遇到的问题类似,如果firstName或lastName为null,上述代码工作起来就会抛NullPointerException。为了解决这个问题,我们在计算hashCode()的时候,经常借助Objects.hash()来计算:

int hashCode() {
    return Objects.hash(firstName, lastName, age);
}

编写equals()和hashCode()遵循的原则是:

equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算;equals()中没有使用到的字段,绝不可放在hashCode()中计算。

另外注意,对于放入HashMap的value对象,没有任何要求

Hash表原理

在这里插入图片描述

hashCode方法

从hashCode的原理中,我们可以看到,key的hashCode()方法的返回值对HashMap存储元素时会起着很重要的作用。而hashCode()方法实际上是Object中定义的。那么应该妥善重写该方法:

  • 对于重写equals方法的对象,一般要妥善的重写继承自Object类的hashCode方法(Object提供的hashCode方法将返回该对象所在内存地址的整数形式)。
  • 重写hashCode方法需注意两点:一、与equals方法的一致性,即equals比较返回true的两个对象其hashCode方法返回值应该相同。二、hashCode返回的数值应符合hash算法要求,试想如果有很多对象的hashCode方法返回值都相同,则会大大降低hash表的效率,一般情况下可以使用IDE提供的工具自动生成hashCode方法

重写hashCode方法

public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + age;
	result = prime * result + ((gender == null) ? 0 : gender.hashCode());
	result = prime * result + ((name== null) ? 0 : name.hashCode());
	long temp;
	Temp = Double.doubleToLongBits(salary);
	result = prime * result + (int) (temp ^ (temp >>> 32));
	return result;
}

装载因子及HashMap优化

  • Capacity:容量,hash表里bucket(桶)的数量,也就是散列表(散列表 ( Hash table ,也叫 哈希表 ))数组的大小。
  • Initial capacity:初始容量,创建hash表时,初始bucket的数量,默认构建容量是16,也可以使用特定的容量。
  • Size:大小,当前散列表中存储数据的数量。
  • Load factor:加载因子,默认值0.75(就是75%),当散列表增加数据时,如果size/capacity的值大于Load factory则发生扩容并且重新散列(rehash)。
  • 性能优化:加载因子较小时,散列查找性能会提高,同时也浪费了散列桶空间容量。0.75是性能和空间相对平衡结果。在创建散列表时指定合理容量,减少rehash提高性能。

Map的遍历

Map提供了三种遍历方式:

  • 遍历所有的Key
  • 遍历所有的Key-Value对
  • 遍历所有的Value(不常用)

遍历所有Key的方法:

Set < K > keySet()
该方法会将当前Map中所有的key存入一个Set集合后返回

遍历所有的键值对的方法:

Set <Entry <K,V>> entrySet()
该方法会将当前Map中每一组key-value对封装为一个Entry对象并存入一个Set集合后返回

EnumMap

Java集合库提供的一种EnumMap,它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。

我们以DayOfWeek这个枚举类型为例,为它做一个“翻译”功能:

import java.time.DayOfWeek;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
        map.put(DayOfWeek.MONDAY, "星期一");
        map.put(DayOfWeek.TUESDAY, "星期二");
        map.put(DayOfWeek.WEDNESDAY, "星期三");
        map.put(DayOfWeek.THURSDAY, "星期四");
        map.put(DayOfWeek.FRIDAY, "星期五");
        map.put(DayOfWeek.SATURDAY, "星期六");
        map.put(DayOfWeek.SUNDAY, "星期日");
        System.out.println(map);
        System.out.println(map.get(DayOfWeek.MONDAY));
    }
}
//{MONDAY=星期一, TUESDAY=星期二, WEDNESDAY=星期三, THURSDAY=星期四, FRIDAY=星期五, SATURDAY=星期六, SUNDAY=星期日} 
//星期一 

如果Map的key是enum类型,推荐使用EnumMap,既保证速度,也不浪费空间。
使用EnumMap的时候,根据面向抽象编程的原则,应持有Map接口。

TreeMap

还有一种Map,它在内部会对Key进行排序,这种Map就是SortedMap。注意到SortedMap是接口,它的实现类是TreeMap。
在这里插入图片描述
使用TreeMap时,放入的Key必须实现Comparable接口。String、Integer这些类已经实现了Comparable接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。

Comparable

  • Collections的sort方法是对集合元素进行自然排序,那么两个元素之间就一定要有大小之分。这个大小之分是如何定的呢?实际上,在使用Collections的sort排序的集合元素都必须是Comparable接口实现的类,该接口表示其子类是可比较的,因为实现该接口必须重写抽象方法:

int compareTo(T t);
该方法用于使当前对象与给定对象进行比较:

  • 若当前对象大于给定对象,那么返回值应为>0的整数
  • 若小于给定对象,那么返回值应为<0 的整数
  • 若两个对象相等,则应返回0
public void testComparable(){
	/*Cell实现了Comparable接口
	CompareTo方法逻辑为按照row值得大小排序
	*/
	//public int compareTo(Cell 0){return this.row - o.row;}
	List<Cell> cells = new ArrayList<Cell>();
	cells.add(new Cell(2, 3));
	cells.add(new Cell(5, 3));
	cells.add(new Cell(6, 5));
	Collections.sort(cells);
	System.out.println(cells);
}

Comparator

  • 一旦Java类实现了Comparable接口,其比较逻辑就已经确定;如果希望在排序的操作中临时指定比较规则,可以采用Comparator接口回调的方式。
  • Comparator接口要求实现类必须重写其定义方法:
  • int compare(T a, T b);

该方法的返回值要求:

  • 如果a<b,则返回负数,通常是-1
  • 如果a==b,则返回0
  • 如果a>b,则返回正数,通常是1

最后来看看一个代码:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
            public int compare(Student p1, Student p2) {
                return p1.score > p2.score ? -1 : 1;
            }
        });
        map.put(new Student("Tom", 77), 1);
        map.put(new Student("Bob", 66), 2);
        map.put(new Student("Lily", 99), 3);
        for (Student key : map.keySet()) {
            System.out.println(key);
        }
        System.out.println(map.get(new Student("Bob", 66))); // null?
    }
}

class Student {
    public String name;
    public int score;
    Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    public String toString() {
        return String.format("{%s: score=%d}", name, score);
    }
}
/*结果为:
{Lily: score=99} 
{Tom: score=77} 
{Bob: score=66} 
null 
*/

小结:SortedMap在遍历时严格按照Key的顺序遍历,最常用的实现类是TreeMap;
作为SortedMap的Key必须实现Comparable接口,或者传入Comparator;
要严格按照compare()规范实现比较逻辑,否则,TreeMap将不能正常工作。

Queue和Deque

Queue

队列(Queue)是常用的数据结构,可以将队列看成特殊的线性表,队列限制了对线性表的访问方式:只能从线性表的一端添加元素,从另一端取出元素。

队列遵循先进先出(FIFO:First In First Out)的原则。

它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:

  • 把元素添加到队列末尾;
  • 从队列头部取出元素。

Queue接口中主要方法如下:

int size()获取队列长度
boolean add(E)/boolean offer(E)添加元素到队尾;
E remove()/E poll()获取队首元素并从队列中删除;
E element()/E peek()获取队首元素但并不从队列中删除

对于具体的实现类,有的Queue有最大队列长度限制,有的Queue没有。注意到添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,这两个方法的行为是不同的。

throw Exception返回false或null
添加元素到队尾add(E e)boolean offer(E e)
取队首元素并删除E remove()E poll()
取队首元素但不删除E element()E peek()

举个栗子,假设我们有一个队列,对它做一个添加操作,如果调用add()方法,当添加失败时(可能超过了队列的容量),它会抛出异常:

Queue<String> q = ...
try {
    q.add("Apple");
    System.out.println("添加成功");
} catch(IllegalStateException e) {
    System.out.println("添加失败");
}

如果我们调用offer()方法来添加元素,当添加失败时,它不会抛异常,而是返回false:

Queue<String> q = ...
if (q.offer("Apple")) {
    System.out.println("添加成功");
} else {
    System.out.println("添加失败");
}

Deque

Queue是队列,只能一头进,另一头出。

如果把条件放松一下,允许两头都进,两头都出,这种队列叫双端队列(Double Ended Queue),学名Deque。

  • Deque是Queue的子接口,定义了所谓的“双端队列”,即从队列的两端分别可以入队(offer)和出队(poll),LinkedList实现了该接口。
  • 如果将Deque限制为只能从一端入队和出队,则可实现“栈(Stack Overflow)”的数据结构,对于栈而言,入栈称之为push,出栈称之为pop。
  • 栈遵循先进后出(FILO First Input Last Output)的原则

在这里插入图片描述

我们来比较一下Queue和Deque出队和入队的方法:

QueueDeque
添加元素到队尾add(E e) / offer(E e)addLast(E e) / offerLast(E e)
取队首元素并删除E remove() / E poll()E removeFirst() / E pollFirst()
取队首元素但不删除E element() / E peek()E getFirst() / E peekFirst()
添加元素到队首addFirst(E e) / offerFirst(E e)
取队尾元素并删除E removeLast() / E pollLast()
取队尾元素但不删除E getLast() / E peekLast()

对于添加元素到队尾的操作,Queue提供了add()/offer()方法,而Deque提供了addLast()/offerLast()方法。添加元素到对首、取队尾元素的操作在Queue中不存在,在Deque中由addFirst()/removeLast()等方法提供。

注意到Deque接口实际上扩展自Queue:

public interface Deque<E> extends Queue<E> {
    ...
}

因此,Queue提供的add()/offer()方法在Deque中也可以使用,但是,使用Deque,最好不要调用offer(),而是调用offerLast():

import java.util.Deque;
import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        Deque<String> deque = new LinkedList<>();
        deque.offerLast("A"); // A
        deque.offerLast("B"); // B -> A
        deque.offerFirst("C"); // B -> A -> C
        System.out.println(deque.pollFirst()); // C, 剩下B -> A
        System.out.println(deque.pollLast()); // B
        System.out.println(deque.pollFirst()); // A
        System.out.println(deque.pollFirst()); // null
    }
}

Stack

刚刚再说Deque的时候说了一下栈,现在再来拓展一下

Stack是这样一种数据结构:只能不断地往Stack中压入(push)元素,最后进去的必须最早弹出(pop)来:
在这里插入图片描述

Stack只有入栈和出栈的操作:

  • 把元素压栈:push(E);
  • 把栈顶的元素“弹出”:pop(E);
  • 取栈顶元素但不弹出:peek(E)。

在Java中,我们用Deque可以实现Stack的功能:

  • 把元素压栈:push(E)/addFirst(E);
  • 把栈顶的元素“弹出”:pop(E)/removeFirst();
  • 取栈顶元素但不弹出:peek(E)/peekFirst()

Stack的作用

Stack在计算机中使用非常广泛,JVM在处理Java方法调用的时候就会通过栈这种数据结构维护方法调用的层次。例如:

static void main(String[] args) {
    foo(123);
}

static String foo(x) {
    return "F-" + bar(x + 1);
}

static int bar(int x) {
    return x << 2;
}

JVM会创建方法调用栈,每调用一个方法时,先将参数压栈,然后执行对应的方法;当方法返回时,返回值压栈,调用方法通过出栈操作获得方法返回值。

因为方法调用栈有容量限制,嵌套调用过多会造成栈溢出,即引发StackOverflowError:

public class Main {
    public static void main(String[] args) {
        increase(1);
    }

    static int increase(int x) {
        return increase(x) + 1;
    }
}

Collections

Collections是JDK提供的工具类,同样位于java.util包中。它提供了一系列静态方法,能更方便地操作各种集合。

注意Collections结尾多了一个s,不是Collection!

我们一般看方法名和参数就可以确认Collections提供的该方法的功能。例如,对于以下静态方法:

public static boolean addAll(Collection<? super T> c, T... elements) { ... }

创建空集合

Collections提供了一系列方法来创建空集合:

  • 创建空List:List emptyList()
  • 创建空Map:Map<K, V> emptyMap()
  • 创建空Set:Set emptySet()

要注意到返回的空集合是不可变集合,无法向其中添加或删除元素。

此外,也可以用各个集合接口提供的of(T…)方法创建空集合。例如,以下创建空List的两个方法是等价的:

List<String> list1 = List.of();
List<String> list2 = Collections.emptyList();

创建单元素集合

Collections提供了一系列方法来创建一个单元素集合:

  • 创建一个元素的List:List singletonList(T o)
  • 创建一个元素的Map:Map<K, V> singletonMap(K key, V value)
  • 创建一个元素的Set:Set singleton(T o)

要注意到返回的单元素集合也是不可变集合,无法向其中添加或删除元素。

此外,也可以用各个集合接口提供的of(T…)方法创建单元素集合。例如,以下创建单元素List的两个方法是等价的:

List<String> list1 = List.of("apple");
List<String> list2 = Collections.singletonList("apple");

排序

Collections是集合的工具类,其中自然就有用于排序的sort()方法

方法定义为:
void sort(List< T > list)

该方法的作用是对给定的集合元素进行自然排序

public void testSort(){
	List<Integer> list = new ArrayList<Integer>();
	Random r = new Random(1);
	for(int i = 0; i < 10; i++){
		list.add(r.nextInt(100));
	}
	System.out.println(r.nextInt(100));
	Collections.sort(list);
	System.out.println(list);
}

洗牌

Collections提供了洗牌算法,即传入一个有序的List,可以随机打乱List内部元素的顺序,效果相当于让计算机洗牌:

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i=0; i<10; i++) {
            list.add(i);
        }
        // 洗牌前:
        System.out.println(list);
        Collections.shuffle(list);
        // 洗牌后:
        System.out.println(list);
    }
}

不可变集合

Collections还提供了一组方法把可变集合封装成不可变集合:

  • 封装成不可变List:List unmodifiableList(List<? extends T> list)
  • 封装成不可变Set:Set unmodifiableSet(Set<? extends T> set)
  • 封装成不可变Map:Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)

这种封装实际上是通过创建一个代理对象,拦截掉所有修改方法实现的。我们来看看效果:

public class Main {
    public static void main(String[] args) {
        List<String> mutable = new ArrayList<>();
        mutable.add("apple");
        mutable.add("pear");
        // 变为不可变集合:
        List<String> immutable = Collections.unmodifiableList(mutable);
        immutable.add("orange"); // UnsupportedOperationException!
    }
}

线程安全集合

ollections还提供了一组方法,可以把线程不安全的集合变为线程安全的集合:

  • 变为线程安全的List:List synchronizedList(List list)
  • 变为线程安全的Set:Set synchronizedSet(Set s)
  • 变为线程安全的Map:Map<K,V> synchronizedMap(Map<K,V> m)

写了好久,也看了很多大佬的博客,因为集合算是Java当做的一大块知识了,复习起来也慢,知识点太多,我也不敢说我全部都懂了,肯定还有我没接触到的。接下来的I/O,线程等都是重点难点,所以我不会很快就更新,我得自己搞透彻了才敢放出来。最后祝所有程序猿,节日快乐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值