这些Java8官方挖的坑,你踩过几个?

发表于 4年以前  | 总阅读数:571 次

导读:系统启动异常日志竟然被JDK吞噬无法定位?同样的加密方法,竟然出现部分数据解密失败?往List里面添加数据竟然提示不支持?日期明明间隔1年却输出1天,难不成这是天上人间?1582年神秘消失的10天JDK能否识别?Stream很高大上,List转Map却全失败……这些JDK8官方挖的坑,你踩过几个?

目录:

  • Base64:你是我解不开的迷
  • 被吞噬的异常:我不敢说出你的名字
  • 日期计算:我想留住时间,让1天像1年那么长
  • List:一如你我初见,不增不减
  • Stream处理:给你,独一无二

01.Base64:你是我解不开的迷

出于用户隐私信息保护的目的,系统上需将姓名、身份证、手机号等敏感信息进行加密存储,很自然选择了AES算法,外面又套了一层Base64,之前用的是sun.misc.BASE64Decoder/BASE64Encoder,网上的资料基本也都是这种写法,运行得很完美。但这种写法在idea或者maven编译时就会有一些黄色告警提示。到了Java 8后,Base64编码已经成为Java类库的标准,内置了 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);
    }

}

程序员的职业操守咱还是有的,构造新老数据、自测、通过,提交测试版本。信心满满,我要继续延续我 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)

关键是这个错还很诡异,部分数据是可以解密的,部分解不开

Base64依赖于简单的编码和解码算法,使用65个字符的US-ASCII子集,其中前64个字符中的每一个都映射到等效的6位二进制序列,第65个字符(=)用于将Base64编码的文本填充到整数大小。后来产生了3个变种:

  • RFC 4648:Basic, 此变体使用RFC 4648和RFC 2045的Base64字母表进行编码和解码。编码器将编码的输出流视为一行; 没有输出行分隔符。解码器拒绝包含Base64字母表之外的字符的编码。
  • RFC 2045:MIME ,此变体使用RFC 2045提供的Base64字母表进行编码和解码。编码的输出流被组织成不超过76个字符的行; 每行(最后一行除外)通过行分隔符与下一行分隔。解码期间将忽略Base64字母表中未找到的所有行分隔符或其他字符。
  • RFC 4648:Url, 此变体使用RFC 4648中提供的Base64字母表进行编码和解码。字母表与前面显示的字母相同,只是-替换+和_替换/。不输出行分隔符。解码器拒绝包含Base64字母表之外的字符的编码。

S.N.:1 方法名称 & 描述:static Base64.Decoder getDecoder() 返回Base64.Decoder解码使用基本型base64编码方案。

S.N.:2 方法名称 & 描述:static Base64.Encoder getEncoder() 返回Base64.Encoder编码使用的基本型base64编码方案。

S.N.:3 方法名称 & 描述:static Base64.Decoder getMimeDecoder() 返回Base64.Decoder解码使用MIME类型的base64解码方案。

S.N.:4 方法名称 & 描述:static Base64.Encoder getMimeEncoder() 返回Base64.Encoder编码使用MIME类型base64编码方案。

S.N.:5 方法名称 & 描述:static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator) 返回Base64.Encoder编码使用指定的行长度和线分隔的MIME类型base64编码方案。

S.N.:6 方法名称 & 描述:static Base64.Decoder getUrlDecoder() 返回Base64.Decoder解码使用URL和文件名安全型base64编码方案。

S.N.:7 方法名称 & 描述:static Base64.Encoder getUrlEncoder() 返回Base64.Decoder解码使用URL和文件名安全型base64编码方案。

关于base64用法的详细说明,可参考:https://juejin.im/post/5c99b2976fb9a070e76376cc

对于上面的错误,网上有的说法是,建议使用Base64.getMimeDecoder()Base64.getMimeEncoder(),对此我只能建议:老的系统如果已经有数据了,就不要使用jdk自带的Base64了。JDK官方的Base64和sun的base64是不兼容的!不要替换!不要替换!不要替换!

02.被吞噬的异常:我不敢说出你的名字

这个问题理解起来还是蛮费脑子的,所以我把这个系统异常发生的过程提炼成了一个美好的故事,放松一下,吟诗一首!

最怕相思浓 一切皆是你 唯独 不敢说出你的名字 -- 码大叔

