985 博士真的会舍弃华为年薪接近 100 万 offer,去选择年薪 20 万的公务员吗?

Posted Java_LingFeng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了985 博士真的会舍弃华为年薪接近 100 万 offer,去选择年薪 20 万的公务员吗?相关的知识,希望对你有一定的参考价值。

第一:从某脉软件上来看,大部分会选择进入事业单位,这是大部分人内心想法的真实写照,还仅仅只是口嗨呢?

第二:按道理来说,能读完博士应该是在某一个领悟有自己独特的见解,以个人发展和薪资的角度出发,是否甘心一毕业就进入事业单位呢?

不考虑加班等因素,不加班的公司也很多

回答一

作者:匿名用户

Top2 cs 博,今年刚拿到的华为天才少年,应该有资格回答这个问题

我身边的博士同学们,据我所知没有一个选择体制内(大学老师除外)

喜欢做学术并且博士期间学术做得比较有成就的同学会选择做学术,其他同学(也有学术巨牛逼的,比如手握十几篇顶会,一作大几篇的)基本上都会选择业界就职,包括华为,Google,MSRA 这种技术公司和量化(国内 top 量化九坤的创始人就是我们院前辈,所以我们去九坤的很多),还有前几年比较好的区块链币圈或者美元基金。当然今年就业形势很差,选择华为的会比较多

首先有一个关键性的前提先说好,到我们这读土博的,大多是普通家庭的孩子,富二代官二代是少数。一个显然的观察是,家里越有钱的,越愿意去体制内(此处包括了大学教职,也就是做科研)或者其他 wlb 更好的公司。理由很简单:资本收入远超劳动收入。中国年薪百万的不多,资产 3000w 的不少,靠打工赚钱的速度可赶不上资产升值,对有钱人来说,卖命打工就没了意义

对于大部分普通人来说,原因无非以下几点:

  • 众所周知,对没有背景的人来说,做公务员前途会比较难,但是私企是看效益的地方,有实力的人只要性格不过于偏执,是容易走出来的

  • 拍须溜马这种事我们实在是不擅长,我们天天抱怨写论文好难,但是大家心里都明白,做科研或许比当官容易

  • 不考虑灰色收入的情况下(我想身边大部分同学在考虑就业的时候并不会把灰色收入纳入未来现金流考虑,even if 以后他真的这么做了,当下也不会这么考虑),公务员的收入是远远比不上华为的

  • 本校其他专业的同学照样可以通过引进生去当副县长,并且有些专业的同学大概比我们更擅长。我们的选择会更多一点,还是更愿意选择能有差异化竞争力的地方

  • 学历一般水平一般(甚至可以说很差的)去体制内 20w 的一大堆,读书人多少有点傲气,和他们做同事只会让自己觉得不值(只是工作上的想法,做朋友没有这种问题,大家还是很包容的)

单独拿出收入这个事说一下,所谓的稳定在我眼里就是扯淡

  • 华为就算一直 100w 年薪,27 岁博士毕业做到 35 也有 600w 左右现金,就算公务员到手 30w,你算算得多少年?

  • 可能很多人不知道,华为对博士比较优待,除非重大错误,不然不会辞退博士,我们这领导很多四五十岁的。绩效和资源也会向博士倾斜。对于天才少年,人多的组可能会直接配两个本硕辅助你工作

  • 华为(包括其他私企)收入并不是固定的,20w 的公务员十年后年收入可能是 30w,但 100w 起步的博士在华为,不犯错的话,干十年之后年收入 300(这个数字还比较保守)绝对不是难事,华为对认真干活的人才不会吝啬

总而言之,对于小镇做题家(留在国内读理工类博的,小镇做题家比例会大一些,我当年就是因为家里不给钱我出国才留在国内的),除了润以外,选择去华为这种公司或者去搞金融(对于学历特别好的,即使家里没资源,也有机会能搞好),基本上是唯二的跨越阶级的稳定路径。对于家里已经很有钱或者当大官的,公务员或许是更好的选择,但他们本身就不会倾向于来读这个理工科博士,所以这个问题的答案不言而喻。

回答二

作者:匿名用户

