menu

Monday, January 13, 2014

Reflective Ziyaretçi (Visitor) Tasarım Örüntüsü

Reflective Ziyaretçi (Visitor) Tasarım ÖrüntüsüGiriş:

     Bugün davranış temelli tasarım örüntülerinden (behavioral design patterns) Ziyaretçi (Visitor) tasarım örüntünün hangi temel soruna çözüm olduğunu ve esnek yapıda bir Ziyaretçi tasarımının reflection ile nasıl gerçekleştirilebileceğini göreceğiz.
Ziyaretçi tasarım örüntüsü farklı veri tipindeki nesnelerin bir collection içerisinde kullanılması ve nesneler üzerinde benzer işlemlerin yapılması gerektiğinde kullanılır. Ziyaretçi tasarım örüntüsünün UML çizimi Şekil 1 de gösterilmiştir.

Reflective Ziyaretçi (Visitor) Tasarım Örüntüsü

                                                                    Şekil 1

Ziyaretçi Tasarım Örüntüsü Gerçekleştirimi:

Öncelikle ziyaretçi kullanmadan farklı nesneler içeren bir collection üzerinde çalışalım.

        List heterogenList = new ArrayList();
      
        heterogenList.add("A");
        heterogenList.add(1);
        heterogenList.add(Calendar.getInstance());
      
        for (Iterator iterator = heterogenList.iterator(); iterator.hasNext();) {
            Object heterogenElement = (Object) iterator.next();
            if (heterogenElement instanceof String) {
                String myString = (String) heterogenElement;   
                System.out.println("myString:" + myString);
                //do sth with the value
            }
            else if (heterogenElement instanceof Integer) {
                Integer myInteger = (Integer) heterogenElement;   
                System.out.println("myInteger:" + myInteger);
                //do sth with the value
            }
            else if (heterogenElement instanceof Calendar) {
                Calendar myCalendar = (Calendar) heterogenElement;   
                System.out.println("myCalendar:" + myCalendar);
                //do sth with the value
            }          
        }

Görüldüğü gibi farklı nesneleri collection içerisinden okumak için döngümüz içerisinde instanceof keyword'ünü kullanmamız gerekiyor. Bu da demek oluyor ki listemize farklı tipte bir liste eklendiğinde hali hazırda çalışan kodumuzu değiştirmek ve yeni bir if eklemek zorunda kalacağız.
Eğer kodumuzda sürekli if kullanıyorsak bir yerlerde yanlış yapıyoruz demektir ve tasarımı gözden geçirmek gerekmektedir. If kullanımı yerine soyutlama (abstraction) yaparak daha esnek, daha izlenebilir ve devam ettirilebilir bir yazılım elde edebiliriz. Ayrıca hali hazırda test edilmiş bir kodu her yeni veri tipinde değiştirmek zorunda kalmamız Open Closed Tasarım Örüntüsüne aykırı olup kaçınılması gereken bir durumdur. Bu durumda yardımımıza Ziyaretçi tasarım örüntüsü gelmektedir.
Öncelikle ihtiyacımız olan arayüz ve sınıfları tanımlayalım.
//Ziyaretçi arayüzü
public interface Visitor {
   void visit(WrappedString wString);
   void visit(WrappedCalendar wCalendar);
   void visit(WrappedInteger wInteger);
}
//Ziyaret edilecek nesne arayüzü
public interface Element {
    void accept(Visitor visitor);
}
//Listemizde kullanacağımız ziyaret edilecek nesneler
public class WrappedString implements Element {
    private String name;
    private String wString;
    public WrappedString(String name, String wString) {
        this.name = name;
        this.wString = wString;
    }
    public String getName() {
        return name;
    }
    public String getwString() {
        return wString;
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

public class WrappedInteger implements Element {
    private String name;
    private int wInt;
    public WrappedInteger(String name, int wInt) {
        this.name = name;
        this.wInt = wInt;
    }
    public String getName() {
        return name;
    }
    public int getwInt() {
        return wInt;
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);    
    } 
}

import java.util.Calendar;

public class WrappedCalendar implements Element{
    private Calendar wCalendar;
    private String name;
    public WrappedCalendar(String name, Calendar wCalendar) {
        this.name = name;
        this.wCalendar = wCalendar;
    }

    public Calendar getWCalendar() {
        return wCalendar;
    }

    public String getName() {
        return name;
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);    
    }
}

//Concrete Ziyaretçi
public class ConcreteVisitor implements Visitor {
    @Override
    public void visit(WrappedString wString) {
        System.out.println(wString.getName());
        // do sth
    }
    @Override
    public void visit(WrappedCalendar wCalendar) {
        System.out.println(wCalendar.getName() + "-" + wCalendar.getWCalendar().getTimeInMillis());
        // do sth
    }
    @Override
    public void visit(WrappedInteger wInteger) {
        System.out.println(wInteger.getName() + "-" + wInteger.getwInt());
        // do sth
    }
}