这个问题是在使用springboot的注解时遇到的,发现JDK在解析注解时,若注解依赖的类定义在JVM加载时不存在,也就是NoClassDefFoundError时,实际拿到的异常将会是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 < paramInt; k++){
        j = paramByteBuffer.get();
        if (j == 99) {
            // 注意这个方法
         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) {
         // 注意这里,异常发生了转化
        return new TypeNotPresentExceptionProxy("[unknown]", localNoClassDefFoundError);
    } catch (TypeNotPresentException localTypeNotPresentException) {
        return new TypeNotPresentExceptionProxy(localTypeNotPresentException.typeName(), localTypeNotPresentException.getCause());
    }
}

parseClassArray这个方法中,预期parseClassValue返回Class对象,但看实际parseClassValue的逻辑,在遇到NoClassDefFoundError时,返回的是TypeNotPresentExceptionProxy,由于类型强转失败,最终抛出的是java.lang.ArrayStoreException:sun.reflect.annotation.TypeNotPresentExceptionProxy,此时只能通过debug到这行代码,找到具体是缺少哪个类定义,才能解决这个问题。

笔者重现一下发现这个坑的场景,有三个module,module3依赖module2但未声明依赖module1,module2依赖module1,但声明的是optional类型,依赖关系图如下

上面每个module中有一个Class,我们命名为ClassInModuleX。ClassInModule3启动时在注解中使用了ClassInModule2的类,而ClassInModule2这个类的继承了ClassInModule1,这几个类的依赖关系图如下:

如此,其实很容易知道在module运行ClassInModule3时,会出现ClassInModule1的NoClassDefFoundError的,但实际运行时,你能看到的异常将不是NoClassDefFoundError,而是java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,此时,若想要知道具体是何许异常,需通过debug在AnnotationParser中定位具体问题,以下展示两个截图,分别对应系统控制台实际抛出的异常和通过debug发现的异常信息。

控制台异常信息: 注意异常实际在红色圈圈这里,自动收缩了,需要展开才可以看到通过debug发现的异常信息:

如果你想体验这个示例,可关注公众号码大叔和笔者交流。如果你下次遇到莫名的java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,请记得用这个方法定位具体问题。

03.日期计算:我想留住时间,让1天像1年那么长

Java8之前日期时间操作相当地麻烦,无论是Calendar还是SimpleDateFormat都让你觉得这个设计怎么如此地反人类,甚至还会出现多线程安全的问题,阿里巴巴开发手册中就曾禁用static修饰SimpleDateFormat。好在千呼万唤之后,使出来了,Java8带来了全新的日期和时间API,还带来了Period和Duration用于时间日期计算的两个API。

Duraction和Period,都表示一段时间的间隔,Duraction正常用来表示时、分、秒甚至纳秒之间的时间间隔,Period正常用于年、月、日之间的时间间隔。

网上的大部分文章也是这么描述的,于是计算两个日期间隔可以写成下面这样的代码:

// parseToDate方法作用是将String转为LocalDate,略。
LocalDate date1 = parseToDate("2020-05-12");
LocalDate date2 = parseToDate("2021-05-13");
// 计算日期间隔
int period = Period.between(date1,date2).getDays();

一个是2020年,一个是2021年,你认为间隔是多少?1年? 恭喜你,和我一起跳进坑里了(画外音:里面的都挤一挤,动一动,又来新人了)。

正确答案应该是:1天。

这个单词的含义以及这个方法看起来确实是蛮误导人的,一不注意就会掉进坑里。Period其实只能计算同月的天数、同年的月数,不能计算跨月的天数以及跨年的月数。

正确写法1

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

toEpochDay():将日期转换成Epoch 天,也就是相对于1970-01-01(ISO)开始的天数,和时间戳是一个道理,时间戳是秒数。显然,该方法是有一定的局限性的

正确写法2

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

使用这个写法,一定要注意一下date1和date2前后顺序:date1 until date2。

正确做法3(推荐)

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

ChronoUnit:一组标准的日期时间单位。这组单元提供基于单元的访问来操纵日期,时间或日期时间。这些单元适用于多个日历系统。这是一个最终的、不可变的和线程安全的枚举。

看到”适用于多个日历系统“这句话,我一下子想起来历史上1582年神秘消失的10天,在JDK8上是什么效果呢?1582-10-15和1582-10-04你觉得会相隔几天呢?11天还是1天?有兴趣的小伙伴自己去写个代码试试吧。

打开你的手机,跳转到1582年10月,你就能看到这消失的10天了。