本人港校 top3 博士,今年(2022 届)秋招刚刚拿的华为 offer,没有百万那么高,17 级 75 万左右吧(不算加班费,年终奖可能超过保底有更多)。家里爸妈都是体制内的,感觉还是和题主说的有相似之处的,可以答一答这个问题。

先说结论,我本人肯定选华为,绝不可能公务员(三四线城市公务员/事业编的人才引进 or 一线城市考公都不会选)。

说句杀人诛心的话,知乎上大多数人既不是博士,也拿不到华为的高薪 offer。他们的回答,就像是农民想象“皇帝用金扁担”那样,毫无用处。我作为一个在自己的研究方向有点小成绩的人,根本就不想去公务员体系琢磨怎么溜须拍马、混日子、升迁。B 乎上那些说公务员隐形福利多好、清闲、稳定的也是泛泛而谈。你说隐形福利好,我告诉你,除了贪咋的你还能赚得比华为多?你还能货币化折现?至于清闲、稳定,那更扯,有几个博士做研究是岁月静好清闲稳定的?我要是爱清闲的人,当初怎么会读博肝论文,写代码,为论文发愁的睡不着觉?

另外,爸妈的体制内经历也让我对公务员有了更清醒认知。这个答案下面的某些回答,纯粹是对于公务员有了过于美好的幻想。进了公务员系统的人,绝大多数和我爸妈一样,是不可能做到很高的位子的,毕竟领导职位永远只有那么几个。所以福利啥的绝不可能很好(极少数地区除外)。另外,现在年轻人进去市里的单位,写材料、整报告,忙的时候根本不比华为轻松(我挺多高中同学就是,有在本市税务系统工作的,有在市组织部的),创文、换届的时候晚上加班到 10 点是常事。

最后,这些人也不清楚,华为博士干几年要是真不想干了,是可以跳槽去小厂拿更高的薪水做管理,或者 35 岁之前还是可以回家乡城市走人才引进的。但是,要是你博士毕业立刻选择了公务员,干了几年后悔的话你是不可能跳去华为、阿里、腾讯等大厂小厂的。

综上,B 乎里一堆非博士、没拿到华为高薪 offer、对公务员有着不切实际的幻想、对华为有着道听途说得恶意的人,在大放厥词说着公务员多么香,劝着你不要去华为,还一副懂王的样子教你人生大道理,你说可不可笑。

回答三

作者:snake doctor

抛开家庭背景谈这个就是耍流氓

你一个穷山沟出来的一路凭着奖学金吃饭读出来的 985 博士,你让他放弃 100w 年薪去当公务员?除非他有远大的政治理想,否则就是脑残。

如果是爹妈在一线城市八九套房资产上亿的,你让他为了 100w 年薪去华为 996?图啥

作者:皇姑区乐购中学

链接:https://www.zhihu.com/question/477628772/answer/2414633558

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

会选体制内,当然不是口嗨。

但是首先要纠正题目中叙述的错误。

一、纠错:以税前计,华为也就是“体制内”的两倍,没有提问者说的五倍

1.1 就算信息学科博士大部分也拿不到 100 万

原问题里面说:

从某脉软件上来看,大部分会选择进入事业单位,这是大部分人内心想法….

既然是“大部分”,那肯定是取 985 博士的众数,不考虑超常人士。华为给普通博士也就 16 级 60 万左右(50-70 万)。而这样的博士,去事业单位的薪资大概在 30 万左右(20-40 万)。这也就两倍差距。

1.2 题目中是公务员,叙述中又变事业单位了

如果说公务员是放弃了专业上的领悟还行,事业单位怎么就是完全放弃专业了?

​自相矛盾

BTW,很多博士是临毕业了也真的没搞明白机关/行政编,事业单位/事业编/人事代理、公益一类,二类、三类事业单位、中央企业/国有企业、正式工(“央企编”)、内聘/外聘/劳务派遣,国有控股企业这些名词的差别。。。

二、最重要的一点是,你是在选 offer,又不是选现金

