متدهایی برای مدیریت اشارهگرهای null

یکی از بدترین کابوسهای برنامهنویسان در هر سطحی از توانایی و تجربه، چک کردن null بودن یا نبودن اشارهگر هاست. متدهایی کاربردی در API وجود دارد که در این زمینه به کمک برنامهنویسان آمده و کار را برایشان راحتتر میکند.
احتمالا شما هم با چنین کدهایی مواجه شدهاید:
public void addAddressToCustomer(Customer customer, Address newAddress){ if ( cutomer == null || newAddress == null) return; if ( customer.getAddresses() == null ){ customer.setAddresses ( new ArrayList<>()); } customer.addAddress(newAddress); }
برای به حداقل رساندن null check در کد، باید تلاش کنیم از هیچ متدی null برنگردانیم. علاوه بر این، برنامهنویس باید بداند لازم نیست ارزیابی اشارهگرها در تمام لایههای برنامه تکرار شود. قاعدتا null check در بالاترین لایه مانند UI، Presentation یا کنترلرها انجام میشود.
ارزیابی متغیرها باید به لایههای بالایی محدود شود. بخشی از نرمافزار که «منطق کار» یا Business logic را پیادهسازی کرده است، نباید دغدغۀ متغیرهای دریافتی را داشته باشد. با این وجود، نیاز به null check هیچ وقت به طور کامل از بین نمیرود. نوشتن «عبارتهای گارد» (یا Guard Clauses) و صحتسنجی شرایط و مقادیر یک متغیر در بخشی از کد، همواره اجتنابناپذیر خواهد بود.
کلاس java.util.Objects حاوی متدهای استاتیکی است که با استفاده از آن میتوان متغیر را چک کرد تا در صورتی که برابر با null بود، Exception مناسب پرتاب کرده یا جایگزینی برای آن ارائه کرد. این توابع بهترین گزینه برای استفاده در عبارتهای گارد هستند. در ادامه به مرور آنها خواهیم پرداخت.
متد اول
public static <T> T requireNonNull(T obj)
این متد، ورودی را بررسی میکند و اگر برابر با null باشد یک NullPointerException با پیغام خالی پرتاب میکند و در غیر این صورت، متغیری که دریافت کرده است را برمیگرداند.
مثال:
String str = null; str = Objects.requireNonNull(str);
خروجی:
Caused by: java.lang.NullPointerException at java.util.Objects.requireNonNull(Objects.java:203) …
متد دوم
public static <T> T requireNonNull(T obj, String message)
این متد، پارامتر اول را بررسی کرده و اگر مقدار آن برابر با null نباشد، همان را به عنوان خروجی باز میگرداند. در غیر این صورت، یک NullPointerException با پیامی که در پارامتر دوم (message) قرار دارد، پرتاب میکند.
مثال:
String str = null; str = Objects.requireNonNull(str, "str cannot be null");
خروجی:
Caused by: java.lang.NullPointerException: str cannot be null at java.util.Objects.requireNonNull(Objects.java:228) ...
متد سوم
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier)
این متد، پارامتر اول را بررسی کرده و اگرمقدار آن برابر با null نباشد، همان را به عنوان خروجی باز میگرداند. در غیر این صورت، با کمک پارامتر دوم یک NullPointerException با پیامی مناسب پرتاب میکند. نمونهای از Supplier که به عنوان پارامترِ دومِ متد ارسال شده است میتواند حاوی «منطق»ای برای تولید متن مناسب باشد و به عنوان تولیدکنندۀ پیام Exception مورد استفاده قرار گیرد.
مثال:
import java.util.Objects; public class RequireNonNullExample3 { public static void main(String... args) { String str = null; //useful when creating the message is expensive Objects.requireNonNull(str, RequireNonNullExample3::createMessage); } private static String createMessage() { return "String can’t be null in "+RequireNonNullExample3.class.getName(); } }
خروجی:
Exception in thread "main" java.lang.NullPointerException: String can’t be null in RequireNonNullExample3 at java.util.Objects.requireNonNull(Objects.java:290) at RequireNonNullExample3.main(RequireNonNullExample3.java:8)
جاوا 9 دو متد جدید به کلاس Objects اضافه کرده است که در ادامه به بررسی آنها خواهیم پرداخت. در این نسخه، java.util در داخل ماژول java.base جای داده شده است.
متد چهارم
static <T> T requireNonNullElse(T obj, T defaultObj)
این متد پارامتر اول را بررسی میکند، اگر برابر با null باشد، مقدار جایگزین یا defautObj را برمیگرداند. در غیر این صورت، پارامتر اول به خروجی ارسال میشود.
مثال:
public Instant convertDateToInstantWithNowDefault(final Date inputDate) { final Date dateToConvert = Objects.requireNonNullElse(inputDate, new Date()); return dateToConvert.toInstant(); }
در تکهکد فوق، در صورتی که پارامتر inputDate برابر با null باشد، یک شی جدید از نوع Date که زمان حال را نشان میدهد ساخته شده و به عنوان جایگزین ارائه میشود.
متد پنجم
requireNonNullElseGet(T obj, Supplier<? extends T> supplier)
این متد یک نسخه از Supplier دریافت میکند. در این نسخه میتوان به جای ارسال جایگزین، یک «منطق» برای ساخت آن ارسال کرد.
در کتاب بسیار عالی Modern Java Recipes، نوشتۀ Ken Kousen در مورد کاربردهای Supplier بحث شده است. در این کتاب آمده: «یکی از کاربردهای Supplier پیادهسازی مفهوم “عبارات اجرایی معوّق”1 است.» سپس نویسنده پس از نشان دادن روش استفاده از آن، مینویسد: «یکی از کاربردهای عبارات اجرایی معوق، ساخت یک مقدار موقع نیاز است، نه اینکه مجبور باشیم ابتدا آن را بسازیم و اگر به آن نیاز داشتیم مورد استفاده قرار دهیم.»
مثال:
public Instant convertDateToInstantWithCalculatedDefault(final Date inputDate) { final Date dateToConvert = Objects.requireNonNullElseGet(inputDate, () -> calculateDate()); return dateToConvert.toInstant(); }
در شرایطی که متد محاسبهکنندۀ مقدار پیشفرض نیازمند محاسبات زیادی باشد، بهتر است از این نسخه استفاده کنیم؛ چرا که محاسبات مذکور تنها در صورتی انجام میشود که مقدار پارامتر اول null باشد.
در صورتی که مقدار inputDate مقداری غیر از null باشد، متد ()calculateDate نیز اجرا نمیشود.
همانطور که گفته شد بیشترین کاربرد این متدها در عبارات گارد است، اما توانایی آنها در ارائۀ شی جایگزین میتواند کاربردهای جدیدی نیز به آنها ببخشد.
[1] deferred execution