HashMap扩容机制跟你的工作真的不相关吗?

说来话长,这事还得从我第一份工作说起,那时候纯纯大菜鸡一个,啥也不会,工作中如履薄冰,举步维艰,满屏荒唐码,一把辛酸泪😭

再说句题外话,如果你是中高级程序员,建议您划走离开,否则这篇文章可能会浪费您的宝贵时间☺️

那年

OK,书归正传,得益于本人工作态度良好,同事和领导都给予了我很大的帮助,只记得那是18年的平常打工人的一天,我写了如下很多打工人都会写,甚至每天都在写的代码(当时的具体代码已经记不清了,现在大概模拟一下案发场景):

java 复制代码
    /**
     * 从Order对象中获取id属性并包装成List返回
     *
     * @param orderList Student列表
     * @return idList
     */
    public List<Long> getOrderIds(List<Order> orderList) {
        List<Long> ids = new ArrayList<>();
        for (Order order : orderList) {
            ids.add(order.getId());
        }
        return ids;
    }

对没错,用Stream流可以一行代码解决这个问题,但当时受限于我们使用的JDK还是1.6和1.7,你懂得

我的直属领导看了我的代码后首先问我,你知道ArrayList初始化容量是多少吗?他是怎么扩容的?

我:。。。。。

这俩问题对现在的程序员来说兼职就是小菜一碟,不值一提,但对当时的我来说,可就有亿点难度了,之前面试之前依稀在那个博客上看别人写过,于是乎我就照着脑袋里模糊不清的知识点模棱两可的回答了这俩问题,emmm,

于是乎 我领导就跟我说,既然你知道List容量不够会扩容,扩容会带来性能损耗(这个日后再细说,先说正事)那么你应该这么写,来避免它扩容呢?

java 复制代码
    public List<Long> getOrderIds(List<Order> orderList) {
        List<Long> ids = new ArrayList<>(orderList.size());
        for (Order order : orderList) {
            ids.add(order.getId());
        }
        return ids;
    }

千万不要小看这些细节哦

听君一席话,如听一席话,于是我悟了,

从那以后再有类似集合初始化的场景,明确知道容量的场景我都会初始化的时候传入构造参数,避免其扩容,无法知道确切容量的时候也会预估一下容量 尽可能的避免减少扩容次数。

去年

时间来到2022年,去年,我已经不是当年的那个懵懵懂懂愣头青了,坐我旁边的一个哥们(技术比我当年强多了去了),他写了一段初始化HashMap的代码也传入了一个初始容量,代码如下:

java 复制代码
    public Map<Long, Order> xxx(List<Order> orderList) {
        Map<Long, Order> orderMap = new HashMap<>(orderList.size());
        for (Order order : orderList) {
            orderMap.put(order.getId(), order);
        }
        return orderMap;
    }

敲黑板,重点来了,前面铺垫了那么多,就是为了说这事

历史惊奇在这一天重演,只不过负责问问题的是我

Q: 咳咳~HashMap的初始容量是16,放第几个个元素的时候会触发扩容呢(这题简单)

A: 元素个数超过16x0.75=12的时候进行扩容呗,扩容为16x2=32

Q: 既然容量为16,只能存12个元素,超过就会扩容,那么你写的new HashMap<>(orderList.size()) 这个能防止扩容吗?

A: emmm,不能

Q: 那初始化容量应该设置多少呢?

A: ......

Q: 16x0.75=12这个计算公式中, 初始容量变成未知假设为N 需存放的元素个数为20 Nx0.75=20N 是多少?(这大概就是经典的大学数学题吧)

A: 20➗0.75呗, 26.666 四舍五入27个, 设置容量为27,可以存放20个元素并且不触发扩容

所以正确的代码应该这么写: new HashMap<>((int) (orderList.size / 0.75 + 1))

别问为啥要+1,问就是因为小数转成int不会四舍五入直接舍弃小数点后的部分

一次轻松的对话就此结束

来看下大佬们是怎么写的

google的guava包 这是一个非常常用的java开发工具包,我从里面真的学到了很多(后续单独开篇文章记录一下)

