Java集合类

Java集合类

集合类

集合类其实就是为了更好地组织、管理和操作我们的数据而存在的,包括列表、集合、队列、映射等数据结构。

集合根接口

Java中已经帮我们将常用的集合类型都实现好了,我们只需要直接拿来用就行了

所有的集合类最终都是实现自集合根接口的,比如我们下面就会讲到的ArrayList类,它的祖先就是Collection接口:

image-20231015094232111

这个接口定义了集合类的一些基本操作:

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);//-------这些是修改相关的操作----------//向集合中添加元素,不同的集合类具体实现可能会对插入的元素有要求,//这个操作并不是一定会添加成功,所以添加成功返回true,否则返回falseboolean add(E e);//从集合中移除某个元素,同样的,移除成功返回true,否则falseboolean remove(Object o);//-------这些是批量执行的操作----------//查询当前集合是否包含给定集合中所有的元素//从数学角度来说,就是看给定集合是不是当前集合的子集boolean containsAll(Collection<?> c);//添加给定集合中所有的元素//从数学角度来说,就是将当前集合变成当前集合与给定集合的并集//添加成功返回true,否则返回falseboolean addAll(Collection<? extends E> c);//移除给定集合中出现的所有元素,如果某个元素在当前集合中不存在,那么忽略这个元素//从数学角度来说,就是求当前集合与给定集合的差集//移除成功返回true,否则falseboolean 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,否则falseboolean retainAll(Collection<?> c);//清空整个集合,删除所有元素void clear();//-------这些是比较以及哈希计算相关的操作----------//判断两个集合是否相等boolean equals(Object o);//计算当前整个集合对象的哈希值int hashCode();//与迭代器作用相同,但是是并行执行的,我们会在下一章多线程部分中进行介绍@Overridedefault 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);}
}

List列表

List列表(线性表),线性表支持随机访问,相比之前的Collection接口定义,功能还会更多一些。

ArrayList的底层是用数组实现的,内部维护的是一个可动态进行扩容的数组,也就是顺序表,跟我们之前自己写的ArrayList相比,它更加的规范,并且功能更加强大,同时实现自List接口。

image-20231015094814270

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);...
}

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("科技与狠活");   //使用add添加元素list.add("上头啊");System.out.println(list);   //打印集合类,可以得到一个非常规范的结果
}

在使用Integer时,要注意传参问题:

public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.add(10);   //添加Integer的值10list.remove((Integer) 10);   //注意,不能直接用10,默认情况下会认为传入的是int类型值,删除的是下标为10的元素,我们这里要删除的是刚刚传入的值为10的Integer对象System.out.println(list);   //可以看到,此时元素成功被移除
}

快速生成一个只读的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);
}

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;}}...
}

LinkedList的使用和ArrayList的使用几乎相同,各项操作的结果也是一样的,在什么使用使用ArrayList和LinkedList,我们需要结合具体的场景来决定,尽可能的扬长避短。

只不过LinkedList不仅可以当做List来使用,也可以当做双端队列使用。

迭代器

集合类都是支持使用foreach语法的:

