Tuesday, August 23, 2016

Java 8'de Optional Kullanımı


Java'da iki tür tip bulunuyor: basit tipler ve sınıflar. Değişkeni bellekte bir kutu olarak düşünebiliriz. Basit tipten bir değişken için bu kutunun içinde tanımlandığı basit tipten bir değer yer alır. Java'daki basit tipler fazla değil: byte, short, int, long, float, double, char ve boolean. Bunun dışındaki her tip bir sınıftır. Sınıf tipinden bir değişken ise referans değişkeni olarak adlandırılır. Referans tipinden bir değişken için ise kutunun içinde bir değer değil, bir adres yer alır. Bu adres ilgili sınıftan bir nesnenin adresidir. Java'da tüm nesneler Heap'de olmak zorundadır. Bu nedenle nesnelere bazen Heap nesnesi dendiği olur. Ancak referans tipinden bir değişken için kutunun içi boş olabilir. Kutunun içinin boş olduğunu, geçerli bir nesneyi göstermediğini tanımlamak için Java dilindeki bir sözcükten yararlanıyoruz: null. Eğer kutunun içinde null değeri var ise bu değişkeni kullanmasanız iyi olur. Eğer referans değişkenimiz null değeri taşıyorsa, bu referansı kullanarak bir metot çağrısı yapmanız, yürütme zamanında NullPointerException (NPE) tipinde bir hata almanıza neden olur. 


NPE bir RuntimeException olduğu için en azından derleyici sizi bu tür bir hatayı ele almak zorunda bırakmaz. Ele almadığınız durumda ise uygulamanız istenmeyen bir şekilde sona erecektir. Kutunun içinin boş olması, referans değişkeninin değerinin null olması, bazen mantıksal bir hatanın sonucudur. Bu hatayı mutlaka gidermeniz gerekir. Kutunun içinin boş olması, bazen de işin doğası gereği olabilir. İşin doğası gereği null değeri taşıyacak ise bu referans üzerinden erişimden önce mutlaka önlem alınması gerekir:
public class MovieSearchService {
. . .
    Collection<Movie> search(Criteria criteria){ . . . }
. . .
}

MovieSearchService içindeki search() metodu verilen bir kritere göre sorgu sonucu dönüyor ve sorgu sonucu boş olabilir. Bu durumda metot null dönüyorsa sorgu sonucuna erişimden önce mutlaka önlem almak gerekir:
MovieSearchService serchSrv= InMemeoryMovieSearchService.getInstance();
Criteria someCriteria= new Criteria(/* irrelavent */); 
Collection<Movie> searchResult= searchSrv.search(someCriteria);
if (searchResult != null){
   System.out.println(String.format("There are %d movies found:",searchResult.size()));
   searchResult.stream().forEach(System.out::println)    
} 

Eğer bir metot torba dönüyorsa, uygulama programcısını NPE'ye neden olmayacak ancak yukarıdaki gibi bir önlem almaya zorlamayacak bir çözüm sunabiliriz. Bunun için eğer arama kriterine uygun hiç bir film yoksa basitçe içi boş bir liste dönebiliriz: Collections.emptyList(). Bunun yanında, Java 8 ile birlikte doğrudan torba dönmek yerine Stream<> ya da CompletableFuture<> dönülebilir:
public interface ManagedServerRepository extends Repository<ManagedServer,Long> {
     Optional<S extends ManagedServer> findOne(Long id);
     <S extends ManagedServer> S save(S server);
     <S extends ManagedServer> S delete(S server);
     <S extends ManagedServer> S delete(Long id);
     @Query("select m from ManagedServer m")
     Stream<ManagedServer> streamAllServers();
     Stream<ManagedServer> findByServerType(ServerType serverType);
     @Async
     CompletableFuture<List<ManagedServer>> findAll();
}

Stream<> ile uyuşuk işleme (=lazy processing) ve CompletableFuture<> ile reaktif fonksiyonel programlama (=reactive functional programming) yapılabilir. Yukarıdaki kodda Spring Data JPA kullanılmaktadır. Spring Data JPA tabanlı kalıcılık çözümünde, sadece kalıcılık işlemlerinin tanımlandığı bir -arayüz tanımlanır, gerçekleme verilmez. Tüm kalıcılık detayları ile Spring Data JPA çatısı ilgilenecektir. Spring Data JPA çatısı Java Persistence API (JPA) çatısı üzerine kuruludur.
Burada tartışılabilecek diğer bir çözüm, arama kriterine uyan hiç bir film bulunamadığında, bunu bir hata durumu olarak kabul edip hatayı açıklayan bir Exception nesnesi fırlatmak olabilir. Bu durumda metot kritere uyan en az bir film bulduğunda bir torba döner ya da sorgu sonucu boş ise Exception nesnesi fırlatır. Her iki çözüm de herhangi bir önlem alınmış olunsun ya da olunmasın NPE'ye neden olmaz. Bu çözümden hangisinin tercih edileceği işin sahibinin vermesi gereken bir karardır. Eğer genel bir API tasarlanıyor ise API'nin her iki çözümü de içermesi tercih edilir. Kararı bırakın çağıran taraf kendi ihtiyacına göre versin.

