ホームページ  >  記事  >  Java  >  Java のエレガントなオプションの null ポインタ処理

Java のエレガントなオプションの null ポインタ処理

高洛峰
高洛峰オリジナル
2016-11-22 15:33:481403ブラウズ

Java 分野では次のような伝説が広まっています。NULL ポインター例外を真に理解するまでは、資格のある Java 開発者とはみなされません。私たちの目まぐるしい Java コード文字のキャリアでは、毎日さまざまな null 処理に遭遇します。次のようなコードを毎日繰り返し書くことがあります。 . null を判断する方法を見つけてみましょう

if(null != obj1){
  if(null != obje2){
     // do something
  }
}

そこで、また疑問が生じます。null が空の文字列を表す場合、「」は何を表すのでしょうか?

そうすると、慣性的に考えると、「」と null は両方とも空の文字列コードではないでしょうか? null 値の判定をアップグレードしただけです:

boolean checkNotNull(Object obj){
  return null == obj ? false : true; 
}

void do(){
  if(checkNotNull(obj1)){
     if(checkNotNull(obj2)){
        //do something
     }
  }
}

時間があれば、現在のプロジェクトまたは自分の過去のコードを見て、上記と同様のコードがどれだけ書かれているかを確認してください。

「null とはどういう意味ですか?」という質問について真剣に考えたことがあるかどうかはわかりません。

簡単な理解 - もちろん、null は「値が存在しない」ことを意味します。

メモリ管理について理解した経験があること - null は、メモリが割り当てられておらず、ポインタが null アドレスを指していることを意味します。

もう少し徹底的に理解しましょう - null は、どこかの処理に問題があることを示したり、特定の値が存在しないことを示したりする可能性があります。

何千回も悪用された後の認識 - おっと、また NullPointerException が発生しました。if (null != value) を追加する必要があるようです。

思い出してください。これまでのコーディングのキャリアで java.lang.NullPointerException に何度遭遇したでしょうか? RuntimeException レベルの例外として、NullPointerException を明示的にキャプチャする必要はありません。慎重に処理しないと、NullPointerException によって引き起こされるさまざまな例外スタック出力が運用ログに記録されることがよくあります。そして、この例外スタック情報からは、NullPointerException がスローされた場所が問題の原因ではないため、問題の原因をまったく特定できません。この null が生成される場所を見つけるにはさらに深く調査する必要がありますが、現時点ではログを追跡できないことがよくあります。

場合によっては、より悲劇的なのは、Null 値が生成される場所が私たち自身のプロジェクト コード内にないことが多いということです。さらに恥ずかしい事実があります。さまざまな品質のさまざまなサードパーティ インターフェイスを呼び出したときに、特定のインターフェイスが何らかの偶然で null を返すかどうかは不明です...

先頭に戻り、null の認知問題に直面してください。多くの Javaer は、null が「何もない」または「値が存在しない」ことを意味すると考えています。この慣性思考によれば、私たちのコードロジックは次のとおりです。あなたは私のインターフェースを呼び出し、あなたが私に与えたパラメータに従って対応する「値」を返します。この条件下で対応する「値」が見つからない場合は、当然、 を返します。あなたにとって「何も」はもうありません。非常に伝統的で標準的な Java コーディング スタイルで書かれた次のコードを見てみましょう:

boolean checkNotBlank(Object obj){  return null != obj && !"".equals(obj) ? true : false; 
}void do(){  if(checkNotBlank(obj1)){     if(checkNotNull(obj2)){        //do something
     }
  }
}

このコード部分は間違いなくこれよりもはるかに複雑ですが、実際には Java の多くが使用されています。コーディングはこれに基づいています このルーチンを書くと、知識のある人なら一目で確実に NullPointerException がスローされることがわかります。しかし、ビジネス コードを書くときに、この可能性のある null を扱うことを考えることはほとんどありません (おそらく API ドキュメントは明確に書かれており、場合によっては null が返されるでしょうが、コードを書き始める前に必ず API ドキュメントをよく読んでください)。あるテスト段階に到達し、NullPointerException が突然ポップアップするまでは、返される可能性のある null 値に対処するには次のような判断を追加する必要があることに気付きました。

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;
       }
   }
}

過去数年間をよく考えてみてください、私たちは皆これを経験しましたか?一部の null によって引き起こされる問題がテスト段階まで発見できない場合、問題が生じます。これらの優雅で複雑な、十分に階層化されたビジネス コードで正しく処理されない null がどれだけあるのかということです。

Null 処理に対する態度によって、プロジェクトの成熟度や厳格さがわかる場合があります。たとえば、Guava は JDK1.6 よりもずっと前に洗練された null 処理メソッドを提供しており、その深い知識を示しています。

幽霊のような null が私たちの進歩を妨げます

あなたが従来のオブジェクト指向開発に重点を置いている Javar であれば、おそらく、null によって引き起こされるさまざまな問題にはすでに慣れているでしょう。しかし何年も前に、マスターはヌルは罠だと言いました。

トニー ホール (この男が誰なのか知りませんか? 自分で調べてみてください) はかつてこう言いました。「私はこれを私の 10 億ドルの間違いと呼んでいます。それは 1965 年のヌル参照の発明でした。私は抵抗できませんでした」単に実装が簡単だったという理由だけで、null 参照を入れたくなる誘惑に駆られたのです。」 (要旨: 「兄は null の発明を貴重な間違いだと言いました。1965 年のコンピュータの野蛮な時代には、null 参照はとても簡単だったからです」ジャン兄弟は誘惑に抵抗できず、ヌル ポインターを発明したことを実装するために。」)

次に、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 機能を現在のプロジェクトに導入することも奨励されています。長期的な協力チームと古いプログラミング言語は、継続的に新しい活力を注入する必要があります。そうしないと、前進しなければ後退してしまいます。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。