这个问题问的非常有迷惑性,好像能在 30 万现金和 60 万现金里做选择一样。如果是选钱,那人人都选 60 万现金。但你不是啊,你只是选两个 offer 而已。如果你想拿到 60 万的薪资,你本人要付出 60 万的价值,退一万步讲,你至少要付出 60 万的努力。。。最笨的道理,你工作时间变成三倍,你还觉得两倍薪资值吗?或者你工作强度变成四倍,你还觉得三倍薪资值吗?

所以不如说是在选择未来想付出 30 万还是 60 万的努力。那我选 30 万不是一件很正常的事情吗?

而且,努力大概率是不等于价值的。大伙一定听过这个故事:

某体制内业务领导,经常跟老板们有业务往来。某一老板酒席间讲客套话,说你要是辞职来我这我给 200 万。结果这个领导真就辞职去了,老板本来是客套一下,很尴尬,但又不能不要人家,就给了他 200 万年薪,但给他定了一些高难指标。但是他已经不在体制内了,人脉也人走茶凉,发挥不了 200 万的价值,达不成指标。过一阵就自己走人了。

想告诉目空一切的本科硕士小朋友们,无论你是 60 万年薪还是百万年薪,是私企还是市场化研究院,薪资都是定指标的。想拿薪资到顶,必须拿满指标,达不到指标,是根本拿不到承诺的这些钱的。

​浙江省某高校-地方政府共建研究院薪资骗局

三、我从出生开始的人生目标就是“我就一普通人,就想过好自己的小日子,不用太多钱,只要不太累太窘迫就好”。

还是用我和本科师妹,现在 Top2 读博的聊天引入

​我们努力的目的,就是为了任何时候都没必要面对窘迫,今后不用再过需要逼自己的生活。你却问我“为什么不逼自己一把多赚点”。。。

我读书科研厉害,那是我牛逼。我读书科研牛逼能找到闲差,我乐意。哪条法律规定能者一定要多劳?有能者不能正好利用自己的能力轻松点?你还想道德绑架?

再讲读博,读博士本身就是有学术动机的。我读博伊始,是热爱学术的,不图名,不图利,不图钱。后来虽然被非升即走割韭菜等因素恶心了,不再从事学术研究,但不可能突然就图名,图利,图钱了吧?

所以上面几个连开始读博都没有的硕士小朋友还是不要强答了,如果未曾有过一刻钟为学术献身的想法,怎么强行替博士回答这道有关金钱观的问题?

四、人活着是要尊严的

我赚钱是为了能堂堂正正体体面面的站着,你竟然问我“为什么不跪着挣更多钱呢”

​我要是签了这种协议让我妈知道,她得怀疑自己花这么多钱和精力培养出来的博士儿子是图什么,remake 算了

顺便说一下,这就是很多人 “不知道自己想要什么”,迷茫不已的原因,因为努力的动机和努力在做的事根本就是矛盾的。。。

五、别打鸡血了,35 岁失业是一种结构化失业

其实与题主类似的问题我根据自己的经历和感悟回答过。35 岁失业是一种结构化失业,是因为他们试图让 40 岁的中老年人工作效率是 20 岁年轻人的两倍,人类达不到,所以只能结构性失业。只有“体制内”(和部分外资企业),才能允许 40 岁员工干的活比 20 岁干的少,带来更加符合人体客观条件的职业发展路径。

20 岁时候我可以活蹦乱跳,但 40 岁时候我也得像王菊一样靠西地那非。体制内现在真不轻松,但体制内 40 岁一定比 20 岁轻松。

有兴趣您可以读一读,不过毕竟跟话题的开始远了,不读也罢。

以上列了五条,直接回答楼主的问题的还是第二条。

回答四

作者:李月亭

链接:

https://www.zhihu.com/question/477628772/answer/2526296300

找工作,除了看钱,还要考虑一下未来的发展空间。

1、985 博士毕业也未必一来就有 100 万,能搞到每年 100 万的终究是极少数。部分博士开到 50、60 还是可以的。我师弟 985 博士毕业去的深圳大疆,第一年 50。这个数可能才是部分博士的价格。如果真的个个博士毕业能拿到 100 万的话,那可能去企业的还是多点,毕竟给的太多了。

