Wednesday, April 8, 2015

Temel Tipler Üzerinde Karşılaştırma İşlemleri

Temel tiplerle çalışırken karşılaştırma işlemi yapmak genellikle kolaydır. Örneğin Java'da tam sayı tiplerin karşılaştırılmasında çok özel durumlar yaşanmaz. Sadece sınır değerlerin aşılmamasına dikkat edilmelidir:
byte max= 127;
max++;
System.err.println(max==128);
byte tipinde bir değişken [-128..127] aralığında değer alır. max değişkeni alabileceği en büyük değeri almışken değerini bir artırırsanız, yeni değeri -128 olacaktır! Şimdi [-128..127] aralığındaki tam sayıların toplamını elde etmek için aşağıdaki kodu yazmış olalım:
 int sum=0; 
 for (byte b = Byte.MIN_VALUE; b <= Byte.MAX_VALUE; b++) {
     sum += b;
 }
Aynı nedenden dolayı yukarıdaki kod sonsuz döngüye girecektir. Eğer elle hesaplamış olsaydık çoktan -128 değerini bulmuştuk! Şimdi ise aşağıdaki koda bir bakalım:
for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) {
   if (b == 0x90)
      System.out.print("Yessss, I have found it!");
}
Döngüde byte tipinden bir değişkenin alabileceği tüm değerleri tarıyoruz. Her halde b'nin değerinin eninde sonunda -112 olacağını ve if koşulunun en az bir kez sağlanacağını düşünüyoruz. Ama öyle olmuyor. Bu kodu çalıştırdığınızda ekran boş kalıyor ve etrafı bir sessizlik kaplıyor. Sorun 0x90 sabitini bir sekizlik olarak yorumlayışımızdan kaynaklanıyor:
  • Tüm tam sayı tipleri (byte, short, int, long) işaretli tam sayıdır
  • En yüksek anlamlı bit işaret bitini gösterir
  • Negatif sayılar ikiye tümleyen aritmetiğine göre kodlanır. Buna göre bir sekizli olarak yorumlandığında 0x90 değeri iki tabanında 0b10010000 ve on tabanında - (0b01101111 + 0b00000001)=-112 değerine eşittir. Eğer yukarıdaki kodda gerçekten sekiz bitlik olarak tanımlamak istiyorsak karşılaştırma koşulunu ( b == (byte) 0x90 ) olarak değiştirmeliyiz. Okunurluğu arttırmak için ise  sabiti aşağıdaki gibi tanımlamak uygun olur:
private static final MASTER_ID = (byte) 0x90 ;
  • char tipi ise işaretsiz 16-bit tam sayı olarak ele alınır:
System.err.println((int)(char)(byte)(-1));
Yukarıdaki kod çalıştırılır ise ekranda 65535 değeri görülür. Nedeni char tipinin işaretsiz, int ve byte tam sayı tiplerinin ise işaretli olmasıdır.
Tam sayılar (byteshortint) ile yapılan aritmetik işlemler (+,-,*,/) sonucu her zaman int tipindedir. Bu nedenle derleyeci aşağıda kod için hata verecektir:
short x= 42, 
      y= 108,
      z= 0;
z = x + y;
Hatanın açıklamasında, "Type mismatch: cannot convert from int to short" yazdığını göreceksiniz. x+y sonucu int tipinde ve bu tipte bir değeri short tipinde değişkene yazmak istiyoruz. Çözüm için ya tüm değişkenleri int tipinde tanımlayacağız ya da tip dönüşümü yapacağız:
short x= 42, 
      y= 108,
      z= 0;
z = (short) ( x + y ) ;
Bir yol daha var: += operatörünü kullanmak. Tüm kısayol operatörleri örtük bir tip dönüşümü barındırırlar. Bu nedenle aşağıdaki kod hatasız derlenir:
short x= 42, 
      y= 108,
      z= 0;
