(Java集合篇)Java学习路线(漫漫人生路,步步架构梦)Java Learning Path (Long Life Path, Step by step Structure Dream)

前言

总结Java集合相关知识,希望能帮助兄弟们,💪O(∩_∩)O哈哈~

Java集合基础知识总结

Java集合框架提供了一套用于保存对象和数据的标准接口和实现。主要包含以下几个部分:

  1. List:有序集合,可以包含重复元素。主要实现有ArrayList、LinkedList和Vector。
  2. Set:无序集合,不可以包含重复元素。主要实现有HashSet、LinkedHashSet和TreeSet。
  3. Map:键值对集合,键是无序的,值可以重复。主要实现有HashMap、LinkedHashMap、TreeMap和Hashtable。
  4. Queue:队列,先进先出。主要实现有PriorityQueue、ArrayDeque和LinkedList。
  5. Collection:集合框架最基本的接口,定义了集合框架中的公共接口方法。
    集合框架主要特点:
  6. 容器性:可以保存多个对象和数据。
  7. 遍历性:可以遍历容器中的所有元素。
  8. 有序性:List集合的元素是有序的,Set和Map集合的元素是无序的。
  9. 唯一性:Set集合的元素是唯一的,List和Map集合的元素可以重复。
  10. 同步性:Vector、Hashtable和Stack这些集合是同步的,线程安全的。其他的集合都是非同步的。
  11. 快速查找:通过HashMap和HashSet可以快速查找元素。
  12. 增删改查:可以对集合进行增加、删除、修改、查询操作。
    重要接口和实现类:

    • Collection:集合框架最基本的接口
    • List:有序集合接口,实现类ArrayList、LinkedList、Vector
    • Set:无序集合接口,实现类HashSet、LinkedHashSet、TreeSet
    • Map:键值对集合接口,实现类HashMap、LinkedHashMap、TreeMap、Hashtable
    • Queue:队列接口,实现类PriorityQueue、ArrayDeque

      List遍历删除元素remove()

      在遍历List的时候直接remove()元素会出现ConcurrentModificationException。这是因为remove()方法会改变List的大小,而遍历List的时候是通过索引来访问元素的,索引正在变化,就会抛出该异常。
      解决方法有两种:

  13. 使用Iterator遍历,在遍历的时候删除元素:
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
    Integer num = iterator.next();
    if (num == 3) {
        iterator.remove();
    }
    }
  14. 使用 list.remove() 方法的时候,意识到索引在变化,所以从后往前删除:
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i) == 3) {
        list.remove(i);
    } 
    }

    除此之外,常见的List集合还有:

    • ArrayList: 动态数组,查询快,增删慢。
    • LinkedList: 双向链表,查询慢,增删快。
    • Vector: 与ArrayList类似,线程安全,查询快,增删慢。
    • Stack: 栈,FILO,线程安全。
    • HashSet: 无序不重复集合。
    • LinkedHashSet: 有序不重复集合。
    • TreeSet: 红黑树,有序集合。

      List<T>和List<?>的区别

      List遍历删除元素remove()需要注意:

  15. 使用Iterator遍历删除元素时, iterator.remove() 方法会删除迭代器刚遍历过的元素。这是合法的。
  16. 如果直接使用list.remove(i) 删除元素,会导致索引i之后的元素全部减小1,这会跳过某些元素不遍历到。
    解决办法是使用iterator遍历删除:

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    Iterator<Integer> it = list.iterator();
    while (it.hasNext()) {
    Integer num = it.next();
    if (num == 2) it.remove(); 
    }

    List和List<?>的区别:

    • List是一个强类型的List,T代表某种具体的类型,比如List
    • List<?>是一个无界通配符的List,表示List中元素类型不确定,可以保存任何类型的元素。
    • 主要区别在于:
    • List可以添加T类型的元素,也可以读出T类型的元素。
    • List<?>只能添加null元素,读取时返回Object类型。
    • List<? extends T>可以添加T类型及T子类型的元素,读取时返回T类型。
    • List<? super T>可以添加T类型及T父类型的元素,读取时返回Object类型。
      所以通常我们会用List<?>表示不关心集合元素类型,只要获取元素就可以了。List更加严格,要求元素类型一致。
      List用于生产者,因为生产者清楚要添加什么类型元素。
      List<?>更适用于消费者,因为消费者只关心获取元素,不关心元素的具体类型。

      Java.lang.Character类详解

      集合概述:
      集合类可分为Collection和Map两大类。Collection存储多个对象,Map存储键值对。
      List遍历删除元素remove():
      List支持随机访问,可以通过索引直接访问元素。遍历List时删除元素需要注意,因为remove会改变List的大小,会影响后续元素的索引。
      正确方式:

      for (int i = 0; i < list.size(); i++) {
      if (满足条件) {
      list.remove(i);
      i--;  // i--很重要,因为remove后索引会变化
      }
      }

      错误方式:

      for (int i = 0; i < list.size(); i++) {
      if (满足条件) {
      list.remove(i);
      }
      }
      // 这种方式会跳过某些元素

      List和List<?>的区别:

    • List是一个原始类型列表,存储相同类型的元素。
    • List<?>是一个通配符列表,可以存储任何类型的元素。
    • List可以添加T类型的元素,List<?>只能添加null元素。
    • List可以获取T类型的元素,List<?>只能获取Object类型的元素。
      Character类详解:
      Character类对char类型做了很好的封装,提供了许多方法:
    • isLetter() 判断是否为字母
    • isDigit() 判断是否为数字
    • isWhitespace() 判断是否为空白字符
    • isUpperCase() 判断是否为大写字母
    • isLowerCase() 判断是否为小写字母
    • toUpperCase() 转为大写字母
    • toLowerCase() 转为小写字母
    • getNumericValue() 获取字符的数值,如'A'的数值是65
    • 比较字符:compareTo()方法
    • 转义字符:'\t'、'\n' 等
      Character还定义了许多常量,如:
    • Character.MIN_VALUE 最小值'\u0000'
    • Character.MAX_VALUE 最大值'\uffff'
    • Character.MIN_RADIX 最小进制数2
    • Character.MAX_RADIX 最大进制数36
      List遍历删除元素remove():
      在迭代List时,如果进行元素的remove()/add()操作,会抛出ConcurrentModificationException。
      这是因为迭代器在创建时会记住列表的modCount(修改次数),若列表在迭代过程中被修改,则modCount会改变,从而抛出异常。
      解决方法是使用迭代器的remove()方法,或使用ListIterator。
      List和List<?>的区别:
      List是具体的范型列表,T代表具体的类型,如List
      List<?>是非具体的范型列表,代表某种未知的类型,通常用于作为方法的参数类型。
      Java.lang.Character类详解:
      Character类用于对单个字符进行操作。
      主要方法:
      isLetter()、isDigit()、isWhitespace()、isUpperCase()、isLowerCase() - 判断字符的类型
      toUpperCase()、toLowerCase() - 字母的大小写转化
      getValue() - 返回字符的ASCII值

      HashMap + 软引用进行缓存

      利用HashMap和SoftReference可以实现一个简单的缓存机制。SoftReference内部维护一个弱引用,允许GC回收其所引用的对象。
      实现方法:

      
      Map<String, SoftReference<Object>> cache = new HashMap<>();

