目录
1. Java集合框架概述
1.1 什么是集合
一方面,面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array存储对象方面具有一些弊端,而Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中。
集合框架,设计好了大量的好用的数据结构:
线性表:数组、栈、队列、链表,哈希表,树,图:多维结构,矩阵
集合、数组都是对多个数据进行存储操作的结构,简称Java容器。
说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)
数组在内存存储方面的特点:
-
数组初始化以后,长度就确定了。
-
数组声明的类型,就决定了进行元素初始化时的类型
数组在存储数据方面的弊端:
-
数组初始化以后,长度就不可变了,不便于扩展
-
数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。 同时无法直接获取存储元素的个数
-
数组存储的数据是有序的、可以重复的。====>存储数据的特点单一
Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。解决数组存储数据方面的弊端。
集合的使用场景
应用在各种应用的数据列表中,如下图,等等
在Android客户端,将JSON对象或JSON数组 转换为Java对象或Java对 象构成的List
在服务器端,将Java对象或Java对象构 成的List转换为JSON对象 或JSON数组,
在数据库端拿到的是List对象
集合貌似看起来比较强大,它啥时用呢?当当对象多的时候,先进行存储。
1.2 集合框架的由来
集合本身是一个工具,它存放在java.util包中。
JDK最早的1.0版本中。提供的集合容器很少。升级到1.2版,为了更多的需求,出现了集合框架。有了更多的容器。可以完成不同的需求。
这些容器怎么区分?区分的方式:每一个容器的数据结构(数据存储的一种方式)不一样。
Java 集合框架图:
从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有ArrayList 、LinkedList、HashSet、LinkedHashSet、HashMap 、LinkedHashMap 等等。
集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:
-
接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象
-
实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
-
算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。
集合框架体系如图所示:
不同的线性容器进行不断的向上抽取,最后形成了一个集合框架,这个框架就是Collection接口。在 Collection接口定义着集合框架中最最共性的内容。在学习时:我们需要看最顶层怎么用, 创建底层对象即可。因为底层继承了父类中的所有功能。
Collection接口继承树:
Map接口继承树
2. Collection接口常用功能
Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List) 实现。
Collection接口:单列数据,定义了存取一组对象的方法的集合
-
List:元素有序、可重复的集合,“动态”数组 --- 线性表
-
ArrayList、LinkedList、Vector
-
-
Queue:非连续、非顺序的存储结构 --- 队列
-
Set:元素无序、不可重复的集合 --- 哈希表
-
HashSet、TreeSet、LinkedHashSet
-
在 JDK 5.0 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。
2.1 Collection接口方法
1、添加
- add(Object obj)
- addAll(Collection coll)
2、获取有效元素的个数
- int size()
3、清空集合
- void clear()
4、是否是空集合
- boolean isEmpty()
5、是否包含某个元素
- boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
- boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
6、删除
- boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
- boolean removeAll(Collection coll):取当前集合的差集
7、取两个集合的交集
- boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
8、集合是否相等
- boolean equals(Object obj)
9、转成对象数组
- Object[] toArray()
10、获取集合对象的哈希值
- hashCode()
11、遍历
- iterator():返回迭代器对象,用于集合遍历
package day19.exer;
import java.util.*;
public class CollectionTest {
// 压制警告
@SuppressWarnings({"rawtypes", "unchecked"})
public static void main(String[] args) {
// Collection是一个接口,不能创建实例
Collection coll = new ArrayList();
Collection coll1 = new ArrayList(2);
coll1.add(12);
coll1.add(21);
// 添加一个元素
coll.add("AA");
coll.add(1000); // 自动装箱
coll.add(new Date());
// 添加另一个集合的元素
coll.addAll(coll1);
System.out.println(coll); // [AA, 123, Fri Aug 05 22:41:50 GMT+08:00 2022, 12, 21]
// 获取有效元素的个数
System.out.println(coll.size()); // 5
// 清空集合
coll1.clear();
System.out.println(coll1); // []
// 是否是空集合
System.out.println(coll1.isEmpty()); // true
// 是否包含某个元素
System.out.println(coll.contains(new String("AA")));
// true, 这里包含的原因是调用String中的equals, 比较两个字符串的值是否相等
coll.add(new Person("小王", 12));
System.out.println(coll.contains(new Person("小王", 12)));
// false, 这里不包含的原因是调用元素的equals方法,比较的两个的内存地址
System.out.println(coll.containsAll(coll1)); // true, 空集合默认包含在另一个集合中
coll1.add(111);
System.out.println(coll.containsAll(coll1)); // false
// 删除元素
coll.add("AA");
System.out.println(coll); // [AA, 1000, Fri Aug 05 23:16:39 GMT+08:00 2022, 12, 21, day19.exer.Person@16795f1, AA]
coll.remove("AA");
System.out.println(coll); // [1000, Fri Aug 05 23:16:39 GMT+08:00 2022, 12, 21, day19.exer.Person@16795f1, AA]
Collection coll2 = new ArrayList(2);
coll2.add(1000);
coll2.add("AA");
coll.removeAll(coll2);
System.out.println(coll); // [Fri Aug 05 23:16:39 GMT+08:00 2022, 12, 21, day19.exer.Person@16795f1]
// 取两个集合的交集
coll.add(111);
System.out.println(coll); // [Fri Aug 05 23:24:44 GMT+08:00 2022, 12, 21, day19.exer.Person@16795f1, 111]
coll1.add(222);
System.out.println(coll1); // [111, 222]
System.out.println(coll.retainAll(coll1)); // true
System.out.println(coll); // [111], retainAll把交集的结果存在当前集合coll中,不影响coll1
System.out.println(coll1); // [111, 222]
// 集合是否相等
System.out.println(coll.equals(coll1)); // false
coll.add(222);
System.out.println(coll); // [111, 222]
System.out.println(coll.equals(coll1)); // true
// 转成对象数组
Object[] objArr = coll2.toArray();
System.out.println(Arrays.toString(objArr)); // [1000, AA]
// 获取集合对象的哈希值
System.out.println(coll.hashCode()); // 4624
// 遍历
// foreach
for (Object o : coll) {
System.out.print(o + ", "); // 111, 222,
}
// 迭代器遍历迭代数据,foreach底层就是使用迭代器实现的
Iterator iterator = coll.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + ", "); // 111, 222,
}
}
}
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() {
}
@Override
public boolean equals(Object o) {
System.out.println(o + " 调用Person对象的equals方法");
return this == o;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
集合的使用细节:
-
集合中,不能保存基本数据类型!如果一定要使用基本数据类型,但是jdk1.5以后可以这么写,则需要使用它们的包装类,所以存储的还是对象(基本数据类型包装类对象)。
-
集合中存储其实都是对象的地址。
-
删除基本数据类型,需要手动装箱,否则,int默认是下标,而不是数据本书
-
向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()
-
存储时提升了Object。取出时要使用元素的特有内容,必须向下转型。
2.2 Iterator迭代器接口
Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素
GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元 素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公 交车上的售票员”、“火车上的乘务员”、“空姐”。
Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所 有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了 Iterator接口的对象。
Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。
集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合 的第一个元素之前。
Iterator迭代器接口中的方法:
-
boolean hasNext():判断Iterator中是否存在下一个元素
-
E next():返回Iterator中的下一个元素
-
void remove():从基础集合中移除迭代器返回的最后一个元素(可选操作)。
2.2.1 遍历iterater中元素
//hasNext():判断是否还有下一个元素
while(iterator.hasNext()){
//next():①指针下移 ②将下移以后集合位置上的元素返回
System.out.println(iterator.next());
}
在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。
注意:
-
Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方 法,不是集合对象的remove方法。
-
如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法, 再调用remove都会报IllegalStateException。
2.2.3 使用 foreach 循环遍历集合元素
Java 5.0 提供了 foreach 循环迭代访问 Collection和数组。
遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
遍历集合的底层调用Iterator完成操作。
foreach还可以用来遍历数组。
语法格式:元素类型
// for (要遍历的元素类型 自定义的元素名称 : 要遍历的结构对象名称)
for (Object o : coll) {
System.out.print(o);
}
3. Collection子接口之一:List接口
鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引
List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector
3.1 List接口接口常用功能
一个有序集合(也被称为序列)。此接口的用户在列表中的每个元素都被插入的地方有精确的控 制。用户可以通过它们的整数索引(在列表中的位置)访问元素,并在列表中搜索元素。 与set不同的 是,列表通常允许重复元素。
3.1.1 List方法
List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
-
void add(int index, Object ele):在index位置插入ele元素
-
boolean addAll(int index, Collection eles):从index位置开始将eles中 的所有元素添加进来
-
Object get(int index):获取指定index位置的元素
-
int indexOf(Object obj):返回obj在集合中首次出现的位置
-
int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
-
Object remove(int index):移除指定index位置的元素,并返回此元素
-
Object set(int index, Object ele):设置指定index位置的元素为ele
-
List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex 位置的子集合
public class ListText {
public static void main(String[] args) {
List l1 = new LinkedList();
List l2 = new Vector();
List l3 = new ArrayList();
l3.add(",happy ");
l3.add("new ");
l3.add("years !");
l3.add(0,"马克");
Iterator it1 = l3.iterator();
while (it1.hasNext()){
Object obj1 = it1.next();
System.out.print(obj1);
}
System.out.println();
System.out.println(l3.get(0)+"?");
l3.add(",happy ");
System.out.println(l3.indexOf(",happy "));
System.out.println(l3.lastIndexOf(",happy "));
l3.remove(4); //删除下标为4的元素
Iterator it2 = l3.iterator();
while (it2.hasNext()){
Object obj2 = it2.next();
System.out.print(obj2);
}
System.out.println();
l3.set(3,"years!!");
Iterator it3 = l3.iterator();
while (it3.hasNext()){
Object obj3 = it3.next();
System.out.print(obj3);
}
}
}
3.1.2 ListIterator 介绍
在迭代过程中,使用了集合的方法对元素进行操作。导致迭代器并不知道集合中的变化,容易引发 数据的不确定性。
解决:在迭代时,不要使用集合的方法操作元素。那么想要在迭代时对元素操作咋办?可以使用迭 代器的方法操作。可是很遗憾:迭代器Iterator的方式只有 hasNext() ,next(),remove();Iterator有一个子接 口ListIterator可以完成该问题的解决。如何获取该子接口对象呢?通过List接口中的listIterator()就可以获取。
注意:该列表迭代器只有List接口有。而且这个迭代器可以完成在迭代过程中的增删改查动 作。
3.2 List实现类之一:ArrayList
ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。ArrayList 继承了 AbstractList ,并实现了 List 接口。
ArrayList 类位于 java.util 包中,使用前需要引入它,语法格式如下:
import java.util.ArrayList; // 引入 ArrayList 类
ArrayList<E> objectName =new ArrayList<>(); // 初始化
使用 Java 5 之前的集合的一个主要问题是编译器允许你向集合中插入不正确的类型,早期的Java使用Object来代表任意类型的,但是向下转型有强转的问题,这样就不太好了,程序不太安全。
Java泛型设计原则:在编译期防止将错误类型的对象放置到集合中,运行时期就不会出现ClassCastException异常。
细节:在 Java 7 之前,必须要在两端都进行类型声明
ArrayList ArrayList apples = new ArrayList();
泛型小结:有了泛型,代码就会更加简洁不用强制转换、程序更加健壮、可读性和稳定性。
- E: 泛型数据类型,用于设置 objectName 的数据类型,只能为引用数据类型。
- 对象名称: 对象名。
ArrayList 是 List 接口的典型实现类、主要实现类;是线程不安全的,效率高;底层使用Object[] elementData存储
本质上,ArrayList是对象引用的一个”变长”数组
ArrayList的JDK1.8之前与之后的实现区别?
-
JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
-
JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元 素时再创建一个始容量为10的数组
Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实例。
Arrays.asList(…) 返回值是一个固定长度的 List 集合
List与数组间的转换:
// 集合 --->数组:toArray()
Object[] arr = coll.toArray();
for(int i = 0;i < arr.length;i++){
System.out.println(arr[i]);
}
// 拓展:数组 --->集合:调用Arrays类的静态方法asList(T ... t)
List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
System.out.println(list);
List arr1 = Arrays.asList(new int[]{123, 456});
System.out.println(arr1.size());//1
List arr2 = Arrays.asList(new Integer[]{123, 456});
System.out.println(arr2.size());//2
常用方法:
-
增:add(Object obj)
-
删:remove(int index) / remove(Object obj)
-
改:set(int index, Object ele)
-
查:get(int index)
-
插:add(int index, Object ele)
-
长度:size()
-
遍历:① Iterator迭代器方式;② 增强for循环;③ 普通的循环
List list = new ArrayList();
list.add("AA");
list.add(123); // 存储基本数据时,会自动装箱
list.add(1);
list.add(2);
list.add(0, "BB");
list.add(1, "呵呵呵");
System.out.println(list);
System.out.println(list.get(0));
System.out.println(list.get(1));
System.out.println(list.remove("AA"));
System.out.println(list);
// 更新值
System.out.println(list.set(0, "test"));
System.out.println(list);
// 注意:删除基本数据类型,需要手动装箱
// 否则,int默认是下标,而不是数据本书
System.out.println(list.remove(Integer.valueOf(2)));
System.out.println(list);
System.out.println(list.contains("呵呵呵"));
// 使用泛型,表示nums只能存储字符串
List<String> nums = new ArrayList<>();
nums.add("张三");
nums.add("李四");
nums.add("王五");
nums.add("赵六");
// System.out.println(nums.get(0));
// 遍历list中的数据
// for (int i = 0; i < nums.size(); i++) {
// System.out.println(nums.get(i));
// }
// foreach
// for (String str : nums) {
// System.out.println(str);
// }
// 迭代器遍历迭代数据,foreach底层就是使用迭代器实现的
// Iterator<String> it = nums.iterator();
// while(it.hasNext()) {
// System.out.println(it.next());
// }
3.3 List实现类之二:LinkedList
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。
对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高,底层使用双向链表存储
3.3.1 LinkedList新增方法
-
void addFirst(Object obj)
-
void addLast(Object obj)
-
Object getFirst()
-
Object getLast()
-
Object removeFirst()
-
Object rem oveLast()
-
default void forEach(Consumer<? super T> action) :
Consumer<T>
接口是被@FunctionalInterface
注解声明的,即函数式接口,当然首先是一个接口,然后就是在这个接口里面只能有一个抽象方法。它们主要用在Lambda表达式和方法引用(实际上也可认为是Lambda表达式)上。
@FunctionalInterface
public interface Person {
void sayHello(String msg);
}
那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):
Person personSay = message -> System.out.println("Hello " + message);
所以我们可以使用forEach遍历列表
public class LinkedListTest {
@Test
void test1() {
List<Integer> arrList = new ArrayList<Integer>();
arrList.add(1);
arrList.add(2);
arrList.add(3);
List<Integer> linkList = new LinkedList<Integer>();
linkList.add(111);
linkList.add(222);
linkList.add(333);
linkList.add(0, 0);
System.out.println(linkList);
linkList.addAll(arrList);
// forEach使用Consumer实现内部类
linkList.forEach(new MyConsumer());
System.out.println();
// forEach使用匿名内部类
linkList.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer t) {
System.out.print(t + " ");
}
});
System.out.println();
// JDK 8 Lambda表达式遍历列表
linkList.forEach(System.out::print);
// 就是把你遍历出来的每一个对象都用来去调用System.out(也就是PrintStream类的一个实例)的println方法。
System.out.println();
linkList.forEach((Integer t) -> System.out.print(t + " "));
System.out.println();
linkList.forEach((t) -> {System.out.print(t + " ");});
System.out.println();
// 上面那种写法是下面这种的一种缩写形式。
linkList.forEach(t -> System.out.print(t + " "));
System.out.println();
}
}
// 使用内部类实现遍历列表
class MyConsumer implements Consumer<Integer>{
@Override
public void accept(Integer t) {
System.out.print(t + " ");
}
}
3.3.2 LinkedList的底层实现原理
LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的first和last, 用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:
-
prev变量记录前一个元素的位置
-
next变量记录下一个元素的位置
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;
}
}
List 接口的链接列表实现。实现所有可选的列表操作,并且允许所有元素(包括 null )。
除了实现 List 接口外, LinkedList 类还为在列表的开头及结尾 get 、 remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列。
所有操作都是按照双重链接列表的需要执行的。在列表中编索引的操作将从开头或结尾遍历列表 (从靠近指定索引的一端)。此实现不是同步的(即线程不安全的)
3.3.3 LinkedList用作双端队列
-
void addFirst(E e) :将指定元素插入此列表的开头。
-
void addLast(E e) :将指定元素添加到此列表的结尾。
-
E removeFirst() :移除并返回此列表的第一个元素。
-
E removeLast() :移除并返回此列表的最后一个元素。
-
E getFirst() :返回此列表的第一个元素。
-
E getLast() :返回此列表的最后一个元素。
-
boolean offerFirst(E e):在此列表的开头插入指定的元素。
-
boolean offerLast(E e):在此列表的末尾插入指定的元素。
-
E pollFirst() :获取并移除此列表的第一个元素;如果此列表为空,则返回 null 。
-
E pollLast() :获取并移除此列表的最后一个元素;如果此列表为空,则返回 null 。
-
E peekFirst() :获取但不移除此列表的第一个元素;如果此列表为空,则返回 null 。
-
E peekLast() :获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null 。
@Test
void test4() {
LinkedList<Integer> nums = new LinkedList<Integer>();
// LinkedList作为一种双端对列
// 加入元素
nums.addFirst(1); //无返回值
nums.addLast(2);
System.out.println(nums.offerFirst(3)); // true
System.out.println(nums.offerLast(4)); // true
System.out.println(nums);
// 移除元素
System.out.println(nums.removeFirst()); // 3
System.out.println(nums.removeFirst()); // 1
System.out.println(nums.pollFirst()); // 4
System.out.println(nums.pollLast()); // 2
// System.out.println(nums.removeFirst()); // 空了之后,抛NoSuchElemnetException
// System.out.println(nums.removeLast()); // 空了之后,抛NoSuchElemnetException
System.out.println(nums.pollFirst()); // null
System.out.println(nums.pollLast()); // null
// 获取元素
// System.out.println(nums.getFirst()); // 空了之后,抛NoSuchElemnetException
// System.out.println(nums.getLast()); // 空了之后,抛NoSuchElemnetException
System.out.println(nums.peekFirst()); // null
System.out.println(nums.peekLast()); // null
nums.addFirst(1);
System.out.println(nums.getFirst()); // 1
System.out.println(nums.getLast()); // 1
System.out.println(nums.peekFirst()); // 1
System.out.println(nums.peekLast()); // 1
}
3.3.4 LinkedList用作队列
-
boolean offerLast(E e):将指定元素添加到此列表的末尾(最后一个元素)。
-
E poll() :获取并移除此列表的头(第一个元素)。
-
E element() :获取但不移除此列表的头(第一个元素)。
@Test
void testLinkedListAsQueue() {
LinkedList<Integer> nums = new LinkedList<>();
// LinkedList不仅仅是一个简单的双向链表
// 同时也是一种队列;
// FIFO 先进先出的队列
// 添加到队列中
nums.offer(1230);
nums.offer(456);
nums.offer(789);
nums.offer(852);
nums.offer(147);
System.out.println(nums);
// 出队列
System.out.println(nums.poll());
System.out.println(nums.poll());
System.out.println(nums.poll());
// 看一看第一个队列顶部元素是什么
System.out.println(nums.element());
System.out.println(nums.size());
}
3.3.5 LinkedList用作栈
-
void push(E e) :将元素推入此列表所表示的堆栈。
-
E pop() :从此列表所表示的堆栈处弹出一个元素。
-
E peek() :获取但不移除此列表的头(第一个元素)。
@Test
void test3() {
LinkedList<Integer> nums = new LinkedList<Integer>();
// LinkedList不仅仅是一个简单的双向链表
// 同时也是一种栈
// FILO 先进先出的队列
// 推入栈中
nums.push(1);
nums.push(2);
nums.push(3);
nums.push(4);
System.out.println(nums); // [4, 3, 2, 1]
// 查看栈顶元素
System.out.println(nums.peek()); // 4
// 弹出栈
System.out.println(nums.pop()); // 4
System.out.println(nums.pop()); // 3
System.out.println(nums.pop()); // 2
}
3.4 List 实现类之三:Vector
Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList 相同,区别之处在于Vector是线程安全的。
在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时, 使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。
Vector 类可以实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件。但是, Vector 的大小可以根据需要增大或缩小,以适应创建 Vector 后进行添加或移除项的操作。 一般被 ArrayList 替代。
3.4.1 Vector新增方法
-
void addElement(Object obj)
-
void insertElementAt(Object obj,int index)
-
void setElementAt(Object obj,int index)
-
void removeElement(Object obj)
-
void removeAllElements()
面试题:请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层 是什么?扩容机制?Vector和ArrayList的最大区别?
解答详见文章:Java集合面试题整理
3.4.2 Vector的子类Stack
将向量视为堆栈。它提供了通常的 push 和 pop 操作,以及取堆栈顶点的 peek 方法、测试堆栈 是否为空的 empty 方法、在堆栈中查找项并确定到堆栈顶距离的 search 方法。 一般被 LinkedList 替代。
4. Collection子接口之二:Queue接口
4.1 Queue接口常用功能
除了基本的 Collection 操作外,队列还提供其他的插入、提取和检查操作。每个方法都存在 两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值( null 或 false ,具体取决于操作)。插入操作的后一种形式是用于专门为有容量限制的 Queue 实现设计的;在大多数实现中, 插入操作不会失败。
操作 | 抛出异常 | 返回特殊值 |
---|---|---|
插入 | add(e) | offer(e) |
移除 | remove() | poll() |
检查 | element() | peek() |
队列通常(但并非一定)以 FIFO(先进先出)的方式排序各个元素。不过优先级队列和 LIFO 队列 (或堆栈)例外,前者根据提供的比较器或元素的自然顺序对元素进行排序,后者按 LIFO(后进先 出)的方式对元素进行排序。无论使用哪种排序方式,队列的头 都是调用 remove() 或 poll() 所移除的元素。在 FIFO 队列中,所有的新元素都插入队列的末尾 。其他种类的队列可能使用不同的元 素放置规则。每个 Queue 实现必须指定其顺序属性。
Queue 实现通常不允许插入 null 元素,尽管某些实现(如 LinkedList )并不禁止插入 null 。
Queue 接口并未定义阻塞队列的方法。
4.2 Deque双端队列(循环队列)
一个线性 collection,支持在两端插入和移除元素。名称 deque 是“double ended queue(双端队列)”的缩写,通常读为“deck”。大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。
此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值( null 或 false ,具体取决于操作)。插入操作的后一种形式是专为使用有容量限制的 Deque 实现设计的;在大多数实现 中,插入操作不能失败。
第一个元素(头部) | 最后一个元素(尾部) | |||
---|---|---|---|---|
抛出异常 | 特殊值 | 抛出异常 | 特殊值 | |
插入 | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
移除 | removeFirst() | pollFirst() | removeLast() | pollLast() |
检查 | getFirst() | peekFirst() | getLast() | peekLast() |
此接口扩展了 Queue 接口。在将双端队列用作队列时,将得到 FIFO(先进先出)行为。将元素 添加到双端队列的末尾,从双端队列的开头移除元素。从 Queue 接口继承的方法完全等效于 Deque 方法,如下表所示:
Queue 方法 | 等效 Deque 方法 |
---|---|
add(e) | addLast(e) |
offer(e) | offerLast(e) |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
双端队列也可用作 LIFO(后进先出)堆栈。应优先使用此接口而不是遗留 Stack 类。在将双端 队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈方法完全等效于 Deque 方法,如下表所示:
堆栈方法 | 等效 Deque 方法 |
---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
与 List 接口不同,此接口不支持通过索引访问元素。
虽然 Deque 实现没有严格要求禁止插入 null 元素,但建议最好这样做。建议任何事实上允许 null 元素的 Deque 实现用户最好不 要利用插入 null 的功能。这是因为各种方法会将 null 用作特殊的 返回值来指示双端队列为空。
4.3 Queue实现子类
-
LinkedList
-
ArrayDeque
Deque 接口是大小可变数组的实现。 数组双端队列没有容量限制;它们可根据需要增加以支持使用。 禁止 null 元素。此类很可能在用作堆栈时快于 Stack ,在用作队列时快于 LinkedList 。
此实现不是同步的,即非线程安全的。
@Test
void testQueue() {
Queue<String> set = new ArrayDeque<>();
set.add("A");
set.add("D");
set.add("F");
set.add("Bs");
set.add("G");
set.add("E");
set.add("Bj");
set.add("C");
System.out.println(set); // [A, D, F, Bs, G, E, Bj, C]
System.out.println(set.poll()); // A
System.out.println(set.poll()); // D
System.out.println(set.poll()); // F
System.out.println(set.poll()); // Bs
System.out.println(set.poll()); // G
System.out.println(set.poll()); // E
System.out.println(set.poll()); // Bj
System.out.println(set.poll()); // C
System.out.println(set.poll()); // null
}
4.3.1 Queue实现子类-PriorityQueue
一个基于优先级堆的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序。
优先级队列不允许使用 null 元素。依靠自然顺序的优先级队列还不允许插入不可比较的对象。
此队列的头是按指定排序方式确定的最小元素。如果多个元素都是最小值,则头是其中一个元素——选择方法是任意的。队列获取操作 poll 、 remove 、 peek 和 element 访问处于队列头的元素。
优先级队列是无界的,但是有一个内部容量 ,控制着用于存储队列元素的数组大小。它通常至少 等于队列的大小。随着不断向优先级队列添加元素,其容量会自动增加。
此实现不是同步的,非线程安全的。
1. 往PriorityQueue 中添加Java已有的类的对象如String、Integer等
@Test
void testPriorityQueue1() {
Queue<String> set = new PriorityQueue<>();
set.add("A");
set.add("D");
set.add("F");
set.add("Bs");
set.add("G");
set.add("E");
set.add("Bj");
set.add("C");
System.out.println(set); // [A, Bs, Bj, C, G, F, E, D]
System.out.println(set.poll()); // A
System.out.println(set.poll()); // Bj
System.out.println(set.poll()); // Bs
System.out.println(set.poll()); // C
System.out.println(set.poll()); // D
System.out.println(set.poll()); // E
System.out.println(set.poll()); // F
System.out.println(set.poll()); // G
System.out.println(set.poll()); // null
}
@Test
void testPriorityQueue2() {
Queue<Integer> set = new PriorityQueue<>();
set.add(123);
set.add(456);
set.add(789);
set.add(1);
set.add(13);
set.add(258);
set.add(10);
System.out.println(set); // [1, 13, 10, 456, 123, 789, 258]
System.out.println(set.poll()); // A
System.out.println(set.poll()); // D
System.out.println(set.poll()); // F
System.out.println(set.poll()); // Bs
System.out.println(set.poll()); // G
System.out.println(set.poll()); // E
System.out.println(set.poll()); // Bj
System.out.println(set.poll()); // C
System.out.println(set.poll()); // null
}
2. 往PriorityQueue 中添加自己声明的类的对象如下面例子中的Person:
package com.openlab.lenrn.entity;
public class Person implements Comparable<Person> {
private int id;
private String name;
private String nickname;
private String gender;
private int age;
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", nickname=" + nickname + ", gender=" + gender + ", age=" + age
+ "]";
}
public Person(int id, String name, String nickname, String gender, int age) {
super();
this.id = id;
this.name = name;
this.nickname = nickname;
this.gender = gender;
this.age = age;
}
public Person() {
super();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(Person o) {
// 排序规则:id的升序排列
// return this.id - o.id;
// 按age的升序排列
return o.getAge() - this.getAge();
}
}
@Test
void testPriorityQueue3() {
Queue<Person> nums = new PriorityQueue<>();
nums.offer(new Person(1, "cc", "曹孟德", "男", 32));
nums.offer(new Person(2, "lb", "刘玄德", "男", 28));
nums.offer(new Person(3, "sq", "孙百万", "男", 18));
nums.offer(new Person(4, "zf", "张翼德", "男", 24));
nums.offer(new Person(5, "gy", "关云长", "男", 26));
nums.offer(new Person(6, "hd", "夏侯惇", "男", 43));
nums.offer(new Person(7, "lb", "吕奉先", "男", 23));
nums.offer(new Person(8, "dc", "貂蝉", "男", 16));
System.out.println(nums);
System.out.println(nums.poll());
System.out.println(nums.poll());
System.out.println(nums.poll());
System.out.println(nums.poll());
System.out.println(nums.poll());
System.out.println(nums.poll());
System.out.println(nums.poll());
}
结果:
注意:我们在往PriorityQueue添加自己创建的类时需要实现Comparable接口,实现他的compareTo方法,定义比较的规则,否则会报ClassCastException异常,而在Java的内置类中其实是帮我们实现好了,所以我们可以直接使用,如String中:
5. Collection子接口之三:Set接口
学习Collection接口时,记得Collection中可以存放重复元素,也可以不存放重复元素,那么我们知 道List中是可以存放重复元素的。那么不重复元素给哪里存放呢?那就是Collection接口中的Set集合中的元素就是不重复的。
Set接口是Collection的子接口,Set接口没有提供额外的方法。
Set接口底层是使用哈希表结构的:无序、不重复的非线性结构,Set 的无序是指存入取出的顺序不一样 。而且方法和Collection一致。Set集合取出元素的方式只有一种:迭代器。
Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
Set集合有多个子类,这里我们介绍其中的HashSet、TreeSet和LinkedHashSet这三个集合。
HashSet:哈希表结构,不同步,保证元素唯一性的方式依赖于:hashCode(),equals()方法。查询速度快。 Set方法完全来自Collection接口。
5.1 什么是哈希表
哈希表底层使用的也是数组机制,数组中也存放对象,而这些对象往数组中存放时的位置比较特 殊,当需要把这些对象给数组中存放时,那么会根据这些对象的特有数据结合相应的算法,算法这个对 象在数组中的位置,然后把这个对象存放在数组中。而这样的数组就称为哈希数组,即就是哈希表。
当给哈希表中存放元素时,需要根据元素的特有数据结合相应的算法,这个算法其实就是Object中 的hashCode方法。由于任何对象都是Object类的子类,所以任何对象有拥有这个方法。即就是在给哈希表中存放对象时,会调用对象的hashCode方法,算出对象在表中的存放位置,这里需要注意,如果两个对象hashCode方法算出结果一样,这样现象称为哈希冲突,这时会调用对象的equals方法,比较这两个对象是不是同一个对象,如果equals方法返回的是true,那么就不会把第二个对象存放在哈希表中,如果返回的是false,就会把这个值存放在哈希表中。
总结:保证元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中 存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。
5.2 Set接口实现子类:HashSet
HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。 是一个存放链表的数组。 此类为基本操作提供了稳定性能,这些基本操作包括 add 、 remove 、 contains 和 size , 假定哈希函数将这些元素正确地分布在桶中。对此 set 进行迭代所需的时间与 HashSet 实例的大小(元素的数量)和底层 HashMap 实例(桶的数量)的“容量”的和成比例。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。 此实现不是同步的,非线程安全的。
HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除 性能。
HashSet 具有以下特点:
-
不能保证元素的排列顺序
-
HashSet 不是线程安全的
-
集合元素可以是 null
HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相 等,并且两个对象的 equals() 方法返回值也相等。
对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
HashSet:底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12) 就会扩大容量为原来的2倍。(16扩容为32,依次为64,128....等)
HashSet常用方法的使用
@Test
void testSet01() {
// HashSet 是java中的一个标准的哈希表
Collection<Integer> nums = new HashSet<>();
nums.add(123);
nums.add(456);
nums.add(789);
nums.add(1);
nums.add(13);
nums.add(258);
nums.add(10);
// add方法会返回一个boolean,表示是否添加成功
System.out.println(nums.add(31));
// 添加失败,返回false,表示之前已经存在了这个值
System.out.println(nums.add(31));
System.out.println(nums.contains(1));
System.out.println(nums.contains(11));
System.out.println(nums);
}
@Test
void testSet02() {
// HashSet 是java中的一个标准的哈希表
Collection<Integer> nums = new HashSet<>();
nums.add(123);
nums.add(456);
nums.add(789);
nums.add(1);
nums.add(13);
nums.add(258);
nums.add(10);
// 注意:哈希表是无序的,因此无法使用常规的循环,通过下标访问
// Iterator<Integer> it = nums.iterator();
// while (it.hasNext()) {
// System.out.println(it.next());
// }
// for (Integer i : nums) {
// System.out.println(i);
// }
// nums.forEach((i) ->System.out.println(i));
}
5.3 Set接口实现子类:LinkedHashSet
LinkedHashSet 是 HashSet 的子类
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。底层通过具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。
LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
LinkedHashSet 不允许集合元素重复
5.4 Set接口实现子类:TreeSet
TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
TreeSet底层使用二叉树 (红黑树) 结构存储数据,实现的一个集合,因此该集合是有序的
基于 TreeMap 的 NavigableSet 实现。使用元素的自然顺序对元素进行排序,或者根据创 建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。此实现不是同步的。
5.4.1 TreeSet类中新增方法
-
Comparator comparator():进行排序的比较器
-
Object first():返回此 set 中当前第一个(最低)元素。
-
Object last():返回此 set 中当前最后一个(最高)元素。
-
Object lower(Object e):返回此 set 中严格小于给定元素的最大元素;如果不存在这样的元素,则返回 null 。
-
Object higher(Object e):返回此 set 中严格大于给定元素的最小元素;如果不存在这样的元素,则返回 null 。
-
SortedSet subSet(fromElement, toElement)
-
SortedSet headSet(toElement)
-
SortedSet tailSet(fromElement)
-
E ceiling(E e) : 返回此 set 中大于等于给定元素的最小元素;如果不存在这样的元素,则返回 null 。
-
E floor(E e) :返回此 set 中小于等于给定元素的最大元素;如果不存在这样的元素,则返回 null 。
TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
TreeSet和后面要讲的TreeMap 采用红黑树的存储结构,特点:有序,查询速度比List快
5.4.2 自然排序
自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元 素之间的大小关系,然后将集合元素按升序(默认情况)排列
如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小
Comparable 的典型实现:
-
BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小 进行比较
-
Character:按字符的 unicode值来进行比较
-
Boolean:true 对应的包装类实例大于 false 对应的包装类实例
-
String:按字符串中字符的 unicode 值进行比较
-
Date、Time:后边的时间、日期比前面的时间、日期大
@Test
void testSet3() {
// 自然排序:元素所属的类实现Comparable接口
TreeSet<String> set = new TreeSet<>();
set.add("A");
set.add("D");
set.add("F");
set.add("Bs");
set.add("G");
set.add("E");
set.add("C");
System.out.println(set); // [A, Bs, C, D, E, F, G]
}
5.4.3 定制排序
TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照 其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来 实现。需要重写compare(T o1,T o2)方法。
@Test
void testSet4() {
// 定制排序:通过实现Comparator接口,将实现的Comparator接口作为参数传递给TreeSet
TreeSet<String> set = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.charAt(0) - o1.charAt(0);
}
});
set.add("A");
set.add("D");
set.add("F");
set.add("Bs");
set.add("G");
set.add("E");
set.add("C");
System.out.println(set); // [G, F, E, D, C, Bs, A]
}