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

it2023-05-30  76

集合详解

什么是集合为什么要引入集合Collection使用ListList的特点遍历ListList和Array转换编写equals方法 使用Mapput()方法get()方法containsKey()方法编写equalsHash表原理hashCode方法重写hashCode方法装载因子及HashMap优化Map的遍历EnumMapTreeMap Queue和DequeQueueDeque StackStack的作用 Collections创建空集合创建单元素集合排序洗牌不可变集合线程安全集合

什么是集合

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

为什么要引入集合

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

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

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

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

在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,线程等都是重点难点,所以我不会很快就更新,我得自己搞透彻了才敢放出来。最后祝所有程序猿,节日快乐

最新回复(0)