2、如果只是拿一般的 50 万,那公务员的 20 万就很有吸引力了。因为体制内工作的福利比较好,可以用较少的钱,达到较好的生活品质。公务员,只要不是北京、上海,这两个地方房价太高了,其他城市的公务员收入至少在中等吧,而且是每个月固定时间发给你。同时,五险一金交齐,公积金也是顶格发放,甚至还有补充公积金。单看公务员的每月到手工资,那确实不高,但是综合收入其实还是很不错的。

对于公务员,只要你借的到房子首付,买房子就没什么大问题。月供,公积金都抵消大半了。如果两口子都是公务员,那公积金还房贷都有剩余。

虽说现在公务员的奖金绩效减少了,但是相比于体制外,各项保障工作还是不错的。你在发达城市月入两万,可能租房就花去 5000,还是跟人合租。想要买房,可能又没有户口,再者钱也不够。即使凑够首付买了房,月供提心吊胆,生怕被裁员。

体制内确实活钱不多,消费水平也不如体制外的企业高薪人员。但是,论生活质量,公务员未必比外面差,甚至大多数情况下,都要好很多。

3、题主似乎很看不起事业单位,所谓“是否甘心一毕业就进入事业单位呢?”,好像事业单位就很差。其实,我想说的是,稳定已经是很大的幸福。确实挣钱没有那么多,但是也不至于那么穷。而且,进了体制,并不意味你就躺平、一辈子无所事事了。体制内还是有很大成长空间的,你只要有能力,一样可以做出很大的成就,一样可以让你的人生过得非常充实。

4、疫情影响之下,博士想进体制内也没那么容易。前几天学院一批面试了 15 个博士,这在往年是不可能的。往年都是来一个,面试一个,而且通知你来面试的,通过的概率挺大的。但是,今年已经是各种挑人了。论文数量已经不是什么重要因素了,因为大家都有。现在要看你得研究方向,是否能跟学院发展结合。太传统的方向,直接 pass。最终有五篇中科院一区的博士被刷掉。

另外,博士想考公务员,现在也没有什么优势了。前几年,博士走定向选调还不用笔试,面试过关直接录用。现在要跟本科、硕士一起参加考试了,笔试不过,一切免谈。

大家都是用脚投票的,体制内要是真的那么不好,就不会这么火热、竞争这么大。

5、大部分都是普通人

实话讲,绝大部分都是普通人。家庭并不富裕,父母身体可能还有一些小问题。去外面闯当然是好的,因为你可能挣到大钱,彻底扬眉吐气,令所有人刮目相看。

但是,这样的机会是很少的。大部分人注定是要失败的,也注定是要回归平淡的。简单的说,绝大部分人,并不适合去商海闯荡、不适合创业,他们更需要一份稳定的工作。稳定意味着,进可攻退可守。你有能力,稳定工作一样能让你出人头地,能力普通,至少有个保障。

我的建议就是,找工作,不能只看眼前谁开的钱多。还是要考虑一下自己到底需要什么、适合什么,以及这份工作是否符合自己的职业发展规划。以及,如果当前这份工作失败,我是否能承受的起,等等。

从发展的角度看择业,不要只关注眼前的一点利益,也许会有助于你的职业选择。

-End-

满地坑!细数List的10个坑

推荐阅读:985 博士真的会舍弃华为年薪接近 100 万 offer,去选择年薪 20 万的公务员吗?

前言

大家好,今天我们主要来说一说List操作在实际使用中有哪些坑,以及面对这些坑的时候我们要怎么解决。

Arrays.asList转换基本类型数组的坑

在实际的业务开发中,我们通常会进行数组转List的操作,通常我们会使用Arrays.asList来进行转换

但是在转换基本类型的数组的时候,却出现转换的结果和我们想象的不一致。

上代码

int[] arr = 1, 2, 3; 
List list = Arrays.asList(arr); 
System.out.println(list.size()); 
// 1

实际上,我们想要转成的List应该是有三个对象而现在只有一个

public static List asList(T... a)  
    return new ArrayList<>(a); 

可以观察到 asList方法 接收的是一个泛型T类型的参数,T继承Object对象

所以通过断点我们可以看到把 int数组 整体作为一个对象,返回了一个 List<int[]>

Arrays.asList不好用

「那我们该如何解决呢?」