z += x + y; 
Tüm tam sayı sabitler int tipindedir ve 32 bittir. Sonuna l ya da L koyarsanız long sabit tanımlamış olursunuz. 1 ile karışmaması için ne yapın edin L kullanın. Bu durumda sabitin uzunluğu iki katına çıkar: 64-bit. En uzun tam sayı tipimiz long, yeterince uzun olsa da bir sınırı var: 9223372036854775807. Bu sınırı aşarsanız, hata yaparsınız ama hata yaptığınızı fark etmeyebilirsiniz. Daha uzun tam sayılar için java.math.BigInteger sınıfını kullanabilirsiniz. Bu iş için float ya da double kullanmayı düşünüyorsanız, yazının devamını okuyun, kararınızı gözden geçirmeniz gerekebilir! Bir de tam sayılarla çalışırken herhangi bir sayıyı sıfıra bölmeye çalışmayın, bölemezsiniz, böldürmezler! 
int inf= 1/0;
int one= 0/0; 
Yukarıdaki her iki bölme işlemi de RuntimeException fırlatır: java.lang.ArithmeticException: / by zero
Ancak kayan noktalı sayıları karşılaştırırken çok daha dikkatli olmak gerekir. Örneğin aşağıdaki kod parçasını inceleyelim:
double myMoney = 4.35;
System.err.println("I have worked hard!");
myMoney *= 100;
if (myMoney == 435.0){
   System.err.println("I have $ 435.00 now!");
}
Ne yazık ki if cümlesindeki karşılaştırma hiçbir zaman true olmaz. 4 ile 5 arasında, sayılamaz sonsuz sayıda sayı bulunur. Ama bizim bu sayılamaz sonsuz sayıda sayıyı kodlamak için sınırlı sayıda bitimiz var: float için 32-bit ve double için ise 64-bit. Kayan noktalı sayılar IEEE-754 standardına göre kodlanırlar. IEEE-754 gösterimi, küçük sayılar için küçük ve büyük sayılar için büyük hatalar yapılacak şekilde tasarlanmıştır. Bu kodlamada her kayan noktalı sayıyı gösteremiyoruz, 4.35'de bu tam olarak gösteremediğimiz sayılardan. Üstelik bu şekilde tam olarak gösteremediğimizden dolayı hata yaptığımız çok sayı var. Ne kadar? Sonsuz sayıda. Üstelik ne kadar hata yapılacağını da kontrol edemiyoruz. O halde kayan noktalı sayılarla hesaplama yapmak istiyorsanız ve noktadan sonra kaç basamak doğru hesaplamak istediğinizi kontrol etmek istiyorsanız float ya da double temel tiplerini kullanmamalısınız. Bu amaçla java.util.BigDecimal sınıfını kullanabilirsiniz:
BigDecimal myMoney = new BigDecimal("4.35");
myMoney.setScale(6);
System.err.println("I have worked hard!");
myMoney = myMoney.multiply(BigDecimal.valueOf(100.0));
if (myMoney.compareTo(BigDecimal.valueOf(435))==0){
 System.err.println("I have 435 TL now...");
}
Burada setScale metodunu kullanarak noktadan sonra kaçıncı basamağa kadar doğru hesaplamak istediğimizi belirtebiliyoruz. BigDecimal sınıfını kullanarak pi sayısını noktadan sonra örneğin 100.000 basamağa kadar hesaplayabiliriz:
import java.math.BigDecimal;
import static java.math.BigDecimal.*;

public class Pi {
 private static final BigDecimal TWO = new BigDecimal(2);
 private static final BigDecimal FOUR = new BigDecimal(4);
 private static final int LENGTH = 100_000;

 public static void main(String[] args) {

  BigDecimal p = compute(LENGTH);  
  for (int start = 0, stop = start + 1_000; 
stop <= LENGTH; 
start += 1_000, stop += 1_000) {
    System.err.println(p.toString().substring(start, stop));
  }
 }

 // Gauss-Legendre Algorithm
 public static BigDecimal compute(final int SCALE) {
  BigDecimal a = ONE;
  BigDecimal b = ONE.divide(sqrt(TWO, SCALE), SCALE, ROUND_HALF_EVEN);
  BigDecimal t = new BigDecimal("0.25");
  BigDecimal x = ONE;
  BigDecimal y;

  while (!a.equals(b)) {
    y = a;
    a = a.add(b).divide(TWO, SCALE, ROUND_HALF_UP);
    b = sqrt(b.multiply(y), SCALE);
    t = t.subtract(x.multiply(y.subtract(a).multiply(y.subtract(a))));
    x = x.multiply(TWO);
  }

  return a.add(b).multiply(a.add(b))
    .divide(t.multiply(FOUR), SCALE, ROUND_HALF_UP);
 }

 // square root method (Newton's method)
 public static BigDecimal sqrt(BigDecimal A, final int SCALE) {
  BigDecimal x0 = new BigDecimal("0");
  BigDecimal x1 = new BigDecimal(Math.sqrt(A.doubleValue()));

  while (!x0.equals(x1)) {
   x0 = x1;
   x1 = A.divide(x0, SCALE, ROUND_HALF_UP);
   x1 = x1.add(x0);
   x1 = x1.divide(TWO, SCALE, ROUND_HALF_UP);
  }

  return x1;
 }
}
Merak etmeyin, ekran çıktısını burada listelemeyeceğim. Çıktıyı buradan indirip inceleyebilirsiniz. Tekrar eden bir örüntü bulursanız haber verin! Kayan noktalı sayılarda büyük sayılarda daha büyük hatalar yapıyoruz. Bir milyar liranız var ve hesabınıza 50 TL yatırıyorsunuz:
float yourMoney = 1_000_000_000F;
yourMoney = yourMoney + 50;
NumberFormat nf = NumberFormat.getCurrencyInstance(
                                    new Locale("tr","TR"));