Şimdi daha önceki liste örneğimizin kodunu Ziyaretçi tasarım örüntüsü ile tekrar yazalım
       List heterogenList = new ArrayList();
       
        heterogenList.add(new WrappedString("wString","A"));
        heterogenList.add(new WrappedInteger("wInteger", 1));
        heterogenList.add(new WrappedCalendar("wCalendar", Calendar.getInstance()));
       
         Visitor visitor = new ConcreteVisitor();
         //Visitor visitor = new AnotherConcreteVisitor();
         for (Iterator iterator = heterogenList.iterator(); iterator.hasNext();) {
            Element element = (Element) iterator.next();
            element.accept(visitor);
        }

Görüldüğü üzere instanceof keyword'ünden kurtulduk.Listedeki her bir sınıfın nesnesi Element arayüzünü gerçekleştirdiğinden tek yapmamız gereken listeden Element arayüzünü okumak ve accept metodunu bir ziyaretçi ile çağırmak.Her bir nesne kendi accept metodunda ziyaretçinin visit metoduna kendisini parametre olarak geçecek ve ziyaretçi sınıfında nesne ile ilgili yapılmak istenen iş mantığı gerçekleştirilecek.Burada önemli nokta client kodunu hiç değiştirmeden nesneler üzerinde yeni iş mantıkları geliştirecek yeni ziyaretçilerin sisteme entegre edilebiliyor oluşu.

Reflective Ziyaretçi Tasarım Örüntüsü Gerçekleştirimi:

Ziyaretçi tasarım örüntüsü yeni ziyaretçi eklemede oldukça esnek davranırken, listemize yeni bir sınıf eklemek gerektiğinde Visitor arayüzünün değiştirilmesini gerektiriyor.Java'da bir arayüzü yayınladıktan sonra değiştirmek en can sıkıcı işlerden birisidir, çünkü arayüz değiştiğinde arayüzü gerçekleştiren tüm concrete ziyaretçilerin de değiştirilmesi gerekmektedir. Bu soruna çözüm üretebilmek için Visitor arayüzünü reflection ile oluşturabiliriz. Bu durumda arayüzün yeni hali;
//Reflective Ziyaretçi arayüzü
public interface ReflectiveVisitor {
   void visit(Object o);
}
//Reflective Element
public interface ReflectiveElement {
    void accept(ReflectiveVisitor visitor);
}
//Yeni ziyaret edilecek nesne (WrappedString sınıfını da implement ReflectiveElement olarak değiştirelim)
public class WrappedDouble implements ReflectiveElement {
    private String name;
    private double wDouble;
    public WrappedDouble(String name, double wDouble) {
        this.name = name;
        this.wDouble = wDouble;
    }
    public String getName() {
        return name;
    }
    public double getWDouble() {
        return wDouble;
    }
    @Override
    public void accept(ReflectiveVisitor visitor) {
        visitor.visit(this);
    } 
}
public class WrappedString implements ReflectiveElement {
    private String name;
    private String wString;
    public WrappedString(String name, String wString) {
        this.name = name;
        this.wString = wString;
    }
    public String getName() {
        return name;
    }
    public String getwString() {
        return wString;
    }
    @Override
    public void accept(ReflectiveVisitor visitor) {
       visitor.visit(this);
    }
    @Override
    public String toString() {
        return name + "-" + wString;
    }
}
//Reflective Concrete Ziyaretçi
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class AnotherConcreteVisitor implements ReflectiveVisitor {
    @Override
    public void visit(Object o) {
        try {
            Method visitMethod = this.getClass().getMethod("visit", new Class[] { o.getClass() });
            if (visitMethod == null) {
                defaultVisit(o);
            } else {
                visitMethod.invoke(this, new Object[] { o });
            }
        } catch (NoSuchMethodException e) {
            this.defaultVisit(o);
        } catch (InvocationTargetException e) {
            this.defaultVisit(o);
        } catch (IllegalAccessException e) {
            this.defaultVisit(o);
        }
    }
    public void defaultVisit(Object o) {
        System.out.println(o.toString());
    }
    public void visit(WrappedDouble wDouble){
        System.out.println(wDouble.getName() + "-" + wDouble.getWDouble());
        // do sth
    } 
}

