|
| 1 | +--- |
| 2 | +title: Jdk14都要出了,还不能使用 Optional优雅的处理空指针? |
| 3 | +# toc: false |
| 4 | +date: 2019-11-04 08:01:01 |
| 5 | +url: jdk/jdk8-optional |
| 6 | +tags: |
| 7 | + - Java8 |
| 8 | + - Optional |
| 9 | +categories: |
| 10 | + - Java 新特性 |
| 11 | +--- |
| 12 | + |
| 13 | +# 1. 前言 |
| 14 | + |
| 15 | +> 如果你没有处理过空指针,那么你不是一位真正的 Java 程序员。 |
| 16 | +
|
| 17 | +空指针确实会产生很多问题,我们经常遇到空的引用,然后又想从这个空的引用上去获取其他的值,接着理所当然的碰到了 `NullPointException`。这是你可能会想,这报错很好处理,然后你看了眼报错行数,对比了下代码。脑海里瞬间闪过 ”对对对,这里有可能为空“,然后加上 `null check`轻松处理。然而你不知道这已经是你处理的第多少个空指针异常了。 |
| 18 | +<!-- more --> |
| 19 | +为了解决上面的问题,在 Java SE8 中引入了一个新类 `java.util.Optional`,这个类可以**缓解**上面的问题。 |
| 20 | + |
| 21 | +你可能已经发现了,上面我用的是**缓解**而不是**解决**。这也是很多人理解不太对的地方,以为 Java SE8 中的 `Optional` 类可以解决空指针问题。其实 Optional 类的的使用只是**提示**你这里可能存在空值,需要特殊处理,并提供了一些特殊处理的方法。如果你把 `Optional` 类当作空指针的救命稻草而不加思考的使用,那么依旧会碰到错误。 |
| 22 | + |
| 23 | +因为 `Optional` 是的 Java SE8 中引入的,因此本文中难免会有一些 JDK8 中的语法,如 **Lambda** 表达式,流处理等,但是都是基本形式,不会有过于复杂的案例。 |
| 24 | + |
| 25 | +# 2. Optional 创建 |
| 26 | + |
| 27 | +Optional 的创建一共有三种方式。 |
| 28 | + |
| 29 | +```java |
| 30 | +/** |
| 31 | + * 创建一个 Optional |
| 32 | + */ |
| 33 | +@Test |
| 34 | +public void createOptionalTest() { |
| 35 | + // Optional 构造方式1 - of 传入的值不能为 null |
| 36 | + Optional<String> helloOption = Optional.of("hello"); |
| 37 | + |
| 38 | + // Optional 构造方式2 - empty 一个空 optional |
| 39 | + Optional<String> emptyOptional = Optional.empty(); |
| 40 | + |
| 41 | + // Optional 构造方式3 - ofNullable 支持传入 null 值的 optional |
| 42 | + Optional<String> nullOptional = Optional.ofNullable(null); |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +其中构造方式1中 `of` 方法,如果传入的值会空,会报出 `NullPointerException` 异常。 |
| 47 | + |
| 48 | +# 3. Optional 判断 |
| 49 | + |
| 50 | +Optional 只是一个包装对象,想要判断里面有没有值可以使用 `isPresent` 方法检查其中是否有值 。 |
| 51 | + |
| 52 | +```java |
| 53 | +/** |
| 54 | + * 检查是否有值 |
| 55 | + */ |
| 56 | +@Test |
| 57 | +public void checkOptionalTest() { |
| 58 | + Optional<String> helloOptional = Optional.of("Hello"); |
| 59 | + System.out.println(helloOptional.isPresent()); |
| 60 | + |
| 61 | + Optional<Object> emptyOptional = Optional.empty(); |
| 62 | + System.out.println(emptyOptional.isPresent()); |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +得到的输出: |
| 67 | + |
| 68 | +```java |
| 69 | +true |
| 70 | +false |
| 71 | +``` |
| 72 | + |
| 73 | +从 JDK11 开始,提供了 `isEmpty`方法用来检查相反的结果:是否为空。 |
| 74 | + |
| 75 | +如果想要在有值的时候进行一下操作。可以使用 `ifPresent`方法。 |
| 76 | + |
| 77 | +```java |
| 78 | +/** |
| 79 | + * 如果有值,输出长度 |
| 80 | + */ |
| 81 | +@Test |
| 82 | +public void whenIsPresent() { |
| 83 | + // 如果没有值,获取默认值 |
| 84 | + Optional<String> helloOptional = Optional.of("Hello"); |
| 85 | + Optional<String> emptyOptional = Optional.empty(); |
| 86 | + helloOptional.ifPresent(s -> System.out.println(s.length())); |
| 87 | + emptyOptional.ifPresent(s -> System.out.println(s.length())); |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +输出结果: |
| 92 | + |
| 93 | +```java |
| 94 | +5 |
| 95 | +``` |
| 96 | + |
| 97 | +# 4. Optional 获取值 |
| 98 | + |
| 99 | +使用 `get`方法可以获取值,但是如果值不存在,会抛出 `NoSuchElementException` 异常。 |
| 100 | + |
| 101 | +```java |
| 102 | +/** |
| 103 | + * 如果没有值,会抛异常 |
| 104 | + */ |
| 105 | +@Test |
| 106 | +public void getTest() { |
| 107 | + Optional<String> stringOptional = Optional.of("hello"); |
| 108 | + System.out.println(stringOptional.get()); |
| 109 | + // 如果没有值,会抛异常 |
| 110 | + Optional<String> emptyOptional = Optional.empty(); |
| 111 | + System.out.println(emptyOptional.get()); |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +得到结果: |
| 116 | + |
| 117 | +```java |
| 118 | +hello |
| 119 | + |
| 120 | +java.util.NoSuchElementException: No value present |
| 121 | + at java.util.Optional.get(Optional.java:135) |
| 122 | + at net.codingme.feature.jdk8.Jdk8Optional.getTest(Jdk8Optional.java:91) |
| 123 | +``` |
| 124 | + |
| 125 | + |
| 126 | + |
| 127 | +# 5. Optional 默认值 |
| 128 | + |
| 129 | +使用 `orElse`, `orElseGet` 方法可以在没有值的情况下获取给定的默认值。 |
| 130 | + |
| 131 | +```java |
| 132 | +/** |
| 133 | + * 如果没有值,获取默认值 |
| 134 | + */ |
| 135 | +@Test |
| 136 | +public void whenIsNullGetTest() { |
| 137 | + // 如果没有值,获取默认值 |
| 138 | + Optional<String> emptyOptional = Optional.empty(); |
| 139 | + String orElse = emptyOptional.orElse("orElse default"); |
| 140 | + String orElseGet = emptyOptional.orElseGet(() -> "orElseGet default"); |
| 141 | + System.out.println(orElse); |
| 142 | + System.out.println(orElseGet); |
| 143 | +} |
| 144 | +``` |
| 145 | +得到的结果: |
| 146 | +```log |
| 147 | +orElse default |
| 148 | +orElseGet default |
| 149 | +``` |
| 150 | +看到这里你可能会有些疑惑了,这两个方法看起来效果是一模一样的,为什么会提供两个呢?下面再看一个例子,你会发现两者的区别。 |
| 151 | +```java |
| 152 | + /** |
| 153 | + * orElse 和 orElseGet 的区别 |
| 154 | + */ |
| 155 | +@Test |
| 156 | +public void orElseAndOrElseGetTest() { |
| 157 | + // 如果没有值,默认值 |
| 158 | + Optional<String> emptyOptional = Optional.empty(); |
| 159 | + System.out.println("空Optional.orElse"); |
| 160 | + String orElse = emptyOptional.orElse(getDefault()); |
| 161 | + System.out.println("空Optional.orElseGet"); |
| 162 | + String orElseGet = emptyOptional.orElseGet(() -> getDefault()); |
| 163 | + System.out.println("空Optional.orElse结果:"+orElse); |
| 164 | + System.out.println("空Optional.orElseGet结果:"+orElseGet); |
| 165 | + System.out.println("--------------------------------"); |
| 166 | + // 如果没有值,默认值 |
| 167 | + Optional<String> stringOptional = Optional.of("hello"); |
| 168 | + System.out.println("有值Optional.orElse"); |
| 169 | + orElse = stringOptional.orElse(getDefault()); |
| 170 | + System.out.println("有值Optional.orElseGet"); |
| 171 | + orElseGet = stringOptional.orElseGet(() -> getDefault()); |
| 172 | + System.out.println("有值Optional.orElse结果:"+orElse); |
| 173 | + System.out.println("有值Optional.orElseGet结果:"+orElseGet); |
| 174 | +} |
| 175 | + |
| 176 | +public String getDefault() { |
| 177 | + System.out.println(" 获取默认值中..run getDeafult method"); |
| 178 | + return "hello"; |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +得到的输出: |
| 183 | + |
| 184 | +```log |
| 185 | +空Optional.orElse |
| 186 | + 获取默认值中..run getDeafult method |
| 187 | +空Optional.orElseGet |
| 188 | + 获取默认值中..run getDeafult method |
| 189 | +空Optional.orElse结果:hello |
| 190 | +空Optional.orElseGet结果:hello |
| 191 | +-------------------------------- |
| 192 | +有值Optional.orElse |
| 193 | + 获取默认值中..run getDeafult method |
| 194 | +有值Optional.orElseGet |
| 195 | +有值Optional.orElse结果:hello |
| 196 | +有值Optional.orElseGet结果:hello |
| 197 | +``` |
| 198 | + |
| 199 | +在这个例子中会发现 `orElseGet` 传入的方法在有值的情况下并不会运行。而 `orElse`却都会运行。 |
| 200 | + |
| 201 | +# 6. Optional 异常 |
| 202 | + |
| 203 | +使用 `orElseThrow` 在没有值的时候抛出异常 |
| 204 | + |
| 205 | +```java |
| 206 | +/** |
| 207 | + * 如果没有值,抛出异常 |
| 208 | + */ |
| 209 | +@Test |
| 210 | +public void whenIsNullThrowExceTest() throws Exception { |
| 211 | + // 如果没有值,抛出异常 |
| 212 | + Optional<String> emptyOptional = Optional.empty(); |
| 213 | + String value = emptyOptional.orElseThrow(() -> new Exception("发现空值")); |
| 214 | + System.out.println(value); |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +得到结果: |
| 219 | + |
| 220 | +```java |
| 221 | +java.lang.Exception: 发现空值 |
| 222 | + at net.codingme.feature.jdk8.Jdk8Optional.lambda$whenIsNullThrowExceTest$7(Jdk8Optional.java:118) |
| 223 | + at java.util.Optional.orElseThrow(Optional.java:290) |
| 224 | + at net.codingme.feature.jdk8.Jdk8Optional.whenIsNullThrowExceTest(Jdk8Optional.java:118) |
| 225 | +``` |
| 226 | + |
| 227 | +# 7. Optional 函数接口 |
| 228 | + |
| 229 | +`Optional` 随 JDK8 一同出现,必然会有一些 JDK8 中的新特性,比如函数接口。`Optional` 中主要有三个传入函数接口的方法,分别是`filter`,`map`,`flatMap`。这里面的实现其实是 JDK8 的另一个新特性了,因此这里只是简单演示,不做解释。后面放到其他 JDK8 新特性文章里介绍。 |
| 230 | + |
| 231 | +```java |
| 232 | +@Test |
| 233 | +public void functionTest() { |
| 234 | + // filter 过滤 |
| 235 | + Optional<Integer> optional123 = Optional.of(123); |
| 236 | + optional123.filter(num -> num == 123).ifPresent(num -> System.out.println(num)); |
| 237 | + |
| 238 | + Optional<Integer> optional456 = Optional.of(456); |
| 239 | + optional456.filter(num -> num == 123).ifPresent(num -> System.out.println(num)); |
| 240 | + |
| 241 | + // map 转换 |
| 242 | + Optional<Integer> optional789 = Optional.of(789); |
| 243 | + optional789.map(String::valueOf).map(String::length).ifPresent(length -> System.out.println(length)); |
| 244 | +} |
| 245 | +``` |
| 246 | + |
| 247 | +得到结果: |
| 248 | + |
| 249 | +```java |
| 250 | +123 |
| 251 | +3 |
| 252 | +``` |
| 253 | + |
| 254 | +# 8. Optional 案例 |
| 255 | + |
| 256 | +假设有计算机、声卡、usb 三种硬件(下面的代码中使用了 `Lombok` 的 `@Data` 注解)。 |
| 257 | + |
| 258 | +```java |
| 259 | +/** |
| 260 | + * 计算机 |
| 261 | + */ |
| 262 | +@Data |
| 263 | +class Computer { |
| 264 | + private Optional<SoundCard> soundCard; |
| 265 | +} |
| 266 | + |
| 267 | +/** |
| 268 | + * 声卡 |
| 269 | + */ |
| 270 | +@Data |
| 271 | +class SoundCard { |
| 272 | + private Optional<Usb> usb; |
| 273 | +} |
| 274 | + |
| 275 | +/** |
| 276 | + * USB |
| 277 | + */ |
| 278 | +@Data |
| 279 | +class Usb { |
| 280 | + private String version; |
| 281 | +} |
| 282 | +``` |
| 283 | + |
| 284 | +计算机可能会有声卡,声卡可能会有 usb。那么怎么取得 usb 版本呢? |
| 285 | + |
| 286 | +```java |
| 287 | +/** |
| 288 | + * 电脑里【有可能】有声卡 |
| 289 | + * 声卡【有可能】有USB接口 |
| 290 | + */ |
| 291 | +@Test |
| 292 | +public void optionalTest() { |
| 293 | + // 没有声卡,没有 Usb 的电脑 |
| 294 | + Computer computerNoUsb = new Computer(); |
| 295 | + computerNoUsb.setSoundCard(Optional.empty()); |
| 296 | + // 获取 usb 版本 |
| 297 | + Optional<Computer> computerOptional = Optional.ofNullable(computerNoUsb); |
| 298 | + String version = computerOptional.flatMap(Computer::getSoundCard).flatMap(SoundCard::getUsb) |
| 299 | + .map(Usb::getVersion).orElse("UNKNOWN"); |
| 300 | + System.out.println(version); |
| 301 | + System.out.println("-----------------"); |
| 302 | + |
| 303 | + // 如果有值,则输出 |
| 304 | + SoundCard soundCard = new SoundCard(); |
| 305 | + Usb usb = new Usb(); |
| 306 | + usb.setVersion("2.0"); |
| 307 | + soundCard.setUsb(Optional.ofNullable(usb)); |
| 308 | + Optional<SoundCard> optionalSoundCard = Optional.ofNullable(soundCard); |
| 309 | + optionalSoundCard.ifPresent(System.out::println); |
| 310 | + // 如果有值,则输出 |
| 311 | + if (optionalSoundCard.isPresent()) { |
| 312 | + System.out.println(optionalSoundCard.get()); |
| 313 | + } |
| 314 | + |
| 315 | + // 输出没有值,则没有输出 |
| 316 | + Optional<SoundCard> optionalSoundCardEmpty = Optional.ofNullable(null); |
| 317 | + optionalSoundCardEmpty.ifPresent(System.out::println); |
| 318 | + System.out.println("-----------------"); |
| 319 | + |
| 320 | + // 筛选 Usb2.0 |
| 321 | + optionalSoundCard.map(SoundCard::getUsb) |
| 322 | + .filter(usb1 -> "3.0".equals(usb1.map(Usb::getVersion) |
| 323 | + .orElse("UBKNOW"))) |
| 324 | + .ifPresent(System.out::println); |
| 325 | +} |
| 326 | +``` |
| 327 | + |
| 328 | +得到结果: |
| 329 | + |
| 330 | +```java |
| 331 | + |
| 332 | +UNKNOWN |
| 333 | +----------------- |
| 334 | +SoundCard(usb=Optional[Usb(version=2.0)]) |
| 335 | +SoundCard(usb=Optional[Usb(version=2.0)]) |
| 336 | +----------------- |
| 337 | +``` |
| 338 | + |
| 339 | +# 9. Optional 总结 |
| 340 | + |
| 341 | +在本文中,我们看到了如何使用 Java SE8 的 `java.util.Optional` 类。`Optional` 类的目的不是为了替换代码中的每个空引用,而是为了帮助更好的设计程序,让使用者可以仅通过观察属性类型就可以知道会不会有空值。另外,`Optional`不提供直接获取值的方法,使用时会强迫你处理不存在的情况。间接的让你的程序免受空指针的影响。 |
| 342 | + |
| 343 | + |
| 344 | + |
| 345 | +文中代码已经上传 [Github](https://github.com/niumoo/jdk-feature)。 |
| 346 | + |
| 347 | +https://github.com/niumoo/jdk-feature |
| 348 | + |
| 349 | + |
| 350 | + |
| 351 | + |
| 352 | + |
| 353 | + |
| 354 | + |
| 355 | + |
| 356 | + |
| 357 | + |
| 358 | + |
0 commit comments