<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          這些Java8官方挖的坑,你踩過幾個(gè)?

          共 3094字,需瀏覽 7分鐘

           ·

          2020-08-22 05:38

          導(dǎo)讀:系統(tǒng)啟動(dòng)異常日志竟然被JDK吞噬無法定位?同樣的加密方法,竟然出現(xiàn)部分?jǐn)?shù)據(jù)解密失敗?往List里面添加數(shù)據(jù)竟然提示不支持?日期明明間隔1年卻輸出1天,難不成這是天上人間?1582年神秘消失的10天JDK能否識(shí)別?Stream很高大上,List轉(zhuǎn)Map卻全失敗……這些JDK8官方挖的坑,你踩過幾個(gè)?

          目錄:

          • Base64:你是我解不開的迷?

          • 被吞噬的異常:我不敢說出你的名字

          • 日期計(jì)算:我想留住時(shí)間,讓1天像1年那么長?

          • List:一如你我初見,不增不減?

          • Stream處理:給你,獨(dú)一無二?


          01

          Base64:你是我解不開的迷


          出于用戶隱私信息保護(hù)的目的,系統(tǒng)上需將姓名、身份證、手機(jī)號(hào)等敏感信息進(jìn)行加密存儲(chǔ),很自然選擇了AES算法,外面又套了一層Base64,之前用的是sun.misc.BASE64Decoder/BASE64Encoder,網(wǎng)上的資料基本也都是這種寫法,運(yùn)行得很完美。但這種寫法在idea或者maven編譯時(shí)就會(huì)有一些黃色告警提示。到了Java 8后,Base64編碼已經(jīng)成為Java類庫的標(biāo)準(zhǔn),內(nèi)置了 Base64 編碼的編碼器和解碼器。于是乎,我手賤地修改了代碼,改用了jdk8自帶的Base64方法

          import?java.util.Base64;

          public?class?Base64Utils?{

          ????public?static?final?Base64.Decoder?DECODER?=?Base64.getDecoder();
          ????public?static?final?Base64.Encoder?ENCODER?=?Base64.getDecoder();

          ????public?static?String?encodeToString(byte[]?textByte)?{
          ????????return?ENCODER.encodeToString(textByte);
          ????}

          ????public?static?byte[]?decode(String?str)?{
          ????????return?DECODER.decode(str);
          ????}

          }

          程序員的職業(yè)操守咱還是有的,構(gòu)造新老數(shù)據(jù)、自測(cè)、通過,提交測(cè)試版本。信心滿滿,我要繼續(xù)延續(xù)我 0 Bug的神話!然后……然后版本就被打回了。

          Caused?by:?java.lang.IllegalArgumentException:?Illegal?base64?character?3f
          ????at?java.util.Base64$Decoder.decode0(Base64.java:714)
          ????at?java.util.Base64$Decoder.decode(Base64.java:526)
          ????at?java.util.Base64$Decoder.decode(Base64.java:549)

          關(guān)鍵是這個(gè)錯(cuò)還很詭異,部分?jǐn)?shù)據(jù)是可以解密的,部分解不開

          Base64依賴于簡單的編碼和解碼算法,使用65個(gè)字符的US-ASCII子集,其中前64個(gè)字符中的每一個(gè)都映射到等效的6位二進(jìn)制序列,第65個(gè)字符(=)用于將Base64編碼的文本填充到整數(shù)大小。后來產(chǎn)生了3個(gè)變種:

          • RFC 4648:Basic, 此變體使用RFC 4648和RFC 2045的Base64字母表進(jìn)行編碼和解碼。編碼器將編碼的輸出流視為一行; 沒有輸出行分隔符。解碼器拒絕包含Base64字母表之外的字符的編碼。
          • RFC 2045:MIME ,此變體使用RFC 2045提供的Base64字母表進(jìn)行編碼和解碼。編碼的輸出流被組織成不超過76個(gè)字符的行; 每行(最后一行除外)通過行分隔符與下一行分隔。解碼期間將忽略Base64字母表中未找到的所有行分隔符或其他字符。
          • RFC 4648:Url,?此變體使用RFC 4648中提供的Base64字母表進(jìn)行編碼和解碼。字母表與前面顯示的字母相同,只是-替換+和_替換/。不輸出行分隔符。解碼器拒絕包含Base64字母表之外的字符的編碼。
          S.N.方法名稱 & 描述
          1static Base64.Decoder getDecoder()
          返回Base64.Decoder解碼使用基本型base64編碼方案。
          2static Base64.Encoder getEncoder()
          返回Base64.Encoder編碼使用的基本型base64編碼方案。
          3static Base64.Decoder getMimeDecoder()
          返回Base64.Decoder解碼使用MIME類型的base64解碼方案。
          4static Base64.Encoder getMimeEncoder()
          返回Base64.Encoder編碼使用MIME類型base64編碼方案。
          5static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator)
          返回Base64.Encoder編碼使用指定的行長度和線分隔的MIME類型base64編碼方案。
          6static Base64.Decoder getUrlDecoder()
          返回Base64.Decoder解碼使用URL和文件名安全型base64編碼方案。
          7static Base64.Encoder getUrlEncoder()
          返回Base64.Decoder解碼使用URL和文件名安全型base64編碼方案。

          關(guān)于base64用法的詳細(xì)說明,可參考:https://juejin.im/post/5c99b2976fb9a070e76376cc

          對(duì)于上面的錯(cuò)誤,網(wǎng)上有的說法是,建議使用Base64.getMimeDecoder()Base64.getMimeEncoder(),對(duì)此我只能建議:老的系統(tǒng)如果已經(jīng)有數(shù)據(jù)了,就不要使用jdk自帶的Base64了。JDK官方的Base64和sun的base64是不兼容的!不要替換!不要替換!不要替換!


          02

          被吞噬的異常:我不敢說出你的名字


          這個(gè)問題理解起來還是蠻費(fèi)腦子的,所以我把這個(gè)系統(tǒng)異常發(fā)生的過程提煉成了一個(gè)美好的故事,放松一下,吟詩一首!

          最怕相思濃
          一切皆是你
          唯獨(dú)
          不敢說出你的名字
          -- 碼大叔

          這個(gè)問題是在使用springboot的注解時(shí)遇到的,發(fā)現(xiàn)JDK在解析注解時(shí),若注解依賴的類定義在JVM加載時(shí)不存在,也就是NoClassDefFoundError時(shí),實(shí)際拿到的異常將會(huì)是ArrayStoreException,而不是NoClassDefFoundError,涉及到的JDK里的類是AnnotationParser.java, 具體代碼如下:

          private?static?Object?parseClassArray(int?paramInt,?ByteBuffer?paramByteBuffer,?ConstantPool?paramConstantPool,?Class?paramClass)?{
          ????Class[]?arrayOfClass?=?new?Class[paramInt];
          ????int?i?=?0;
          ????int?j?=?0;
          ????for?(int?k?=?0;?k?????????j?=?paramByteBuffer.get();
          ????????if?(j?==?99)?{
          ????????????//?注意這個(gè)方法
          ?????????arrayOfClass[k]?=?parseClassValue(paramByteBuffer,?paramConstantPool,?paramClass);
          ????????}?else?{
          ?????????skipMemberValue(j,?paramByteBuffer);
          ?????????i?=?1;
          ????????}
          ????}
          ????return?i?!=?0???exceptionProxy(j)?:?arrayOfClass;
          }
          private?static?Object?parseClassValue(ByteBuffer?paramByteBuffer,?ConstantPool?paramConstantPool,?Class?paramClass)?{
          ????int?i?=?paramByteBuffer.getShort()?&?0xFFFF;
          ????try
          ????{
          ????????String?str?=?paramConstantPool.getUTF8At(i);
          ????????return?parseSig(str,?paramClass);
          ????}?catch?(IllegalArgumentException?localIllegalArgumentException)?{
          ????????return?paramConstantPool.getClassAt(i);
          ????}?catch?(NoClassDefFoundError?localNoClassDefFoundError)?{
          ?????????//?注意這里,異常發(fā)生了轉(zhuǎn)化
          ????????return?new?TypeNotPresentExceptionProxy("[unknown]",?localNoClassDefFoundError);
          ????}?catch?(TypeNotPresentException?localTypeNotPresentException)?{
          ????????return?new?TypeNotPresentExceptionProxy(localTypeNotPresentException.typeName(),?localTypeNotPresentException.getCause());
          ????}
          }

          parseClassArray這個(gè)方法中,預(yù)期parseClassValue返回Class對(duì)象,但看實(shí)際parseClassValue的邏輯,在遇到NoClassDefFoundError時(shí),返回的是TypeNotPresentExceptionProxy,由于類型強(qiáng)轉(zhuǎn)失敗,最終拋出的是java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,此時(shí)只能通過debug到這行代碼,找到具體是缺少哪個(gè)類定義,才能解決這個(gè)問題

          筆者重現(xiàn)一下發(fā)現(xiàn)這個(gè)坑的場景,有三個(gè)module,module3依賴module2但未聲明依賴module1,module2依賴module1,但聲明的是optional類型,依賴關(guān)系圖如下:

          上面每個(gè)module中有一個(gè)Class,我們命名為ClassInModuleX。ClassInModule3啟動(dòng)時(shí)在注解中使用了ClassInModule2的類,而ClassInModule2這個(gè)類的繼承了ClassInModule1,這幾個(gè)類的依賴關(guān)系圖如下:

          如此,其實(shí)很容易知道在module運(yùn)行ClassInModule3時(shí),會(huì)出現(xiàn)ClassInModule1的NoClassDefFoundError的,但實(shí)際運(yùn)行時(shí),你能看到的異常將不是NoClassDefFoundError,而是java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,此時(shí),若想要知道具體是何許異常,需通過debug在AnnotationParser中定位具體問題,以下展示兩個(gè)截圖,分別對(duì)應(yīng)系統(tǒng)控制臺(tái)實(shí)際拋出的異常和通過debug發(fā)現(xiàn)的異常信息。

          控制臺(tái)異常信息:注意異常實(shí)際在紅色圈圈這里,自動(dòng)收縮了,需要展開才可以看到通過debug發(fā)現(xiàn)的異常信息:

          如果你想體驗(yàn)這個(gè)示例,可關(guān)注公眾號(hào)碼大叔和筆者交流。如果你下次遇到莫名的java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,請(qǐng)記得用這個(gè)方法定位具體問題。


          03

          日期計(jì)算:我想留住時(shí)間,讓1天像1年那么長


          Java8之前日期時(shí)間操作相當(dāng)?shù)芈闊瑹o論是Calendar還是SimpleDateFormat都讓你覺得這個(gè)設(shè)計(jì)怎么如此地反人類,甚至還會(huì)出現(xiàn)多線程安全的問題,阿里巴巴開發(fā)手冊(cè)中就曾禁用static修飾SimpleDateFormat。好在千呼萬喚之后,使出來了,Java8帶來了全新的日期和時(shí)間API,還帶來了Period和Duration用于時(shí)間日期計(jì)算的兩個(gè)API。

          Duraction和Period,都表示一段時(shí)間的間隔,Duraction正常用來表示時(shí)、分、秒甚至納秒之間的時(shí)間間隔,Period正常用于年、月、日之間的時(shí)間間隔。

          網(wǎng)上的大部分文章也是這么描述的,于是計(jì)算兩個(gè)日期間隔可以寫成下面這樣的代碼:

          // parseToDate方法作用是將String轉(zhuǎn)為LocalDate,略。
          LocalDate?date1?=?parseToDate("2020-05-12");
          LocalDate?date2?=?parseToDate("2021-05-13");
          //?計(jì)算日期間隔
          int?period?=?Period.between(date1,date2).getDays();

          一個(gè)是2020年,一個(gè)是2021年,你認(rèn)為間隔是多少?1年? 恭喜你,和我一起跳進(jìn)坑里了(畫外音:里面的都擠一擠,動(dòng)一動(dòng),又來新人了)。

          正確答案應(yīng)該是:1天。

          這個(gè)單詞的含義以及這個(gè)方法看起來確實(shí)是蠻誤導(dǎo)人的,一不注意就會(huì)掉進(jìn)坑里。Period其實(shí)只能計(jì)算同月的天數(shù)、同年的月數(shù),不能計(jì)算跨月的天數(shù)以及跨年的月數(shù)。

          正確寫法1

          ?long?period?=?date2.toEpochDay()-date1.toEpochDay();

          toEpochDay():將日期轉(zhuǎn)換成Epoch 天,也就是相對(duì)于1970-01-01(ISO)開始的天數(shù),和時(shí)間戳是一個(gè)道理,時(shí)間戳是秒數(shù)。顯然,該方法是有一定的局限性的

          正確寫法2

          long?period?=?date1.until(date2,ChronoUnit.DAYS);

          使用這個(gè)寫法,一定要注意一下date1和date2前后順序:date1 until date2。

          正確做法3(推薦)

          ?long?period?=?ChronoUnit.DAYS.between(date1,?date2);

          ChronoUnit:一組標(biāo)準(zhǔn)的日期時(shí)間單位。這組單元提供基于單元的訪問來操縱日期,時(shí)間或日期時(shí)間。這些單元適用于多個(gè)日歷系統(tǒng)。這是一個(gè)最終的、不可變的和線程安全的枚舉。

          看到”適用于多個(gè)日歷系統(tǒng)“這句話,我一下子想起來歷史上1582年神秘消失的10天,在JDK8上是什么效果呢?1582-10-15和1582-10-04你覺得會(huì)相隔幾天呢?11天還是1天?有興趣的小伙伴自己去寫個(gè)代碼試試吧。


          打開你的手機(jī),跳轉(zhuǎn)到1582年10月,你就能看到這消失的10天了。


          04

          List:一如你我初見,不增不減


          這個(gè)問題其實(shí)在JDK里存在很多年了,JDK8中依然存在,也是很多人最容易跳的一個(gè)坑!直接上代碼:

          public?List?allUser()?{
          ????//?省略
          ????List?currentUserList?=?getUser();
          ????currentUserList.add("碼大叔");
          ????//?省略
          }

          就是上面這樣一段代碼,往一個(gè)list里添加一條數(shù)據(jù),你覺得結(jié)果是什么呢?“碼大叔”成功地添加到了List里?天真,不報(bào)個(gè)錯(cuò)你怎么能意識(shí)到JDK存在呢。

          Exception?in?thread?"main"?java.lang.UnsupportedOperationException
          ????at?java.util.AbstractList.add(AbstractList.java:148)

          原因:因?yàn)樵趃etUser方法里,返回的List使用的是Arrays.asList生成的,示例:

          ????private?List?getUser(){
          ????????return?Arrays.asList("劍圣","小九九");
          ????}

          我們來看看Arrays.asList的源碼

          ????@SafeVarargs
          ????@SuppressWarnings("varargs")
          ????public?static??List?asList(T...?a)?{
          ????????return?new?ArrayList<>(a);
          ????}
          ?private?static?class?ArrayList<E>?extends?AbstractList<E>
          ????????implements?RandomAccess,?java.io.Serializable
          ????
          {
          ?????private?final?E[]?a;
          ????????//?部分代碼略
          ????????ArrayList(E[]?array)?{
          ????????????//?返回的是一個(gè)定長的數(shù)組
          ????????????a?=?Objects.requireNonNull(array);
          ????????}
          ????????//?部分代碼略
          ???}

          很明顯,返回的實(shí)際是一個(gè)定長的數(shù)組,所以只能“一如你我初見”,初始化什么樣子就什么樣子,不能新增,不能減少。如果你理解了,那我們就再來一個(gè)栗子

          ???int[]?intArr??=?{1,2,3,4,5};
          ???Integer[]?integerArr??=?{1,2,3,4,5};
          ???String[]?strArr?=?{"1",?"2",?"3",?"4",?"5"};
          ???List?list1?=?Arrays.asList(intArr);
          ???List?list2?=?Arrays.asList(integerArr);
          ???List?list3?=?Arrays.asList(strArr);
          ???System.out.println("list1中的數(shù)量是:"?+?list1.size());
          ???System.out.println("list2中的數(shù)量是:"?+?list2.size());
          ???System.out.println("list3中的數(shù)量是:"?+?list3.size());

          你覺得答案是什么?預(yù)想3秒鐘,揭曉答案,看跟你預(yù)想的是否一致呢?

          list1中的數(shù)量是:1
          list2中的數(shù)量是:5
          list3中的數(shù)量是:5

          是不是和你預(yù)想又不一樣了?還是回到Arrays.asList方法,該方法的輸入只能是一個(gè)泛型變長參數(shù)。基本類型是不能泛型化的,也就是說8個(gè)基本類型不能作為泛型參數(shù),要想作為泛型參數(shù)就必須使用其所對(duì)應(yīng)的包裝類型,那前面的例子傳遞了一個(gè)int類型的數(shù)組,為何程序沒有報(bào)編譯錯(cuò)誤呢?在Java中,數(shù)組是一個(gè)對(duì)象,它是可以泛型化的,也就是說我們的例子是把一個(gè)int類型的數(shù)組作為了T的類型,所以在轉(zhuǎn)換后在List中就只有1個(gè)類型為int數(shù)組的元素了。除了int,其它7個(gè)基本類型的數(shù)組也存在相似的問題。

          JDK里還為我們提供了一個(gè)便捷的集合操作工具類Collections,比如多個(gè)List合并時(shí),可以使用Collections.addAll(list1,list2), 在使用時(shí)也同樣要時(shí)刻提醒自己:“請(qǐng)勿踩坑”!


          05

          Stream:給你,獨(dú)一無二


          Java8中新增了Stream流 ,通過流我們能夠?qū)现械拿總€(gè)元素進(jìn)行一系列并行或串行的流水線操作。當(dāng)使用一個(gè)流的時(shí)候,通常包括三個(gè)基本步驟:獲取一個(gè)數(shù)據(jù)源(source)→ 數(shù)據(jù)轉(zhuǎn)換→執(zhí)行操作獲取想要的結(jié) 果,每次轉(zhuǎn)換原有 Stream 對(duì)象不改變,返回一個(gè)新的 Stream 對(duì)象(可以有多次轉(zhuǎn)換),這就允許對(duì)其操作可以 像鏈條一樣排列,變成一個(gè)管道。

          Stream用起來你真的是爽,根本停不下來。當(dāng)然不可避免的,還是有一些小坑的。例如我們分析用戶的訪問日志,放到list里。

          l list.add(new?User("碼大叔",?"登錄公眾號(hào)"));
          list.add(new?User("碼大叔",?"編寫文章"));

          因?yàn)橐恍┰颍覀円vlist轉(zhuǎn)為map,Steam走起來

          private?static?void?convert2MapByStream(List?list)?{
          ????Map?map?=?list.stream().collect(Collectors.toMap(User::getName,?User::getValue));
          ????System.out.println(map);
          }

          咣當(dāng),掉坑里了,程序?qū)伋霎惓#?/span>

          Exception?in?thread?"main"?java.lang.IllegalStateException:?Duplicate?key?碼大叔

          使用Collectors.toMap() 方法中時(shí),默認(rèn)key值是不允許重復(fù)的。當(dāng)然,該方法還提供了第三個(gè)參數(shù):也就是出現(xiàn) duplicate key的時(shí)候的處理方案

          如果在開發(fā)的時(shí)候就考慮到了key可能重復(fù),你需要在這樣定義convert2MapByStream方法,聲明在遇到重復(fù)key時(shí)是使用新值還是原有值:

          ????private?static?void?convert2MapByStream(List?list)?{
          ????????Map?map?=?list.stream().collect(Collectors.toMap(User::getName,?User::getValue,?(oldVal,?newVal)?->?newVal));
          ????????System.out.println(map);
          ????}

          關(guān)于Stream的坑其實(shí)還是蠻多的,比如尋找list中的某個(gè)對(duì)象,可以使用findAny().get(),你以為是找到就返回找不到就就返回null?依然天真,找不到會(huì)拋出異常的,需要使用額外的orElse方法。

          06

          紙上得來終覺淺,絕知此事要躬行

          所謂JDK官方的坑,基本上都是因?yàn)槲覀儗?duì)技術(shù)點(diǎn)了解的不夠深入,望文生義,以為是怎樣怎樣的,而實(shí)際上我們的自以為是讓我們掉進(jìn)了一個(gè)又一個(gè)坑里。面對(duì)著這些坑,我流下了學(xué)藝不精的眼淚!但也有些坑,確實(shí)發(fā)生的莫名其妙,比如吞噬異常,沒有理解JDK為什么這么設(shè)計(jì)。還有些坑,誤導(dǎo)性確實(shí)太強(qiáng)了,比如日期計(jì)算、list操作等。最后只能說一句:

          紙上得來終覺淺,絕知此事要躬行!編碼不易,且行且珍惜!

          | 作者:lhldyf

          —?【 THE END 】—
          本公眾號(hào)全部博文已整理成一個(gè)目錄,請(qǐng)?jiān)诠娞?hào)里回復(fù)「m」獲取!


          3T技術(shù)資源大放送!包括但不限于:Java、C/C++,Linux,Python,大數(shù)據(jù),人工智能等等。在公眾號(hào)內(nèi)回復(fù)「1024」,即可免費(fèi)獲取!!




          瀏覽 89
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美日韩国产高清视频 | 亚洲五码高清视频 | 福利国产在线 | 精品传媒一区二区三区 | 大香蕉伊人在线网 |