public static void main(String[] args) {List<String> list = Arrays.asList("A", "B", "C");for (String s : list) {   //集合类同样支持这种语法System.out.println(s);}
}

编译之后:

public static void main(String[] args) {List<String> list = Arrays.asList("A", "B", "C");Iterator var2 = list.iterator();   //这里使用的是List的迭代器在进行遍历操作while(var2.hasNext()) {String s = (String)var2.next();System.out.println(s);}}

迭代器默认有一个指向集合中第一个元素的指针。

每一次next操作,都会将指针后移一位,直到完成每一个元素的遍历,此时再调用next将不能再得到下一个元素。

集合类的实现方案有很多,可能是链式存储,也有可能是数组存储,不同的实现有着不同的遍历方式,而迭代器则可以将多种多样不同的集合类遍历方式进行统一,只需要各个集合类根据自己的情况进行对应实现就行了。

迭代器操作:

public interface Iterator<E> {//看看是否还有下一个元素boolean hasNext();//遍历当前元素,并将下一个元素作为待遍历元素E next();//移除上一个被遍历的元素(某些集合不支持这种操作)default void remove() {throw new UnsupportedOperationException("remove");}//对剩下的元素进行自定义遍历操作default void forEachRemaining(Consumer<? super E> action) {Objects.requireNonNull(action);while (hasNext())action.accept(next());}
}

在Java8提供了一个支持Lambda表达式的forEach方法,这个方法接受一个Consumer,也就是对遍历的每一个元素进行的操作:

public static void main(String[] args) {List<String> list = Arrays.asList("A", "B", "C");list.forEach(System.out::println);
}
default void forEach(Consumer<? super T> action) {Objects.requireNonNull(action);for (T t : this) {   //foreach语法遍历每一个元素action.accept(t);   //调用Consumer的accept来对每一个元素进行消费}
}

实际上只要是实现了迭代器接口的类(我们自己写的都行),都可以使用foreach语法

ListIterator迭代器是针对于List的强化版本,增加了更多方便的操作,因为List是有序集合,所以它支持两种方向的遍历操作,不仅能从前向后,也可以从后向前:

public interface ListIterator<E> extends Iterator<E> {//原本就有的boolean hasNext();//原本就有的E next();//查看前面是否有已经遍历的元素boolean hasPrevious();//跟next相反,这里是倒着往回遍历E previous();//返回下一个待遍历元素的下标int nextIndex();//返回上一个已遍历元素的下标int previousIndex();//原本就有的void remove();//将上一个已遍历元素修改为新的元素void set(E e);//在遍历过程中,插入新的元素到当前待遍历元素之前void add(E e);
}

Queue和Deque

LinkedList除了可以直接当做列表使用之外,还可以当做其他的数据结构使用,可以看到它不仅仅实现了List接口:

public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{

Deque接口的继承结构:

image-20231015110824056

public interface Queue<E> extends Collection<E> {//队列的添加操作,是在队尾进行插入(只不过List也是一样的,默认都是尾插)//如果插入失败,会直接抛出异常boolean add(E e);//同样是添加操作,但是插入失败不会抛出异常boolean offer(E e);//移除队首元素,但是如果队列已经为空,那么会抛出异常E remove();//同样是移除队首元素,但是如果队列为空,会返回nullE poll();//仅获取队首元素,不进行出队操作,但是如果队列已经为空,那么会抛出异常E element();//同样是仅获取队首元素,但是如果队列为空,会返回nullE 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());
}

双端队列就是队列的升级版,允许在队列的两端进行入队和出队操作,普通队列中从队尾入队,队首出队

双端队列既可以当做普通队列使用,也可以当做栈来使用

Deque双端队列接口的:

//在双端队列中,所有的操作都有分别对应队首和队尾的
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();}

除了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());
}

优先级队列并不是队列中所有的元素都是按照优先级排放的,优先级队列只能保证出队顺序是按照优先级进行的

Set集合

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();//这个方法我们同样会放到多线程中进行介绍@Overridedefault Spliterator<E> spliterator() {return Spliterators.spliterator(this, Spliterator.DISTINCT);}
}

HashSet,它的底层就是采用哈希表实现的,可以非常高效的从HashSet中存取元素

在Set接口中并没有定义支持指定下标位置访问的添加和删除操作,只能简单的删除Set中的某个对象

由于底层采用哈希表实现,无法维持插入元素的顺序

想要使用维持顺序的Set集合可以使用LinkedHashSet,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,它会在元素插入时进行排序,可以自定义排序规则

public static void main(String[] args) {TreeSet<Integer> set = new TreeSet<>((a, b) -> b - a);  //同样是一个Comparatorset.add(1);set.add(3);set.add(2);System.out.println(set);
}

Map映射

通过保存键值对的形式来存储映射关系,就可以轻松地通过键找到对应的映射值,在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);//清空整个Mapvoid 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,它的底层采用哈希表实现

