什么是 null
在 Java 中,null 是一个非常常见的关键字,用于表示“没有值”或“空”。然而,对于初学者来说,null 的本质可能会感到有些困惑。在本文中,我们将详细探讨 null 在 Java 中的含义和使用。
在 Java 中,null 表示“没有值”或“空”。它是一个关键字,用于表示一个对象变量不引用任何对象。这意味着该变量没有指向任何有效的内存地址,因此它不指向任何对象。如果尝试在 null 引用上调用任何方法或字段,则会引发 NullPointerException 异常。
以下是一个示例:
String str = null;
int length = str.length(); // This will cause a NullPointerException
在这个例子中,str 被赋值为 null,因此它不引用任何有效的字符串对象。当试图调用 str.length() 时,将抛出 NullPointerException 异常。
在 Java 中,null 有许多用途。以下是一些常见的用途:
- 初始化对象引用 String str; // str is initialized tonull
- 表示无效的值 Person person1= new Person(“Alice”, null);
- 释放内存, 如果将一个对象变量设置为 null,它将不再引用该对象,并且该对象将变为不可访问。这意味着它可以被垃圾收集器回收,释放内存。
null的来源
Null 的产生是由于 1965 年的一个偶然事件。
托尼·霍尔(Tony Hoare)是快速排序算法的创造者,也是图灵奖(计算机领域的诺贝尔奖)的获得者。他把 Null 添加到了 ALGOL 语言中,因为它看起来很实用而且容易实现。但几十年后,他后悔了。Tony 表示,1965 年把 Null 引用加进 ALGOL W 时的想法非常简单,“就是因为这很容易实现。”但如今再次谈到当初的决定时,他表示这是个价值十亿美元的大麻烦:
“我称之为我的十亿美元错误……当时,我正在设计第一个全面的类型系统,用于面向对象语言的引用。我的目标是确保所有对引用的使用都是绝对安全的,由编译器自动执行检查。但是我无法拒绝定义一个 Null 引用的诱惑,因为它实在太容易实现了。这导致了无法计数的错误、漏洞和系统崩溃。在过去的四十年里,这些问题可能已经造成了十亿美元的损失。”
“NULL”:计算机科学中的最严重错误,造成十亿美元损失 - 计算机 - 软件编程 - 深度开源
搞笑的问题
桌子上有空杯子:杯子里有 0 mL水
桌子上没有杯子:杯子里有 null mL水
往空杯子里倒入一些水:杯子里有 114 mL水
往没有杯子的桌子上倒入等量水:你妈看见桌子上地上都是水拿出铜头皮带把你抽得如键山雏一般旋转。
北纬51度,东经0度:伦敦
北纬51度,东经null度:幻想乡
上午8点0分:周末这个点我表姐极有可能在赖床
上午8点null分:表坏了
往一张有0元的银行卡里转50块钱:卡的主人会拿着卡去吃疯狂星期四
往一张有null元的银行卡里转50块钱:这张卡不存在
计算 5-0 :5
计算 5-null :程序错误
我给你解释下为什么null有问题。
房间的左边有一堆苹果,小明吃光了。
房间的右边有一堆橘子,小红吃光了。
这时候,小刚进了屋,别人问他,这两堆东西是个什么情况……
小刚一脸蒙蔽:啊?两堆?你在说啥?
0说:“身是菩提树,心如明镜台,时时勤拂拭,勿使惹尘埃。”
null说:“菩提本无树,明镜亦非台,本来无一物,何处惹尘埃。”
null和0就不是一个境界
已知空解决方案
对于null,大家普遍都拿他当作“没有”的表示方式。比如getUser(userId)函数返回null,表示“找不到这个userId对应的user”。你可以强调说应该抛异常,或者使用Optional,或者以其他的形式表达“没有”,但是现实就是,使用null表示“没有”已经成为了某种“共识”。
而一旦一个东西没有,对其进行某个操作就是未定义的。例如getUser(userId).name可能就会NPE,因为user没有的情况下,获取其name也就没有意义。程序不会晓得没有user的情况下name应该怎么处理。如果不写代码特殊照顾,程序就只能傻乎乎的抛NPE等价的错误。于是这就需要程序员必须不断的做NPE检查。而人做这种工作,在没有其他支持下,有遗漏是必然的。某些遗漏造成的NPE会带来灾难性的结果。你能想象一个做手术的机器人程序,或者飞机飞控程序出现一个NPE是什么结果吗?
对于NPE有这么几种方案
1)完全不接受null。但“没有”这个概念得另外想办法。比如每个字段foo,都得弄一个对应的hasFoo的字段表示。取值也总得弄两次。但这样作和上面的问题是等价的,会遗漏if (foo != null)的程序员肯定也会遗漏if (hasFoo)。要不就把foo和hasFoo外边套一层,逼着程序员用getOrElse这样的方式做NPE检查。Option这类方案就是如此。
2)允许null,但是控制null的范围。假设程序在对外接触的界面上(比如接口的controller,访问DB时)进行判空,处理所有“没有”的情况。然后处理干净后对内部的程序总是not null的,不用管“没有”。编译器就负责保证那些not null的部分肯定不出问题。kotlin是这么干的。
3)允许null,通过与语法糖的形式让每次有null时的处理写的方便点。比如js里你可以这样写“(getUser(userId) || {}).name”或者“_.get(getUser(userId), ‘name’)”,这样用户找不到,至少能得到一个undefined的name,而不会NPE。简短的语法糖可以提高开发者做处理的概率。上面的这段代码如果用java就得写几行,程序员也许会更倾向于漏掉。
至于除0造成的问题,相对于null会少一些。只有遇到除0才会有问题。我所在领域见过的大多数程序根本不会做除法。但如果是数据分析等领域的,除0问题也会像null问题一样恶心。你总是要定义数据除0表达什么意思。也许就是一个错误需要报,也许是有实际含义的(比如垂直的斜率),你需要换一个方式表达它。
null 在 Java 的问题
以下是一些常见的问题:
1. 可能引发 NullPointerException
如果尝试在 null 引用上调用任何方法或字段,则会引发 NullPointerException 异常。这可以在编译时很难发现,因此需要小心处理 null 引用。
2. 可能导致代码复杂性
在使用 null 时,可能需要添加一些额外的逻辑来检查是否为空。这可能会使代码变得更加复杂,并增加错误的机会。
3. 可能会导致歧义
有时 null 可以引起歧义。例如,如果将一个方法的返回值设置为 null,则无法确定返回的值是否表示“未找到”或“出错”等意义。这可能导致代码更加难以维护和理解。
总结:
null 是 Java 中的一个关键字,表示“没有值”或“空”。它用于表示对象变量不引用任何对象,并且在某些情况下可以表示无效或缺失的值。在使用 null 时,需要小心处理可能引发 NullPointerException 和增加代码复杂性的问题。在确定使用 null 时,应该考虑使用其他替代方案,例如 Optional 类型和默认值。
如果经常使用Optional 的会发现, 很痛苦,为什么会这么说呢? 主要是抉择问题和Optional 不能够完全代替 if , 因为Optional 同一时间只能处理一个值的判空, 而遇到多值判空或者链路判空, 那么就会显得非常啰嗦而且麻烦还不如直接使用 if 来的实在,下面我针对上问题提出的一个解决方案就是链式判空处理。
为什么需要空链处理?
我们经常会处理,A-》B-》C-》D 这种对象链路, 转换为 java 代码如下:
if(a!=null&&a.getB()!=null&&a.getB().getC()!=null&&a.getB().getC().getD()!=null){}
如果我想在 C 不是空的时候处理点东西,然后在获取 D 代码如下:
if(a!=null&&a.getB()!=null&&a.getB().getC()!=null){///a.getB().getC() xxxxxx处理逻辑if(a.getB().getC().getD()!=null){}
}
还有一种情况就是一个对象中有多个字段需要进行判空处理, 这种场景是非常常见的也是非常容易出错的, 容易忘记判空或者自信觉得没有空。
if(a.getName()!=null){//xxxxx
}
if(a.getAddr()!=null){//xxxxx
}
if(a.getAddr()!=null&&a.getName()&&.....){//xxxxx
}
//.........................
以上举例只是生活中开发代码的一小部分, 实际业务稍微复杂点的场景, 是要比这个复杂 N 倍的 ,如何能搞高效的处理判空和避免发生NullPointerException 问题呢? 往下接着看。
最新版安装
settings.xml
<servers><server><id>java-huanmin-utils</id><username>60df3fd26da2f73831f05bfa</username><password>RXP3_Dmx[=gX</password></server>
</servers><repositories><repository><id>java-huanmin-utils</id><url>https://packages.aliyun.com/60df3fde4690c27532d3dd6c/maven/java-huanmin-utils</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></repository>
</repositories>
配置参考
settings_aly.xml
maven
<dependency><groupId>com.gitee.huanminabc</groupId><artifactId>null-chain</artifactId><version>1.0.7-RELEASE</version>
</dependency>
使用教程
概念(必读)
在null-chain 工具中主要思想就是让程序以任务的维度按照工厂流水线模式进行处理, 一旦某个任务是空的那么就不继续往下执行了 , 在代码编程的世界中 <font style="color:#DF2A3F;">可读性远比性能重要</font>
。
在空链处理中主要概念为:
- 开始
- 过程
- 转换
- 终结
注意: 开始 是必须的, 2,3,4
可以自己任意组合进行操作, 想要获取值,必须使用终结方法。
作为空链处理的实体类(强制要求):
- 类中必须实现 get/set 方法 (强制必须实现)
- 类中全部字段必须使用
**强制**必须使用包装类型
- 类中必须实现空构造方法
- 一个lombok 注解
<font style="color:#080808;background-color:#ffffff;">@Data</font>
可以搞定大部分要求。
报错符号:
链式处理最麻烦的就是一旦出问题非常难以定位到位置, 在空链中为了避免这个问题在设计之初就考虑到了。
在空链路符号就两种:
->
表示正常?
表示这个节点是空的
下面是报错效果:
报错信息主要包括: 代码位置和具体哪个链路环节是空的或者问题, 大大的节约的排查问题的成本。
核心对象:
- NULL(所有任务的开端,最常用的类)
- NullChain(同步处理, 链式判空类)
- NullChainAsync(异步处理, 链式判空类)
起始方法常用方法(NULL):
- of (遇空不异常,后续任务不执行)
- ofs (各种数组相关的对象转化为空链)
- ofMap (对象和 map 转化为空链)
- ofMerge (将多个空链对象合并为一个空连, 适用于对多个不同类型对象内部的值合并到一个对象中, do->dto->vo)
过程常用方法(NullChain****):
- of (遇空不异常,后续任务不执行)
- peek (不改变对象类型,提前偷看内容,然后修改对象内容,影响后续任务)
- map(将一个类型转换另一个类型, 也可以说将 a 映射为 b, 常用于 bo->dto->vo )
- or(如果上一个任务为空,那么就执行supplier 来换一个任务,如果不为空那么就返回当前任务)
- pick(将对象内的多个字段提取出来转换为新的对象, 常用于 bo->dto->vo , 和 map 不一样的是通过 lmabda 方式, 使用起来更加简洁 )
- copy 浅拷贝
- deepCopy 深度拷贝
转换常用方法(NullConvert):
- str (如果值是字符串类型,那么可以通过此方法获取不为空的字符串, 包括长度不能小于 0)
- integer (如果是字符串或者数值类型的, 可以通过此方法将值转换为数字)
- dateFormat(将时间类型(Date,LocalDate,LocalDateTime), 10或13位时间戳(数值或字符串), 转换为指定格式化后的时间字符串)
- json(将对象转换为json , )
- bytes(序列化) 反序列化需要使用
NULL.toNULL(byte[] bytes, Class<T> tClass)
方法 - async (将代码以异步的方式运行, 下一个任务会等到上一个任务结束才执行, 并且可接收上一个任务的返回值) 支持自定义线程池
终结常用方法:(NullFinality)
- is 和 iss 判断是否是空(true 是空, false 不是空)
- get(取值, 如果有空就异常, 并且打印具体异常的位置)
- ifPresent (如果上一个任务不是空那么就执行action,否则不执行)
- orThrow(如果是空那么自定义抛出异常)
- orElse(如果是空那么给默认值)
- stream 和 parallelStream (直接将空链无缝衔接流, 后期会自己重写流不然入参还需要指定类型)
- forEach (兼容集合,数组和 map )
性能: 单线程 10万次循环链路处理, 高配电脑测试基本都在 20~50 毫秒左右, 如果电脑差点也是在 100~200 毫秒左右,这还是算上了初始化和循环的时间, 实际代码运行后执行效率会比这个要高很多的。
@Testpublic void time() throws Exception {CodeTimeUtil.creator(()->{for (int i = 0; i < 100000; i++) {NULL.of(userEntity).of(UserEntity::getRoleData).of(RoleEntity::getRoleDescription).get();}});}
:::color4
测试实体
:::
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserEntity implements Serializable {private static final long serialVersionUID = 1L;private Integer id;private String name; //名称private String pass; //密码private Integer age; //年龄private String sex;//性别private String site; //地址private Boolean del; //是否删除@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date date; //日期private RoleEntity roleData;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoleEntity implements Serializable {private static final long serialVersionUID = 1L;private long id;private String roleName;// 防止 JSON parse error: Cannot deserialize value of type `java.util.Date` from String 错误@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date roleCreationTime;private String roleDescription;private boolean roleStatus;
}
初始化值
UserEntity userEntity = new UserEntity();@Beforepublic void before() {userEntity.setId(1);userEntity.setName("huanmin");userEntity.setAge(33);userEntity.setDate(new Date());RoleEntity roleEntity = new RoleEntity();roleEntity.setRoleName("admin");roleEntity.setRoleDescription("123");userEntity.setRoleData(roleEntity);}
判空
在链路中间处理,然后经过各种转化,发现是空的然后报错了
String roleName = NULL.of(userEntity).peek((data) -> {data.getRoleData().setRoleName(null);return data;
}).of(UserEntity::getRoleData).map(UserEntity::getRoleData).of(RoleEntity::getRoleName).map(RoleEntity::getRoleName).get();
System.out.println(roleName);//error
如果不想报错那么可以给一个默认值
String roleName = NULL.of(userEntity).peek((data) -> {data.getRoleData().setRoleName(null);return data;
}).of(UserEntity::getRoleData).map(UserEntity::getRoleData).of(RoleEntity::getRoleName).map(RoleEntity::getRoleName).orElse("default");
System.out.println(roleName);//default
a 转 b 转换为 b 对象, 拿 b 对象的值, 如果有空抛出自定义异常
userEntity.getRoleData().setRoleName(null);
String s2 = NULL.of(userEntity).map(UserEntity::getRoleData).map(RoleEntity::getRoleName).orThrow(RuntimeException::new);
System.out.println(s2);
还有很多用法就自己探索把…
转换
转换的意思很简单就是将类型转换,或者将 a 对象转换为 b 对象…
将字符串转换为数值
Integer convert = NULL.of(userEntity).map(UserEntity::getRoleData).map(RoleEntity::getRoleDescription).map(Integer::parseInt).get();
System.out.println(convert);
Integer anInt = NULL.of(userEntity).map(UserEntity::getRoleData).map(RoleEntity::getRoleDescription).integer().get();
System.out.println(anInt);
将一个对象映射为另一个对象
UserData userData1 = NULL.of(userEntity).of(UserEntity::getRoleData).map((data) -> {UserData userData = new UserData();userData.setName("name");userData.setSex("1");return userData;
}).get();
System.out.println(userData1);
将 a 转化为 b 然后提取 b 部分值返回新的对象
RoleEntity roleEntity = NULL.of(userEntity).map(UserEntity::getRoleData).pick(RoleEntity::getRoleName, RoleEntity::getRoleDescription).get();
System.out.println(roleEntity);
将对象转换为 json
String json = NULL.of(userEntity).map(UserEntity::getRoleData).json().get();
System.out.println(json);
对时间进行格式
String dateFormat = NULL.of(userEntity).map(UserEntity::getRoleData).map(RoleEntity::getRoleCreationTime).dateFormat(DateEnum.DATETIME_PATTERN).get();
System.out.println(dateFormat);
还有很多用法就自己探索把…
终结
准确来说能拿到结果的都算是终结方法, 在上面我们已经演示了 get 方法这就是终结方法。 这里就不在演示使用了自习查看NullFinality
类, 上面已经演示了这么多的案例了。
NULL.of(null,false).ifPresent(System.out::println);//false
NULL.of(" ","123").ifPresent(System.out::println); //123
异步编排
在空链中提供了一个非常好用的链式异步编排,拥有和同步一样的功能。,需要通过async
将当前处理流转换为异步处理流。当然也可以转换回去sync
。
异步处理
@Testpublic void test1(){NULL.of(userEntity).of(UserEntity::getRoleData).async().map(RoleEntity::getRoleName).get((data)->{System.out.println(data);SleepTools.second(2);});System.out.println("====");}
异步转同步
//不推荐这样一条龙进行异步转同步, 因为这样意义不大(和同步没啥区别都,都阻塞在这一行了, 异步链路的前一个没处理完毕后面的会等待前一个结果)
NULL.of(userEntity).map(UserEntity::getRoleData).async().map(RoleEntity::getRoleName).get((data) -> {System.out.println(data);SleepTools.second(2);
});System.out.println("====");
userEntity.getRoleData().setRoleName(null);//推荐这样的写法, 先异步各种处理, 让代码脱离主线程, 之后我们可以写自己的逻辑, 然后再同步获取结果
NullChainAsync<String> map = NULL.of(userEntity).async().map(UserEntity::getRoleData).map(RoleEntity::getRoleName);
//执行其他逻辑代码
//xxxxx//获取之前异步的结果
try {String res = map.get();System.out.println(res);
} catch (NullChainCheckException e) {log.error("空值error",e);
}
自定义线程池 (如果不是 cpu 密集的任务长时间占用线程, 一般用默认的线程池就足够了)
ThreadFactoryUtil.addExecutor("test1",10,100); //建议在项目启动的时候配置, 不要放到业务代码里面, 这个key可以做一个枚举统一管理
//指定使用的线程池key
NULL.of(userEntity).map(UserEntity::getRoleData).async("test1").map(RoleEntity::getRoleName).ifPresent((data) -> {System.out.println(data);SleepTools.second(2);
});
继承扩展
让一个实体类继承NULLExt,就能直接调用空链处理, 需要保证最开头的实体类变量不能是空的 , 如果不能保证还是老老实实的使用 NULL.of
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserExtEntity implements Serializable, NULLExt<UserExtEntity> {
UserExtEntity userExtEntity = new UserExtEntity();
userExtEntity.setId(1);
//userExtEntity 不能是空的
Integer i = userExtEntity.map(UserExtEntity::getId).get();
System.out.println(i);
提示: 一般来说 web接口
的入参 和 prc 接口
的对象入参基本不可能是 null 这种情况是可以直接使用 继承扩展的
集合 , Stream , Map
遍历
- 集合相关的比如 List Set Queue 等 都必须使用
<font style="color:#080808;background-color:#ffffff;">NULL.ofCollection</font>
起手或者继承<font style="color:#080808;background-color:#ffffff;">NULLExt</font>
- Map 相关的比如 HashMap LinkedHashMap 等 都必须使用
<font style="color:#080808;background-color:#ffffff;">NULL.ofKV</font>
起手或者继承<font style="color:#080808;background-color:#ffffff;">NULLExt</font>
List<String> list= Arrays.asList("1","2","3");
//list=null;
NULL.ofCollection(list).stream().forEach((data) -> {System.out.println(data);
});NULL.ofCollection(userEntityList).stream().forEach((data) -> {System.out.println(data);
});String a="1";
NULL.of(a).stream().forEach(System.out::println);//直接遍历list
NULL.ofCollection(list).forEach(System.out::println);
Map<String, String> stringStringMap = new HashMap<String, String>() {{put("1", "1");put("2", "2");put("3", "3");
}};//转流遍历map
NULL.ofKV(stringStringMap).stream().map((node) -> node.getKey() + "__" + node.getValue()).forEach(System.out::println);//直接遍历map
NULL.ofKV(stringStringMap).forEach((node) -> {System.out.println(node.getKey() + "__" + node.getValue());
});
操作
增删改在在大部分情况下都是安全的不会出现空的情况, 所以我们只针对各种查询做处理
Map<String, String> stringStringMap = new HashMap<String, String>() {{put("1", "1");put("2", "2");put("3", "3");
}};
boolean bool = NULL.ofKV(stringStringMap).isKey("1", "2");
System.out.println(bool); //falseNode<String, String> stringStringNode = NULL.ofKV(stringStringMap).map("1").orElse(Node.NEW("1", "1"));
System.out.println(stringStringNode);NULL.ofKV(stringStringMap).map("3").map(Node::getValue).ifPresent(System.out::println);ListTest<UserEntity> listTest = new ListTest<>();
listTest.add(userEntity);
NULL.ofCollection(listTest).map(0).ifPresent(System.out::println);UserEntity[] userEntities = new UserEntity[]{userEntity};
NULL.ofArray(userEntities).map(0).ifPresent(System.out::println);
其他工具
自行查看源码接口文件:
- NULL , NULLExt , NullChain , NullConvert , NullFinality , NullChainAsync 。。。。。。
- NULL 继承了 NullUtil 万能判空
NULL.non(accountFlowNew, query.getBelongingPartnerId())
最佳实践
无论是入参还是出参始终保持空链对象(支持 PRC) , 这样就能极大地减少空处理引发的问题
// 实践1
public NullChain<RoleEntity> handle(NullChain<UserEntity> userEntityNullChain){RoleEntity roleEntity = NULL.of(userEntity).map(UserEntity::getRoleData).orElse(new RoleEntity());roleEntity.setId(1);//xxxxreturn NULL.of(roleEntity);
}// 实践2
public NullChain<String> handle2() {//1. 不知道从哪里来的数据UserEntity userEntity = new UserEntity();NULL.of(userEntity).is(UserEntity::getId, UserEntity::getName);//2. 从数据库查询return NULL.of(userEntity).map2((chain,data) -> {if (chain.is(UserEntity::getId, UserEntity::getName, UserEntity::getDate)) {//xxxxxxx,取出数据经过处理return new RoleEntity();}//如果不满足条件,返回空return null;}).map(RoleEntity::getRoleName);
}
//拿到用户角色, 如果没有就默认admin
String roleName = getRoleName().orElse("admin");//实践3
private void pageBelongingPartnerIdHandle(AccountxxxQuery query) {//如果xxid不是空的那么就需要把相关xxx查询出来query.map(AccountxxxQuery::getBelongingPartnerId).map((belongingPartnerId) -> accountNewManager.queryAccountByBelongingPartnerId(belongingPartnerId, ProductEnum.LIJIANJIN)).ifPresent((accountOns) -> query.getAccountNos().addAll(accountOns));
}//实践4
public ResultX<AccountxxxDTO> getAccountLastFlow(AccountxxxQuery query) {NullChain<Accountxxx> accountxxxNullChain = accountxxxManager.queryxxxlowOne(query);if (query.non(AccountxxxQuery::getBelongingPartnerId)) {//判断是否存在xxid, 如果存在那么校验账户的xxxboolean notEq = accountxxxNullChain.map((data) -> accountNewManager.queryxxxxtNo(data.getAxxxNo())).map(AccountNew::getBelongingPartnerId).notEq(query.getBelongingPartnerId());if (notEq) {log.warn("getAccounxxx失败query:{}", query);return ResultX.success();}}return accountxxxNullChain.map(accountxxxConverter::toDTO).map(ResultX::success).orElse(ResultX.success());
}
如果在 Controller 中返回值也想返回NullChain ,那么就需要在统一返回拦截的地方把空链里面的值取出来返回给前端
警告问题
vararg 形参的未检查的泛型数组创建,
可以在IDEA中给禁止了不然警告的烦人
支持
相关联系方式(添加烦请注明来意):
- QQ:3426154361
- 邮件:3426154361@qq.com
文章原文地址: https://www.yuque.com/huanmin-4bkaa/eeags4/by8xf3rw826bycso?singleDoc# 《Java-空链处理》