方案一:Java8以上,利用Arrays.stream(arr).boxed()将装箱为Integer数组

List collect = Arrays.stream(arr).boxed().collect(Collectors.toList()); System.out.println(collect.size()); 
System.out.println(collect.get(0).getClass()); 
// 3 
// class java.lang.Integer

方案二:声明数组的时候,声明类型改为包装类型

Integer[] integerArr = 1, 2, 3; 
List integerList = Arrays.asList(integerArr); 
System.out.println(integerList.size()); System.out.println(integerList.get(0).getClass()); 
// 3 
// class java.lang.Integer

Arrays.asList返回的List不支持增删操作

我们将数组对象转成List数据结构之后,竟然不能进行增删操作了

private static void asListAdd()
    String[] arr = "1", "2", "3";
    List<String> strings = new ArrayList<>(Arrays.asList(arr));
    arr[2] = "4";
    System.out.println(strings.toString());
    Iterator<String> iterator = strings.iterator();
    while (iterator.hasNext())
        if ("4".equals(iterator.next()))
            iterator.remove();
        
    
    strings.forEach(val ->
        strings.remove("4");
        strings.add("3");
    );


    System.out.println(Arrays.asList(arr).toString());


[1, 2, 4] 
Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.remove(AbstractList.java:161) at java.util.AbstractList$Itr.remove(AbstractList.java:374) at java.util.AbstractCollection.remove(AbstractCollection.java:293) at JavaBase.List.AsListTest.lambda$asListAdd$0(AsListTest.java:47) at java.util.Arrays$ArrayList.forEach(Arrays.java:3880) at JavaBase.List.AsListTest.asListAdd(AsListTest.java:46) at JavaBase.List.AsListTest.main(AsListTest.java:20)

初始化一个字符串数组,将字符串数组转换为 List,在遍历List的时候进行移除和新增的操作

抛出异常信息UnsupportedOperationException。

根据异常信息java.lang.UnsupportedOperationException,我们看到他是从AbstractList里面出来的,让我们进入源码一看究竟

我们在什么时候调用到了这个 AbstractList 呢?

其实 Arrays.asList(arr) 返回的 ArrayList 不是 java.util.ArrayList,而是 Arrays的内部类

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    private static final long serialVersionUID = -2764017481108945198L;
    private final E[] a;
    ArrayList(E[] array) 
        a = Objects.requireNonNull(array);
    

    @Override
    public E get(int index) 

    @Override
    public E set(int index, E element) ...

...

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> 
    public boolean add(E e) 
        add(size(), e);
        return true;
    
    public void add(int index, E element) 
        throw new UnsupportedOperationException();
    

    public E remove(int index) 
        throw new UnsupportedOperationException();
    

他是没有实现 AbstractList 中的 add() 和 remove() 方法,这里就很清晰了为什么不支持新增和删除,因为根本没有实现。

对原始数组的修改会影响到我们获得的那个List

一不小心修改了父List,却影响到了子List,在业务代码中,这会导致产生的数据发生变化,严重的话会造成影响较大的生产问题。

第二个坑的源码中,完成字符串数组转换为List之后,

我们将字符串数组的第三个对象的值修改为4,但是很奇怪在打印List的时候,发现List也发生了变化。

public static <T> List<T> asList(T... a) 

    return new ArrayList<>(a);



ArrayList(E[] array) 
    a = Objects.requireNonNull(array);

asList中创建了 ArrayList,但是他直接引用了原本的数组对象

所以只要原本的数组对象一发生变化,List也跟着变化

所以在使用到引用的时候,我们需要特别的注意。

解决方案:

重新new一个新的 ArrayList 来装返回的 List

List strings = new ArrayList<>(Arrays.asList(arr));

java.util.ArrayList如果不正确操作也不支持增删操作

在第二个坑的时候,我们说到了 Arrays.asList 返回的 List 不支持增删操作,

是因为他的自己实现了一个内部类 ArrayList,这个内部类继承了 AbstractList 没有实现 add() 和 remove() 方法导致操作失败。

但是第三个坑的时候,我们利用 java.util.ArrayList 包装了返回的 List,进行增删操作还是会失败,那是为什么呢?

删除方法逻辑:

删除方法

在foreach中操作增删,因为因为 modCount 会被修改,与第一步保存的数组修改次数不一致,抛出异常 ConcurrentModificationException

在正确操作是什么?我总结了四种方式

正确操作

ArrayList中的 subList 强转 ArrayList 导致异常

阿里《Java开发手册》上提过

[强制] ArrayList的sublist结果不可強转成ArrayList,否则会抛出ClassCastException

异常,即java.util.RandomAccesSubList cannot be cast to java. util.ArrayList.

说明: subList 返回的是ArrayList 的内部类SubList, 并不是ArrayList ,而是

ArrayList的一个视图,対于SubList子列表的所有操作最终会反映到原列表上。

private static void subListTest()

    List<String> names = new ArrayList<String>() 

    add("one");

    add("two");

    add("three");

;
    ArrayList strings = (ArrayList) names.subList(0, 1);
    System.out.println(strings.toString());


Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

我猜问题十有八九就是出现在subList这个方法上了

private class SubList extends AbstractList<E> implements RandomAccess 

    private final AbstractList<E> parent;
    private final int parentOffset;
    private final int offset;
    int size;
    SubList(AbstractList<E> parent,

    int offset, int fromIndex, int toIndex) 
    this.parent = parent;
    this.parentOffset = fromIndex;
    this.offset = offset + fromIndex;
    this.size = toIndex - fromIndex;
    this.modCount = ArrayList.this.modCount;

其实 SubList 是一个继承 AbstractList 的内部类,在 SubList 的构建函数中的将 List 中的部分属性直接赋予给自己

SubList 没有创建一个新的 List,而是直接引用了原来的 List(this.parent = parent),指定了元素的范围

所以 subList 方法不能直接转成 ArrayList,他只是ArrayList的内部类,没有其他的关系

因为是引用的关系,所以在这里也需要特别的注意,如果对原来的List进行修改,会对产生的 subList结果产生影响。

List<String> names = new ArrayList<String>() 
    add("one");
    add("two");
    add("three");
;

List strings = names.subList(0, 1);

strings.add(0, "ongChange");

System.out.println(strings.toString());

System.out.println(names.toString());

[ongChange, one]

[ongChange, one, two, three]

对subList产生的List做出结构型修改,操作会反应到原来的List上,ongChange也添加到了names中

如果修改原来的List则会抛出异常ConcurrentModificationException

List<String> names = new ArrayList<String>() 

    add("one");
    add("two");
    add("three");

;

List strings = names.subList(0, 1);

names.add("four");

System.out.println(strings.toString());

System.out.println(names.toString());

Exception in thread "main" java.util.ConcurrentModificationException

原因:

subList的时候记录this.modCount为3

ConcurrentModificationException

原来的List插入了一个新元素,导致this.modCount不第一次保存的不一致则抛出异常

解决方案:在操作SubList的时候,new一个新的ArrayList来接收创建subList结果的拷贝

List strings = new ArrayList(names.subList(0, 1));

ArrayList中的subList切片造成OOM

在业务开发中的时候,他们经常通过subList来获取所需要的那部分数据

在上面的例子中,我们知道了subList所产生的List,其实是对原来List对象的引用

这个产生的List只是原来List对象的视图,也就是说虽然值切片获取了一小段数据,但是原来的List对象却得不到回收,这个原来的List对象可能是一个很大的对象

为了方便我们测试,将vm调整一下 -Xms20m -Xmx40m

private static void subListOomTest()

    IntStream.range(0, 1000).forEach(i ->
        List<Integer> collect = IntStream.range(0, 100000).boxed().collect(Collectors.toList());
            data.add(collect.subList(0, 1));

        );

    

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

出现OOM的原因,循环1000次创建了1000个具有10万个元素的List

因为始终被collect.subList(0, 1)强引用,得不到回收

解决方式:

  1. 在subList方法返回SubList,重新使用new ArrayList,来构建一个独立的ArrayList

List list = new ArrayList<>(collect.subList(0, 1));
  1. 利用Java8的Stream中的skip和limit来达到切片的目的

List list = collect.stream().skip(0).limit(1).collect(Collectors.toList());