Map中无法添加相同的键,同样的键只能存在一个,即使值不同。如果出现键相同的情况,那么会覆盖掉之前的

为了防止意外将之前的键值对覆盖掉,可以使用:

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, "小明");System.out.println(map.getOrDefault(3, "备胎"));   //Java8新增操作,当不存在对应的键值对时,返回备选方案
}

HashMap底层采用哈希表实现,所以不维护顺序,我们在获取所有键和所有值时,可能会是乱序的

如果需要维护顺序,我们同样可以使用LinkedHashMap,它的内部对插入顺序进行了维护

哈希表可能会出现哈希冲突,这样保存的元素数量就会存在限制,而我们可以通过连地址法解决这种问题

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
}

当HashMap的一个链表长度过大时,会自动转换为红黑树

这样始终治标不治本,受限制的始终是底层哈希表的长度,还需要进一步对底层的这个哈希表进行扩容才可以从根本上解决问题,来看看resize()方法:

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;   //先把下面这几个旧的东西保存一下int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;  //这些是新的容量和扩容阈值if (oldCap > 0) {  //如果旧容量大于0,那么就开始扩容if (oldCap >= MAXIMUM_CAPACITY) {  //如果旧的容量已经大于最大限制了,那么直接给到 Integer.MAX_VALUEthreshold = 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;   //新的容量直接等于默认容量16newThr = (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的全部性质,同时得益于每一个节点都是一个双向链表,在插入键值对时,同时保存了插入顺序

static class Entry<K,V> extends HashMap.Node<K,V> {   //LinkedHashMap中的结点实现Entry<K,V> before, after;   //这里多了一个指向前一个结点和后一个结点的引用Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}
}

TreeMap内部直接维护了一个红黑树,因为它会将我们插入的结点按照规则进行排序,所以说直接采用红黑树会更好,我们在创建时,直接给予一个比较规则即可,跟之前的TreeSet是一样的

HashSet几乎都在操作内部维护的一个HashMap,也就是说,HashSet只是一个表壳,而内部维护的HashMap才是灵魂

Map中定义的compute方法:

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) -> {   //compute会将指定Key的值进行重新计算,若Key不存在,v会返回nullreturn v+"M";     //这里返回原来的value+M});map.computeIfPresent(1, (k, v) -> {   //当Key存在时存在则计算并赋予新的值return v+"M";     //这里返回原来的value+M});System.out.println(map);
}

使用computeIfAbsent,当不存在Key时,计算并将键值对放入Map中

merge方法用于处理数据:

public static void main(String[] args) {List<Student> students = Arrays.asList(new Student("yoni", "English", 80),new Student("yoni", "Chiness", 98),new Student("yoni", "Math", 95),new Student("taohai.wang", "English", 50),new Student("taohai.wang", "Chiness", 72),new Student("taohai.wang", "Math", 41),new Student("Seely", "English", 88),new Student("Seely", "Chiness", 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;}
}

replace方法可以快速替换某个映射的值:

public static void main(String[] args) {Map<Integer , String> map = new HashMap<>();map.put(0, "单走");map.replace(0, ">>>");   //直接替换为新的map.replace(0, "巴卡", "玛卡");   //只有键和值都匹配时,才进行替换System.out.println(map);
}

remove方法:

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)得到前面处理的结果。

image-20231015164727663

可以把一个Stream当做流水线处理:

public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("A");list.add("B");list.add("C");//移除为B的元素Iterator<String> iterator = list.iterator();while (iterator.hasNext()){if(iterator.next().equals("B")) iterator.remove();}//Stream操作list = list     //链式调用.stream()    //获取流.filter(e -> !e.equals("B"))   //只允许所有不是B的元素通过流水线.collect(Collectors.toList());   //将流水线中的元素重新收集起来,变回ListSystem.out.println(list);list = list.stream().distinct()   //去重(使用equals判断).sorted((a, b) -> b - a)    //进行倒序排列.map(e -> e+1)    //每个元素都要执行+1操作.limit(2)    //只放行前两个元素.collect(Collectors.toList());
}