Nesneyi Optional ile Sarmallamak

Java 8 ile birlikte üçüncü bir çözüm daha geldi: Optional. Optional sınıfı bir nesneyi saklama ve erişmek için bir çözüm sunuyor. Eğer saklayacağı nesne null olabiliyor ise Optional.ofNullable metodu ile yaratıyoruz:
@Override
public Optional<Country> findCountryByCode(String code) {
 Country country = countries.get(code);
 return Optional.ofNullable(country);
}

Eğer nesnenin null olmaması gerekiyorsa değeri Optional.of() metodu ile sarmallıyoruz:
Optional<Double> average= Optional.of(Double.NaN);

Optional.of() metodu null kontrolü yapar ve parametre null ise NPE fırlatır:
public final class Optional<T> {

   . . .

   public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
   }

    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }

    . . .

}

 
public final class Objects {

    . . .

   public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }   

    . . .

}

Eğer sarmallanacak değer null ise bunu doğrudan Optional.empty() çağrısı ile oluşturabiliriz:
Optional<Double> average= Optional.empty(); 

Optional ile Nesneye Ulaşmak

Optional içinde sakladığımız nesneye ulaşmak için yukarıda tartıştığımız şekillerde tasarlanmış farklı davranışlara sahip metotlar bulunuyor. get() metodu ile Optional'da saklanan nesnenin referansına ulaşırsınız. Ancak Optional içinde nesne yoksa NoSuchElementException() alırsınız:
public final class Optional<T> {

    . . .

    public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }

    . . .

}

NoSuchElementException yine bir RuntimeException türünde bir hatadır. Collection API içindeki Iterator arayüzündeki next() metodu da eğer torbada ziyaret edilecek başka bir eleman yoksa NoSuchElementException fırlatır. Bu hatayı almamak için Optional içinde bir değer olup olmadığını sorgulayabilirsiniz:
Optional<Country> country= countryDao.findCountryByCode("KLM");
if (country.isPresent())
    System.out.println(country.get().getName()); 

Optional içinde yukarıdaki çözümü daha yalın bir şekilde karşılayacak  başka bir metot yer alıyor:
Consumer<Country> countryNameConsumer = c -> System.out.println(c.getName());
country.ifPresent(countryNameConsumer);

Yukarıdaki çözümü daha da yalın ifade edebiliriz:
country.map(Country::getName).ifPresent(System.out::println);

Eğer Optional içinde değer yoksa alternatif bir değerin kullanılmasını isteyebiliriz:
String countryName= country.map(Country::getName).orElse("Not found!");

Şimdi orElse() metodunun Optional sınıfındaki tanımına bakalım:
public final class Optional<T> {
    . . .
    public T orElse(T other) {
        return value != null ? value : other;
    }
    . . .
}

Eğer aradığımız kodda bir ülke yoksa yeni bir ülke yaratmak için Optional sınıfındaki orElseGet() çağrısını kullanıyoruz:
CountryDao countryDao= InMemoryWorldDao.getInstance();
Optional<Country> country= countryDao.findCountryByCode("KLM");
Country found= country.orElseGet(Country::new);

Eğer aradığımız kodda bir ülke yoksa Exception fırlatmak istersek Optional sınıfındaki orElseThrow() çağrısını kullanabilirsiniz:
CountryDao countryDao= InMemoryWorldDao.getInstance();
Optional<Country> country= countryDao.findCountryByCode("KLM");
Country existing= country.orElseThrow(NoSuchElementException::new);

Son olarak Optional içindeki fonksiyonel programlamaya yönelik flatMap() metodunu çalışacağız:
CountryDao countryDao= InMemoryWorldDao.getInstance();
Optional<Country> country= countryDao.findCountryByCode("TUR");
country.flatMap(Country::getCities).ifPresent(System.out::println);

Yukarıdaki örnekte ülke kodu ile aradığımız ülkenin şehirlerini listelemek istiyoruz. Yukarıdaki kodu anlamak için Country sınıfındaki getCities() metodunun tanımına göz atmamız gerekir:
public class Country {

