In der Java-Welt kursiert eine Legende: Man kann erst dann als qualifizierter Java-Entwickler gelten, wenn man die Nullzeiger-Ausnahme wirklich versteht. In unserer schillernden Karriere als Java-Code-Charakter stoßen wir jeden Tag auf verschiedene Nullverarbeitungen. Wir schreiben möglicherweise jeden Tag wiederholt Code wie den folgenden:
if(null != obj1){ if(null != obje2){ // do something } }
Wenn Sie eine kleine Vision haben, kann Javaer etwas tun Seien Sie kreativer und erstellen Sie eine Methode zur Bestimmung von Null
boolean checkNotNull(Object obj){ return null == obj ? false : true; } void do(){ if(checkNotNull(obj1)){ if(checkNotNull(obj2)){ //do something } } }
Dann stellt sich erneut die Frage: Wenn eine Null eine leere Zeichenfolge darstellt, was stellt „“ dar?
Dann sagt uns das Trägheitsdenken, sind „“ und null nicht beides leere String-Codes? Ich habe einfach den Nullwert der Beurteilung aktualisiert:
boolean checkNotBlank(Object obj){ return null != obj && !"".equals(obj) ? true : false; }void do(){ if(checkNotBlank(obj1)){ if(checkNotNull(obj2)){ //do something } } }
Wenn Sie Zeit haben, können Sie einen Blick auf das aktuelle Projekt oder Ihren eigenen früheren Code werfen, um zu sehen, wie viele Codes, die den oben genannten ähneln, geschrieben wurden.
Ich weiß nicht, ob Sie ernsthaft über eine Frage nachgedacht haben: Was bedeutet eine Null?
Einfaches Verständnis – null bedeutet natürlich „der Wert existiert nicht“.
Haben Sie etwas Erfahrung im Verständnis der Speicherverwaltung – null bedeutet, dass der Speicher nicht zugewiesen wurde und der Zeiger auf eine Nulladresse zeigt.
Ein etwas gründlicheres Verständnis – null kann darauf hinweisen, dass irgendwo ein Problem bei der Verarbeitung vorliegt, oder es kann darauf hinweisen, dass ein bestimmter Wert nicht vorhanden ist.
Erkennung nach tausendfachem Missbrauch – Hoppla, wieder eine NullPointerException, anscheinend muss ich ein if (null != value) hinzufügen.
Erinnern Sie sich: Wie oft sind wir in unserer bisherigen Karriere als Programmierer auf java.lang.NullPointerException gestoßen? Als Ausnahme auf RuntimeException-Ebene muss NullPointerException nicht explizit erfasst werden. Wenn nicht sorgfältig gehandhabt wird, werden im Produktionsprotokoll häufig verschiedene durch NullPointerException verursachte Ausnahmestapelausgaben angezeigt. Und basierend auf diesen Informationen zum Ausnahmestapel können wir die Ursache des Problems überhaupt nicht finden, da das Problem nicht durch den Ort verursacht wurde, an dem NullPointerException ausgelöst wurde. Wir müssen tiefer gehen, um herauszufinden, wo dieser Nullwert generiert wird, und zu diesem Zeitpunkt können die Protokolle oft nicht zurückverfolgt werden.
Manchmal ist es noch tragischer, dass der Ort, an dem Nullwerte generiert werden, oft nicht in unserem eigenen Projektcode liegt. Es gibt noch eine peinlichere Tatsache: Wenn wir verschiedene Schnittstellen von Drittanbietern unterschiedlicher Qualität aufrufen, ist unklar, ob eine bestimmte Schnittstelle zufällig eine Null zurückgibt ...
Zurück zum vorherigen kognitiven Problem mit Null . Viele Javaer glauben, dass null „nichts“ oder „der Wert existiert nicht“ bedeutet. Nach diesem Trägheitsdenken lautet unsere Codelogik: Sie rufen meine Schnittstelle auf und geben den entsprechenden „Wert“ gemäß den von Ihnen angegebenen Parametern zurück. Wenn der entsprechende „Wert“ unter dieser Bedingung nicht gefunden werden kann, gebe ich natürlich einen zurück null für dich. Es gibt kein „irgendetwas“ mehr. Schauen wir uns den folgenden Code an, der in einem sehr traditionellen und standardmäßigen Java-Codierungsstil geschrieben ist:
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; } } }
Dieser Code für den täglichen Gebrauch ist definitiv viel komplizierter als dieser, aber in Tatsächlich haben wir eine Menge Java-Programmierung, die alle nach dieser Routine geschrieben ist, und eine sachkundige Person kann auf einen Blick erkennen, dass definitiv eine NullPointerException geworfen wird. Aber wenn wir Geschäftscode schreiben, denken wir selten daran, mit dieser möglichen Null umzugehen (vielleicht wurde die API-Dokumentation klar geschrieben und in einigen Fällen wird Null zurückgegeben, aber stellen Sie sicher, dass Sie die API-Dokumentation sorgfältig lesen, bevor Sie mit dem Schreiben von Code beginnen?). Bis wir ein bestimmtes Teststadium erreichten und plötzlich eine NullPointerException auftauchte und uns klar wurde, dass wir ein Urteil wie das folgende hinzufügen mussten, um mit dem möglicherweise zurückgegebenen Nullwert umzugehen.
// 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"); } } }
Denken Sie sorgfältig über die letzten Jahre nach, haben wir das alle schon einmal erlebt? Wenn die durch einige Nullen verursachten Probleme erst in der Testphase entdeckt werden können, stellt sich nun die Frage: Wie viele Nullen werden in diesen eleganten, komplexen und gut geschichteten Geschäftscodes nicht korrekt behandelt?
Die Einstellung gegenüber Null kann oft die Reife und Genauigkeit eines Projekts erkennen. Beispielsweise stellte Guava lange vor JDK1.6 eine elegante Nullverarbeitungsmethode bereit, was sein tiefes Wissen zeigt.
Die gespenstische Null behindert unseren Fortschritt
Wenn Sie ein Javar sind, der sich auf die traditionelle objektorientierte Entwicklung konzentriert, sind Sie vielleicht bereits an die verschiedenen Probleme gewöhnt, die durch Null verursacht werden. Aber vor vielen Jahren sagte der Meister, dass Null eine Falle sei.
Tony Hall (weißt du nicht, wer dieser Typ ist? Schau es dir selbst an) sagte einmal: „Ich nenne es meinen Milliarden-Dollar-Fehler. Es war die Erfindung der Null-Referenz im Jahr 1965.“ Ich konnte der Versuchung nicht widerstehen, einen Nullverweis einzufügen, einfach weil es so einfach zu implementieren war.“ Damals waren Nullverweise zu einfach zu implementieren, also konnte ich der Versuchung nicht widerstehen und erfand das Nullzeiger-Ding. “).
Dann wollen wir sehen, welche anderen Probleme Null mit sich bringen wird.
Sehen Sie sich den folgenden Code an:
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.Optional
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虽然优雅,但是个人感觉有一些效率问题,不过还没去验证。如果有谁有确实的数据,请告诉我。
Ich bin auch kein „Unterstützer der funktionalen Programmierung“. Aus der Sicht eines Teammanagers steigen mit jedem Anstieg der Lernschwierigkeiten auch die Kosten für den Personaleinsatz und die Teaminteraktion. Wie in der Legende kann Lisp dreißigmal weniger Code als C haben und ist effizienter in der Entwicklung. Aber wenn ein inländisches reguläres IT-Unternehmen Lisp wirklich für Projekte verwendet, wo kann man dann hingehen und wie viel kostet es, diese Lisp zu bekommen? -basierte Programme? Alter?
Aber ich ermutige jeden dringend, die Ideen der funktionalen Programmierung zu lernen und zu verstehen. Gerade für Entwickler, die sich bisher nur mit Java als Sprache beschäftigt haben und noch nicht wissen, welche Änderungen Java8 mit sich bringen wird, ist Java8 eine gute Gelegenheit. Es wird auch empfohlen, neue Java8-Funktionen in aktuelle Projekte einzuführen. Ein langfristiges Kooperationsteam und eine alte Programmiersprache müssen kontinuierlich neue Vitalität einbringen, sonst werden sie sich zurückziehen, wenn sie nicht vorankommen.