文章目录
集合表示一组对象,每一个对象我们都称其为元素;不同的集合有着不同的性质,比如一些集合允许重复的元素,而另一些则不允许;
集合类是为了更好地组织、管理和操作我们的数据而存在的,包括列表、集合、队列、映射等数据结构
集合跟数组一样,可以表示同样的一组元素
相同
都是容器,都能容纳一组元素
不同
数组的大小是固定的,集合的大小是可变的
数组可以存放基本数据类型,集合只能存放对象
集合根接口
ArrayList 类,祖先是Collection接口
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("789");
}
}
这个接口定义了集合类的一些基本操作
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
//查询当前集合中是否包含某个元素
boolean contains(Object o);
//返回当前集合的迭代器
Iterator<E> iterator();
//将集合转换为数组的形式
Object[] toArray();
//支持泛型的数据转换
<T> T[] toArray(T[] a);
//并不一定会添加成功
boolean add(E e);
//移除某个元素,移除成功返回true,不成功返回false
boolean remove(Object o);
//查询当前集合是否包含给定集合中所有的元素
//从数学角度来说,就是看给定集合是不是当前集合的子集
boolean containsAll(Collection<?> c);
//添加给定集合中所有的元素
//从数学角度来说,就是将当前集合变成当前集合与给定集合的并集
//添加成功返回true,否则返回false
boolean addAll(Collection<? extends E> c);
//移除给定集合中出现的所有元素,如果某个元素在当前集合中不存在,那么忽略这个元素
//从数学角度来说,就是求当前集合与给定集合的差集
//移除成功返回true,否则false
boolean removeAll(Collection<?> c);
//Java8新增方法,根据给定的Predicate条件进行元素移除操作
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator(); //这里用到了迭代器,我们会在后面进行介绍
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
//只保留当前集合中在给定集合中出现的元素,其他元素一律移除
//从数学角度来说,就是求当前集合与给定集合的交集
//移除成功返回true,否则false
boolean retainAll(Collection<?> c);
//清空整个集合,删除所有元素
void clear();
//-------这些是比较以及哈希计算相关的操作----------
//判断两个集合是否相等
boolean equals(Object o);
//计算当前整个集合对象的哈希值
int hashCode();
//与迭代器作用相同,但是是并行执行的,我们会在下一章多线程部分中进行介绍
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
//生成当前集合的流,我们会在后面进行讲解
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
//生成当前集合的并行流,我们会在下一章多线程部分中进行介绍
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
————————————————
版权声明:本文为CSDN博主「青空の霞光」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_25928447/article/details/127161387
}
List列表(线性表)
线性表支持随机访问,相比之前的Collection接口定义,功能会更多一点,实现自List接口
底层是由数组实现的,内部维护的是一个可动态进行扩容的数组;
List是集合类型的一个分支。他的主要特性有:
- 是一个有序的集合,插入元素是插入到尾部的,按顺序从前往后放,每个元素都有一个自己的下标位置
- 列表中允许存在重复元素
在List接口里,定义了列表类型需要支持的全部操作,List直接继承自前面的Collection接口,很多地方重新定义了Collection接口中定义的方法,从而更加明确方法的具体功能
//List是一个有序的集合类,每个元素都有一个自己的下标位置
//List中可插入重复元素
//针对于这些特性,扩展了Collection接口中一些额外的操作
public interface List<E> extends Collection<E> {
...
//将给定集合中所有元素插入到当前结合的给定位置上(后面的元素就被挤到后面去了,跟我们之前顺序表的插入是一样的)
boolean addAll(int index, Collection<? extends E> c);
...
//Java 8新增方法,可以对列表中每个元素都进行处理,并将元素替换为处理之后的结果
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator(); //这里同样用到了迭代器
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
//对当前集合按照给定的规则进行排序操作,这里同样只需要一个Comparator就行了
@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
...
//-------- 这些是List中独特的位置直接访问操作 --------
//获取对应下标位置上的元素
E get(int index);
//直接将对应位置上的元素替换为给定元素
E set(int index, E element);
//在指定位置上插入元素,就跟我们之前的顺序表插入是一样的
void add(int index, E element);
//移除指定位置上的元素
E remove(int index);
//------- 这些是List中独特的搜索操作 -------
//查询某个元素在当前列表中的第一次出现的下标位置
int indexOf(Object o);
//查询某个元素在当前列表中的最后一次出现的下标位置
int lastIndexOf(Object o);
//------- 这些是List的专用迭代器 -------
//迭代器我们会在下一个部分讲解
ListIterator<E> listIterator();
//迭代器我们会在下一个部分讲解
ListIterator<E> listIterator(int index);
//------- 这些是List的特殊转换 -------
//返回当前集合在指定范围内的子集
List<E> subList(int fromIndex, int toIndex);
...
}
可以看到在List接口里,扩展了大量列表支持的操作,比如直接根据下标位置进行的增删改查操作。而在ArrayList中,底层就是采用数组实现的,跟之前顺序表的思路差不多
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//默认的数组容量
private static final int DEFAULT_CAPACITY = 10;
...
//存放数据的底层数组,这里的transient关键字我们会在后面I/O中介绍用途
transient Object[] elementData;
//记录当前数组元素数的
private int size;
//这是ArrayList的其中一个构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity]; //根据初始化大小,创建当前列表
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
...
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 这里会判断容量是否充足,不充足需要扩容
elementData[size++] = e;
return true;
}
...
//默认的列表最大长度为Integer.MAX_VALUE - 8
//JVM都C++实现中,在数组的对象头中有一个_length字段,用于记录数组的长
//度,所以这个8就是存了数组_length字段(这个只做了解就行)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容规则跟我们之前的是一样的,也是1.5倍
if (newCapacity - minCapacity < 0) //要是扩容之后的大小还没最小的大小大,那么直接扩容到最小的大小
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) //要是扩容之后比最大的大小还大,需要进行大小限制
newCapacity = hugeCapacity(minCapacity); //调整为限制的大小
elementData = Arrays.copyOf(elementData, newCapacity); //使用copyOf快速将内容拷贝到扩容后的新数组中并设定为新的elementData底层数组
}
}
如果我们要使用一个集合类,会使用接口的引用
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("89");
list.add("19");
System.out.println(list);
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10); //添加Integer的值10
list.remove((Integer) 10); //注意,不能直接用10,默认情况下会认为传入的是int类型值,删除的是下标为10的元素,我们这里要删除的是刚刚传入的值为10的Integer对象
System.out.println(list); //可以看到,此时元素成功被移除
}
//集合在删除元素的时候,只会调用equals()进行判断是否为指定元素,而不是进行等号判断。所以说,如果两个对象使用equals方法相等,那么集合中就是相同的两个对象
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(new Integer(10)); //添加的是一个对象
list.remove(new Integer(10)); //删除的是另一个对象
System.out.println(list);
}
ArrayList源码部分
public boolean remove(Object o) {
if (o == null) {
...
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) { //这里只是对两个对象进行equals判断
fastRemove(index);
return true; //只要判断成功,直接认为就是要删除的对象,删除就完事
}
}
return false;
}
列表中允许存在相同元素,所以说可以添加两个一模一样的,但是使用remove()
的时候,只会删除排在前面的第一个元素
public static void main(String[] args) {
List<String> list = new ArrayList<>();
String str = "哟唉嘛干你";
list.add(str);
list.add(str);
System.out.println(list);
}
集合类是支持嵌套使用的,一个集合当中可以存放多个集合
public static void main(String[] args) {
List<List<String>> list = new LinkedList<>();
list.add(new LinkedList<>());
System.out.println(list.get(0).isEmpty());
}
在Array工具类中,可以快速生成一个只读的List:
public static void main(String[] args) {
List<String> list = Arrays.asList("A","B","C");
System.out.println(list);
}
这个List是不能进行修改操作的,只能使用获取内容相关的方法,否则就会抛出UnsupportedOperationException
异常,要生成正常使用的,可以将这个作为只读的列表作为参数传入:
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("A","B","C"));
System.out.println(list);
}
也可以利用静态代码块
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {{ //使用匿名内部类(匿名内部类在Java8无法使用钻石运算符,但是之后的版本可以)
add("A");
add("B");
add("C");
}};
System.out.println(list);
}
双向链表,同时保存两个方向
LinkedList的使用和ArrayList的使用几乎相同,各项操作的结果也是一样的,在什么使用使用ArrayList和LinkedList,我们需要结合具体的场景来决定,尽可能的扬长避短。
只不过LinkedList不仅可以当做List来使用,也可以当做双端队列使用
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
//引用首结点
transient Node<E> first;
//引用尾结点
transient Node<E> last;
//构造方法,很简单,直接创建就行了
public 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 & Deque
LInkedList除了可以直接当作列表使用之外,还可以当作其他的数据结构使用,它不仅仅实现了List接口
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
先看看Deque的继承结构
先看看Queue接口,扩展了大量队列相关的操作
public interface Queue<E> extends Collection<E> {
//队列的添加操作,是在队尾进行插入(只不过List也是一样的,默认都是尾插)
//如果插入失败,会直接抛出异常
boolean add(E e);
//同样是添加操作,但是插入失败不会抛出异常
boolean offer(E e);
//移除队首元素,但是如果队列已经为空,那么会抛出异常
E remove();
//同样是移除队首元素,但是如果队列为空,会返回null
E poll();
//仅获取队首元素,不进行出队操作,但是如果队列已经为空,那么会抛出异常
E element();
//同样是仅获取队首元素,但是如果队列为空,会返回null
E peek();
}
我们可以直接将LinkedList当做一个队列来使用
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>(); //当做队列使用,还是很方便的
queue.offer("AAA");
queue.offer("BBB");
System.out.println(queue.poll());
System.out.println(queue.poll());
}
双端队列
普通队列中,从队尾入队,队首出队;然而双端队列允许在队列的两端进行入队和出队操作;利用这种特性,双端队列既可以当作普通队列进行使用,也可以当作栈来使用
Java中的定义
//在双端队列中,所有的操作都有分别对应队首和队尾的
public interface Deque<E> extends Queue<E> {
//在队首进行插入操作
void addFirst(E e);
//在队尾进行插入操作
void addLast(E e);
//不用多说了吧?
boolean offerFirst(E e);
boolean offerLast(E e);
//在队首进行移除操作
E removeFirst();
//在队尾进行移除操作
E removeLast();
//不用多说了吧?
E pollFirst();
E pollLast();
//获取队首元素
E getFirst();
//获取队尾元素
E getLast();
//不用多说了吧?
E peekFirst();
E peekLast();
//从队列中删除第一个出现的指定元素
boolean removeFirstOccurrence(Object o);
//从队列中删除最后一个出现的指定元素
boolean removeLastOccurrence(Object o);
// *** 队列中继承下来的方法操作是一样的,这里就不列出了 ***
...
// *** 栈相关操作已经帮助我们定义好了 ***
//将元素推向栈顶
void push(E e);
//将元素从栈顶出栈
E pop();
// *** 集合类中继承的方法这里也不多种介绍了 ***
...
//生成反向迭代器,这个迭代器也是单向的,但是是next方法是从后往前进行遍历的
Iterator<E> descendingIterator();
}
比如我们可以直接当作栈来使用
public static void main(String[] args) {
Deque<String> deque = new LinkedList<>();
deque.push("AAA");
deque.push("BBB");
System.out.println(deque.pop());
System.out.println(deque.pop());
}
再来测试一下反向迭代器和正向迭代器:
public static void main(String[] args) {
Deque<String> deque = new LinkedList<>();
deque.addLast("AAA");
deque.addLast("BBB");
Iterator<String< descendingIterator = deque.descendingIterator();
System.out.println(descendingIterator.next());
Iterator<String> iterator = deque.iterator();
System.out.println(iterator.next());
}
除了LinkedList实现了队列接口以外,还有其他但是不常用的实现类,做了解就行
public static void main(String[] args) {
Deque<String> deque = new ArrayDeque<>(); //数组实现的栈和队列
Queue<String> queue = new PriorityQueue<>(); //优先级队列
}
优先级队列
public static void main(String[] args) {
Queue<Integer> queue = new PriorityQueue<>();
queue.offer(10);
queue.offer(4);
queue.offer(5);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
只不过需要注意的是,优先级队列并不是队列中所有的元素都是按照优先级排放的,优先级队列只能保证出队顺序是按照优先级进行的,我们可以打印一下:
我们可以自定义比较,需要一个Comparator
的实现:
public static void main(String[] args) {
Queue<Integer> queue = new PriorityQueue<>((a, b) -> b - a); //按照从大到小顺序出队
queue.offer(10);
queue.offer(4);
queue.offer(5);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
Set集合
先看定义,会发现接口中的定义方法都是Collection中直接继承的,因此,Set支持的功能其实也就和Collection中定义的差不多,除:
- 不允许出现重复元素
- 不支持随机访问
public interface Set<E> extends Collection<E> {
// Set集合中基本都是从Collection直接继承过来的方法,只不过对这些方法有更加特殊的定义
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
//添加元素只有在当前Set集合中不存在此元素时才会成功,如果插入重复元素,那么会失败
boolean add(E e);
//这个同样是删除指定元素
boolean remove(Object o);
boolean containsAll(Collection<?> c);
//同样是只能插入那些不重复的元素
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
//这个方法我们同样会放到多线程中进行介绍
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
}
HashSet
源码
先认识下HashSet,先看底层
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map; //对,你没看错,底层直接用map来做事
// 因为Set只需要存储Key就行了,所以说这个对象当做每一个键值对的共享Value
private static final Object PRESENT = new Object();
//直接构造一个默认大小为16负载因子0.75的HashMap
public HashSet() {
map = new HashMap<>();
}
...
//你会发现所有的方法全是替身攻击
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
}
通过观察可以发现,HashSet几乎都在操作内部维护一个HashMap,也就是说HashSet只是一个表壳,而内部的灵魂在于HashMap;所以说,HashSet利用了HashMap内部的数据结构,轻松实现Set定义的全部功能
应用
由于底层是采用哈希表实现的,可以非常高效的从HashSet当中存取元素
public static void main(String[] args) {
Set<String> set = new HashSet<>();
System.out.println(set.add("AAA")); //这里我们连续插入两个同样的字符串
System.out.println(set.add("AAA"));
System.out.println(set); //可以看到,最后实际上只有一个成功插入了
}
在set接口中并没有定义支持指定下标位置访问的添加和删除操作,只能简单的删除对象
public static void main(String[] args) {
Set<String> set = new HashSet<>();
System.out.println(set.add("AAA"));
System.out.println(set.remove("AAA"));
System.out.println(set);
}
由于底层采用哈希表实现,所以说无法维持插入元素的顺序
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.addAll(Arrays.asList("A","0","-","+"));
System,out.println(set);
}
想要使用维持顺序的Set集合,可以使用LinkedHashSet
,它底层维护的不再是一个HashMap。而是LinkedHashMap
,它能够在插入数据的时候里用链表自动维护顺序,这样能保证插入顺序和最后的迭代顺序一致
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<>();
set.addAll(Arrays.asList("A", "0", "-", "+"));
System.out.println(set);
}
TreeSet
源码
看TreeSet的源码也可以知道,实际上我们用的是TreeMap
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
//底层需要一个NavigableMap,就是自动排序的Map
private transient NavigableMap<E,Object> m;
//不用我说了吧
private static final Object PRESENT = new Object();
...
//直接使用TreeMap解决问题
public TreeSet() {
this(new TreeMap<E,Object>());
}
...
}
他会在插入元素的时候就进行排序
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); //同样是一个Comparator
set.add(1);
set.add(3);
set.add(2);
System.out.println(set);
}
Map映射
映射指两个元素的之间相互“对应”的关系,也就是说,我们的元素之间是两两对应的,是以键值对的形式存在
而Map就是为了实现这种数据结构而存在的,我们通过保存键值对的形式来存储映射关系,就可以轻松地通过键找到对应的映射值,比如现在我们要保存很多学生的信息,而这些学生都有自己的ID,我们可以将其以映射的形式保存,将ID作为键,学生详细信息作为值,这样我们就可以通过学生的ID快速找到对应学生的信息了
在Map中,这些映射关系被存储为键值对,先看定义操作
//Map并不是Collection体系下的接口,而是单独的一个体系,因为操作特殊
//这里需要填写两个泛型参数,其中K就是键的类型,V就是值的类型,比如上面的学生信息,ID一般是int,那么键就是Integer类型的,而值就是学生信息,所以说值是学生对象类型的
public interface Map<K,V> {
//-------- 查询相关操作 --------
//获取当前存储的键值对数量
int size();
//是否为空
boolean isEmpty();
//查看Map中是否包含指定的键
boolean containsKey(Object key);
//查看Map中是否包含指定的值
boolean containsValue(Object value);
//通过给定的键,返回其映射的值
V get(Object key);
//-------- 修改相关操作 --------
//向Map中添加新的映射关系,也就是新的键值对
V put(K key, V value);
//根据给定的键,移除其映射关系,也就是移除对应的键值对
V remove(Object key);
//-------- 批量操作 --------
//将另一个Map中的所有键值对添加到当前Map中
void putAll(Map<? extends K, ? extends V> m);
//清空整个Map
void clear();
//-------- 其他视图操作 --------
//返回Map中存放的所有键,以Set形式返回
Set<K> keySet();
//返回Map中存放的所有值
Collection<V> values();
//返回所有的键值对,这里用的是内部类Entry在表示
Set<Map.Entry<K, V>> entrySet();
//这个是内部接口Entry,表示一个键值对
interface Entry<K,V> {
//获取键值对的键
K getKey();
//获取键值对的值
V getValue();
//修改键值对的值
V setValue(V value);
//判断两个键值对是否相等
boolean equals(Object o);
//返回当前键值对的哈希值
int hashCode();
...
}
...
}
先尝试使用最常见的HashMap
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "小明"); //使用put方法添加键值对,返回值我们会在后面讨论
map.put(2, "小红");
System.out.println(map.get(2)); //使用get方法根据键获取对应的值
}
注意,Map中无法添加相同的键,同样的键只能存在一个,即使值不同。如果出现键相同的情况,那么会覆盖掉之前的
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "小明");
map.put(1, "小红"); //这里的键跟之前的是一样的,这样会导致将之前的键值对覆盖掉
System.out.println(map.get(1));
}
为了防止意外将之前的键值对覆盖掉,我们可以使用:
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "小明");
map.putIfAbsent(1, "小红"); //Java8新增操作,只有在不存在相同键的键值对时才会存放
System.out.println(map.get(1));
}
我们在获取一个不存在的映射时,默认会返回null作为结果:
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "小明"); //Map中只有键为1的映射
System.out.println(map.get(3)); //此时获取键为3的值,那肯定是没有的,所以说返回null
}
我们也可以为这个情况添加一个预备方案,当Map中不存在,可以返回一个备选的返回值:
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1,"xiao");
System.out.println(map.getOrDefault(3,"nothing"));
}
因为HashMap底层采用哈希表实现,所以不维护顺序,我们在获取所有键和所有值时,可能是乱序的:
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("0", "zero");
map.put("+","one");
map.put("1","plus");
System.out.println(map);
System.out.println(map.keySet());
System.out.println(map.values());
}
输出
{0=zero, 1=plus, +=one}
[0, 1, +]
[zero, plus, one]
如果要维护顺序,我们同样可以使用LinkedHashMap
,它的内部对插入顺序进行了维护:
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<>();
map.put("0", "zero");
map.put("+","one");
map.put("1","plus");
System.out.println(map);
System.out.println(map.keySet());
System.out.println(map.values());
}
输出
{0=zero, +=one, 1=plus}
[0, +, 1]
[zero, one, plus]
Map的底层实现
HashMap
底层采用哈希表,结合之前说过的可能会出现哈希冲突,保存的元素就会出现限制,因此我们可以采用链地址法来解决
实际上这个表就是一个存放头结点的数组+若干结点,而HashMap也是这样的,先看定义
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
...
static class Node<K,V> implements Map.Entry<K,V> { //内部使用结点,实际上就是存放的映射关系
final int hash;
final K key; //跟我们之前不一样,我们之前一个结点只有键,而这里的结点既存放键也存放值,当然计算哈希还是使用键
V value;
Node<K,V> next;
...
}
...
transient Node<K,V>[] table; //这个就是哈希表本体了,可以看到跟我们之前的写法是一样的,也是头结点数组,只不过HashMap中没有设计头结点(相当于没有头结点的链表)
final float loadFactor; //负载因子,这个东西决定了HashMap的扩容效果
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; //当我们创建对象时,会使用默认的负载因子,值为0.75
}
...
}
可以看到,实际上底层大致结构跟我们之前学习的差不多,只不过多了一些特殊的东西:
- HashMap支持自动扩容,哈希表的大小并不是一直不变的,否则太过死板
- HashMap并不是只使用简单的链地址法,当链表长度到达一定限制时,会转变为效率更高的红黑树结构
研究一下他的put方法
public V put(K key, V value) {
//这里计算完键的哈希值之后,调用的另一个方法进行映射关系存放
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) //如果底层哈希表没初始化,先初始化
n = (tab = resize()).length; //通过resize方法初始化底层哈希表,初始容量为16,后续会根据情况扩容,底层哈希表的长度永远是2的n次方
//因为传入的哈希值可能会很大,这里同样是进行取余操作
//(n - 1) & hash 等价于 hash % n 这里的i就是最终得到的下标位置了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); //如果这个位置上什么都没有,那就直接放一个新的结点
else { //这种情况就是哈希冲突了
Node<K,V> e; K k;
if (p.hash == hash && //如果上来第一个结点的键的哈希值跟当前插入的键的哈希值相同,键也相同,说明已经存放了相同键的键值对了,那就执行覆盖操作
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //这里直接将待插入结点等于原本冲突的结点,一会直接覆盖
else if (p instanceof TreeNode) //如果第一个结点是TreeNode类型的,说明这个链表已经升级为红黑树了
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //在红黑树中插入新的结点
else {
for (int binCount = 0; ; ++binCount) { //普通链表就直接在链表尾部插入
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); //找到尾部,直接创建新的结点连在后面
if (binCount >= TREEIFY_THRESHOLD - 1) //如果当前链表的长度已经很长了,达到了阈值
treeifyBin(tab, hash); //那么就转换为红黑树来存放
break; //直接结束
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) //同样的,如果在向下找的过程中发现已经存在相同键的键值对了,直接结束,让p等于e一会覆盖就行了
break;
p = e;
}
}
if (e != null) { // 如果e不为空,只有可能是前面出现了相同键的情况,其他情况e都是null,所有直接覆盖就行
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue; //覆盖之后,会返回原本的被覆盖值
}
}
++modCount;
if (++size > threshold) //键值对size计数自增,如果超过阈值,会对底层哈希表数组进行扩容
resize(); //调用resize进行扩容
afterNodeInsertion(evict);
return null; //正常插入键值对返回值为null
}
根据上面的推导,我们在正常插入一个键值对时,会得到null返回值,而冲突时会得到一个被覆盖的值
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
System.out.println(map.put("0","zero"));
System.out.println(map.put("0","one"));
}
输出
null
zero
当HashMap的一个链表长度过长的时候,会自动转换成红黑树
但是这样治标不治本,受限制的始终时底层哈希表的长度,还需要进一步对这个底层的哈希表进行扩容才能够,所以有了resize()
方法
static final int MAXIMUM_CAPACITY = 1 << 30;
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //先把下面这几个旧的东西保存一下
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; //The next size value at which to resize
int newCap, newThr = 0; //这些是新的容量和扩容阈值
if (oldCap > 0) { //如果旧容量大于0,那么就开始扩容
if (oldCap >= MAXIMUM_CAPACITY) { //如果旧的容量已经大于最大限制了,那么直接给到 Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return oldTab; //这种情况不用扩了
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) //新的容量等于旧容量的2倍,同样不能超过最大值
newThr = oldThr << 1; //新的阈值也提升到原来的两倍
}
else if (oldThr > 0) // 旧容量不大于0只可能是还没初始化,这个时候如果阈值大于0,直接将新的容量变成旧的阈值
newCap = oldThr;
else { // 默认情况下阈值也是0,也就是我们刚刚无参new出来的时候
newCap = DEFAULT_INITIAL_CAPACITY; //新的容量直接等于默认容量16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //阈值为负载因子乘以默认容量,负载因子默认为0.75,也就是说只要整个哈希表用了75%的容量,那么就进行扩容,至于为什么默认是0.75,原因很多,这里就不解释了,反正作为新手,这些都是大佬写出来的,我们用就完事。
}
...
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; //将底层数组变成新的扩容之后的数组
if (oldTab != null) { //如果旧的数组不为空,那么还需要将旧的数组中所有元素全部搬到新的里面去
... //详细过程就不介绍了
}
}
LinkedHashMap
是直接继承自HashMap,具有HashMap的全部性质,同时得益于每一个结点都是一个双向链表,在插入键值对的同时保存了插入顺序
LinkedHashMap中的结点实现
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; //这里多了一个指向前一个结点和后一个结点的引用
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
这样我们在遍历LinkedHashMap的时候,顺序就同我们的插入顺序一致,当然也可以使用访问顺序,也就是刚刚被访问过的元素,会被排到最后一位
TreeMap
它的内部直接维护一个红黑树,没有使用哈希表,因为他会将我们插入的结点按规则进行排序,所以说直接采用红黑树会更好,我们在创建时,直接给予一个比较规则即可,跟之前的TreeSet是一样的
public static void main(String[] args) {
Map<Integer , String> map = new TreeMap<>((a, b) -> b - a);
map.put(0, "单走");
map.put(1, "一个六");
map.put(3, "**");
System.out.println(map);
}
输出
{3=**, 1=一个六, 0=单走}
Map’s method
compute()
compute
会将指定的Key的值进行重新计算,如果Key不存在,v会返回null
computeIfPresent()
当Key存在时则计算并赋予新的值
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "A");
map.put(2, "B");
map.compute(1, (k, v)-> {
return v + "M";
}) ;
map.computeIfPresent(1, (k, v) -> {
return v + "M";
});
System.out.println(map);
}
输出
{1=AMM, 2=B}
也可以使用computeIfAbsent()
,当不存在Key时,计算并将键值对放入Map中:
public static void main(String[] args) {
map.put(1, "A");
map.put(2, "B");
map.computeIfAbsent(0, (k)-> {
return "M";
});
System.out.println(map);
}
输出
{0=M, 1=A, 2=B}
merge()
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("yoni", "English", 80),
new Student("yoni", "Chinese", 98),
new Student("yoni", "Math", 95),
new Student("taohai.wang", "English", 50),
new Student("taohai.wang", "Chinese", 72),
new Student("taohai.wang", "Math", 41),
new Student("Seely", "English", 88),
new Student("Seely", "Chinese", 89),
new Student("Seely", "Math", 92)
);
Map<String, Integer> scoreMap = new HashMap<>();
//merge方法可以对重复键的值进行特殊操作,比如我们想计算某个学生的所有科目分数之后,那么就可以像这样:
students.forEach(student -> scoreMap.merge(student.getName(), student.getScore(), Integer::sum));
scoreMap.forEach((k, v) -> System.out.println("key:" + k + "总分" + "value:" + v));
}
static class Student {
private final String name;
private final String type;
private final int score;
public Student(String name, String type, int score) {
this.name = name;
this.type = type;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
public String getType() {
return type;
}
}
输出
key:Seely总分value:269
key:taohai.wang总分value:163
key:yoni总分value:273
replace()
可以快速替换某个映射的值
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(0, "zero");
map.replace(0, ">>>");
System.out.println(map);
}
也可以精准匹配
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(0,"zero");
map.replace(0,"maka","baka"); //map.replace(key,oldValue,newValue);
System.out.println(map);
}
输出
{0=zero}
remove()
使用方法跟replace差不多,也支持键同时匹配
public static void main(String[] args) {
Map<Integer , String> map = new HashMap<>();
map.put(0, "单走");
map.remove(0, "单走"); //只有同时匹配时才移除
System.out.println(map);
}
Stream流
Java 8 API 新增了一个新的抽象流Stream
,可以让我们以一种声明的方式处理数据
Stream使用一种类似于SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。
Stream API 可以极大提高Java程序员的生产力。这种处理方法将要处理的元素集合看作一种流,流在管道中运输,并且可以在管道的节点上进行处理,比如筛选、排序、聚合等。元素流在管道中经过中间操作 (intermediate operation) 的处理,最后由最终操作(terminal operation)得到前面处理的结果
我们可以把一个Stream
当作流水线处理:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
//the first method :remove "B"
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
if(iterator.next().equals("B")) {
iterator.remove();
}
}
//the second method: Stream 操作
list = list //链式调用
.stream() //获取流
.filter(e -> !e.equals("B")) //只允许不是B的通过
.collect(Collectors.toList()); //重新收集通过流水线的元素,变回list
System.out.println(list);
}
输出
[A, C]
再来一例
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(3);
list = list
.stream()
.distinct() //去重(使用equals判断)
.sorted((a, b) -> b - a) //进行倒序排列
.map(e -> e+1) //每个元素都要执行+1操作
.limit(2) //只放行前两个元素
.collect(Collectors.toList());
System.out.println(list);
}
输出
[4, 3]
当遇到大量的复杂操作的时候,我们可以使用Stream来快速编写代码,逻辑更加明了
注意:不能认为每一步都是直接依次执行的
实际上Stream会先记录每一步操作,而不是直接开始执行内容,当整个链式调用完成时,才会依次进行,也就是需要的时候,工厂的及其才会按照预定的流程启动
接下来,用一堆随机数来进行更多流操作的演示
public static void main(String[] args) {
Random random = new Random();
random
.ints(-100, 100)
.limit(10);
.filter(i -> i< 0)
.sorted()
.forEach(System.out::println);
}
我们可以生成一个统计实例来帮助我们快速进行统计:
public static void main(String[] args) {
Random random = new Random();
IntSummaryStatistics statistics = random
.ints(0, 100)
.limit(100)
.summaryStatistics();
System.out.println(statistics.getMax());
System.out.println(statistics.getCount());
System.out.println(statistics.getAverage());
}
输出
99
100
51.38
普通的List只需要一个method,就可以直接转换到方便好用的IntStream
:
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.stream()
.mapToInt(i -> i)
.summaryStatistics();
}
我们还可以通过使用flat
来对整个流进行一个细分:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A,B");
list.add("C,D");
list.add("E,F");
//让每一个元素通过,进行分割,变成独立的6个元素
list = list
.stream()
.flatMap(e -> Arrays.stream(e.split(","))) //分割字符串并生成新的流
.collect(Collectors.toList()); //汇成新的list
System.out.println(list); //得到结果
}
输出
[A, B, C, D, E, F]
我们也可以只通过Stream来完成所有数字的和,使用reduce
方法:
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
int sum = list
.stream()
.reduce((a, b) -> a + b) //计算规则为:a是上一次计算的值,b是当前要计算的参数,这里是求和
.get(); //我们发现得到的是一个Optional类实例,通过get方法返回得到的值
System.out.println(sum);
}
Collections工具类
JDK为我们准备的Collections类,就是专门用于集合的工具类
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
Collections.max(list);
Collections.min(list);
}
同样的,我们可以对集合进行二分搜索
注意:集合的类型必须是实现Comparable接口
的类:
public static void main(String[] args) {
List<Integer> list = Arrays.asList(2, 3, 8, 9, 10, 13);
System.out.println(Collections.binarySearch(list,8));
}
我们也可以对集合的元素进行快速填充,但是,此填充是对集合中的已有元素进行覆盖:
也就是说,如果集合中本身没有元素,那么fill
操作不会生效
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collections.fill(list, 6);
System.out.println(list);
}
输出
[6, 6, 6, 6, 6]
有时候我们可能需要生成一个空的集合类返回,那么我们可以使用emptyList()
来快速生成一个only read的空集合
public static void main(String[] args) {
List<Integer> list = Collections.emptyList();
//Collections.singletonList() 会生成一个只有一个元素的List
list.add(10); //不支持,会直接抛出异常
}
我们也可以寻找子集合的位置:
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
System.out.println(Collections.indexOfSubList(list, Arrays.asList(4, 5)));
}
输出
3
得益于泛型的类型擦除机制,实际上最终只要是Object的实现类,都可以保存到集合类中,那么就会出现这种情况
public static void main(String[] args) {
//使用原始类型接收一个Integer类型的ArrayList
List list = new ArrayList<>(Arrays.asList(1,2,3,4,5));
list.add("aaa"); //我们惊奇地发现,这玩意居然能存字符串进去
System.out.println(list);
}
输出
[1, 2, 3, 4, 5, aaa]
由于泛型机制的一些漏洞,实际上对应类型的集合类有可能会存放其它类型的值,泛型的类型检查只存在于编译阶段,只要绕过这个阶段,在实际运行时,不会进行类型检查;
checkedList()
,可以将给定集合类进行包装,在运行时进行类型检查,如果add的不是当前类型集合支持的类型,就会抛出类型转换异常
public static void main(String[] args) {
List list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
list = Collections.checkedList(list, Integer.class); //表示Integer这个类型
list.add("aaa");
System.out.println(list);
}
输出