Daha önceki liste örneğimizin kodunu Reflective Ziyaretçi tasarım örüntüsü ile tekrar yazalım
        List heterogenList = new ArrayList();
       
        heterogenList.add(new WrappedString("wString","A"));//default visit method
        heterogenList.add(new WrappedDouble("wDouble", 1.0));
       
         ReflectiveVisitor visitor = new AnotherConcreteVisitor();
         for (Iterator iterator = heterogenList.iterator(); iterator.hasNext();) {
            ReflectiveElement reflectiveElement = 
(ReflectiveElement) iterator.next();
            reflectiveElement.accept(visitor);
        } 

Bu yaklaşımda listemizdeki tüm nesneler ziyaretçi sınıfının generic visit metodunu çağırmaktadır.Parametre olarak Object alan bu generic visit metodu, gelen parametrenin tipine göre gerçekleştirilmiş olan ilgili visit metodunu çağırmaktadır. Burada önemli olan ziyaretçi sınıfının hangi veri tipleri için işlem yapmak istiyorsa o tipler için visit(veri:VeriTipi) metodunu tanımlamasıdır.Eğer ilgili visit metodu ziyaretçi sınıfında bulunamazsa defaultVisit metodu nesnenin String gösterimini ekrana yazma ya da default olarak belirleyeceğimiz başka bir işi gerçekleştirmeyi yapacaktır.

Sonuç:

     Bu tasarımda ziyaretçi eklemedeki esneklik, üzerinde çalıştığımız listeye yeni sınıf eklendiğinde de sağlanmış oluyor.Hali hazırda kullanılan bir ziyaretçiye yeni bir sınıf eklemek istediğimizde, yeni sınıf için bir visit metodunu ziyaretçi sınıfına eklememiz ve implementasyonunu yapmamız yeterli, ReflectiveInterface arayüzünü ve diğer ziyaretçileri değiştirmemize gerek kalmamış oldu.