当遇到大量的复杂操作时,我们就可以使用Stream来快速编写代码,这样不仅代码量大幅度减少,而且逻辑也更加清晰明了

stream会先记录每一步操作,而不是直接开始执行内容,当整个链式调用完成后,才会依次进行

随机数来进行更多流操作的演示:

public static void main(String[] args) {Random random = new Random();  //没想到吧,Random支持直接生成随机数的流random.ints(-100, 100)   //生成-100~100之间的,随机int型数字(本质上是一个IntStream).limit(10)   //只获取前10个数字(这是一个无限制的流,如果不加以限制,将会无限进行下去!).filter(i -> i < 0)   //只保留小于0的数字.sorted()    //默认从小到大排序.forEach(System.out::println);   //依次打印
}

生成一个统计实例来帮助我们快速进行统计:

public static void main(String[] args) {Random random = new Random();  //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());   //获取平均值
}

只通过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);
}

通过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());   //汇成新的ListSystem.out.println(list);   //得到结果
}

Collections工具类

快速求得List中的最大值和最小值:

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));
}

对集合的元素进行快速填充:

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);
}

注意这个填充是对集合中已有的元素进行覆盖,如果集合中本身没有元素,那么fill操作不会生效。

使用emptyXXX来快速生成一个只读的空集合:

public static void main(String[] args) {List<Integer> list = Collections.emptyList();//Collections.singletonList() 会生成一个只有一个元素的Listlist.add(10);   //不支持,会直接抛出异常
}

将一个可修改的集合变成只读的集合:

public static void main(String[] args) {List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4,5));List<Integer> newList = Collections.unmodifiableList(list);newList.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)));
}

由于泛型机制上的一些漏洞,实际上对应类型的集合类有可能会存放其他类型的值,泛型的类型检查只存在于编译阶段,只要我们绕过这个阶段,在实际运行时,并不会真的进行类型检查,要解决这种问题很简单,就是在运行时进行类型检查:

public static void main(String[] args) {//使用原始类型接收一个Integer类型的ArrayListList list = new ArrayList<>(Arrays.asList(1,2,3,4,5));list.add("aaa");   //我们惊奇地发现,这玩意居然能存字符串进去System.out.println(list);
}
public static void main(String[] args) {List list = new ArrayList<>(Arrays.asList(1,2,3,4,5));list = Collections.checkedList(list, Integer.class);   //这里的.class关键字我们会在后面反射中介绍,表示Integer这个类型list.add("aaa");System.out.println(list);
}

List();
//Collections.singletonList() 会生成一个只有一个元素的List
list.add(10); //不支持,会直接抛出异常
}


将一个可修改的集合变成只读的集合:~~~java
public static void main(String[] args) {List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4,5));List<Integer> newList = Collections.unmodifiableList(list);newList.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)));
}

由于泛型机制上的一些漏洞,实际上对应类型的集合类有可能会存放其他类型的值,泛型的类型检查只存在于编译阶段,只要我们绕过这个阶段,在实际运行时,并不会真的进行类型检查,要解决这种问题很简单,就是在运行时进行类型检查:

public static void main(String[] args) {//使用原始类型接收一个Integer类型的ArrayListList list = new ArrayList<>(Arrays.asList(1,2,3,4,5));list.add("aaa");   //我们惊奇地发现,这玩意居然能存字符串进去System.out.println(list);
}
public static void main(String[] args) {List list = new ArrayList<>(Arrays.asList(1,2,3,4,5));list = Collections.checkedList(list, Integer.class);   //这里的.class关键字我们会在后面反射中介绍,表示Integer这个类型list.add("aaa");System.out.println(list);
}

checkedXXX可以将给定集合类进行包装,在运行时同样会进行类型检查,如果通过上面的漏洞插入一个本不应该是当前类型集合支持的类型,那么会直接抛出类型转换异常

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/110927.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

什么是客户端?一文了解客户端定义、特点与功能、搭建方法