在这里我们可以看到,只要用一个新的容器来装结果,就可以切断与原始List的关系

LinkedList的插入速度不一定比ArrayList快

学习数据结构的时候,我们就已经得出了结论

●对于数组,随机元素访问的时间复杂度是0(1), 元素插入操作是O(n);

●对于链表,随机元素访问的时间复杂度是O(n), 元素插入操作是0(1).

元素插入对于链表来说应该是他的优势

但是他就一定比数组快? 我们执行插入1000w次的操作

private static void test()
    StopWatch stopWatch = new StopWatch();
    int elementCount = 100000;
    stopWatch.start("ArrayList add");
    List<Integer> arrayList = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(ArrayList::new));
    // ArrayList插入数据
    IntStream.rangeClosed(0, elementCount).forEach(i ->arrayList.add(ThreadLocalRandom.current().nextInt(elementCount), 1));
    stopWatch.stop();

    stopWatch.start("linkedList add");
    List<Integer> linkedList = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(LinkedList::new));
    // ArrayList插入数据
    IntStream.rangeClosed(0, elementCount).forEach(i -> linkedList.add(ThreadLocalRandom.current().nextInt(elementCount), 1));
    stopWatch.stop();
    System.out.println(stopWatch.prettyPrint());


StopWatch '': running time = 44507882 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
043836412  098%  elementCount 100 ArrayList add
000671470  002%  elementCount 100 linkedList add

StopWatch '': running time = 196325261 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
053848980  027%  elementCount 10000 ArrayList add
142476281  073%  elementCount 10000 linkedList add

StopWatch '': running time = 26384216979 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
978501580  004%  elementCount 100000 ArrayList add
25405715399  096%  elementCount 100000 linkedList add

看到在执行插入1万、10完次操作的时候,LinkedList的插入操作时间是 ArrayList的两倍以上

那问题主要就是出现在linkedList的 add()方法上

public void add(int index, E element) 

    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));

    
/**
* Returns the (non-null) Node at the specified element index.
    */
Node<E> node(int index) 

    // assert isElementIndex(index);

    if(index < (size >> 1)) 
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
     else 
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    

linkedList的 add()方法主要逻辑

  1. 通过遍历找到那个节点的Node

  2. 执行插入操作

ArrayList的 add()方法

public void add(int index, E element) 
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
  1. 计算最小容量

  2. 最小容量大于数组对象,则进行扩容

  3. 进行数组复制,根据插入的index将数组向后移动一位

  4. 最后在空位上插入新值

根据试验的测试,我们得出了在实际的随机插入中,LinkedList并没有比ArrayList的速度快

所以在实际的使用中,如果涉及到头尾对象的操作,可以使用LinkedList数据结构来进行增删的操作,发挥LinkedList的优势

最好再进行实际的性能测试评估,来得到最合适的数据结构。

CopyOnWriteArrayList内存占用过多

CopyOnWrite,顾名思义就是写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。

CopyOnWriteArrayList的add()方法

public boolean add(E e) 
    // 获取独占锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try 
        // 获取array
        Object[] elements = getArray();
        // 复制array到新数组,添加元素到新数组
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        // 替换数组
        setArray(newElements);
        return true;
     finally 
        // 释放锁
        lock.unlock();
    

CopyOnWriteArrayList 内部维护了一个数组,成员变量 array 就指向这个内部数组,所有的读操作都是基于新的array对象进行的。

因为上了独占锁,所以如果多个线程调用add()方法只有一个线程会获得到该锁,其他线程被阻塞,知道锁被释放, 由于加了锁,所以整个操作的过程是原子性操作

CopyOnWriteArrayList 会将 新的array复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将复制的结果指向这个新的数组。

由于每次写入的时候都会对数组对象进行复制,复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,所以当列表中的元素比较少的时候,这对内存和 GC 并没有多大影响,但是当列表保存了大量元素的时候,

对 CopyOnWriteArrayList 每一次修改,都会重新创建一个大对象,并且原来的大对象也需要回收,这都可能会触发 GC,如果超过老年代的大小则容易触发Full GC,引起应用程序长时间停顿。

CopyOnWriteArrayList是弱一致性的