Ancak bu esneklik bir performans kaybı ile beraber gelmektedir.Reflection ile çağırılacak metot dinamik olarak runtime'da belirlendiğinden bazı JVM optimizasyonları yapılamamaktadır ve reflection sık çalışan kod bölümleri için çok kötü performans verebilmektedir.Reflection kullanarak ve kullanmayarak 1000000 nesne için yukarıdaki kodu çalıştırdığımızda reflection ile 50 kat yavaş sonuç aldığımızı görmekteyiz.Ayrıca 10000000 nesne için aynı kodu çalıştırdığımızda reflection temelli yaklaşım Out of Memory hatası almaktadır.
Bunun sebebi java heap space in dolmasından olabileceği gibi JVM'in reflected metodun daha performanslı çalışması için kullandığı "inflation" mekanizmasından kaynaklı olarak perm gen space den kaynaklı da olabilir. Normalde belirli bir çağırım sayısına kadar java'nın Method classının içerisindeki MethodAccessor sınıfının bir nesnesi oluşturulup her invoke çağırımı MethodAccessor sınıfının aynı isimli invoke metoduna JNI(Java Native Interface) yöntemi ile yönlendirilmektedir. JNI metot çağırımı çok yavaş olduğundan belirli miktarda metot çağırımı yapıldığında, JVM runtime'da bir sınıf oluşturur.Bu sınıfta invoke metodu içermekte olup, bu sınıftan oluşturulan instance ile artık invoke metot çağırımları JNI yerine bu sınıfın nesneninin invoke metoduna yönlendirilir. Inflation olarak adlandırılan bu mekanizma ile reflected metot çağırımında performans problemi en aza indirilmiş olur. Ancak bu durumda dahi reflected metot çağırımı ile 50 kat yavaş sonuç aldığımızı söylemiştik.
Runtime'da oluşturulan sınıflar ve her sınıf için oluşturulan bir nesnenin referansı permanent generation dediğimiz heap space'de tutulur.(Permanent generation sınıf, metot tanımlarını, constant pool bilgisini (constant pool verisi class file'dan alınır),  class ile ilişkili object dizilerinin ya da type dizilerinin referanslarını, JVM tarafından runtime'da oluşturulan inflation dan kaynaklı sınıfları, static değişkenlerin referanslarını ve optimizasyon için JIT tarafından kullanılan bilgileri tutmaktadır.) Çok fazla farklı nesne üzerinden reflected metot çağırımı yapıldığında runtime'da inflation için oluşturulan sınıflardan kaynaklı olarak permanent generation dolup Out of Memory hatasına sebep olabilmektedir. Çok fazla reflected metot çağırımı yapılacağı durumlarda permanent generation için yeterli miktarda hafıza ayrılmalıdır.

Referanslar ve Kaynaklar:

Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Desing Patterns -  Elements of Reusable Object-Oriented Software
http://anshuiitk.blogspot.com/2010/11/excessive-full-garbage-collection.html

Kaynak kodu buradan indirebilirsiniz.

Sunday, January 12, 2014

Java'da Kesirli Sayıların Kullanımı

Java'da Kesirli Sayıların KullanımıGiriş:     

     Bildiğiniz gibi onluk sistemde bazı kesirli sayılar tam olarak gösterilemez ve devirli olarak ifade edilir. Örneğin; 1/3 = 0.33..... ya da 1/6 = 0.166..... gibi.Aynı durum bilgisayarın sayıları tuttuğu ikilik sistemde de geçerlidir.Örneğin onluk sistemdeki 7/10 sayısının ikilik sistemdeki karşılığı olan 111/1010 sayısını ele alalım. Bu ikilik sistemdeki bölme işlemini gerçekleştirdiğimizde 0.1011001100... gibi 1100 sayı dizisinin devirli olarak devam ettiği bir sonuca varırız. Aynı sonuca bölme işlemi yerine Şekil 1 deki yöntemle de ulaşabiliriz.Onluk  sistemdeki bir kesirli sayının ikilik sistemdeki karşılığının devirli olduğunu, bölme işlemi ile anlayabildiğimiz gibi sayıyı a/b şeklinde yazıp,sadeleştirip, b'nin 2'nin kuvveti olup olmadığına bakarakta anlayabiliriz. Eğer b 2'nin kuvveti değilse sayı devirli olacaktır.
                                                                                                               
    0.7 * 2 = 1.4  ---> 1 (Ondalık ayracın solundaki sayıyı al)
    0.4 * 2 = 0.8  ---> 0 (>1 ise 1 çıkararak çarpmaya devam et)
    0.8 * 2 = 1.6  ---> 1
    0.6 * 2 = 1.2  ---> 1
    0.2 * 2 = 0.4  ---> 0
    0.4 * 2 = 0.8  ---> 0
    0.8 * 2 = 1.6  ---> 1 (Tekrar eden dizi sayısını belirledik)

 Sonuç = 0.1011001100...  ---> 1100 sayı dizisi sürekli tekrar ediyor.

Şekil 1

Java Devirli Sayıları Nasıl Tutar ?

      Şimdi, ikilik sistemde tam olarak ifade edilemeyen 0.7 sayısının java'da nasıl gösterilmekte olduğuna bakalım. Java dilinde kesirli sayıların gösteriminde IEEE 754 (IEEE Standard for Floating-Point Arithmetic) standartları kullanılmaktadır.Bu standart çoğu modern programlama dillerinde kullanılmaktadır.Float veri tipi 32 bit tutabildiğinden IEEE 754 single precision formatını, Double veri tipi ise 64 bit tutabildiğinden IEEE 754 double precision formatını kullanır.Double precision 1 bit'i işaret biti olarak, 11 bit'i üst olarak, 52 bit'i ise anlamlı rakam(significant bits ; mantissa) olarak kullanırken single precision 1 bit'i işaret biti,  8 bit'i üst , 23 bit'i ise anlamlı rakam(significant bits ; mantissa) olarak kullanır.Ancak mantissada normalization için değer olarak default 1 değerine sahip olan fazladan 1 bit daha kullanılmaktadır.Dolayısıyla double-precision'da 53 bit, single-precision da ise 24 bit mantissa kullanılabilmektedir.(Şekil 2)
Burada Java'nın davranış şekli istenen sonuca en yakın sonucu elde etmek için double-precision da 52 ya da 53 bit, single-precision da ise 23 ya da 24 bit kullanmak ve gerekirse yuvarlama yapmak olacaktır.Bu en yakın sonuç istenen sonuçtan büyük ya da küçük olabilir.
                                       
                                        İşaret        Üst                 Mantissa
 1 
11
 52 + 1
      64 bit double-precision format

 1
 8
 23 + 1
  32 bit single-precision format 

  Şekil 2

0.7 sayısını Double ve Float ile nasıl tutabileceğimizi gösterelim.
double repeatingFractionNumber = 0.7;
ikilik sistemde; 0.1011001100110011001100110011001100110011001100110011(52.bit)...
  
     Burada double-precision'da mantissa için  52. bit sonrası truncate edilir.IEEE 754 standartlarına göre 52.bit 1 ise olduğu gibi bırakılır. Bu durumda 0.7 sayısını truncate ettiğimizden, 0.7 den çok az  küçük bir sayı edilmiş olur.
0.1011001100110011001100110011001100110011001100110011 sayısını tekrar onluk sisteme çevirirsek 0.6999999999999999555910790149937383830547332763671875 sayısını elde ederiz.
Bu sonuca aşağıdaki kod parçasını çalıştırarak 0.7 nin aslında bu şekilde ifade edildiğini görebiliriz.

BigDecimal bd = new BigDecimal(repeatingFractionNumber);
System.out.println("0.7 java'da aslında bu sayıdır : " + bd);

52. ve 53. bit 1 ise ikiside kullanılır, 52.bit ve 53.bit 0 ise asıl sayıya yakınlık durumuna göre ya sayı olduğu gibi bırakılır ya da 53.bit 1 olarak yuvarlanır. 52.bit 0 53.bit 1 ise asıl sayıya yakınlığa göre ya sayı olduğu gibi bırakılır ya da 52.bit 1'e yuvarlanıp 53. bit kullanılmaz. 52.bit 1 ve 53.bit 0 ise  asıl sayıya yakınlık durumuna göre ya sayı olduğu gibi bırakılır ya da 53.bit 1 e yuvarlanır.1'e yuvarlama yapılan her durumda asıl sayıdan daha büyük bir sayı , yapılmadığında ise daha küçük bir sayı elde edilir.(Aynı durum single-precision da 23 ve 24. bitler içinde geçerlidir.)

Elde etmek istediğimiz sayıdan çok az büyük bir sayı elde.Bu durumu göstermek için ondalık sistemdeki 0.1 sayısını ele alalım. 0.1'i Şekil 1 de gösterdiğimiz yöntemle ikilik sisteme çevirirsek şu sonucu elde ederiz.
0.000110011... 0011 sayı dizisi devirli olarak tekrar ediyor.
64 bit double-precision da bu sayı aşağıdaki sayıya dönüşür.(Noktadan sonraki 3 sıfır anlamlı rakam değil, bu yüzden 53 bit içinde sayılmıyor)
0.0001100110011001100110011001100110011001100110011001101(52. bit 0 iken 1 yaptık ve 53 ve sonraki bitler için yuvarlama yaptık)
0.0001100110011001100110011001100110011001100110011001101 sayısını tekrar onluk sisteme çevirirsek 0.1000000000000000055511151231257827021181583404541015625 sayısını elde ederiz, görüldüğü gibi 53. bit ve sonrasının 52. bit'in 1 yapılması sonucu yaptığımız yuvarlama ile  0.1 den çok az büyük bir sayı elde ettik.

0.7 sayısını Float veri tipinde yani 32 bit single-precision da gösterimine bakalım.
float repeatingFractionNumber = 0.7f;
ikilik sistemde; 0.101100110011001100110011(24.bit)... (Java burada 24 bit kullanmayı seçiyor çünkü 23 bit ile elde edeceğimiz sayı 0.69999992847442626953125 iken 24 bit ile elde edeceğimiz sayı 0.699999988079071044921875 olup 0.7 ye daha yakındır. )
24.bit 1 olduğundan yuvarlama yapmadık. Yuvarlama yapılmıyorsa sonuç istenen sonuçtan daha küçük olmaktadır.
Yine aşağıdaki kod parçası ile 0.7 nin float veri tipi ile aslında nasıl ifade edildiğini görebiliriz.

BigDecimal bd = new BigDecimal(repeatingFractionNumber);
System.out.println("0.7f java'da aslında bu sayıdır : " + bd); 

Buraya kadar java'da IEEE Floating-Point standartlarına göre double ve single precision formatlarında double ve float veri tipindeki sayıların nasıl tutulduğunu gördük.
Temel prensip istenen sonuca en yakın sonucu elde etmek için double-precision da 52 ya da 53, single-precision da 23 ya da 24 bit kullanmak ve istenen sonuçtan çok az büyük ya da çok az küçük en yakınsak sayıyı elde etmek. Böyle bir yaklaşımla sürekli birbirine eklenenen double ya da float sayıları düşünelim; sayılar eklendikçe yuvarlamalar yapılacak ve toplam hata sürekli artacaktır. Double ve float bilimsel hesaplamlar için uygunken finansal hesaplamalarda bu şekilde artan toplam hata kabul edilemez. Bu yüzden toplam hatayı en aza indirecek yöntemleri uygulamamız gerekmektedir. Şimdi bu yöntemleri inceleyelim.

BigDecimal kullanmak:

Double kullandığımız şu 2 örneği inceleyelim.

Örnek 1:

double d1 = 0.1, d2 = 0.2, d3 = 0.3;         
if (d1 + d2 == d3)
 System.out.println("0.1+0.2=0.3");
else
 System.out.println("0.1+0.2=?");

Bu program parçacığını çalıştırdığımızda ekrana 0.1+0.2 = ? basılmakta.Normalde şaşırtıcı olacak bu sonuç ilk bölümde java'da double sayıların nasıl tutulduğunu düşündümüzde gayet normal bir sonuç olmakta. Hatırlayalım,
0.1  = 0.1000000000000000055511151231257827021181583404541015625
0.2 = 0.200000000000000011102230246251565404236316680908203125
0.3 = 0.299999999999999988897769753748434595763683319091796875
Bu duruma göre 0.1 + 0.2  0.3 'den büyük olmakta.

Örnek 2:

double elimizdekiTutar = 7.0;
int satinAlinanUrunSayisi = 0;       
for (double urunTutar = 0.7; elimizdekiTutar >= urunTutar;) {
  elimizdekiTutar -= urunTutar;       
  satinAlinanUrunSayisi++;
}              
System.out.println("elimizdekiTutar="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);

Bu program parçacığını çalıştırdığımızda elimizdeki tutar'ın 0.6999999999999988, satın alınan ürün sayısının ise 9 olduğunu görüyoruz. Burada beklenen sonuç elimizde hiç para kalmayıp 10 ürün satılması idi.
Şimdi aynı örnekleri BigDecimal kullanarak yapalım.

Örnek 1:

BigDecimal d1 = new BigDecimal("0.1");
BigDecimal d2 = new BigDecimal("0.2");
BigDecimal d3 = new BigDecimal("0.3"); 
if (d1.add(d2).equals(d3))
 System.out.println("0.1+0.2=0.3");
else
 System.out.println("0.1+0.2=?");

Bu durumda sonuç beklendiği gibi "0.1+0.2=0.3" olacaktır.

Örnek 2(Yöntem 1):
           
BigDecimal elimizdekiTutar = new BigDecimal("7.0");
int satinAlinanUrunSayisi = 0;
final BigDecimal URUN_TUTAR = new BigDecimal("0.7");
for (BigDecimal urunTutar = URUN_TUTAR;                             
  elimizdekiTutar.compareTo(urunTutar) >= 0;) {
  elimizdekiTutar = elimizdekiTutar.subtract(urunTutar);
  satinAlinanUrunSayisi++;
} 
System.out.println("elimizdekiTutar=" + elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi=" +satinAlinanUrunSayisi);

Bu durumda sonuç beklendiği gibi elimizde hiç para kalmayıp satın alınan ürün sayısının 10 olması olacaktır.

Burada dikkat edilmesi gereken önemli bir nokta, istenen sonuca ulaşabilmek için Big Decimal kullanırken String parametre alan constructor'ı kullanmaktır. BigDecimal'ın double
parametre alan constructor'ı daha önce yaşadığımız sorunları ortadan kaldırmamaktadır.
Bu teknikte scale'i değiştirmek için setScale metodu ile roundlama yapabiliriz.

Örnek 2 (Yöntem 2):

double elimizdekiTutar = 7.0;
int satinAlinanUrunSayisi = 0;
final int DECIMAL_PLACE = 2;      
for (double urunTutar = 0.7; elimizdekiTutar >= urunTutar;) {
  elimizdekiTutar = round(elimizdekiTutar -= urunTutar);
  satinAlinanUrunSayisi++;
 }                
System.out.println("elimizdekiTutar="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);

double round(Double d) {   
   BigDecimal bd = new BigDecimal(d.toString());
   bd = bd.setScale(DECIMAL_PLACE , BigDecimal.ROUND_HALF_EVEN);
   d = bd.doubleValue();
   return d;
}

Bu yöntemi BigDecimal'ın daha yavaş olan add, substract gibi metotlarını kullanmak istemediğimizde kullanabiliriz. Burada yapılması gereken her ara işlemde elde edilen tutarı BigDecimal'ın round metodunu kullanarak istenilen decimal place'de roundlamak ve sonucu yine double olarak dönmek olacaktır.
round metodunda BigDecimal constructor'ı olarak yine String parametreli olanın kullanıldığına dikkat ediniz.
round metodunda scale 2 olarak verilirken round mode olarak BigDecimal.ROUND_HALF_EVEN verilmiştir. Bu mode toplam hatayı en aza indirgeyen roundlama modudur. Alt sınır ve üst sınıra eşit uzaklıkta olunduğunda en son decimal place'in yanındaki sayı çift ise ROUND_DOWN tek ise ROUND_UP gibi davranır.Örneğin;
0.685  2 decimal place ile 0.68 'e yuvarlanırken,  0.675 2 decimal place ile yine 0.68 e yuvarlanacaktır.
Ancak bu davranışın tutarlı olması için yine BigDecimal'ın String constructor'ı kullanılmalıdır.Aksi takdirde 0.685'in hafızada gösterildiği değer olan 0.685000000000000053290705182007513940334320068359375 değeri üzerinden yuvarlama yapılıp 0.685   0.69'a yuvarlanacaktır.

BigDecimal kullanarak finansal hesaplamalarda ve tam sonuç gereken işlemlerde hata oranını ortadan kaldırıp, istediğimiz yuvarlama tekniklerinden birini kullanma şansını elde etmiş oluyoruz. Ancak primitive tipler yerine BigDecimal kullanmak hem kullanım açısından daha karmaşıktır hem de performans açısından sorun teşkil edebilmektedir.Örneğin benim bilgisayarımda Örnek 2 (Yöntem 1) , Örnek 2 ve Örnek 2 (Yöntem 2) ye göre 50 kat daha yavaş çalışmakta.(Bu testte farkı net görebilmek için elimizdekiTutar değişkeni 7000000.0 olarak değiştirilmiştir. ) Bu durumda performans ihtiyacı ön planda ise BigDecimal'a alternatif bir yöntem olarak kuruş hesabı üzerinden giderek int ya da long kullanıp, decimal point yönetimini kendimiz yapabiliriz.

int ya da long kullanmak:

Örnek 2 'yi bu yöntemle tekrar implement edelim.

int elimizdekiTutar = 700; // 7.0 tl 700 kuruş
int satinAlinanUrunSayisi = 0;         
//0.7 tl 70 kuruş
for (int urunTutar = 70; elimizdekiTutar >= urunTutar;) {
 elimizdekiTutar -= urunTutar;       
 satinAlinanUrunSayisi++;
}              
System.out.println("elimizdekiTutar(kuruş cinsinden)="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);

Bu yöntemle tutarların büyüklüğüne göre int(9-10 haneye kadar ) ya da daha büyük tutarlar için long(18-19 haneye kadar) kullanarak kuruş hesabı yapıyoruz. Daha fazla hane gerektiğinde BigDecimal kullanmak gerekmektedir.
Bu teknikte BigDecimal'ın kullanımında olduğu gibi roundlamaya ihtiyaç duymuyoruz.Ancak elde edilen sonuçta decimal point'i kullanmak gerektiğinde decimal point yönetimini kendimiz yapmalıyız.

Yuvarlama Teknikleri:

     BigDecimal kullanarak istediğimiz decimal place'de yuvarlama yapabildiğimizden bahsetmiştik. BigDecimal ile ilgili olarak daha önce bahsettiğimiz performans problemi yuvarlamada da karşımıza çıkmakta. BigDecimal'ın round metodu yerine iki ayrı yöntem kullanabiliriz.

   1- BigDecimal.setScale() yöntemi:
      
     Örnek 2 (Yöntem 2) örneğinde kullandığımız şekilde BigDecimal'ın setScale metodunu kullanarak istenen seviyede yuvarlama yapabiliriz.Daha önce söylediğimiz gibi , BigDecimale'a parametre olarak String geçmeli ve setScale metodunda istediğimiz decimal place ve round mod'u vermeliyiz.
                          
Double d = some double;
BigDecimal bd = new BigDecimal(bd.toString());
bd = bd.setScale(decimalPlace, BigDecimal.ROUND_HALF_EVEN);
d = bd.doubleValue();

   2- Math.round() yöntemi:

double elimizdekiTutar = 7.0;
int satinAlinanUrunSayisi = 0;
final double DECIMAL_PLACE_CARPANI = 100; // decimal place 2 ise 100, 3 ise                                        // 1000 vb.     
for (double urunTutar = 0.7; elimizdekiTutar >= urunTutar;) {
 elimizdekiTutar = round(elimizdekiTutar -= urunTutar);
 satinAlinanUrunSayisi++;
 }              
System.out.println("elimizdekiTutar="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);
       
double round(Double d) {   
        d = Math.round(d * DECIMAL_PLACE_CARPANI ) /                                    DECIMAL_PLACE_CARPANI ;
       return d;
 }

Bu yöntemle yuvarlama yaptığımızda benim bilgisayarımda 7000000 toplam tutar ile yapılan testlerde  BigDecimal round metoduna göre yaklaşık 30 kat daha hızlı sonuç alabiliyoruz.
Ancak burada BigDecimal'ın yuvarlama tekniklerini kullanamıyoruz ve alt ve üst sınıra eşit uzaklıkta olan 0.685 gibi sayılar her durumda ROUND_UP gibi davranıyor.

    3- Cast yöntemi:

double elimizdekiTutar = 7.0;
int satinAlinanUrunSayisi = 0;
final double DECIMAL_PLACE_CARPANI = 100; // decimal place 2 ise 100, 3 ise                                        // 1000 vb.      
for (double urunTutar = 0.7; elimizdekiTutar >= urunTutar;) {
  elimizdekiTutar = round(elimizdekiTutar -= urunTutarDECIMAL_PLACE_CARPANI);
  satinAlinanUrunSayisi++;
}              
System.out.println("elimizdekiTutar="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);

double round(double d, int decimalPlaceCarpani) {
   long l  =(long) (d < 0 ? d * decimalPlaceCarpani - 0.5 : d *                             decimalPlaceCarpani + 0.5);
   return l / decimalPlaceCarpani;
}

Bu yöntemde temel prensip yine decimalPlaceCarpani değişkeni ile istediğimiz decimal place'i elde etmek ve sonuca 0.5 ekleyerek alt ve üst sınıra eşit uzaklıkta olan durumunda yuvarlama işleminin ROUND_UP gibi davranmasını sağlamak, alt sınıra yakın sayılarına alt'a, üst sınıra yakın sayıların ise üst'e yuvarlanmasını sağlamak.
Bu yöntemle yuvarlama yaptığımızda benim bilgisayarımda 7000000 toplam tutar ile yapılan testlerde  BigDecimal round metoduna göre yaklaşık 60 kat, Math.round() yöntemine göre 2 kat daha hızlı sonuç alabiliyoruz.
Ancak yine burada BigDecimal'ın yuvarlama tekniklerini kullanamıyoruz ve alt ve sınıra eşit olan 0.685 gibi sayılar her durumda ROUND_UP gibi davranıyor. 
Burada istersek kendi implementasyonumuzu geliştirerek BigDecimal'ın ROUND_HALF_EVEN metoduna benzer bir metot geliştirebiliriz.

Float ve Double Kullanımı Üzerine Bir Not: float ve double veri tipleri ile ilgili dikkat edilmesi gereken bir diğer nokta, bu tipleri hashcode imaplementasyonunda kullanmak istediğimizde NaN, Infinity gibi değerler üzerinde işlem yapılamayacağından  double ve float değerlerini Double.doubleToLongBits() ve  Float.floatToIntBits() metotları ile,  gösterilmiş oldukları bit dizisinin karşılık geldiği long(double için 64 bit long) ve int(float için 32 bit int) değerlerine çevirerek işleme tabi tutmak gerekmektedir.

Sonuç:

     Java'da kesirli sayılarla işlem yaparken finansal ya da tam sonuç gerektiren işlemlerde float ya da double veri tiplerini doğrudan kullanmak hatalı sonuçlar doğurmaktadır.Bunun sebebi ikilik sistemde devirli olan kesirli sayılardır.Çözüm için BigDecimal'ın String parametreli constructor'ını ve ekleme, çıkarma gibi işlemler için BigDecimal'da tanımlı olan metotlar kullanılabilir.Yine yuvarlama için de BigDecimal'ın setScale metodu yukarı, aşağı yuvarlama ya da toplam hatayı azaltacak şekilde ROUND_HALF_EVEN modu ile yuvarlamaya imkan sağlamaktadır.Ancak BigDecimal'ın kullanımı primitive tiplerin kullanımına nazaran daha karmaşıktır ve daha önemlisi performans açısından soruna yol açabilmektedir. Önceliğimizin performans olduğu durumlarda BigDecimal yerine int veya long kullanarak kuruş hesabı yapıp, decimal point'i  hesaplama sonunda ayarlayabiliriz.Ancak long ile tutabileceğimiz 18-19 haneden fazlasında yine BigDecimal kullanmamız gerekmektedir.Yine roundlama için BigDecimal'ın setScale metodu yerine Math.round() ya da daha performanslı olan Cast yöntemlemlerinden birini kullanabiliriz. 
Scale olarak ihtiyacımızı doğru belirleyip, hesaplamalarımız sonunda maximum oluşabilecek scale'e göre tasarımımızı yaparsak yuvarlamadan dolayı kayıp yaşama şansımızı azaltmış oluruz.Ancak bu durumda dahi kesirli sayıların tutulma biçiminden dolayı roundlama gerekmektedir ve BigDecimal ROUND_HALF_EVEN toplam hatayı en aza indirmektedir.Uygulamamızın roundlama işleminde kullandığımız mod'u tüm hesaplamalarda aynı şekilde uygulamazsak farklı zamanlarda farklı modüllerden farklı sonuçlar alabiliriz.
Ayrıca eğer hiç roundlama yapmazsak toplamda elde ettiğimiz sayı, örneğin 2098.81999 olduğunda, veritabanına doğrudan insert sonucunda ve veritabanında scale 2 olduğunda sayı tabloda 2098.81 olarak tutulacaktır.Halbuki bu sayı yuvarlama sonunda 2098.82 olmalı idi.Aynı şekilde veritabanında sum() işlemi ile scale 2 olan kalemlerin toplamıyla elde edilen sayıya , java'da bu kalemleri topladığımızda en yakın ROUND_HALF_EVEN modu ile ulaşabiliriz.

Referanslar ve Kaynaklar:

Joshua Bloch. Effective Java - Programming Language Guide

http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

http://mindprod.com/jgloss/floatingpoint.html

Kaynak kodu buradan indirebilirsiniz.