     . . .

     public Optional<List<City>> getCities() {
         return Optional.ofNullable(cities);
     }

     . . .

}

Dikkat ederseniz getCities() metodu Optional<List<City>> tipinden bir değer dönüyor. Böylelikle flatMap() metodu Optional<Country>'yi Optional<List<City>>'ye dönüştürüyor.

Modellemede Optional Kullanımı

Nesneye dayalı tasarımda Optional<> ile neleri sarmallamalıyız? Sınıf özniteliklerini (=attributes, fields) ve metod parametrelerini olduğu gibi bırakın, Optional<> içinde sarmallamaya çalışmayın. Optional sınıfının en doğru kullanımı, yukarıdaki örneklerde olduğu gibi metod dönüş değerleri olacaktır. Çağıran tarafta ise Optional ile sarmallanan değerlere get() yerine orElse() ile ulaşmaya çalışın.

Java 9'da Optional'daki Yenilikler

Önümüzdeki yıl Mart ayında Java SE'nin yeni sürümü 9'un çıkması planlanıyor. Java 9Optional sınıfına yeni üç metot tanıştırıyor. Şimdi bu yenilikleri örneklerle çalışalım.  TUR, USA, ITA ve FRA kodlu ülkelerin bilgilerini ekrana yazmaya çalışalım. Java 9 öncesinde bunu aşağıdaki kodla gerçekleştirebiliriz:
Stream.of("TUR","USA","ITA","FRA")
        .map(countryDao::findCountryByCode)
        .map(c -> c.isPresent() ? Stream.of(c.get()) : Stream.empty())
        .forEach(System.out::println);

Java 9'daki ilk yenilik yukarıdaki c -> c.isPresent() ? Stream.of(c.get()) : Stream.empty() ifadesinin kısa yolunu oluşturan stream() çağrısı:
Stream.of("TUR","USA","ITA","FA")
        .map(countryDao::findCountryByCode)
        .map(Optional::stream)
        .forEach(System.out::println);

Gerçekten de Optional sınıfında stream çağrısının gerçeklemesine baktığımızda bu kısa yolu görüyoruz:
/**
 * If a value is present return a sequential {@link Stream} containing only
 * that value, otherwise return an empty {@code Stream}.
 *
 * @apiNote This method can be used to transform a {@code Stream} of
 * optional elements to a {@code Stream} of present value elements:
 *
 * <pre>{@code
 *     Stream<Optional<T>> os = ..
 *     Stream<T> s = os.flatMap(Optional::stream)
 * }</pre>
 *
 * @return the optional value as a {@code Stream}
 * @since 1.9
 */
public Stream<T> stream() {
 if (!isPresent()) {
  return Stream.empty();
 } else {
  return Stream.of(value);
 }
}

Diğer yeni gelen metot ise ifPresentOrElse(). Aşağıdaki örnekte hatalı ülke kodlarını saymak, hatasız ülke kodlarına karşı gelen ülke bilgilerini ise ekrana yazmak istiyoruz:
AtomicInteger counter= new AtomicInteger(0);
Stream.of("TUR","USA","ITA","FA")
      .map(countryDao::findCountryByCode)
      .forEach(
           c -> c.ifPresentOrElse(
                   System.out::println,
                   () -> { counter.incrementAndGet(); } 
                )
       );
System.out.println(counter.get());

Bu uygulama çalıştırıldığında aşağıdaki ekran ekran görüntüsü ile karşılaşılır:
Country{gnp=210721.0, code='TUR', name='Turkey', continent='Asia', surfaceArea=774815.0, population=66591000}
Country{gnp=8510700.0, code='USA', name='United States', continent='North America', surfaceArea=9363520.0, population=278357000}
Country{gnp=1161755.0, code='ITA', name='Italy', continent='Europe', surfaceArea=301316.0, population=57680000}
1

Sonuncu ve daha çok ihtiyaç duyulan metot ise or(). Şimdi, TUR kodlu ülkeyi önce cep (=cache) servisinde , orada yoksa InMemoryWorldDao servisinde arayalım, orada da yoksa en son JdbcWorldDao servisinde arayalım:
Optional<Country> turkey= cache.findCountryByCode("TUR")
                                .or(() -> countryDao.findCountryByCode("TUR"))
                                .or(() -> jdbcDao.findCountryByCode("TUR"));

2 comments:

  1. Hocam elinize sağlık. Yalnız blogun tasarımını değiştiremez misiniz? Göz yoruyor ve her şey iç içe. Okumayı zorlaştırıyor.

    ReplyDelete