public Iterator<E> iterator() 
    return new COWIterator<E>(getArray(), 0);


static final class COWIterator<E> implements ListIterator<E> 
    /** Snapshot of the array */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) 
        cursor = initialCursor;
        snapshot = elements;
    

    public boolean hasNext() 
        return cursor < snapshot.length;
    

    public boolean hasPrevious() 
        return cursor > 0;
    

    @SuppressWarnings("unchecked")
    public E next() 
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    

调用iterator方法获取迭代器返回一个COWIterator对象

COWIterator的构造器里主要是 保存了当前的list对象的内容和遍历list时数据的下标。

snapshot是list的快照信息,因为CopyOnWriteArrayList的读写策略中都会使用getArray()来获取一个快照信息,生成一个新的数组。

所以在使用该迭代器元素时,其他线程对该lsit操作是不可见的,因为操作的是两个不同的数组所以造成弱一致性。

private static void CopyOnWriteArrayListTest()
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList();
    list.add("test1");
    list.add("test2");
    list.add("test3");
    list.add("test4");
    
    Thread thread = new Thread(() -> 
        System.out.println(">>>> start");
        list.add(1, "replaceTest");
        list.remove(2);
    );
    
    // 在启动线程前获取迭代器
    Iterator<String> iterator = list.iterator();

    thread.start();

    try 
        // 等待线程执行完毕
        thread.join();
     catch (InterruptedException e) 
        e.printStackTrace();
    

    while (iterator.hasNext())
        System.out.println(iterator.next());
    


>>>> start
test1
test2
test3
test4

上面的demo中在启动线程前获取到了原来list的迭代器,

在之后启动新建一个线程,在线程里面修改了第一个元素的值,移除了第二个元素

在执行完子线程之后,遍历了迭代器的元素,发现子线程里面操作的一个都没有生效,这里提现了迭代器弱一致性。

CopyOnWriteArrayList的迭代器不支持增删改

private static void CopyOnWriteArrayListTest()
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("test1");
    list.add("test2");
    list.add("test3");
    list.add("test4");

    Iterator<String> iterator = list.iterator();

    while (iterator.hasNext())
        if ("test1".equals(iterator.next()))
            iterator.remove();
        
    

    System.out.println(list.toString());


Exception in thread "main" java.lang.UnsupportedOperationException
 at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178)

CopyOnWriteArrayList 迭代器是只读的,不支持增删操作

CopyOnWriteArrayList迭代器中的 remove()和 add()方法,没有支持增删而是直接抛出了异常

因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。

/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; @code remove
 *         is not supported by this iterator.
 */
public void remove() 
    throw new UnsupportedOperationException();


/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; @code set
 *         is not supported by this iterator.
 */
public void set(E e) 
    throw new UnsupportedOperationException();


/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; @code add
 *         is not supported by this iterator.
 */
public void add(E e) 
    throw new UnsupportedOperationException();

总结

由于篇幅的限制,我们只对一些在业务开发中常见的关键点进行梳理和介绍

在实际的工作中,我们不单单是要清除不同类型容器的特性,还要选择适合的容器才能做到事半功倍。

我们主要介绍了Arrays.asList转换过程中的一些坑,以及因为操作不当造成的OOM和异常,

到最后介绍了线程安全类CopyOnWriteArrayList的一些坑,让我们认识到在丰富的API下藏着许多的陷阱。

在使用的过程中,需要更加充分的考虑避免这些隐患的发生。

最后一张思维导图来回顾一下~

List中的坑

来源:juejin.cn/post/7143266514722881544

以上是关于985 博士真的会舍弃华为年薪接近 100 万 offer,去选择年薪 20 万的公务员吗?的主要内容,如果未能解决你的问题,请参考以下文章

腾讯员工痛诉从阿里来的同事秀“内卷”

200 万年薪!西交大 2 位计算机博士入选华为天才少年

武大94年暖男型博士入选华为“天才少年”计划,最高年薪201万

武大94年暖男型博士入选华为“天才少年”计划,最高年薪201万

武大94年暖男型博士入选华为“天才少年”计划,最高年薪201万

94 年武大博士 201 万年薪入职华为!学霸日程表曝光,这简直降维打击。。