System.err.println("Your money is " + nf.format(yourMoney));
Bakalım bu işlem sonunda hesabınızda ne kadar para var? Sizi şaşıracağınız bir sonuç bekliyor:
Your money is 1.000.000.064,00 TL
Evet, banka için kötü bir haber, kasada açık var! Gerçekten, jUnit kullanarak birim testi yazıyorsanız, benzer şekilde hiçbir zaman kayan noktalı sayıların mutlak eşitliğini test etmemelisiniz. Bunun yerine sınayacağınız değişkenin beklenen değerinin, bir aralık içine düşmesi ile yetinmelisiniz:
public class VehicleTest {

 @Test
 public void addBox_overloaded() {
  Vehicle vehicle = new Vehicle(1_000, 1_500);
  assertFalse(vehicle.addBox(501));
  assertEquals(1_000, vehicle.getWeight(), 0.001);
 }

 @Test
 public void addBox_ok() throws Exception {
  Vehicle vehicle = new Vehicle(1_000, 1_500);
  assertTrue(vehicle.addBox(100));
  assertEquals(1_100, vehicle.getWeight(), 0.001);
 }
}
Kayan noktalı sayılarda bölme işlemi RuntimeException fırlatmaz:
double inf= 1./0;
double negative_inf= -1./0;
double nan= 0./0;
System.err.println(inf);
System.err.println(negative_inf);
System.err.println(nan);
Yukarıdaki kodu çalıştırırsanız aşağıdaki ekran görüntüsü oluşur:
Infinity
-Infinity
NaN
IEEE-754'e göre sonsuz 0x7FF0000000000000, eksi sonsuz 0xFFF0000000000000 ve belirsizlik 0x7FF8000000000000 olarak kodlanır. 
Tam sayılarla çalışırken özel bir durum olmasa da bunların sarmal sınıfları ile çalışırken dikkatli olmamazı gerektiren bir durum var:
Integer lostIsland= 108;
Integer helixIsland= 108;
System.err.println(lostIsland==helixIsland);
Integer perfectNumber= 496;
Integer nothingSpecial= 496;
System.err.println(perfectNumber==nothingSpecial);
Başlangıçta her iki mantıksal ifadenin de true ya da false değer almasını bekliyorsunuz. Ancak uygulamayı çalıştırdığınızda ilk ifadenin true ikinci ifadenin ise false değer ürettiğini göreceksiniz. Bu durumun açıklaması için Integer sınıfının kaynak kodunu başvurmalısınız. Sarmal sınıftan bir referans değişkene bir sabiti atarken, derleyici gerçekte bu atamayı valueOf metod çağrısı üzerinden gerçekleştirir. valueOf metodu size bir ipucu verecektir. Burada Integer sınıfının bir sekizliğe sığan tam sayıları [-128..127] cepte tuttuğunu göreceksiniz:
public static Integer valueOf(int i) {
        assert IntegerCache.high >= 127;
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
Bu nedenle karşılaştırmanın her durumda doğru sonuçlanmasını istiyorsanız, referansları değil içeriği karşılaştırın. İçeriği ise her durumda equals metodunu kullanarak karşılaştırabilirsiniz.
Integer lostIsland= 108;
Integer helixIsland= 108;
System.err.println(lostIsland.equals(helixIsland));
Integer perfectNumber= 496;
Integer nothingSpecial= 496;
System.err.println(perfectNumber.equals(nothingSpecial));
Şimdi çıktı aşağıdaki gibi olacaktır:
true
true
Benzer durumla String sınıfında da karşılaşıyoruz. String aslında temel bir tip değil. Ancak onu temel tipler ile birlikte incelememizi gerektirecek ve diğer sınıflarda olmayan bir kullanımı var. Java programlama dilinde, bir sınıftan nesne yaratmak istediğinizde, new operatörünü kullanmanız gerekir. Ancak String ile çalışırken iki farklı şekilde tanımlama yapabiliyoruz:
String jack1 = "Jack";
String jack2 = new String("Jack");
İlk kullanımda, String nesnesi bir "String havuzunda" yaratılır. Eğer daha önce aynı karakter katarı yaratılmışsa, jack1 referansı bu nesneyi gösterir. İkinci kullanımda ise String nesnesi her zaman Heap'de yaratılır. Bu nedenle String referansları (jack1 ve jack2) karşılaştırılır ise false değer üretir:
String jack1 = "Jack";
String jack2 = new String("Jack");
String jack3 = "Jack";
System.out.println(jack1==jack2);
System.out.println(jack1==jack3);
System.out.println(jack1.equals(jack2));
System.out.println(jack1.equals(jack3));
Karşılaştırma equals çağrısı üzerinden yapılırsa her zaman doğru sonucu verir:
false
true
true
true
String sınıfında yer alan intern() metodu ile Heap'deki bir String nesnesini String havuzuna getirmek mümkündür:
String jack1 = "Jack";
String jack2 = new String("Jack");
jack2= jack2.intern();
System.out.println(jack1==jack2);
Şimdi karşılaştırma true değer üretecektir. 
String sınıfı değiştirilemez bir sınıftır. Metodlarının hepsi yeni bir String nesnesi döndürür, mevcut String nesnesinin karakterleri üzerinde bir değişikliğe neden olmazlar:
String name= "Jack";
name.toUpperCase();
if (name.equals("JACK")){
   System.err.println("Jack the Ripper");
} else {
   System.err.println("Jack Bauer");
}
Yukarıdaki kod çalıştırılır ise ekranda "Jack Bauer" iletisi görülür. name.toUpperCase() ifadesi yeni bir String nesnesi döndü "JACK", ama bu sonucu referans değişkeninde saklamadığımız için bu nesne çöp oldu. String sınıfı içindeki toUpperCase, toLowerCase gibi bazı metotlar dil bilgisini kullanırlar:
String name="ışiğöçü";
System.err.println(name);
System.err.println(name.toUpperCase(Locale.US));
System.err.println(name.toUpperCase(new Locale("tr","TR")));
Yukarıdaki kodu çalıştırdığımızda İngilizce ve Türkçe için toUpperCase() davranışını inceleyiniz:
ışiğöçü
IŞIĞÖÇÜ
IŞİĞÖÇÜ
Son olarak String karşılaştırmasını Collator sınıfını kullanarak nasıl kontrol edebildiğimizi görelim:
String basicRules=  "< a < b < c < ç < d < e < f < g < ğ < h "+
        "< ı < i < j < k < l < m < n < o < ö < p "+
       "< r < s < ş < t < u < ü < v < y < z ";
String trExpension= "& şi ; she & ş ; sch & s ; ş & u ; ü & i ; ı " + 
                    "& c ; ç & o ; ö & ğ ; g" ;
final Collator collator= new RuleBasedCollator(basicRules + trExpension);
Collator.getInstance(new Locale("tr","TR"));
collator.setStrength(Collator.PRIMARY);
String name1="şima";
String name2="sima";
System.err.println("name1="+name1);
System.err.println("name2="+name2);
System.err.println("name1.equals(name2): "+collator.equals(name1, name2));
name1="şule";
name2="schule";
System.err.println("name1="+name1);
System.err.println("name2="+name2);
System.err.println("name1.equals(name2): "+collator.equals(name1, name2));
name1="ışçöüğ";
name2="iscoug";
System.err.println("name1="+name1);
System.err.println("name2="+name2);
System.err.println("name1.equals(name2): "+collator.equals(name1, name2));
Burada "şima" ile "shema" ve "şule" ile "schule" karşılaştırmalarının doğru sonuç vermesi için String karşılaştırmasını yeniden kendi ihtiyaçlarımıza göre Collator sınıfı yardımı ile tanımlıyoruz. Buna göre g ile ğ, s ile ş gibi karşılaştırmaların eşit olmasını sağlıyoruz. Yukarıdaki kodu çalıştırdığımızda aşağıdaki ekran çıktısı üretilecektir:
name1=şima
name2=shema
name1.equals(name2): true
name1=şule
name2=schule
name1.equals(name2): true
name1=ışçöüğ
name2=iscoug
name1.equals(name2): true
Ayrıca sözlük sıralamasını yeniden tanımlanabildiğini görüyoruz:
< a < b < c < ç < d < e < f < g < ğ < h
< ı < i < j < k < l < m < n < o < ö < p
< r < s < ş < t < u < ü < v < y < z
Buna göre sıralamayı nasıl değiştirebildiğimizi aşağıdaki örnek uygulamadan görelim:
List<String> names= new ArrayList<>();
names.add("şule");
names.add("sema");
names.add("şima");
names.add("zehra");
names.add("ayşegül");
Collections.sort(names);
System.out.println(names);
Comparator<String> stringComparator= new Comparator<String>() {
 @Override
 public int compare(String o1, String o2) {
  return collator.compare(o1, o2);
 }
};
Collections.sort(names,stringComparator);
System.out.println(names);
Yukarıdaki kodu çalıştırdığımızda oluşan ekran görüntüsünü inceleyelim:
[ayşegül, sema, zehra, şima, şule]
[ayşegül, sema, şima, şule, zehra]

1 comment:

  1. Binnur hocam çok güzel bir belge olmuş, eline sağlık. Teşekkürler

    ReplyDelete