04.List:一如你我初见,不增不减

这个问题其实在JDK里存在很多年了,JDK8中依然存在,也是很多人最容易跳的一个坑!直接上代码:

public List<String> allUser() {
    // 省略
    List<String> currentUserList = getUser();
    currentUserList.add("码大叔");
    // 省略
}

就是上面这样一段代码,往一个list里添加一条数据,你觉得结果是什么呢?“码大叔”成功地添加到了List里?天真,不报个错你怎么能意识到JDK存在呢。

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

原因:因为在getUser方法里,返回的List使用的是Arrays.asList生成的,示例:

    private List<String> getUser(){
        return Arrays.asList("剑圣","小九九");
    }

我们来看看Arrays.asList的源码

    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> 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) {
            // 返回的是一个定长的数组
            a = Objects.requireNonNull(array);
        }
        // 部分代码略
   }

很明显,返回的实际是一个定长的数组,所以只能“一如你我初见”,初始化什么样子就什么样子,不能新增,不能减少。如果你理解了,那我们就再来一个栗子

   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中的数量是:" + list1.size());
   System.out.println("list2中的数量是:" + list2.size());
   System.out.println("list3中的数量是:" + list3.size());

你觉得答案是什么?预想3秒钟,揭晓答案,看跟你预想的是否一致呢?

list1中的数量是:1
list2中的数量是:5
list3中的数量是:5

是不是和你预想又不一样了?还是回到Arrays.asList方法,该方法的输入只能是一个泛型变长参数。基本类型是不能泛型化的,也就是说8个基本类型不能作为泛型参数,要想作为泛型参数就必须使用其所对应的包装类型,那前面的例子传递了一个int类型的数组,为何程序没有报编译错误呢?在Java中,数组是一个对象,它是可以泛型化的,也就是说我们的例子是把一个int类型的数组作为了T的类型,所以在转换后在List中就只有1个类型为int数组的元素了。除了int,其它7个基本类型的数组也存在相似的问题。

JDK里还为我们提供了一个便捷的集合操作工具类Collections,比如多个List合并时,可以使用Collections.addAll(list1,list2), 在使用时也同样要时刻提醒自己:“请勿踩坑”!

05.Stream:给你,独一无二

Java8中新增了Stream流 ,通过流我们能够对集合中的每个元素进行一系列并行或串行的流水线操作。当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结 果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以 像链条一样排列,变成一个管道。

Stream用起来你真的是爽,根本停不下来。当然不可避免的,还是有一些小坑的。例如我们分析用户的访问日志,放到list里。

list.add(new User("码大叔", "登录公众号"));
list.add(new User("码大叔", "编写文章"));

因为一些原因,我们要讲list转为map,Steam走起来

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

咣当,掉坑里了,程序将抛出异常:

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

使用Collectors.toMap() 方法中时,默认key值是不允许重复的。当然,该方法还提供了第三个参数:也就是出现 duplicate key的时候的处理方案

如果在开发的时候就考虑到了key可能重复,你需要在这样定义convert2MapByStream方法,声明在遇到重复key时是使用新值还是原有值:

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

关于Stream的坑其实还是蛮多的,比如寻找list中的某个对象,可以使用findAny().get(),你以为是找到就返回找不到就就返回null?依然天真,找不到会抛出异常的,需要使用额外的orElse方法。

06.纸上得来终觉浅,绝知此事要躬行

所谓JDK官方的坑,基本上都是因为我们对技术点了解的不够深入,望文生义,以为是怎样怎样的,而实际上我们的自以为是让我们掉进了一个又一个坑里。面对着这些坑,我流下了学艺不精的眼泪!但也有些坑,确实发生的莫名其妙,比如吞噬异常,没有理解JDK为什么这么设计。还有些坑,误导性确实太强了,比如日期计算、list操作等。最后只能说一句:

纸上得来终觉浅,绝知此事要躬行!编码不易,且行且珍惜!

本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/xpij96oujyj05-rnpVi9HA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Java 中验证时间格式的 4 种方法 2年以前  |  3809次阅读
Java经典面试题答案解析(1-80题) 4年以前  |  3594次阅读
CentOS 配置java应用开机自动启动 4年以前  |  2760次阅读
IDEA依赖冲突分析神器—Maven Helper 4年以前  |  2747次阅读
SpringBoot 控制并发登录的人数教程 4年以前  |  2424次阅读
 目录