java 复制代码
//入口
HashMap<String, String> map= Maps.newHashMapWithExpectedSize(20);

  public static <K extends @Nullable Object, V extends @Nullable Object>
      HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
    return new HashMap<>(capacity(expectedSize));
  }

  // 看这里看这里, 还考虑了一些其他的情况,专业!
  static int capacity(int expectedSize) {
    if (expectedSize < 3) {
      checkNonnegative(expectedSize, "expectedSize");
      return expectedSize + 1;
    }
    if (expectedSize < Ints.MAX_POWER_OF_TWO) {
      // This is the calculation used in JDK8 to resize when a putAll
      // happens; it seems to be the most conservative calculation we
      // can make.  0.75 is the default load factor.
      return (int) ((float) expectedSize / 0.75F + 1.0F);
    }
    return Integer.MAX_VALUE; // any large value
  }

大佬写的代码就是专业!

org.apache.curator包 无意之间发现的,实现有点意思

java 复制代码
//这里写法一样
HashMap<String, String> map = Maps.newHashMapWithExpectedSize(20);

  
    public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
        return new HashMap(capacity(expectedSize));
    }
    
    //看这里 看这里  expectedSize + expectedSize / 3
		static int capacity(int expectedSize) {
        if (expectedSize < 3) {
            CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
            return expectedSize + 1;
        } else {
            return expectedSize < 1073741824 ? expectedSize + expectedSize / 3 : Integer.MAX_VALUE;
        }
    }

expectedSize + expectedSize / 3 说实话第一次看到这段代码的时候还是有点懵的,wocc这是啥写法,后来用几个数值带入计算了一下,还真是那么回事 👍🏻👍🏻

Hutool工具包

java 复制代码
//入口
HashMap<String, String> map = MapUtil.newHashMap(20);

    public static <K, V> HashMap<K, V> newHashMap(int size) {
        return newHashMap(size, false);
    }
    
    //看这里, 平平无奇,什么档次?代码跟我写的一样,😄
    public static <K, V> HashMap<K, V> newHashMap(int size, boolean isOrder) {
        int initialCapacity = (int)((float)size / 0.75F) + 1;
        return (HashMap)(isOrder ? new LinkedHashMap(initialCapacity) : new HashMap(initialCapacity));
    }
    

说实话这个实现相较前者来说就显得不那么细了,居然跟我写的一样。。。

这件事情带来的思考

说起HashMap的知识点,晚上的文章博客简直满天飞,大家现在谁还不能说上几句,但是! 后来在我面试的很多初中级开发时,我问他们准备往Map中存放20个元素,初始化容量设置多少不会触发扩容 时,基本上很少有人能答上来,10个人当中差不多有一个能回答上来?为什么会这样呢? 明明这些人是懂的初始容量16,超过出初始容量的75%会触发扩容,反过来问一下就不会了~😒 这充分说明了,学习要融会贯通举一反三 ,要细!!!

那段代码现在怎么写

据说JDK都出到21了,最近没怎么关注过~ 不过JDK8已经流行很久了,那段代码用JDK8应该这么写:

  • list
java 复制代码
public List<Long> getOrderIds(List<Order> orderList) {
        List<Long> ids = new ArrayList<>(orderList.size());
        for (Order order : orderList) {
            ids.add(order.getId());
        }
        return ids;
    }
    
    //一行代码搞定,简洁明了
    public List<Long> getOrderIds(List<Order> orderList) {
        return orderList.stream().map(Order::getId).collect(Collectors.toList());
    }

通过StreamCollectors.toList()来返回一个崭新的List,难道就没人好奇他这个List创建的时候有没有指定容量呢?如过不指定,在上面说到的那些明确知道存放容量的场景里岂不是要白白的扩容耗费性能???

答案是:NO 我们看下来Collectors.toList()的实现

