理解高斯林List.copyOf()的用意

最近在刷题时要做这样一个事情,将Set<Map.Entry<String,Integer>> 按照每个元素的value排序,返回前八个(不够八个则返回所有)

比较优雅的方式是使用Stream完成sort

JAVA 复制代码
List<Map.Entry<String, Integer>> sortedList = entrySet.stream()
        .sorted(Map.Entry.comparingByValue())
        .toList();

不过我写了一个更直白的代码:

JAVA 复制代码
Set<Map.Entry<String, Integer>> entry = count.entrySet();
List<Map.Entry<String, Integer>> entries = List.copyOf(entry);
Collections.sort(entries, new Comparator<Map.Entry<String, Integer>>() {
    @Override
    public int compare(Map.Entry<String, Integer> o1,
                       Map.Entry<String, Integer> o2) {
        return o1.getValue() - o2.getValue();
    }
});

结果出现异常:

仔细一看,问题出现在List.copyOf()方法中,代码很简单

java 复制代码
static <E> List<E> copyOf(Collection<? extends E> coll) {
    return ImmutableCollections.listCopy(coll);
}

返回了一个拷贝,但是是不可变的List;

为什么高斯林(或者是其他某位大佬)要把copyOf设计为返回不可变类型?

直观理解是:这个复制体就是一个快照image,代表了原本集合在当时的一个状态,所以不可变情有可原。

我们再看集合里另一个方法

java 复制代码
static <E> List<E> of(E e1, E e2, E e3, E e4) {
    return ImmutableCollections.listFromTrustedArray(e1, e2, e3, e4);
}

这里用不可变集合的原因显而易见:用来专门刻画某些元素的集合,按理来说只能随着原本元素的变动而变化;这里的不可变集合就像是MySQL的视图,视图代表的并不是独立的信息,所以其含义不可随意修改。

但这个"不可修改"只是说集合的元素个数和各个元素的引用不可修改,对单个元素内部的修改 在集合层面是无法控制的,比如下面的伪代码

java 复制代码
        Set<Student> set = new HashSet<>();
        //添加元素代码省略
        List<Student> studentList = List.copyOf(set);
        studentList.get(2).setName("郭德纲");

所以可以看出,不可变的力度并不高,只能避免浅层次的不可变(也就是对于集合可见的都不可变)

这样能够解决一些并发问题,使得对于这类不可修改集合的并发访问是线程安全的,比如说以下几个例子

  • 删除元素

    JAVA 复制代码
          List<Integer> list = new ArrayList<>();
    
          // 添加一些初始元素到列表中
          for (int i = 0; i < 10; i++) {
              list.add(i);
          }
    
          // 线程1:遍历列表并打印元素
          Thread thread1 = new Thread(() -> {
              for (Integer val : list) {
                  System.out.println("Thread 1 - Value: " + val);
                  try {
                      Thread.sleep(100); // 模拟一些工作
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          });
    
          // 线程2:从列表中删除一个元素
          Thread thread2 = new Thread(() -> {
              try {
                  Thread.sleep(300); // 确保此操作在迭代过程中发生
                  list.remove(Integer.valueOf(5));
                  System.out.println("Thread 2 - Removed value 5");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          });
    
          thread1.start();
          thread2.start();

    解决方案:

    1. 加最狠的锁,串行所有操作

      List<Integer> synchronizedList = Collections.synchronizedList(list);

    2. 使用写时复制,读在老版,写在新副本,串行所有写操作:即使用CopyOnWriteArrayList

      List<String> unmodifiableList = new CopyOnWriteArrayList<>();

    3. 对集合浅层次锁定,禁止对个数和引用的修改 List<String> unmodifiableList = Collections.unmodifiableList(list);

  • 改变引用

    java 复制代码
      private static final List<String> list = new ArrayList<>();
    
      public static void main(String[] args) throws InterruptedException {
          // 初始化列表
          list.add("Element1");
          list.add("Element2");
    
          // 线程1:交换第一个和第二个元素
          Thread swapperThread = new Thread(() -> {
              lock.lock();
              try {
                  if (list.size() > 1) {
                      String temp = list.get(0);
                      list.set(0, list.get(1));
                      //为了暴露问题,在此强制sleep
                      Thread.sleep(30);
                      list.set(1, temp);
                      System.out.println("Swapped elements: " + list);
                  }
              } finally {
                  lock.unlock();
              }
          });
    
          // 线程2:访问第二个元素
          Thread readerThread = new Thread(() -> {
              lock.lock();
              try {
                  if (!list.isEmpty()) {
                      System.out.println("Accessed first element: " + list.get(1));
                  }
              } finally {
                  lock.unlock();
              }
          });
    
          // 启动线程
          swapperThread.start();
          readerThread.start();

    问题描述:一个线程在对集合中元素引用更改时,前后操作是有关联的(或者说是原子性的),而在中间被第二个线程意外访问;

    1. ReentrantLock同步两个线程的操作
    2. 使用不可变集合,防止对集合的引用擅自更改内部引用
相关推荐
华仔啊几秒前
Java序列化详解:什么情况下必须用它?
java
cxyxiaokui0013 分钟前
Exception和Error:一场JVM内部的“家庭伦理剧”
后端·面试
义达4 分钟前
Django环境下使用wsgi启动MCP服务
后端·django·mcp
用户4099322502125 分钟前
如何在FastAPI中巧妙实现延迟队列,让任务乖乖等待?
后端·ai编程·trae
码事漫谈9 分钟前
为什么我们应该避免使用 abort、exit、getenv 和 system?
后端
码事漫谈10 分钟前
为什么动态内存分配在关键系统中被视为“不合规”?
后端
_風箏11 分钟前
Ollama【部署 02】Linux本地化部署及SpringBoot2.X集成Ollama(ollama-linux-amd64.tgz最新版本 0.6.2)
人工智能·后端·ollama
张同学的IT技术日记13 分钟前
详细实例说明+典型案例实现 对动态规划法进行全面分析 | C++
后端
小华同学ai16 分钟前
炸裂!Github 6000+ star 开源免费易用,支持1000+格式转换,值得收藏!
前端·后端·github
BillKu1 小时前
Spring Boot Controller 使用 @RequestBody + @ModelAttribute 接收请求
java·spring boot·后端