您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

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

时间:2020-07-22 13:05:37  来源:  作者:

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

1、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字母表之外的字符的编码。
这些 Java 8 官方挖的坑,你踩过几个?

 

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

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

2、被吞噬的异常:我不敢说出你的名字

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

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

这个问题是在使用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类型,依赖关系图如下:

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

 

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

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

 

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

控制台异常信息:

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

 

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

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

 

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

3、日期计算:我想留住时间,让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天?有兴趣的小伙伴自己去写个代码试试吧。

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

 

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

4、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), 在使用时也同样要时刻提醒自己:“请勿踩坑”!

5、Stream:给你,独一无二

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

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

 

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

l 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方法。

6、纸上得来终觉浅,绝知此事要躬行

所谓JDK官方的坑,基本上都是因为我们对技术点了解的不够深入,望文生义,以为是怎样怎样的,而实际上我们的自以为是让我们掉进了一个又一个坑里。

面对着这些坑,我流下了学艺不精的眼泪!但也有些坑,确实发生的莫名其妙,比如吞噬异常,没有理解JDK为什么这么设计。还有些坑,误导性确实太强了,比如日期计算、list操作等。最后只能说一句:

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



Tags:Java 8   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
导读:系统启动异常日志竟然被JDK吞噬无法定位?同样的加密方法,竟然出现部分数据解密失败?往List里面添加数据竟然提示不支持?日期明明间隔1年却输出1天,难不成这是天上人间?1582年...【详细内容】
2020-07-22  Tags: Java 8  点击:(41)  评论:(0)  加入收藏
Stream Performance已经对 Stream API 的用法鼓吹够多了,用起简洁直观,但性能到底怎么样呢?会不会有很高的性能损失?本节我们对 Stream API 的性能一探究竟。为保证测试结果真实...【详细内容】
2020-03-17  Tags: Java 8  点击:(47)  评论:(0)  加入收藏
▌简易百科推荐
面向对象的特征之一封装 面向对象的特征之二继承 方法重写(override/overWrite) 方法的重载(overload)和重写(override)的区别: 面向对象特征之三:多态 Instanceof关键字...【详细内容】
2021-12-28  顶顶架构师    Tags:面向对象   点击:(2)  评论:(0)  加入收藏
一、Redis使用过程中一些小的注意点1、不要把Redis当成数据库来使用二、Arrays.asList常见失误需求:把数组转成list集合去处理。方法:Arrays.asList 或者 Java8的stream流式处...【详细内容】
2021-12-27  CF07    Tags:Java   点击:(3)  评论:(0)  加入收藏
文章目录 如何理解面向对象编程? JDK 和 JRE 有什么区别? 如何理解Java中封装,继承、多态特性? 如何理解Java中的字节码对象? 你是如何理解Java中的泛型的? 说说泛型应用...【详细内容】
2021-12-24  Java架构师之路    Tags:JAVA   点击:(5)  评论:(0)  加入收藏
大家好!我是老码农,一个喜欢技术、爱分享的同学,从今天开始和大家持续分享JVM调优方面的经验。JVM调优是个大话题,涉及的知识点很庞大 Java内存模型 垃圾回收机制 各种工具使用 ...【详细内容】
2021-12-23  小码匠和老码农    Tags:JVM调优   点击:(11)  评论:(0)  加入收藏
前言JDBC访问Postgresql的jsonb类型字段当然可以使用Postgresql jdbc驱动中提供的PGobject,但是这样在需要兼容多种数据库的系统开发中显得不那么通用,需要特殊处理。本文介绍...【详细内容】
2021-12-23  dingle    Tags:JDBC   点击:(13)  评论:(0)  加入收藏
Java与Lua相互调用案例比较少,因此项目使用需要做详细的性能测试,本内容只做粗略测试。目前已完成初版Lua-Java调用框架开发,后期有时间准备把框架进行抽象,并开源出来,感兴趣的...【详细内容】
2021-12-23  JAVA小白    Tags:Java   点击:(11)  评论:(0)  加入收藏
Java从版本5开始,在 java.util.concurrent.locks包内给我们提供了除了synchronized关键字以外的几个新的锁功能的实现,ReentrantLock就是其中的一个。但是这并不意味着我们可...【详细内容】
2021-12-17  小西学JAVA    Tags:JAVA并发   点击:(11)  评论:(0)  加入收藏
一、概述final是Java关键字中最常见之一,表示“最终的,不可更改”之意,在Java中也正是这个意思。有final修饰的内容,就会变得与众不同,它们会变成终极存在,其内容成为固定的存在。...【详细内容】
2021-12-15  唯一浩哥    Tags:Java基础   点击:(17)  评论:(0)  加入收藏
1、问题描述关于java中的日志管理logback,去年写过关于logback介绍的文章,这次项目中又优化了下,记录下,希望能帮到需要的朋友。2、解决方案这次其实是碰到了一个问题,一般的情况...【详细内容】
2021-12-15  软件老王    Tags:logback   点击:(19)  评论:(0)  加入收藏
本篇文章我们以AtomicInteger为例子,主要讲解下CAS(Compare And Swap)功能是如何在AtomicInteger中使用的,以及提供CAS功能的Unsafe对象。我们先从一个例子开始吧。假设现在我们...【详细内容】
2021-12-14  小西学JAVA    Tags:JAVA   点击:(21)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条