客户端&#xff1a;定义、特点与功能、搭建方法 1. 定义&#xff1a; 客户端是计算机网络中的一个术语&#xff0c;指的是在网络通信中充当主动发起请求并接收服务响应的一方。通常&#xff0c;客户端是指运行在终端设备上的软件或硬件实体&#xff0c;通过与服务器进行通信来…

华为数通方向HCIP-DataCom H12-831题库(单选题:301-310)

第301题 关于配置防火墙安全区域的安全级别的描述,错误的是 A、同一系统中,两个安全区域不允许配置相同的安全级别 B、只能为自定义的安全区域设定安全级别 C、安全级别一旦设定不允许更改 D、新建的安全区域,系统默认其安全级别为1 答案:D 解析: 新创建的安全区域缺省未…

交通目标检测-行人车辆检测流量计数 - 计算机竞赛

文章目录 0 前言1\. 目标检测概况1.1 什么是目标检测&#xff1f;1.2 发展阶段 2\. 行人检测2.1 行人检测简介2.2 行人检测技术难点2.3 行人检测实现效果2.4 关键代码-训练过程 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 毕业设计…

在前端html页面中向服务器发送post登录请求

目录 前言 搭建服务器 搭建前端登录页面 获取表单值 使用axios发送post登录请求 前言 一般在html页面中向服务器发送post请求的模块为登录请求&#xff0c;本文将介绍如何向服务器发送post请求 搭建服务器 如何搭建服务器请看JWT认证这篇文章&#xff0c;有详细的解说。…

SpringCloud学习笔记-gateway网关自定义全局过滤器

需求&#xff1a;定义全局过滤器&#xff0c;拦截请求&#xff0c;判断请求的参数是否满足下面条件&#xff1a; 参数中是否有authorization&#xff0c; authorization参数值是否为admin 如果同时满足则放行&#xff0c;否则拦截 实现&#xff1a; 在gateway中定义一个过…

《SQLi-Labs》04. Less 23~28a

title: 《SQLi-Labs》04. Less 23~28a date: 2023-10-19 19:37:40 updated: 2023-10-19 19:38:40 categories: WriteUp&#xff1a;Security-Lab excerpt: 联合注入&#xff0c;注释符过滤绕过之构造闭合&#xff0c;%00 截断、二次注入、报错注入&#xff0c;空格过滤绕过&…

【Java基础面试二十四】、String类有哪些方法?

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;String类有哪些方法&…

欧科云链研究院:人类或将成为仅次于AI第二聪明物种?Web3不允许

出品&#xff5c;欧科云链研究院 在 AI行业“掘金买铲”的英伟达&#xff0c;60%的红杉投资在AI相关领域&#xff0c;之前只专注Web3的顶级VC&#xff0c;Paradigm 正在从转向人工智能等 "前沿 "技术。 资本的追逐让AI迷人且危险。 OKG RESEARCH IN FT AI教父Geoffre…

并发容器(Map、List、Set)实战及其原理

一. JUC包下的并发容器 Java的集合容器框架中&#xff0c;主要有四大类别&#xff1a;List、Set、Queue、Map&#xff0c;大家熟知的这些集合类ArrayList、LinkedList、HashMap这些容器都是非线程安全的。 所以&#xff0c;Java先提供了同步容器供用户使用。 同步容器可以简单地…

深入了解RPA业务流程自动化的关键要素

在RPA业务流程自动化实施过程中&#xff0c;哪些因素起着至关重要的作用&#xff1f;这其实没有一个通用的答案&#xff0c;每一个RPA业务流程自动化的部署&#xff0c;都需要结合具体场景去调整&#xff0c;并且进行全面的规划。 首当其冲是要关注以下几点&#xff1a; 1、专…

AutoGPT:自动化GPT原理及应用实践

一、AutoGPT介绍 想象一下&#xff0c;生活在这样一个世界里&#xff0c;你有一个人工智能助手&#xff0c;它不仅能够理解你的需求&#xff0c;而且还能够与你一起学习与成长。人工智能已无缝融入我们工作、生活&#xff0c;并帮助我们有效完成各种目标。大模型技术的发展与应…