Object obj = cache.get(key).get();
if (obj == null) {
obj = createObj(); // 创建新的对象
cache.put(key, new SoftReference<>(obj));
}

这样,当内存不足时,GC会回收SoftReference所引用的对象,从而实现缓存的回收。
# System.arraycopy详解
System.arraycopy()方法用于复制数组中的元素。该方法的签名为:
```java
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

参数说明:

  • src:源数组
  • srcPos:源数组中的起始位置
  • dest:目的数组
  • destPos:目的数组中的起始位置
  • length:要复制的元素个数
    注意:

    1. src和dest应该是同类型或者可以进行转换类型的数组。
    2. srcPos和destPos表示要复制的起始位置,length表示要复制的长度。
    3. 如果src和dest有重叠,则dest的元素会被源元素覆盖。
    4. 该方法执行快速的数组元素复制,比手动for循环复制更加高效。
      例子:

      int[] src = {1, 2, 3, 4};
      int[] dest = {5, 6, 7, 8};
      System.arraycopy(src, 0, dest, 1, 2);
      // dest is now {5, 1, 2, 8}

      该方法对于合并两个数组,修改数组元素顺序等场景都非常有用。
      总结:
      System.arraycopy()用于在数组内部或者数组之间复制数据元素。该方法执行速度快,是数组元素复制的最佳选择。

      Java队列Queue

      Queue接口表示一个线性集合,按照“先进先出”的规则来组织元素。常用实现类有:

  • LinkedList:底层使用链表结构,线程不安全,可以用于栈和队列。
  • ArrayBlockingQueue:基于数组结构,线程安全,可以用于生产者消费者场景。
  • PriorityQueue:基于优先堆结构,线程不安全,可以用于实现优先级队列。
  • DelayQueue:基于优先堆结构,线程安全,可以用于定时任务调度。
    Queue接口中的主要方法有:
  • add(e): 将元素e添加到队列尾部。
  • offer(e): 将元素e添加到队列尾部。
  • remove(): 移除队列头部的元素并返回。
  • poll(): 移除队列头部的元素并返回。
  • element(): 获取队列头部的元素。
  • peek(): 获取队列头部的元素。
    add()和offer()的区别在于:
  • add(e): 如果添加失败则报IllegalStateException。
  • offer(e): 如果添加失败则返回false,不抛出异常。
    remove()和poll()的区别在于:
  • remove(): 如果队列为空则报NoSuchElementException。
  • poll(): 如果队列为空则返回null,不抛出异常。
    element()和peek()的区别在于:
  • element(): 如果队列为空则报NoSuchElementException。
  • peek(): 如果队列为空则返回null,不抛出异常。
    使用示例:

    Queue<Integer> queue = new LinkedList<>();
    queue.add(1);
    queue.add(2);
    queue.add(3);
    queue.element();  // 1
    queue.remove();   // 1
    queue.poll();     // 2
    queue.peek();     // 3

    Queue接口为FIFO(先进先出)的数据结构提供了一套标准的接口规范,我们在实际开发中根据需要选择不同的实现类即可。

    Java中Queue和Deque的区别

    Queue和Deque都是Java集合框架中的队列结构,主要区别在于:
    Queue: 队列,只有两端操作,一端插入(add),一端删除(remove)。实现类有LinkedList,ArrayBlockingQueue等。
    Deque: 双端队列,两端都可以插入(addFirst,addLast)和删除(removeFirst,removeLast)。实现类有LinkedList,ArrayDeque等。
    相同点:

  • 都遵循先进先出(FIFO)的原则
  • 都是线程安全的
    不同点:
  • Queue只有两端操作,Deque两端都可以操作。
  • Queue插入和删除只在一端,Deque可以在两端同时进行插入和删除操作。
  • Queue没有size的最大值限制,Deque可以设置最大值。
    示例代码:
    Queue:

    Queue<Integer> queue = new LinkedList<>();
    queue.add(1);   // [1]
    queue.add(2);   // [1, 2] 
    queue.remove(); // 1  [2]
    queue.remove(); // 2  []

    Deque:

    Deque<Integer> deque = new LinkedList<>();
    deque.addFirst(1);   // [1]
    deque.addLast(2);   // [1, 2]
    deque.removeFirst(); // 1  [2] 
    deque.removeLast();  // 2  []

    Queue和Deque都是线性集合,遵循FIFO规则。但Deque更加灵活,可以在两端同时进行插入和删除操作。根据具体需求选择Queue或者Deque。

    Java中的ConcurrentHashMap中为什么不能存储null?

    ConcurrentHashMap不能存储null键值对的原因主要有:

    1. null键会导致拉链冲突,因为hash算法结果都一样。ConcurrentHashMap使用分段锁技术来实现线程安全,不同的key会定位到不同的segment上。如果有两个线程都要操作null键的值,那么会导致线程不安全。
    2. AbstractMap中的keySet(), entrySet(), values()方法返回的视图,包含null会导致NullPointerException。ConcurrentHashMap继承自AbstractMap,所以也会有这个问题。
    3. 计算hash值时,公共的hash算法对null运算结果都是0,所以null键的元素会聚集到table[0]上,导致哈希冲突。ConcurrentHashMap使用拉链法来解决哈希冲突,但过多的冲突会导致性能下降。
      这里举个例子说明第一点:

      ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
      Thread t1 = new Thread() {
      public void run() {
      map.put(null, 1);
      }
      };
      Thread t2 = new Thread() {
      public void run() {
      map.put(null, 2);
      }
      };
      t1.start();
      t2.start();

      这里t1和t2线程都要插入null键的值,但由于ConcurrentHashMap使用分段锁,null键必定会定位到同一个segment上,所以会产生线程安全问题。
      解决方法是:

    4. 使用其他的Map实现,如Hashtable, HashMap。这两个实现都是线程不安全的,但可以存储null键值对。
    5. 使用ConcurrentHashMap,但不存储null键值对。可以用一个默认的初始化值来表示null键。
    6. 自己实现一个线程安全的可以存储null键值对的Map。
      所以总结来说,ConcurrentHashMap之所以不可以存储null键值对,主要是出于线程安全考虑,以及hash算法的特点。在实际使用中需要根据业务场景选择合适的Map实现。
(Java集合篇)Java学习路线(漫漫人生路,步步架构梦)Java Learning Path (Long Life Path, Step by step Structure Dream)

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

滚动到顶部