java 复制代码
public static <T>
    Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }
    
    static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
        private final Supplier<A> supplier;
        private final BiConsumer<A, T> accumulator;
        private final BinaryOperator<A> combiner;
        private final Function<A, R> finisher;
        private final Set<Characteristics> characteristics;

        CollectorImpl(Supplier<A> supplier,
                      BiConsumer<A, T> accumulator,
                      BinaryOperator<A> combiner,
                      Function<A,R> finisher,
                      Set<Characteristics> characteristics) {
            this.supplier = supplier;
            this.accumulator = accumulator;
            this.combiner = combiner;
            this.finisher = finisher;
            this.characteristics = characteristics;
        }

        CollectorImpl(Supplier<A> supplier,
                      BiConsumer<A, T> accumulator,
                      BinaryOperator<A> combiner,
                      Set<Characteristics> characteristics) {
            this(supplier, accumulator, combiner, castingIdentity(), characteristics);
        }

CollectorImplCollectors中的一个内部类,构造函数的第一个参数是Supplier<A> supplier这是一个函数式接口,就是说你得传给我一个实现,告诉我应该如何去创建一个集合,上面是这么传参的ArrayList::new, 这个写法其实就是new ArrayList(), 看到没!他并没有指定集合容量哦~~~

那么如果想提前指定好集合容量应该怎么写呢? 不卖关子了,直接贴代码了,写个B博客,真TM累死个人😌

java 复制代码
public List<Long> getOrderIds(List<Order> orderList) {
        return orderList.stream().map(Order::getId).collect(Collectors.toCollection(() -> new ArrayList<>(orderList.size())));
    }

这就行了,看下Collectors.toCollection()的源码

java 复制代码
public static <T, C extends Collection<T>>
    Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
        return new CollectorImpl<>(collectionFactory, Collection<T>::add,
                                   (r1, r2) -> { r1.addAll(r2); return r1; },
                                   CH_ID);
    }

这和Collectors.toList()基本上市一样的,只不过Collectors.toCollection()把如何创建集合的这个步骤抽象起来叫给我们开发者来个性化实现了,是不是又学到了一招~~~(#^.^#)

  • map
java 复制代码
public Map<Long, Order> xxx(List<Order> orderList) {
        Map<Long, Order> orderMap = new HashMap<>(orderList.size());
        for (Order order : orderList) {
            orderMap.put(order.getId(), order);
        }
        return orderMap;
    }
    
    
    //这点破代码用Stream也是分分钟搞定
    public Map<Long, Order> xxx(List<Order> orderList) {
        return orderList.stream().collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1));
    }

和上面的List一样,这玩意初始化Map的时候也没有指定容量

java 复制代码
public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) {
        return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
    }

Map的创建也是通过一个函数式接口Supplier<M> mapSupplier定义的,传的参数是 HashMap::new,这也是一个方法引用,写法等于 new hashMap(), 想指定容量怎么办呢? 看代码

java 复制代码
    public Map<Long, Order> xxx(List<Order> orderList) {
        return orderList.stream()
                .collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1, 
                        () -> new HashMap<>((int) (orderList.size() / 0.75 + 1))));
    }

这个写法我们同样自己掌控如何创建需要的Map,容量自己定~

写在末尾

没啥要写的了,就这吧,累挺

相关推荐
喜欢打篮球的普通人几秒前
rust高级特征
开发语言·后端·rust
代码小鑫1 小时前
A032-基于Spring Boot的健康医院门诊在线挂号系统
java·开发语言·spring boot·后端·spring·毕业设计
豌豆花下猫1 小时前
REST API 已经 25 岁了:它是如何形成的,将来可能会怎样?
后端·python·ai
喔喔咿哈哈1 小时前
【手撕 Spring】 -- Bean 的创建以及获取
java·后端·spring·面试·开源·github
夏微凉.2 小时前
【JavaEE进阶】Spring AOP 原理
java·spring boot·后端·spring·java-ee·maven
彭亚川Allen2 小时前
数据冷热分离+归档-亿级表优化
后端·性能优化·架构
Goboy2 小时前
Spring Boot 和 Hadoop 3.3.6 的 MapReduce 实战:日志分析平台
java·后端·架构
不会编程的懒洋洋3 小时前
Spring Cloud Eureka 服务注册与发现
java·笔记·后端·学习·spring·spring cloud·eureka
NiNg_1_2343 小时前
SpringSecurity入门
后端·spring·springboot·springsecurity
Lucifer三思而后行4 小时前
YashanDB YAC 入门指南与技术详解
数据库·后端