Unity之ShaderGraph如何模拟水波实现顶点波动

前言 今天我们实现类似水波纹的顶点波动效果 如下所示&#xff1a; 主要节点 Tilling And Offset&#xff1a;分别通过输入Tiling和Offset平铺和偏移输入UV的值。这通常用于细节贴图和随时间滚动的纹理。 Gradient Noise&#xff1a;根据输入UV生成梯度或Perlin噪声。生成…

【扩散模型】如何用最几毛钱生成壁纸

通过学习扩散模型了解到了统计学的美好&#xff0c;然后顺便记录下我之前文生图的基础流程~ 扩散模型简介 这次是在DataWhale的组队学习里学习的&#xff0c;HuggingFace开放扩散模型学习地址 扩散模型训练时通过对原图增加高斯噪声&#xff0c;在推理时通过降噪来得到原图&…

【UE4 材质编辑篇】1.0 shader编译逻辑

UE4新手&#xff0c;学起来&#xff08;&#xff09;文章仅记录自己的思考。 参考&#xff1a;虚幻4渲染编程(材质编辑器篇)【第一卷&#xff1a;开篇基础】 - 知乎 (zhihu.com) 开篇基础就摸不着头脑&#xff0c;原因是此前完全没有摸过UE4&#xff0c;一点一点记录吧&#x…

25台兰博基尼跑车赛道巡游!泡泡玛特MOLLY攒的局就是这么拉风

入秋以来气温逐渐转冷&#xff0c;但泡泡玛特的市场活动却持续升温&#xff1a;国内首个潮玩行业沉浸式IP主题乐园泡泡玛特城市乐园正式开园&#xff1b;2023PTS上海国际潮流玩具展&#xff1b;入驻美国第二大商场、布里斯班再拓新店等海外布局步伐不停……将广大消费者的身心带…

解决电脑出现msvcp140.dll丢失问题,msvcp140.dll丢失的详细解决方法

在我们日常使用电脑的过程中&#xff0c;可能会遇到各种问题&#xff0c;其中之一就是MSVCCP140.DLL文件缺失。这个文件是Microsoft Visual C 2015 Redistributable的一部分&#xff0c;通常用于支持一些软件或游戏运行。如果这个文件丢失或损坏&#xff0c;可能会导致程序无法…

SpringBoot集成Lettuce客户端操作Redis

目录 一、前言二、基础集成配置&#xff08;redis单节点&#xff09;2.1、POM2.2、添加配置文件application.yml2.3、编写配置文件2.4、编写启动类2.5、编写测试类测试是否连接成功 一、前言 spring-boot-starter-data-redis有两种实现 lettuce 和 jedis&#xff0c;spring bo…

手机应用app打开游戏显示连接服务器失败是什么原因?排查解决方案?

亲爱的同学们&#xff0c;有时候我们在使用手机设备时&#xff0c;可能会遇到一个很头疼的问题——连接服务器失败。这个问题不仅让我们感到困扰&#xff0c;还影响到了我们的用户体验。那么&#xff0c;我们究竟能如何解决这个问题呢&#xff1f;今天&#xff0c;笔者就和大家…

罗技鼠标接收器丢失或损坏后用另一个接收器配对的方法

本文介绍罗技鼠标在丢失、损坏其自身原有的接收器后&#xff0c;将另一个新的接收器与原有鼠标相互配对的方法。 在开始之前&#xff0c;大家需要首先查看两个内容&#xff1a;首先是原有的鼠标——大家需要查看自己的鼠标&#xff08;罗技键盘也是同样的操作&#xff09;底部&…

使用轮廓分数提升时间序列聚类的表现

我们将使用轮廓分数和一些距离指标来执行时间序列聚类实验&#xff0c;并且进行可视化 让我们看看下面的时间序列: 如果沿着y轴移动序列添加随机噪声&#xff0c;并随机化这些序列&#xff0c;那么它们几乎无法分辨&#xff0c;如下图所示-现在很难将时间序列列分组为簇: 上面…