Java 분야에는 다음과 같은 전설이 돌고 있습니다. 널 포인터 예외를 완전히 이해하기 전까지는 자격을 갖춘 Java 개발자로 간주될 수 없습니다. 눈부신 Java 코드 문자 경력에서 우리는 매일 다양한 null 처리를 접하게 됩니다. 다음과 같은 코드를 매일 반복적으로 작성할 수 있습니다.
if(null != obj1){ if(null != obje2){ // do something } }
약간의 비전이 있으면 Javaer가 할 수 있는 일 좀 더 전문적으로 null을 결정하는 방법을 만듭니다.
boolean checkNotNull(Object obj){ return null == obj ? false : true; } void do(){ if(checkNotNull(obj1)){ if(checkNotNull(obj2)){ //do something } } }
그런 다음 다시 질문이 생깁니다. null이 빈 문자열을 나타내는 경우 ""는 무엇을 의미합니까?
그렇다면 관성적 사고는 ""와 null이 모두 빈 문자열 코드가 아닌가? 판정 null 값만 업그레이드 했습니다:
boolean checkNotBlank(Object obj){ return null != obj && !"".equals(obj) ? true : false; }void do(){ if(checkNotBlank(obj1)){ if(checkNotNull(obj2)){ //do something } } }
시간이 되시면 현재 프로젝트나 본인의 과거 코드를 보시면서 위와 비슷한 코드가 몇개나 작성되었는지 확인하실 수 있습니다.
다음 질문에 대해 진지하게 생각해 보셨는지 모르겠습니다. null이 무엇을 의미합니까?
단순 이해 - null은 물론 "값이 존재하지 않음"을 의미합니다.
메모리 관리에 대한 이해가 필요합니다. null은 메모리가 할당되지 않았으며 포인터가 null 주소를 가리킨다는 의미입니다.
조금 더 자세히 이해해 보면 null은 어딘가 처리에 문제가 있다는 의미일 수도 있고, 특정 값이 존재하지 않는다는 의미일 수도 있습니다.
수천 번 학대를 당한 후 인식 - 앗, 또 NullPointerException이 발생했는데, if(null != 값)을 추가해야 하는 것 같습니다.
이전 코딩 경력에서 java.lang.NullPointerException을 몇 번이나 접했습니까? RuntimeException 수준 예외로서 NullPointerException은 명시적으로 캡처할 필요가 없습니다. 주의 깊게 처리하지 않으면 프로덕션 로그에서 NullPointerException으로 인해 발생하는 다양한 예외 스택 출력을 자주 볼 수 있습니다. 그리고 이 예외 스택 정보를 토대로 보면 문제의 원인을 전혀 찾을 수 없습니다. 왜냐하면 문제가 NullPointerException이 발생한 장소에서 발생한 것이 아니기 때문입니다. 이 null이 생성된 위치를 쿼리하려면 더 깊이 들어가야 하는데 현재 로그에서 이를 추적할 수 없는 경우가 많습니다.
때로 더 비극적인 것은 null 값이 생성되는 곳이 우리 프로젝트 코드에 없는 경우가 많다는 것입니다. 더 당황스러운 사실은 다양한 품질의 다양한 타사 인터페이스를 호출할 때 특정 인터페이스가 우연히 null을 반환할지 여부가 불분명하다는 것입니다...
null에 대한 이전 인지 문제로 돌아가기 . 많은 Java 사용자는 null이 "아무것도 없음" 또는 "값이 존재하지 않음"을 의미한다고 생각합니다. 이러한 관성적 사고에 따르면, 우리의 코드 논리는 다음과 같습니다. 내 인터페이스를 호출하고 사용자가 제공한 매개변수에 따라 해당 "값"을 반환합니다. 이 조건에서 해당 "값"을 찾을 수 없으면 당연히 다음을 반환합니다. 당신에게는 더 이상 "아무것도" 없습니다. 매우 전통적이고 표준적인 Java 코딩 스타일로 작성된 다음 코드를 살펴보겠습니다.
class MyEntity{ int id; String name; String getName(){ return name; } }// mainpublic class Test{ public static void main(String[] args) final MyEntity myEntity = getMyEntity(false); System.out.println(myEntity.getName()); } private getMyEntity(boolean isSuc){ if(isSuc){ return new MyEntity(); }else{ return null; } } }
이 코드는 확실히 이보다 훨씬 더 복잡합니다. 사실 우리가 가지고 있는 많은 Java 코딩은 모두 이 루틴에 따라 작성되었으며, 지식이 있는 사람은 NullPointerException이 확실히 발생한다는 것을 한 눈에 알 수 있습니다. 그러나 비즈니스 코드를 작성할 때 우리는 가능한 null을 처리하는 것을 거의 생각하지 않습니다(아마도 API 문서가 명확하게 작성되어 어떤 경우에는 null이 반환되지만 코드 작성을 시작하기 전에 API 문서를 주의 깊게 읽어야 합니까?). 특정 테스트 단계에 도달하고 갑자기 NullPointerException이 나타날 때까지 우리는 반환될 수 있는 null 값을 처리하기 위해 다음과 같은 판단을 추가해야 한다는 것을 깨달았습니다.
// mainpublic class Test{ public static void main(String[] args) final MyEntity myEntity = getMyEntity(false); if(null != myEntity){ System.out.println(myEntity.getName()); }else{ System.out.println("ERROR"); } } }
지난 몇 년을 잘 생각해 보세요. 우리 모두 이런 일을 해봤나요? 일부 Null로 인한 문제를 테스트 단계까지 발견할 수 없다면 이제 질문이 제기됩니다. 우아하고 복잡하며 잘 계층화된 비즈니스 코드에서 얼마나 많은 Null이 올바르게 처리되지 않습니까?
null 처리에 대한 태도는 프로젝트의 성숙도와 엄격함을 알 수 있는 경우가 많습니다. 예를 들어 Guava는 JDK1.6 이전에 우아한 null 처리 방법을 제공했는데, 이는 깊은 지식을 보여줍니다.
유령 null이 우리의 발전을 방해합니다
전통적인 객체 지향 개발에 중점을 두는 Javar라면 아마도 null로 인해 발생하는 다양한 문제에 이미 익숙할 것입니다. 그러나 몇 년 전 스승님은 null이 함정이라고 말씀하셨습니다.
Tony Hall(이 사람이 누구인지 모르시나요? 직접 확인해보세요)은 다음과 같이 말했습니다. 구현하기가 너무 쉽기 때문에 널 참조를 삽입하고 싶은 유혹을 뿌리칠 수 없었습니다." 당시에는 널 참조가 구현하기 너무 쉬웠기 때문에 그 유혹을 참지 못하고 널 포인터라는 것을 발명했습니다. ").
그럼 null이 어떤 다른 문제를 야기하는지 살펴보겠습니다.
아래 코드를 보세요.
String address = person.getCountry().getProvince().getCity();
如果你玩过一些函数式语言(Haskell、Erlang、Clojure、Scala等等),上面这样是一种很自然的写法。用Java当然也可以实现上面这样的编写方式。
但是为了完满的处理所有可能出现的null异常,我们不得不把这种优雅的函数编程范式改为这样:
if (person != null) { Country country = person.getCountry(); if (country != null) { Province province = country.getProvince(); if (province != null) { address = province.getCity(); } } }
瞬间,高逼格的函数式编程Java8又回到了10年前。这样一层一层的嵌套判断,增加代码量和不优雅还是小事。更可能出现的情况是:在大部分时间里,人们会忘记去判断这可能会出现的null,即使是写了多年代码的老人家也不例外。
上面这一段层层嵌套的 null 处理,也是传统Java长期被诟病的地方。如果以Java早期版本作为你的启蒙语言,这种get->if null->return 的臭毛病会影响你很长的时间(记得在某国外社区,这被称为:面向entity开发)。
利用Optional实现Java函数式编程
好了,说了各种各样的毛病,然后我们可以进入新时代了。
早在推出Java SE 8版本之前,其他类似的函数式开发语言早就有自己的各种解决方案。下面是Groovy的代码:
String version = computer?.getSoundcard()?.getUSB()?.getVersion():"unkonwn";
Haskell用一个 Maybe 类型类标识处理null值。而号称多范式开发语言的Scala则提供了一个和Maybe差不多意思的Option[T],用来包裹处理null。
Java8引入了 java.util.Optional8742468051c85b06f0a0af9e3e506b5c来处理函数式编程的null问题,Optional8742468051c85b06f0a0af9e3e506b5c的处理思路和Haskell、Scala类似,但又有些许区别。先看看下面这个Java代码的例子:
public class Test { public static void main(String[] args) { final String text = "Hallo world!"; Optional.ofNullable(text)//显示创建一个Optional壳 .map(Test::print) .map(Test::print) .ifPresent(System.out::println); Optional.ofNullable(text) .map(s ->{ System.out.println(s); return s.substring(6); }) .map(s -> null)//返回 null .ifPresent(System.out::println); } // 打印并截取str[5]之后的字符串 private static String print(String str) { System.out.println(str); return str.substring(6); } } //Consol 输出 //num1:Hallo world! //num2:world! //num3: //num4:Hallo world!
(可以把上面的代码copy到你的IDE中运行,前提是必须安装了JDK8。)
上面的代码中创建了2个Optional,实现的功能基本相同,都是使用Optional作为String的外壳对String进行截断处理。当在处理过程中遇到null值时,就不再继续处理。我们可以发现第二个Optional中出现s->null之后,后续的ifPresent不再执行。
注意观察输出的 //num3:,这表示输出了一个""字符,而不是一个null。
Optional提供了丰富的接口来处理各种情况,比如可以将代码修改为:
public class Test { public static void main(String[] args) { final String text = "Hallo World!"; System.out.println(lowerCase(text));//方法一 lowerCase(null, System.out::println);//方法二 } private static String lowerCase(String str) { return Optional.ofNullable(str).map(s -> s.toLowerCase()).map(s->s.replace("world", "java")).orElse("NaN"); } private static void lowerCase(String str, Consumer<String> consumer) { consumer.accept(lowerCase(str)); } } //输出 //hallo java! //NaN
这样,我们可以动态的处理一个字符串,如果在任何时候发现值为null,则使用orElse返回预设默认的"NaN"。
总的来说,我们可以将任何数据结构用Optional包裹起来,然后使用函数式的方式对他进行处理,而不必关心随时可能会出现的null。
我们看看前面提到的Person.getCountry().getProvince().getCity()怎么不用一堆if来处理。
第一种方法是不改变以前的entity:
import java.util.Optional;public class Test { public static void main(String[] args) { System.out.println(Optional.ofNullable(new Person()) .map(x->x.country) .map(x->x.provinec) .map(x->x.city) .map(x->x.name) .orElse("unkonwn")); } }class Person { Country country; }class Country { Province provinec; }class Province { City city; }class City { String name; }
这里用Optional作为每一次返回的外壳,如果有某个位置返回了null,则会直接得到"unkonwn"。
第二种办法是将所有的值都用Optional来定义:
import java.util.Optional;public class Test { public static void main(String[] args) { System.out.println(new Person() .country.flatMap(x -> x.provinec) .flatMap(Province::getCity) .flatMap(x -> x.name) .orElse("unkonwn")); } }class Person { Optional<Country> country = Optional.empty(); }class Country { Optional<Province> provinec; }class Province { Optional<City> city; Optional<City> getCity(){//用于:: return city; } }class City { Optional<String> name; }
第一种方法可以平滑的和已有的JavaBean、Entity或POJA整合,而无需改动什么,也能更轻松的整合到第三方接口中(例如spring的bean)。建议目前还是以第一种Optional的使用方法为主,毕竟不是团队中每一个人都能理解每个get/set带着一个Optional的用意。
Optional还提供了一个filter方法用于过滤数据(实际上Java8里stream风格的接口都提供了filter方法)。例如过去我们判断值存在并作出相应的处理:
if(Province!= null){ City city = Province.getCity(); if(null != city && "guangzhou".equals(city.getName()){ System.out.println(city.getName()); }else{ System.out.println("unkonwn"); } }
现在我们可以修改为
Optional.ofNullable(province) .map(x->x.city) .filter(x->"guangzhou".equals(x.getName())) .map(x->x.name) .orElse("unkonw");
到此,利用Optional来进行函数式编程介绍完毕。Optional除了上面提到的方法,还有orElseGet、orElseThrow等根据更多需要提供的方法。orElseGet会因为出现null值抛出空指针异常,而orElseThrow会在出现null时,抛出一个使用者自定义的异常。可以查看API文档来了解所有方法的细节。
写在最后的
Optional只是Java函数式编程的冰山一角,需要结合lambda、stream、Funcationinterface等特性才能真正的了解Java8函数式编程的效用。本来还想介绍一些Optional的源码和运行原理的,但是Optional本身的代码就很少、API接口也不多,仔细想想也没什么好说的就省略了。
Optional虽然优雅,但是个人感觉有一些效率问题,不过还没去验证。如果有谁有确实的数据,请告诉我。
저는 '함수형 프로그래밍 지지자'도 아닙니다. 팀 관리자의 입장에서는 학습 난이도가 높아질 때마다 인력 사용 비용과 팀 상호 작용 비용이 높아집니다. 전설처럼 Lisp는 C++보다 30분의 1 적은 코드를 가질 수 있고 개발 효율성이 더 높습니다. 그러나 국내의 기존 IT 회사가 실제로 Lisp를 사용하여 프로젝트를 수행한다면 이러한 Lisp를 구입하려면 어디로 가야 하며 비용은 얼마나 듭니까? 기반 소프트웨어요?
하지만 저는 모든 사람이 함수형 프로그래밍의 개념을 배우고 이해하도록 적극 권장합니다. 특히 과거에 Java라는 언어에만 빠져 있었고 아직도 Java8이 어떤 변화를 가져올지 모르는 개발자들에게 Java8은 좋은 기회입니다. 또한, 현재 프로젝트에 새로운 Java8 기능을 도입하는 것이 좋습니다. 장기적인 협력 팀과 고대 프로그래밍 언어는 지속적으로 새로운 활력을 주입해야 합니다. 그렇지 않으면 발전하지 못할 것입니다.