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

简介:

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

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

@

目录
一、Base64:你是我解不开的迷
二、被吞噬的异常:我不敢说出你的名字
三、日期计算:我想留住时间,让1天像1年那么长
四、List:一如你我初见,不增不减
五、Stream处理:给你,独一无二
六、结尾:纸上得来终觉浅,绝知此事要躬行!
推荐阅读
一、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编码方案。
2 static Base64.Encoder getEncoder()
返回Base64.Encoder编码使用的基本型base64编码方案。
3 static Base64.Decoder getMimeDecoder()
返回Base64.Decoder解码使用MIME类型的base64解码方案。
4 static Base64.Encoder getMimeEncoder()
返回Base64.Encoder编码使用MIME类型base64编码方案。
5 static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator)
返回Base64.Encoder编码使用指定的行长度和线分隔的MIME类型base64编码方案。
6 static Base64.Decoder getUrlDecoder()
返回Base64.Decoder解码使用URL和文件名安全型base64编码方案。
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是不兼容的!不要替换!不要替换!不要替换!

二、被吞噬的异常:我不敢说出你的名字
这个问题理解起来还是蛮费脑子的,所以我把这个系统异常发生的过程提炼成了一个美好的故事,放松一下,吟诗一首!

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

这个问题是在使用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,请记得用这个方法定位具体问题。

三、日期计算:我想留住时间,让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天了。

四、List:一如你我初见,不增不减
这个问题其实在JDK里存在很多年了,JDK8中依然存在,也是很多人最容易跳的一个坑!直接上代码:

public List 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 extends AbstractList

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

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

项目上千万不要使用Stream,因为一旦用起来你会觉得真屏蔽词爽,根本停不下来。当然不可避免的,还是有一些小坑的。

假设我们分析用户的访问日志,放到list里。

list.add(new User("码大叔", "登录公众号"));
list.add(new User("码大叔", "编写文章"));
因为一些原因,我们要讲list转为map,Steam走起来,

private static void convert2MapByStream(List 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方法。

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

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

原文地址https://www.cnblogs.com/madashu/p/13023193.html

相关文章
|
1月前
|
IDE Java 编译器
滚雪球学Java(02):入门Java必学 | 环境配置详细教程
【2月更文挑战第13天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!!
51 3
|
3月前
|
Java 开发者
Java 学习路线 2024 最新版!
又对上次分享的 Java 学习路线进行了简单修改完善,并增加了免登录下载和黑夜模式,这里重发一下。 花了一个月零碎的时间,我根据当下 Java 后端求职和招聘的最新要求,对之前写的 Java 后端学习路线进行了全面的优化和改进。
|
26天前
|
Java
小白9天快速掌握Java基础
本课程将会从零基础学员角度出发,带你9天快速掌握Java,课程知识点编排循序渐进,能够将每一个知识点落地到实际案例,拒绝“听得懂、不会练、不会敲”并在课程最后通过《学生管理系统》案例整合基础知识,巩固加深知识理解。
11 1
小白9天快速掌握Java基础
|
消息中间件 NoSQL Java
最全Java面试题及答案整理(2023最新版)
所有的面试题目都不是一成不变的,面试题目只是给大家一个借鉴作用,最主要的是给自己增加知识的储备,有备无患。
最全Java面试题及答案整理(2023最新版)
|
Java
Java小白踩坑录 - Java类型的七十二变揭秘
Java小白踩坑录 - Java类型的七十二变揭秘
63 0
Java小白踩坑录 - Java类型的七十二变揭秘
|
Java 程序员 开发工具
动力节点Java基础视频教程,Java最新版全套学习资料
Java零基础教程专门为Java零基础小白打造,课程细度前无古人,适合绝对Java零基础的小白入门学习,课程从企业实战的角度出发,每个知识点以“掰开了揉碎了”的方式讲解。 从开发工具的安装到每日的课堂作业,每一关都有详细攻略、案例实操,笔记、面试题、作业、相关文档、开发工具绝对全程无死角极度干燥。杜老师课堂全程风趣幽默,案例通俗易懂,将多年实战经验融入课程体系带入课堂中,直击学习核心问题,对重点知识深入浅出,结合实际,娓娓道来。
361 0
|
Oracle Java 关系型数据库
你打算用Java 8一辈子都不打算升级到Java 14,真香(1)
你打算用Java 8一辈子都不打算升级到Java 14,真香
135 0
你打算用Java 8一辈子都不打算升级到Java 14,真香(1)
|
SQL 算法 Java
你打算用Java 8一辈子都不打算升级到Java 14,真香(2)
你打算用Java 8一辈子都不打算升级到Java 14,真香
96 0
免费分享一套详细的Java面试视频
最近在整理资料的发现了一套Java面试方面比较齐全的视频,想着与其让它一直带在云盘里没啥用还不如分享出来给想这方面内容的小伙伴。
免费分享一套详细的Java面试视频
|
IDE Java 关系型数据库
如何搭建开发环境整理大全 Java篇
由于最近真的被问了太多关于环境搭建的问题,所以特地来整理了几篇文章介绍开发环境搭建。我们都知道开发一个系统、软件流程:
如何搭建开发环境整理大全 Java篇