Przetwarzanie danych: liczby, daty, algorytmy


1. Opakowanie typów prostych i autoboxing

Bardzo często występuje potrzeba traktowania liczb jako obiektów.
Możemy np. zechcieć utworzyć tablicę, która zawiera i liczby i napisy. Wiemy, że można utworzyć tablicę dowolnych obiektów np.:

Object[] arr = new Object[3];

Ale liczby są reprezentowane przez typy proste (i nie są obiektami).

Zatem kompilator nie zgodzi się na wstawienie do niej elementu typu prostego np. taka konstrukcja będzie niedopuszczalna.
Object[] arr = { "ala", 3, "asia", 5 };
 
Również kolekcje mogą zawierać dowolne obiekty, ale nie uda nam się do "surowej" listy:

ArrayList list = new ArrayList();

czy też listy elementów typu Object:

ArrayList<Object> list = new ArrayList<Object>();

dodać elementy typów prostych.

Kolekcje mogą zawierać tylko referencje do obiektów
.

Dlatego w standardowym pakiecie java.lang umieszczono specjalne klasy, opakowujące typy proste i czyniące z nich obiekty.
Są to klasy:
Obiekty tych klas reprezentują (w sposób obiektowy) dane odpowiednich typów.

Mówimy tu o opakowaniu liczby (czy też danej typu boolowskiego), bowiem liczba (lub wartość true|false) jest umieszczone "w środku" obiektów odpowiedniej klasy, jako jego element.

Klasy opakowujące nie dostarczają żadnych metod do wykonywania operacji na liczbach. Operatory arytmetyczne zaś możemy stosować tylko wobec liczbowych typów prostych.

A przecież często potrzebujemy liczby jako obiektu, a jednocześnie jako typu prostego - do wykonania operacji arytmetycznych
Musimy więc umieć zapakować liczbę do obiektu klasy opakowującej (po to by użyć wtedy, gdy wymagany jest obiekt)  i wyciągnąć ją stamtąd (wtedy gdy potrzebne są obliczenia).

Pakować i rozpakowywać typy proste możemy "ręcznie".

Np. obiektowy odpowiednik liczby 5 typu int możemy uzyskać tworząc obiekt klasy Integer:

Integer a = new Integer(5);

Z obiektu takiej klasy możemy  liczbę "wyciągnąć" za pomocą odpowiednich metod, np.

int i = a.intValue(); // zwraca wartość typu int, "zawartą" w obiekcie a

Podobnie:

Double dd = new Double(7.1);
double d = dd.doubleValue(); // d == 7.1

Jest to jednak nieco żmudne.  Zobaczmy przykład wczytania liczb całkowitych z pliku, posortowania ich i wypisania sumy pierwszej i ostatniej liczby z posortowanej sekwencji.
import java.io.*;
import java.util.*;

public class Test1 {

  public static void main(String[] args) throws FileNotFoundException {
    ArrayList<Integer> list = new ArrayList<Integer>();
    Scanner in = new Scanner(new File("liczby.txt"));
    while (in.hasNextInt()) {
      Integer n = new Integer(in.nextInt());
      list.add(n);
    }
    
    Collections.sort(list);
    int wynik = list.get(0).intValue() + list.get(list.size()-1).intValue();
    System.out.println(wynik);
  }

} 
Nieco za dużo tu pisania - new Integer albo intValue.
Dlatego w Javie wprowadzono mechanizm zwany autoboxingiem.

Autoboxing to automatyczne przekształcanie pomiędzy typami prostymi a typami obiektów klas, opakowujących typy proste.


Boxing

Automatyczne przekształcenie z typu prostego do obiektowego.
Typ prosty (i)  -> typ obiektowy (r), tak, że : r.value() == i

boolean -> Boolean
byte  -> Byte
char -> Character
short -> Short
int -> Integer
long -> Long
float -> Float
double -> Double

Unboxing

Automatyczne przekształcenie z typu obiektowego do prostego.
Typ obiektowy (r)  -> typ prosty (i), tak, że i == r.value()

Boolean   ->   boolean
Byte      ->   byte
Character ->   char
Short     ->   short
Integer   ->   int
Long      ->   long
Float     ->   float
Double    ->   double

Kiedy zachodzą te automatyczne konwersje?

Przykład.

Poniższy program:
public class AutoBoxing {

  private int intField;

  public AutoBoxing() {
    // Konwersje przy przypisaniu
    int x = 1;
    Integer a = x;
    int y = a;
    System.out.println("Konwersje przy przypisaniu: " + a + " " + y);

    // Konwersje przy wywołaniu metod
    methodInvocationConversion1(x);
    methodInvocationConversion2(a);

    // Konwersje przy zwrocie wyników  
    y = getIntFromInteger();
    a = getIntegerFromInt();
    System.out.println("Konwersje przy zwrocie wyniku: " + a + " " + y);
  }

  public AutoBoxing(Integer i) {
    intField = i;
    System.out.println("W konstruktorze bezparametrowym: " + intField);
  }
  
  void methodInvocationConversion1(Integer p) {
    System.out.println("Parametr Integer, argument int " + p);
  }

  void methodInvocationConversion2(int p) {
    System.out.println("Parametr int, argument Integer " + p);
  }

  int getIntFromInteger() {
    return new Integer(2);
  }

  Integer getIntegerFromInt() {
    return 2;
  }

  public static void main(String[] args) {
     new AutoBoxing();
     // Zauważmy, że konstruktor możemy wołać z arg. int lub Integer
     new AutoBoxing(3);
     new AutoBoxing(new Integer(3));
  }


}
da w wyniku:

Konwersje przy przypisaniu: 1 1
Parametr Integer, argument int 1
Parametr int, argument Integer 1
Konwersje przy zwrocie wyniku: 2 2
W konstruktorze bezparametrowym: 3
W konstruktorze bezparametrowym: 3

Poprzedni programik sumowania pierwszej i ostatniej liczb posortowanej sekwencji możemy teraz napisać dużo prościej:

public class Test1 {

  public static void main(String[] args) throws FileNotFoundException {
    ArrayList<Integer> list = new ArrayList<Integer>();
    Scanner in = new Scanner(new File("liczby.txt"));
    while (in.hasNextInt()) 
      list.add(in.nextInt());
    Collections.sort(list);
    int wynik = list.get(0) + list.get(list.size()-1);
    System.out.println(wynik);
  }

}

Warto zauwazyć, że opakowanie typów prostych zachodzi również przy przypisaniach (w tym przekazywaniu argumentów i zwrocie wyników) na zmienne typu Object.
Np. przypisanie:

    Object o = 1;

spowoduje stworzenie obiektu klasy Integer opakowującego liczbe 1 i przypisanie referencji do tego obiektu na zmienną o.

Jednak wypakowanie nie jest już automatyczne.
W kontekście:

    Object o = 1;

przy próbie podstawienia na zmienną typu int:

    int  x = o;

wystąpi błąd w kompilacji.
Aby tego uniknąć musimy uzyć operatora rzutowania i to do typu Integer:

   int x = (Integer) o;

Choć głównego powodu istnienia klas opakowujących upatrywać można w konieczności ich obiektowej reprezentacji, to - niejako przy okazji - w klasach tych zdefiniowano wiele użytecznych metod.

Znamy już statyczne metody przekształcające napisową reprezentację liczb w ich wartości binarne:


            public static ttt  parseTtt(String s)

zwracające wartości typu ttt reprezentowane przez napis s

gdzie: ttt nazwa typu prostego (np. int, double), Ttt - ta sama nazwa z kapitalizowaną pierwszą literą (np. Int, Double)


Oprócz tego w klasach tych znaleźć można wiele metod do operowania na bitach liczb, przekształcania pomiędzy różnymi systemami liczbowymi oraz przekształcania wartości pomiędzy typami prostymi (np. doubleValue() zwraca wartość jako double).

Użytecznych dodatkowych metod dostarcza klasa Character (opakowująca typ char).
Należą do nich metody stwierdzania rodzaju znaku np.:

isDigit()            // czy znak jest znakiem cyfry

isLetter()           // czy znak jest znakiem litery

isLetterOrDigit()    // czy znak jest litera lub cyfra

isWhiteSpace()       // czy to "biały" znak (spacja, tabulacja etc.)

isUpperCase()        // czy to duża litera   

isLowerCase()        // czy to mała litera
Metody te zwracają wartości true lub false.


W klasach opakowujących typy numeryczne zdefiniowano także wiele użytecznych statycznych stałych. Należą do nich stałe zawierające maksymalne i minimalne wartości danego typu.
Mają one nazwy MAX_VALUE i MIN_VALUE.
Dzięki temu nie musimy pamiętać zakresu wartości danego typu.
Możemy np. zsumować wszystkie dodatnie liczby typu short:

public class MaxVal {

  public static void main(String[] args) {

    long sum = 0;
    for (int i=1; i <= Short.MAX_VALUE; i++) sum +=i;
    System.out.println(sum);

  }

}
Uwaga: w tym kodzie nie wolno nadać licznikowi (zmiennej i) typu short, bo przy dojściu do wartości Short.MAX_VALUE (warunek pętli spełniony) w kolejnej iteracji zmienna i uzyska wartość o 1 większą, a więc przekraczająca dopuszczalny zakres. Nie spowoduje to wyjątku tylko arytmetyczne przepełnienie - zmienna i uzyska wartość Short.MIN_VALUE. W rezultacie pętla będzie działać w nieskończoność.

2. Operacje na bitach

Liczby calkowite (typów byte, short, int, long) są reprezentowane w pamięci komputera w postaci binarnej jako ciągi bitów. Bit, który znajduje się w takim zapisie najbardziej z lewej strony nazywa się bitem najstarszym (najbardziej znaczącym), a ten najbardziej z prawej - najmłodszym (najmniej znaczącym).
Najstarszy bit jest tzw. bitem znaku - jesli jest ustawiony (ma wartość 1) to liczba jest ujemna, a jeśli nie (ma wartość 0) - to liczba jest dodatnia.

r


Jak pamiętamy z wykładu 1 - dziesiętną wartość liczby zapisaną w systemie dwójkowym możemy uzyskać sumując kolejne iloczyny wartości bitów (1 lub 0) i odpowiednich potęg 2. Uwzględnienie liczb ujemnych wymaga, by wartość bitu znaku "brana była" z minusem. Dlatego na powyższym rysunku:
7 = -0*27 + 0*26 + 0*25 + 0*24 +0*23 + 1*22 + 1*21 + 1*20
-7 = -1*27 + 1*26 + 1*25 + 1*24 +1*23 + 0*22 + 0*21 + 1*20

Taki zapis liczb całkowitych nazywa się zapisem z uzupełnieniem do dwóch
Łatwo dostrzec, że z tego powodu wartość bezwzględna największej całkowitej liczby ujemnej jest zawsze większa od wartości bezwględnej największej całkowitej liczby dodatniej. W przypadku wartości typu byte najmniejsza możliwa wartość równa jest 100000000, czyli -1*27 = - 128, natomiast największa możliwa licznba dodatnia równa jest 01111111 = 127.

Pamiętamy też, że wartości typu char (reprezentujące znaki) mogą być traktowane jak liczby całkowite. W tym przypadku jednak najstarszy bit nie jest traktowany jako bit znaku - zatem wartości typu char (zajmujące 2 bajty) mogą kształtować się w przedziale od 0 do 65536.

Na bitach wartości wszystkich całkowitych typów numerycznych (w tym bitach wartości typu  char) możemy wykonywać operacje.
Do operowania na bitach liczb całkowitych służą następujące operatory:
Każdy z argumentów wszystkich wymienionych operatorów bitowych musi być typu całkowitego.
Operatory przesunięcia bitowego powodują przesunięcie bitów swojego lewego argumentu o liczbę pozycji specyfikowaną przez prawy argument w lewo (operator <<), lub w prawo (operator >>). Przy przesuwaniu w lewo, młodsze (zwalniane) bity zapełniane są zerami; przy przesunięciu w prawo zwalniane bity zapełniane są wartością bitu znakowego (najstarszego). co nazywa się propagację bitu znakowego. W przypadku użycia operatora >>> taka propagacja nie występuje i niezaleznie od wartości bitu znaku zwalniane bity są zapełniane zerami.

Rozpatrzmy przykłady:

byte b = 1; -> bitowa reprezentacja: 00000001 (1)
b << 1 -> 00000010 (2)
b << 4 -> 00010000 (16)

A zatem w przypadku, gdy a jest którąś z potęg dwójki, kolejne przesunięcie w lewo mnoży wartość a przez 2. A co się stanie gdy będziemy chcieli przesunąć w lewo bity największej z możliwych liczb danego typu?
Warto wiedzieć, że lewy argument operatora << (jeśli był typu byte, short lub char) jest promowany do typu int i całe wyrażenie ma wartość typu int (jesli argument był typu long - wartość wyrażenia jest typu long).
Zatem taki program:
public class Test {

  public static void main(String[] args) {
    byte b = Byte.MAX_VALUE;
    System.out.println(b + " " + (b << 1));
    System.out.println(b + " " + (byte) (b << 1));
    int i = Integer.MAX_VALUE;
    System.out.println(i + " " + (i << 1));
  }

}
wyprowadzi:
127 254
127 -2
2147483647 -2

Pierwszy wynik jest faktycznie rezultatem mnożenia wartości b przez dwa tylko dlatego, że nastąpiła promocja wartości b do typu int i typem wyniku wyrażenia b << 1 jest int.
Trzeci wynik pokazuje, że przesunięcie bitów może doprowadzić do zmiany bitu znaku z 0 na 1. W tym przypadku nie było miejsca na promocję (czyli faktycznie zwiększenie liczby bitów zajmowanych przez liczbę) w konsekwencji bit znaku stał się 1 i otrzymaliśmy liczbę ujemną.
Pośredni, drugi wynik uwidacznia dokładnie to samo. Dokonaliśmy tu konwersji do typu byte, zatem rezultat jest taki jakbyśmy operowali na wartościach typu byte (bez promocji):

127 = 01111111

a po przesunięciu bitów o jedno miejsce, otrzymujaemy:

11111110

co jest rowne dokładnie -2.

Zobaczmy jak dziala operator >> (przesunięcie bitów w prawo - z propagacja znaku)

byte b = 127; -> 01111111
b >> 1 -> 00111111 (63)
b >> 2 -> 00011111 (31)
b >> 3 -> 00001111 (15)

Operacja a >> n dla liczb całkowitych jest równoważna całkowitoliczbowemu dzieleniu (dającemu w rezultacie całkowitą część rezultatu dzielenia) a/2n.

Dla liczb ujemnych wygląda to w następujący sposób:
byte b = -64; -> 11000000
b >> 1 -> 11100000 (-32)
b >> 2 -> 11110000 (-16)
b >> 3 -> 11111000 (-8)
c >> 4 -> 11111100 (-4)

(zwalniane bity są zapełniane wartością bitu znakowego tj. 1)
Zauważmy więc, że jeśli b = -1 (11111111), to przesunięcie bitów w prawo da wynik -1.


Logiczne operatory bitowe ( ~,^, &, |) służą do ustalania wartości bitów rezultatu za pomocą wartości bitów argumentów. Operator uzupełnienia jedynkowego ~ zamienia w swoim jedynym argumencie wszystkie zerowe bity na 1, a 1 na 0. Sposób w jaki określane są bity rezultatu pozostałych logicznych operacji bitowych obrazuje poniższa tablica, w której a1 i a2 oznaczają argumenty operatorów bitowych.

Wartość bitu
w a1
Wart. odpo-
wiadajęcego
mu bitu w a2
Wart. odpowiadającego bitu rezultatu oper.
a1 & a2
a1 ^ a2
a1 | a2
00000
10011
01011
11101

Np. jeśli a1 = 5, a2 = 7, to logiczne operacje bitowe są przeprowadzane w następujący sposób:

a1 & a2 = ...0101
        & ...0111
         ---------
          ...0101 = 5
a1 ^ a2 = ...0101
        ^ ...0111
         ---------
          ...0010 = 2
a1 | a2 = ...0101
        | ...0111
        ---------
          ...0111 = 7

Zwróćmy uwagę, że operacje te dotyczą tylko najmłodszych 3 bitów, niezależnie od tego jakiego są typu i ile miejsca w pamięci zajmują argumenty a1 i a2.
Np. aby wyzerować najmłodszy bit całkowitej zmiennej x - niezależnie od jej typu  możemy napisać:

x = x & ~0x1; 

zerowe bity stałej 0x1 staną się 1, a jedyny bit o wartości 1 - zerem; w rezultacie w x zerowany będzie najmłodszy bit - niezależnie od tego jakiego typu jest i ile miejsca zajmuje x.

Operacje bitowe są właśnie najbardziej przydatne do ustawiania i testowania wartości bitów. Często w programowaniu wykorzystuje się tzw. flagi - znaczniki, których wartości określają np. charakterystyki jakiegoś zdarzenia, albo jaki jest stan jakiegoś obiektu. Flagi mają zwykle charakter zero-jedynkowy - szkoda więc byłoby na każdą z nich tracić 8 bitów, gdy wystarczy 1. Zestawy flag zapisujemy więc zwykle jako bity wartości jakiejś zmiennej - typu byte (8 flag), albo int (32 flagi), albo long (64 flagi). Za pomocą operatorów bitowych możemy sprawdzać które z flag przechowywanych w tych zmiennych są ustawione i modyfikować ich ustawienie.

Popatrzmy na następujący przykladowy program (wykorzystamy w nim statyczną metodę getBits(wart) z klasy Unspec, która zwraca tablicę znaków 0 - 1, pokazujących bitową reprezentację przekazanej jako argument wartości; klasę tę za chwilę zbudujemy i omówimy).
public class Flags {

  public static void main(String[] args) {

    final int  FL1 = 0x01,    // stałe pokazujące które bity (flagi) mają
               FL2 = 0x02,    // być testowane lub ustawiane
               FL3 = 0x04,
               FL4 = 0x08;

    int flags = 0;
    flags = flags | FL1;                   // ustawia flagę 1 (najmłodszy bit)
    show("flags = flags | FL1",
          flags);
    flags = 0;
    flags =  flags | FL3;                  // ustawia flagę 3 (trzeci bit)
    show("flags = flags | FL3",
          flags);
    flags = 0;
    flags = flags | (FL1 | FL4);           // ustawia 1 i 4 flagę
    show("flags = flags | (FL1 | FL4)",
          flags);

   // czy flaga 1 ustawiona
   if ((flags & FL1) > 0) System.out.println("Flaga 1 ustawiona");
   else System.out.println("Flaga 1 NIE ustawiona");

   // czy flaga 2 ustawiona
   if ((flags & FL2) > 0) System.out.println("Flaga 2 ustawiona");
   else System.out.println("Flaga 2 NIE ustawiona");

   // czy flagi 1 i 4 ustawione (jednoczesnie)
   int mask = FL1 | FL4;
   if ((flags  &  mask) ==  mask) System.out.println("Flagi 1 i 4 ustawione");
   else System.out.println("Flagi 1 i 4 NIE ustawione");

   // czy flagi 1 i 2 ustawione (jednoczesnie)
   mask = FL1 | FL2;
   if ((flags & mask) == mask) System.out.println("Flagi 1 i 2 ustawione");
   else System.out.println("Flagi 1 i 2 NIE ustawione");

  }

  static void show(String s, int flags) {
    System.out.println(s);
    char[] bits =  Unspec.getBits(flags);
    System.out.println(new String(bits) + "   --- val: " + flags);
  }

}
Program wyprowadzi następujące informacje:

flags = flags | FL1
00000000000000000000000000000001   --- val: 1
flags = flags | FL3
00000000000000000000000000000100   --- val: 4
flags = flags | (FL1 | FL4)
00000000000000000000000000001001   --- val: 9
Flaga 1 ustawiona
Flaga 2 NIE ustawiona
Flagi 1 i 4 ustawione
Flagi 1 i 2 NIE ustawione


Znając już działanie operatorów bitowych możemy pokusić się o stworzenie klasy Unspec, ktra dostarczy następujących statycznych metod, zwracających w tablicy bitowe reprezentacje wartości różnych typów:

public static char[]  getBits(byte)
public static char[]  getBits(char)
public static char[]  getBits(int)
public static char[]  getBits(long)


Możliwe rozwiązanie:

public class Unspec {

  public static char[] getBits(byte v)  {
    return getBits(8, (long) v);
  }

  public static char[] getBits(char v)  {
    return getBits(16, (long) v);
  }


  public static char[] getBits(int v)  {
    return getBits(32, (long) v);
  }


  public static char[] getBits(long v)  {
    return getBits(64, v);
  }

  private static char[] getBits(int n, long v)  {
    char[] bits = new char[n];
    long mask = 1;
    for (int k = n-1; k >= 0; k--) {
      if ((v & mask) != 0) bits[k] = '1';
      else bits[k] = '0';
      mask =  mask << 1;
    }
    return bits;
  }
}

oraz program testujący, w którym - jako argumenty wywolania podajemy kolejno liczbę typu long, liczbę typu int, znak, liczbę typu byte:

class Test {
  public static void main(String[] args) {
    long l = Long.parseLong(args[0]);
    int i  = Integer.parseInt(args[1]);
    char c = args[2].charAt(0);
    byte b = Byte.parseByte(args[3]);
    System.out.println("Wartość typu long " + l);
    System.out.println(new String(getBits(l)));
    l = Long.MAX_VALUE;
    System.out.println("Maksymalna wartość typu long " + l);
    System.out.println(new String(getBits(l)));
    l = Long.MIN_VALUE;
    System.out.println("Minimalna wartość typu long " + l);
    System.out.println(new String(getBits(l)));
    System.out.println("Wartość typu int " + i);
    System.out.println(new String(getBits(i)));
    System.out.println("Wartość typu char: znak " + "'" + c + "'" + ", kod " + (int) c);
    System.out.println(new String(getBits(c)));
    System.out.println("Wartość typu byte " + b);
    System.out.println(new String(getBits(b)));
  }
}

Program ten - przy podanych argumentach: 1 -1 a -7 - wyprowadzi:
Wartość typu long 1
0000000000000000000000000000000000000000000000000000000000000001
Maksymalna wartość typu long 9223372036854775807
0111111111111111111111111111111111111111111111111111111111111111
Minimalna wartość typu long -9223372036854775808
1000000000000000000000000000000000000000000000000000000000000000
Wartość typu int -1
11111111111111111111111111111111
Wartość typu char: znak 'a', kod 97
0000000001100001
Wartość typu byte -7
11111001

Warto zauważyć, że w klasach opakowujących numeryczne typy proste jest wiele gotowych metod do operowania na bitach np.
Przykładowy program pokazuje zastosowanie "bitowych metod" klasy Integer.
public class BitFun {
  
  static void show(int x) {
    System.out.println("Wartość: " + x);
    String s = Integer.toBinaryString(x);
    System.out.println("Bity: " + s);
    System.out.println("Liczba jedynek: " + Integer.bitCount(x));
    System.out.println("Liczba wiodących zer: " + Integer.numberOfLeadingZeros(x));
    System.out.println("Liczba zer na końcu: " + Integer.numberOfTrailingZeros(x));
  }

  public static void main(String[] args) {
    int x = 101;
    show(x);
    System.out.println("Po rotacji w lewo o 1 pozycję");
    show(Integer.rotateLeft(x, 1));
    System.out.println("Po rotacji w prawo o 1 pozycję");
    show(Integer.rotateRight(x, 1));
    System.out.println("Po odwróceniu bitów");
    show(Integer.reverse(x));
    System.out.println("Po odwróceniu bajtów");
    show(Integer.reverseBytes(x));
  }

}
Wynik:
Wartość: 101
Bity: 1100101
Liczba jedynek: 4
Liczba wiodących zer: 25
Liczba zer na końcu: 0
Po rotacji w lewo o 1 pozycję
Wartość: 202
Bity: 11001010
Liczba jedynek: 4
Liczba wiodących zer: 24
Liczba zer na końcu: 1
Po rotacji w prawo o 1 pozycję
Wartość: -2147483598
Bity: 10000000000000000000000000110010
Liczba jedynek: 4
Liczba wiodących zer: 0
Liczba zer na końcu: 1
Po odwróceniu bitów
Wartość: -1509949440
Bity: 10100110000000000000000000000000
Liczba jedynek: 4
Liczba wiodących zer: 0
Liczba zer na końcu: 25
Po odwróceniu bajtów
Wartość: 1694498816
Bity: 1100101000000000000000000000000
Liczba jedynek: 4
Liczba wiodących zer: 1
Liczba zer na końcu: 24

3. Działania matematyczne

Wiemy już dość dużo o operacjach arytmetycznych. Warto jednak zwrócić krótko uwagę na cztery kwestie:

Jeżeli zdarzy się nam w programie podzielić liczbę całkowitą przez całkowite zero, to powstanie wyjątek (sygnał o błedzie) ArithmeticException.
Całkiem inaczej jest w przypadku liczb rzeczywistych.
Dzielenie przez zero nie powoduje błędu, ale daje w wyniku specjalną wartość, reprezentującą nieskończoność. Wartości +- nieskończoność są dostępne jako statyczne stałe klasy Double: Double.POSITIVE_INFINITY i Double.NEGATIVE_INFINITY.
Dzięli temu łatwo możemy wykryć przypadek dzielenia przez zero.
Inną specjalną wartością typu rzeczywistego jest NaN ("Not-a-Number" - nie-liczba. liczba nie zdefiniowana). Reprezentuje ona wyniki pewnych operacji arytmetycznych (takich jak np. 0.0 / 0.0). W klasie Double zdefiniowano stałą statyczną NaN. Ale nie powinna ona być wykorzystywana do sprawdzania czy wynik operacji jest "Not-a-Number", bowiem wartości Nan mają ciekawą własciwość: NaN != Nan.
Do sprawdzania czy wartość jest Nan czy nie należy używac metody isNan z klasy Double.

 Przykładowy program ilustruje wyżej powiedziane:
public class InfNan {

  public static void main(String[] args) {

    double d = 1.0 / 0.0;
    System.out.println("d = " + d);
    if (d == Double.POSITIVE_INFINITY)
     System.out.println("to jest + nieskończoność");
    d = -1.0 / 0.0;
    System.out.println("d = " + d);
    if (d == Double.NEGATIVE_INFINITY)
     System.out.println("to jest - nieskończoność");
    d = 0.0 / 0.0;
    System.out.println("d = " + d);
    if (d == Double.NaN)
     System.out.println("to nie jest liczba");
    else System.out.println("to jest liczba");
    Double dd = new Double(d);
    System.out.println("naprawdę: to nie jest liczba ? - " + dd.isNaN());

  }

}

Wynik działania programu:

d = Infinity
to jest + nieskończoność
d = -Infinity
to jest - nieskończoność
d = NaN
to jest liczba
naprawdę: to nie jest liczba ? - true



W klasie Math pakietu java.lang zdefiniowano statyczne metody matematyczne.
Między innymi:

Metody matematyczne
int abs(int arg)wartość bezwzględna argumentu
double abs(double arg)
int min(int arg1, int arg2)minimum z arg1, arg2
double min(double arg1, double arg2)minimum z arg1, arg2
int max(int arg1, int arg2) maksimum z arg1, arg2
double max(double arg1, double arg2)maksimum z arg1, arg2
double ceil(arg)zwraca najmniejszą wartość rzeczywistą większą lub równą argumentowi i matematycznie tożsamą z liczbą całkowitą, np. ceil(2.1) == 3
double floor(arg)zwraca największą wartość rzeczywistą mniejszą lub róną argumentowi i matematycznie tożsamą z liczbą całkowitą np. floor(2.1) == 2
double sqrt(double arg)pierwiastek kwadratowy arg
double cbrt(double arg)pierwiastek 3 stopnia z arg
double exp(double arg)Zwraca potęgę o podstawie e i wykładniku arg
double expm(double x)Zwraca ex -1
double pow(double base, double exp)zwraca potęgę o podstawie base i wykładniku exp. Jeśli base ma wartość 0, a exp jest ujemne albo niecałkowite, to zwraca NaN.
double log(double arg)Zwraca logarytm naturalny arg. Jeśli argument nie jest dodatni, to zwraca NaN.
double sin(double arg)Zwraca sinus argumentu podanego w radianach.
double cos(double arg)cosinus
double tan(double arg)tangens


W klasie Math znajduje się również generator liczb pseudolosowych - metoda random. Jej użycie jest jednak mniej wygodne niż użycie niestatycznych metod klasy Random, znajdującej się w pakiecie java.util.

Generator liczb pseudolowowych należy najpierw zainicjować za pomocą konstruktorów klasy Random:

Random() // inicjuje generator na podstawie aktualnego czasu w milisekundach
Random(long seed) // inicjuje generator na podstawie podanej liczby

Następnie można generować kolejne liczby pseudolosowe za pomocą odpowiednich niestatycznych metod np:

double nextDouble()
zwraca kolejną pseudolosową liczbę rzeczywistą z równomiernego rozkładu w przedziale [0,1]

int nextInt(int n)
zwraca kolejną pseudolosową liczbę całkowitą z równomiernego rozkładu w przedziale [0, 1) tj. 0 <= nextInt(n) < n.

Zadanie: napisać program do losowania liczb w totolotku (6 z 49).
Umożliwić - na życzenie - inicjowania generatora na podstawie sumy podanch przez użytkownika liczb.
Uwaga: uwzględnić to, że w jednym losowaniu liczby nie mogą się powtarzać.

Możliwe rozwiązanie.
import static javax.swing.JOptionPane.*;
import java.util.*;

public class Toto {

  public static void main(String[] args) {
    String msg = "Podaj magiczne liczby,\n" +
                 "nic nie wpisuj = automatyczna inicjacja,\n" +
                 "lub wybierz Cancel, by skończyć losowania";
    
    Random rand;                          // generator

    boolean[] isDrawn = new boolean[49];  // i-ty element tablicy
                                          // ma wartość true
                                          // gdy liczba i została wylosowana
                                          // inicjalnie jest false (żadna nie wylosowana)  

    String inp;
    while ((inp = showInputDialog(msg)) != null) {

      if (!inp.equals("")) {  // inicjacja generatora sumą podanych liczb
         Scanner sc = new Scanner(inp);
         long sum = 0;
         while (sc.hasNextInt()) sum += sc.nextInt();
         rand = new Random(sum);
      }
      else rand = new Random();  // gdy nic nie podano - inicjacja czasem

      // Losowanie

      final int ILE = 6;  // liczb do wylosowania
      int k = 0;          // licznik niepowtarzających się liczb

      String out = "";   // tu będzie wynik 
      while (k < ILE) {
        int n = rand.nextInt(49);  // losowanie:  0 <= n < 49
        if (isDrawn[n]) continue;  // jeżeli ta liczba już była wylosowana
        else {                     // nie była - bierzemy ją!
          k++;                     // licznik wziętych + 1
          isDrawn[n] = true;       // teraz jest naprawdę wylosowana
          out += " " + (n+1);      // n+1, bo mamy mieć od 1 do 49
        }
      }
      showMessageDialog(null, "Wylosowane liczby:\n" + out);
    }
  }

}
Zobacz demo programu



Szczególnego komentarza wymaga porównywanie wyników operacji na liczbach rzeczywistych. Rozważmy krótki program:

public class FloatRep {

  public static void main(String[] args) {
    double liczba = 3 * 0.2;
    double wynik  = 0.6;
    if (liczba != wynik)
       System.out.println("Nie równe. Różnica wynosi: " + (liczba - wynik));
    else System.out.println("Równe");

  }

}

Wbrew intuicyjnym oczekiwaniom wynikeim jego działania będzie, stwierdzenie, że 3 * 0.2 nie jest równe 0.6. Wyprowadzi on bowiem następujący napis:

    Nie równe. Różnica wynosi: 1.1102230246251565E-16

Dzieje się tak ze względu na reprezentację liczb rzeczywistych, która w Javie opiera się na standardzie IEEE 754, w którym niektóre liczby rzeczywiste nie mogą mieć dokładnej reprezentacji. Innym problemem związanym z liczbami jest ich precyzja - maksymalna liczba miejsc dziesiętnych. Ponieważ dla liczb typu double mamy w Javie 53 bity precyzji, czyli ok. 16 miejsc dziesiętnych, to okaże się że np. taki fragment:
   double licz2 = 0.1111111111111111,
          licz3 = 1111 + licz2,
          licz4 = licz3 - 1111;

   System.out.println(licz4 - licz2);

wyprowadzi wartość różną od zera (bowiem przy dodawania 1111 i licz2 tracimy precyzję: liczbę która ma 20 znaczących cyfr dziesiętnych możemy przedstawić tylko na 16 miejscach dziesiętnych).

Zatem porównywanie wyników operacji na liczbach rzeczywistych może być tylko przybliżone, wykonywane z pewną dokładnością. Jeżeli mamy dwie liczby rzeczywiste d1 i d2, to ich porównanie powinniśmy wykonywać za pomocą następującej konstrukcji:

if (Math.abs(d1-d2)) <= epsilon) // liczby równe
else // liczby nierówne

gdzie: epsilon - zadana dokładność porównania.

Jeśli natomiast chcemy wykonywać operacje matematyczne z pełną dokładnością i w dowolnej precyzji, należy zastosować klasę BigDecimal, która pozwala na posługiwanie się dowolnymi liczbami z dowolną dokładnością (ograniczeniem są tylko rozmiary pamięci operacyjnej).
Oto przykład:
import java.math.*;

public class BigDec {

  public static void main(String[] args) {
    BigDecimal liczbaA = new BigDecimal("3");
    BigDecimal liczbaB = new BigDecimal("0.2");
    BigDecimal wynik  = liczbaA.multiply(liczbaB);
    if (!wynik.equals(new BigDecimal("0.6")))
       System.out.println("Nie równe.");
    else System.out.println("Równe");

    BigDecimal licz2 = new BigDecimal("0.111111111111111111111111111111111"),
               licz1111 = new BigDecimal("1111"),
               licz3 = licz1111.add(licz2),
               licz4 = licz3.subtract(licz1111);

    System.out.println(licz3);
    System.out.println(licz4);
    System.out.println(licz4.subtract(licz2).toPlainString());

  }

}
Wyniki działania tego program - w przeciwieńswie do podobnych operacji na liczbach typu double - będą zgodne z regułami matematycznymi. Zauważmy też, że możemy posługiwać się dowolną liczbą cyfr znaczących (tu  liczba reprezentowana przez zmienną licz3 ma ich 37):

Równe
1111.111111111111111111111111111111111
0.111111111111111111111111111111111
0.000000000000000000000000000000000


W klasie BigDecimal dostępne są także operacje skalowania i zaokrąglania liczb.

Do prowadzenia dokładnych obliczeń (np. finansowych) nalezy uzywać klasy BigDecimal.

Działając na liczbach rzeczywistych (typu double lub float) należy też pamiętać, ze  w komputerze nie jest możliwa reprezentacja wszystkich liczb rzeczywistych.
W pobliżu liczby 1 jest przecież nieskończenie wiele liczb rzeczywistych, ale komputerowa reprezentacja widzi tylko niektóre z nich, bowiem są one przechowywane w skończonej liczbie bitów.
Wartość tzw. ULP (Unit in the last place) pokazuje odległość pomiędzy dwoma sąsiednimi liczbami  - mówiąc dokładniej: ULP(x) to odległośc między dwoma najbliższymi x liczbami.
Za pomocą metody ulp(x) z klasy Math możemy dowiedzieć się ile wynosi ta odległosć, a metoda nextUp(x) zwraca najbliższą mozliwą do uzyskania liczbę rzeczywistą więksża od x.
Przykładowy program pokazuje ulp dla wartości 1 oraz nastepną (po 1) możliwą do uzyskania liczbę rzeczywistą:
import static java.lang.Math.*;
public class Ulp1 {

  public static void main(String[] args) {
    double d1 = 1;
    System.out.println("Dla wartości:" + d1);
    System.out.println("ULP: " + ulp(d1));
    System.out.println("Następna możliwa liczba: " + nextUp(d1));
  }

}
Wynik:
Dla wartości: 1.0
ULP: 2.220446049250313E-16
Następna możliwa liczba: 1.0000000000000002

Ma to ważne konsekwencje, bowiem dodawanie do x wartości mniejszej od ulp(x) nie będzie miało żadnego efektu.
Weźmy np. dwie zmienne d1 i d2, zainicjujmy obie wartością 1e16 (10 do potęgi 16), po czym milion razy zwiększmy d2 o jeden. Jaka jest różnica między d2 i d1? Intuicyjnie - milion. Niestety, ulp dla liczby 1e16  wynosi 2, wobec tego dodawanie jedynki (nawet miliony razy) nie ma żadnego efektu. zatem taki programik:
  public static void main(String[] args) {
    double d1 = 1e16,
           d2 = d1;
    for(int i = 1; i<=1000000; i++) d2++;
    System.out.println("Różnica: " + (d2 - d1));
  }
da w wyniku:
Różnica: 0.0


4. Daty i czas

Informacje o datach i czasie są w Javie reprezentowane przez obiekty klasy Calendar.

Informacje o bieżącej dacie i czasie możemy uzyskać m.in. za pomocą odwołania:

        Calendar c = Calendar.getInstance();

które zwraca obiekt - domyślny kalendarz dla domyślnej lokalizacji ustawiony na bieżącą datę i czas w strefie czasowej właściwej dla domyślnej lokalizacji.


Informacje o dacie i czasie są zapisane w polach obiektu-kalendarza. Dostęp do tych pól uzyskujemy za pomocą metody get(...) , użytej na rzecz obiektu-kalendarza, z argumentem - stałą statyczną klasy Calendar, okreslającą o jaki rodzaj informacji nam chodzi. 
import java.util.*;
import static java.util.Calendar.*;

public class Kal1 {

  public static void say(String s) { System.out.println(s); }

  public static void main(String[] args) {

    // uzyskanie kalendarza domyślnego
    // (obowiązującgo dla domyślnej lokalizacji - tu dla Polski)
    // ustawionego na bieżącą datę i czas

    Calendar cal = Calendar.getInstance();

    say("ERA.............. " + cal.get(ERA) +
        " (tu: 0=pne, 1=AD)");

    say("ROK.............. " + cal.get(YEAR));
    say("MIESIĄC.......... " + cal.get(MONTH) +
        " (0-styczeń, 2-luty, ..., 11-grudzień)");

    say("LICZBA DNI\n" +
        "W MIESIĄCU....... " + cal.getActualMaximum(DAY_OF_MONTH));

    say("DZIEŃ MIESIĄCA... " + cal.get(DAY_OF_MONTH));
    say("DZIEŃ MIESIĄCA... " + cal.get(DATE));
    say("TYDZIEŃ ROKU..... " + cal.get(WEEK_OF_YEAR));
    say("TYDZIEŃ MIESIĄCA. " + cal.get(WEEK_OF_MONTH));
    say("DZIEŃ W ROKU..... " + cal.get(DAY_OF_YEAR));

    say("PIERWSZY DZIEŃ\n" +
        "TYGODNIA......... " + cal.getFirstDayOfWeek() +
        " (1-niedziela, 2-poniedziałek, ..., 7 sobota)");

    say("DZIEŃ TYGODNIA... " + cal.get(DAY_OF_WEEK) +
        " (1-niedziela, 2-poniedziałek, ..., 7-sobota)");

    say("GODZINA.......... " + cal.get(HOUR) +
        " (12 godzinna skala; następne odwolanie czy AM czy PM)");

    say("AM/PM............ " + cal.get(AM_PM) +
        " (AM=0, PM=1)");

    say("GODZINA.......... " + cal.get(HOUR_OF_DAY) +
        " (24 godzinna skala)");

    say("MINUTA........... " + cal.get(MINUTE));
    say("SEKUNDA.........  " + cal.get(SECOND));
    say("MILISEKUNDA:      " + cal.get(MILLISECOND));

    int msh = 3600*1000; // liczba milisekund w godzinie

    say("RÓŻNICA CZASU\n" +
        "WOBEC GMT........ " + cal.get(ZONE_OFFSET)/msh);

    say("PRZESUNIĘCIE\n" +
        "CZASU............ " + cal.get(DST_OFFSET)/msh +
        " (w Polsce obowiązuje w lecie)");

  }

}
Uwaga: warto zauwazyć jak bardzo przydał się tu statyczny import.

Na wydruku pokazano wyniki działania programu:
ERA.............. 1 (tu: 0=pne, 1=AD)
ROK.............. 2008
MIESIĄC.......... 7 (0-styczeń, 2-luty, ..., 11-grudzień)
LICZBA DNI
W MIESIĄCU....... 31
DZIEŃ MIESIĄCA... 14
DZIEŃ MIESIĄCA... 14
TYDZIEŃ ROKU..... 33
TYDZIEŃ MIESIĄCA. 2
DZIEŃ W ROKU..... 227
PIERWSZY DZIEŃ
TYGODNIA......... 2 (1-niedziela, 2-poniedziałek, ..., 7 sobota)
DZIEŃ TYGODNIA... 5 (1-niedziela, 2-poniedziałek, ..., 7-sobota)
GODZINA.......... 5 (12 godzinna skala; następne odwolanie czy AM czy PM)
AM/PM............ 0 (AM=0, PM=1)
GODZINA.......... 5 (24 godzinna skala)
MINUTA........... 38
SEKUNDA.........  12
MILISEKUNDA:      140
RÓŻNICA CZASU
WOBEC GMT........ 1
PRZESUNIĘCIE
CZASU............ 1 (w Polsce obowiązuje w lecie)
Uwaga: należy zwrócić baczną uwagę na to, że indeksowanie miesięcy rozpoczyna się od 0, a nie od 1 (czyli styczeń ma numer 0). 

Za pomocą metod set... kalendarza możemy ustawiać jego bieżącą datę i czas.
Np. aby ustawić kalendarz na 7 maja 2003 roku na tę samą godzinę co "teraz" możemy napisać:

    Calendar c = Calendar.getInstance();
    c.set(2003, 4, 7);  // rok 2003, indeks miesiąca = 4 (maj), dzień 7

a jeśli chcemy zarazem ustalić godzinę 18 minut 05 napiszemy:

    c.set(2003, 4, 7, 18, 5);

Możemy też zmieniać (ustawiać) wartości poszczególnych pól.
Służą do tego metody, które wykonują operacje na datach.

Operacje na datach wykonujemy za pomocą następujących metod:

        set(id_pola,  wartość)
        add(id_pola, wartość)
        roll(id_pola, wartość)

    gdzie:
        id_pola - stała statyczna z klasy Calendar, określająca pole na którym wykonywana jest oparacja,
        wartość - nowa wartość pola.


Wszystkie w/w operacje uwzględniają reguły danego kalendarza, a różnica pomiędzy nimi jest następująca:
Dokładne reguły obliczeniowe są podane w dokumentacji klasy Calendar.
Przykładowy program na wydruku testuje działanie omówionych metod.
import java.util.*;
import javax.swing.*;

public class TestKal {

  public static void main(String[] args) {
    String in;
    int d = 0;
    while ((in = JOptionPane.showInputDialog("DATE:")) != null) {
      d = Integer.parseInt(in);
      show("set",  "DATE", Calendar.DATE, d);
      show("add",  "DATE", Calendar.DATE, d);
      show("roll", "DATE", Calendar.DATE, d);
    }
    System.exit(0);
  }

  static void say(String s) { System.out.println(s); }


  static void show(String oper, String what, int field, int value) {
    Calendar c = Calendar.getInstance();
    say("Teraz jest: " + c.getTime());
    say("Operacja: " + oper + "(Calendar." + what + ", " + value + ")");
    if (oper.equals("set")) c.set(field, value);
    else if (oper.equals("add")) c.add(field, value);
         else if (oper.equals("roll")) c.roll(field,value);

    say("Aktualne ustawienia kalendarza: " + c.get(Calendar.YEAR) + '-'
                               + (c.get(Calendar.MONTH) + 1) + '-' +
                               + c.get(Calendar.DATE));
     say("-----------------------------------------------------------");
  }

}
Wynik:
Teraz jest: Thu Aug 14 05:48:58 CEST 2008
Operacja: set(Calendar.DATE, 20)
Aktualne ustawienia kalendarza: 2008-8-20
-----------------------------------------------------------
Teraz jest: Thu Aug 14 05:48:58 CEST 2008
Operacja: add(Calendar.DATE, 20)
Aktualne ustawienia kalendarza: 2008-9-3
-----------------------------------------------------------
Teraz jest: Thu Aug 14 05:48:58 CEST 2008
Operacja: roll(Calendar.DATE, 20)
Aktualne ustawienia kalendarza: 2008-8-3
-----------------------------------------------------------
Uwaga: CEST oznacza Central European Summer Time

Zwróćmy uwagę na metodę getTime(). Wykorzystano ją w omawianym programie, by łatwo wypisać datę i czas w jakiejś ludzkiej postaci,
Metody zdezaktualizowane (deprecated) są utrzymywane w standardowych pakietach Javy ze względu na aplikacje, które zostały kiedyś napisane i odwołują sie do tych metod. Pisząc nowe programy nie nalezy używac takich metod, nie tylko dlatego, że istnieją inne, nowsze i lepsze metody, ale również dlatego, że użycie metod zdezaktualizowanych w niektórych okolicnościach może prowadzić do błędnego działania aplikacji.
Metoda ta zwraca obiekt klasy Date, reprezentujący datę i czas dla danego ustawienia kalendarza. Obiekty klasy Date zawierają czas w postaci liczby milisekund, które upłynęły od 1 stycznia 1970 r. (data zwana "początkiem epoki", ponieważ jest to data premiery Unixa). Klasa ta w dawnych wersjach Javy była stosowana do działania na datach i czasie, ale ponieważ nie uwzględniała on różnych ustawień regionalnych (lokalizacji) i istnienia różnych kalendarzy, to w tej chwili większość jej metod  jest zdezaktualizowana i zamiast nich należy stosować metody klasy Calendar.
Wewnętrznie klasa kalendarza prowadzi równoległą podwójną reprezentację daty i czasu - za pomocą omówionych wcześniej pól oraz za pomocą  liczby milisekund od początku epoki  (wartość typu long, zwracana przez metoda getTimeInMillis()). Wartość milisekund można pobierać  za pomoca metody getTimeInMiiss() i ustawiać za pomocą metody setTimeInMillis(long).
Za pomoca pomiaru milisekund można porównywac daty, ale również można wykorzystać metodę
c1.compareTo(Calendar c2) klasy Calendar, zwracającą -1, gdy data ustawiona w c1 jest wcześniejsza od określanej przez c2, 0 - gdy daty są takie same i 1 - gdy data w c1 jest późniejsza niż w c2.

Do szybszego stwierdzania następstwa dat można wykorzystać metody before(...) i after(...).



5. Formatowanie liczb i dat

Wyprowadzając liczby rzeczywiste i daty w postaci napisów zetkniemy się z problemem formatowania.
Poniższy program:
import java.util.*;

public class BrakFormat {

  public static void main(String[] args) {
    double cena = 1.52;
    double ilosc = 3;
    double koszt = cena * ilosc;
    System.out.println("Koszt wynosi: " + koszt);
    Calendar c = Calendar.getInstance();
    System.out.println("Data: " + c.getTime());
  }

}
da w wyniku niezbyt sympatyczny wydruk:
Koszt wynosi: 4.5600000000000005
Data: Thu Aug 14 06:32:36 CEST 2008

A chcielibyśmy może uzyskać informację o koszcie z dokładnością do groszy (dwa miejsca po przecinku), a datę w jakimś sensownym, dopuszczalnym formacie.

Klasa java.utill.Formatter zapewnia możliwości formatowania danych.

Tworząc formator (za pomocą wywołania konstruktora) możemy określić:

 
Uwaga: formatory dla plików powinny być po użyciu zamykane lub wymiatane (close(), flush()), co powoduje zamknięcie lub wymiecenie buforów tych destynacji.

Formatowanie polega na wywołaniu jednej z dwóch wersji metody format (na rzecz fornatora):
        
 Formatter format(String format, Object... arg)
 Formatter format(Locale l, String format, Object... arg) 

Druga z tych metod pozwala na podanie lokalizacji, m.in. wpływającej na sposób formatowania liczb.

Łańcuch formatu (zmienna format) zawiera dowolne ciągi znaków oraz specjalne elementy formatujące. Dalej następują dane do "wstawienia" w łańcuch formatu w miejsce elementów formatu i do sformatowania podług zasad określanych przez te elementy (zmienna liczba argumentów dowolnego typu - formalnie Object). Dzięki autoboxingowi nie ma problemu z formatowaniem danych typów prostych.

Dla uproszczenia dostępne są:
Elementy formatu mają następującą ogólną postać:

%[arg_ind$][flagi][szerokość_pola][.precyzja]konwersja

gdzie:
Uwaga: nawiasy kwadratowe oznaczają opcjonalność.

Symboli konwersji jest b. dużo, dla różnych symboli mogą być stosowane też dodatkowe flagi.
Wszystko to jest opisane w sposób systematyczny w dokumentacji (proszę sięgać).
Tutaj przedstawione zostaną wybrane konwersje i flagi.

Wybrane konwersje - skrót
KonwersjaMoże być stosowana wobecWynik
s lub Sdowolnych danychJeżeli argument jest null - napis "null":
w przeciwym razie
   jeżeli klasa arg na to zezwala
     - wynik wywołania arg.formatTo(...)
   w przeciwnym razie wynik wywołania arg.toString()
Uwaga: użycie jako symbolu konwersji dużego S spowoduje zamianę liter napisu na duże.
c lub Ctypów reprezentujących znaki Unicodeznak Unicode
dtypów reprezentujących liczby całkowiteliczba całkowita (dziesiętna)
ffloat,  double, Float, Double, BigDecimalliczba rzeczywista z separatorem miejsc dzisiętnych
tHdanych reprezentujących czas, czyli:
long, Long, Calendar, Date
godzina na zegarze 24-godzinnym-2 cyfry (00-23)
tMminuty - 2 cyfry (00 - 59)
tSsekundy  - 2 cyfry (00-60)
tYrok - 4 cyfry (np. 2008)
tmmiesiąc - 2 cyfry (01-12)
tddzień miesiąca - 2 cyfry (01 -31)
tRczas na zegarze 24 godzinnym sformatowany jako  "%tH:%tM"
tTczas na zegarze 24 godzinnym sformatowany jako  "%tH:%tM:%tS"
tFdata sformatowana jako "%tY-%tm-%td"

Wśród flag na szczególną uwagę zasługują:
'-' - wynik wyrównany w polu do lewej (domyslnie jest wyrównany do prawej),
'+' - wynik zawiera zawsze znak (dla typów liczbowych),
' ' - wynik zawiera wiodąca spację dla argumentów nieujmenych (tylko dla typów liczbowych).

Zatem, aby uzyskać sformatowane wyniki w poprzednim przykładowym programie (liczbę z dwoma miejscami dziesiętnymi, datę w postaci rok-miesiąc-dzień) możemy napisać:
import java.util.*;

public class Format1 {

  public static void main(String[] args) {
    double cena = 1.52;
    double ilosc = 3;
    double koszt = cena * ilosc;
    System.out.printf("Koszt wynosi %.2f zł", koszt);
    System.out.printf("\nData: %tF", Calendar.getInstance());
  }

}
Wynik:
Koszt wynosi 4,56 zł
Data: 2008-08-14

Warto tu zwrócić uwagę na to, że dla lokalizacji polskiej liczba pokazywana jest z przecinkiem jako separatorem miejsc dziesiętnych.
Aby uzyskac kropkę można napisać:
System.out.printf(Locale.ROOT, "Koszt wynosi %.2f zł", koszt);
W tym przypadku stała statyczna Locale.ROOT oznacza neutralną lokalizację (bez wybranego kraju i języka).

Kilka innych przykładów pokazuje program na wydruku.
import javax.swing.*;

public class Format2 {

  public static void main(String[] args) {
    System.out.println("Wyrównany wydruk tablicy (po 2 elementy w wierszu)");
    int[] arr = { 1, 100, 200, 4000 };
    int k = 1;
    for (int i : arr) {
      System.out.printf("%5d", i);
      if (k%2 ==0) System.out.println();
      k++;
    }
    // Zastosowanie znaku < (elment formatu stosowany wobec argumentu z poprzedniego formatowania)
    System.out.println("Zaokraglenia");
    System.out.printf("%.3f %<.2f %<.1f", 1.256 );
    
    // Znak < szczególnie przydatny w datach/czasie
    Calendar c = Calendar.getInstance();
    c.set(Calendar.MONTH, 1);
    System.out.printf("\nW roku %tY i miesiącu %<tm mamy %d dni", c, c.getActualMaximum(Calendar.DATE) );
    
    // Oczywiście możemy formatować do Stringów
    String dateNow = String.format("%td-%<tm-%<tY", System.currentTimeMillis());
    JOptionPane.showMessageDialog(null, dateNow);
  }

}
Wynik działania programu (konsolę i okno dialogowe) pokazuje rysunek.
r



6. Proste sortowanie i wyszukiwanie

Co prawda o sortowaniu była już mowa i na pewno jest ono intuicyjnie zrozumiałe, to warto jednak trochę uporządkować pojęcia, a przy okazji poćwiczyć technikę programowania.

Wielokrotnie zetkniemy się z potrzebą uporządkowania informacji, ułożenia danych w określonej kolejności. W uporządkowanych zestawach informacji łatwiej jest odnajdywać dane, zresztą samo uporządkowanie może być niezbędną cechą jakiegoś zbioru danych. Najprostsze przykłady:
Ułożenie danych w określonej kolejności oznacza jakiś ich porządek. Zwykle przy takim układaniu będziemy posługiwać się tutaj rosnącymi (lub malejącymi) wartościami jakiejś cechy danych. Np. lista 10 zawodników, którzy zdobyli największą liczbę punktów powinna być uporządkowana wedlug liczby punktów - od największej do najmniejszej (wtedy będziemy mogli stwierdzić kto wygrał zawody i  jakie miejsca zajęli poszczególni zawodnicy)
Samo porządkowanie zbioru danych nazywa się sortowaniem. Będziemy mówić więc o sortowaniu w porządku rosnącym lub malejącym.

Istnieje wiele algorytmów sortowania. Tutaj przedstawiony zostanie jeden z najprostszych (ale za to mało efektywny - czyli wolny) zwany po angielsku selection sort (co możemy przetłumaczyć jako "sortowanie przez wybór").

Załóżmy, że mamy tablicę 5 liczb całkowitych i chcemy je ułożyć w porządku niemalejącym (czyli w kolejności wzrostu liczb - od najmniejszej do największej - z uwzględnieniem tego, że niektóre liczby w tablicy mogą być takie same i wtedy znajdą się na kolejnych pozycjach, mimo, że nie występuje ich wzrost).

Przykładowo możemy mieć taką tablicę.

int[] a = {  52, 3, 33, 56, 14 };

Na ostatniej pozycji w tablicy powinna znaleźć się największa liczba. Możemy ją znaleźć przeglądając tablicę od początku do ostatniego elementu tablicy i określając na której pozycji znajduje się wielkość maksymalna. Aha, maksymalną liczbą jest 56 i znajduje się na pozycji 4 (indeks 3). Powinna znaleźć się na ostatniej pozycji (pozycja 5, indeks 4), Ale tam stoi liczba 14. Gdybyśmy po prostu zapisali: a[4] = a[3] - stracilibyśmy liczbę 14. Musimy tę liczbę gdzieś zapisać. Musi ona być uwzględniona przy dalszym porządkowaniu tablicy, zatem nie może być zapisana gdzieś poza tablicą. Jeśli liczbę 56 zapiszemy z pozycji 4 na pozycję 5, to pozycja 4 będzie wolna i tam właśnie możemy umieścić liczbę 14. Oznacza to, że musimy przestawić elementy tablicy np. tak:
int temp = a[3];
a[3] = a[4];
a[4] = temp;

W tej chwili nasza tablica będzie wyglądać tak:

52 3 33 14 56

i kawałek tej tablicy jest już uporządkowany (ostatni element zawiera właściwą, największą, liczbę). Teraz poszukamy kolejnego największego elementu. Szukamy od początku tablicy, ale nie musimy już brać pod uwagę ostatniego elementu (bo już jest właściwie umiejscowiony i na pewno nie będziemy go przestawiać). Tym razem przeszukiwanie tablicy zakończymy na przedostatnim elemencie. Znaleziony element maksymalny to 52 i ma indeks 0. Powinien znaleźć się w tablicy jako jej przedostatni element. Zatem znowu dokonujemy przestawienia (14 <-> 52) i otrzymujemy tablicę:

14 3 33 52 56

W tej tablicy we własciwym porządku są już dwa ostatnie elementy, zatem dalsze kroki ograniczamy do trzech pierwszych elementów: znowu wyszukujemy największy. Tym razem okazuje się, że jest to 33 i że już znajduje się na właściwej pozycji. Uporządkowana część tablicy zwiększyła się do trzech elementów. Pozostało zobaczyć, który z pierwszych dwóch elementów jest większy i ewentualnie je przestawić. Zamieniamy miejscami 14 i 3. Tablica jest posortowana.

Kolejne kroki  ilustruje poniższy rysunek na którym kolorem żółtym zaznaczono nieposortowaną część tablicy, kolorem niebieskim - już posortowaną, a niebieskim kółkiem - maksymalny element odszukiwany w każdym kroku w nieposortowanej części tablicy.

r



Możliwy sposób oprogramowania algorytmu sortowania przez wybór przedstawia wydruk:

  public void selectionSort(int[] a) {

    // toInd - oznacza ostatni indeks nieposortowanej części tablicy
    // na początku jest to ostatni indeks tablicy
    // w kolejnych krokach toInd będzie zmniejszany o 1
    // bo zmniejszają się rozmiary nieposortowanej częsci tablicy

    // Gdy toInd osiągnie wartość 0 - "nieposortowany" będzie pierwszy
    // element. Ale nie mamy go już gdzie przstawić, faktycznie znajduje się
    // on na własciwym miejscu.
    // Zatem nie musimy dokonywać żadnego przestawienia.
    // Tablica jest posortowana. Kończymy pętle.

    for (int toInd=a.length-1; toInd>0; toInd--) {

      int indMax = 0;   // indeks maksymalnego elementu
                        // w nieposortowanej części tablicy

      // szukamy tego indeksu, przeglądając nieposortowaną część tablicy
      for (int k=1; k <= toInd; k++)
 	if (a[indMax] < a[k]) indMax = k;

      // Przestawiamy elementy:
      // maksymalny element idzie na ostatnią pozycję w nieposortowanej
      // części tablicy; a liczba spod tego indeksu jest zapisywana
      // w miejscu okupowanym poprzednio przez max element

      int temp = a[toInd];
      a[toInd] = a[indMax];
      a[indMax] = temp;
    }
  }

Do przetestowania napisanej metody możemy użyć następującej klasy:

import java.util.*;

public class SelSort {

  public SelSort(int n, int m) {

    Random rand = new Random();

    int[] a = new int[n];
    for (int i=0; i < n; i++) {
      a[i] = rand.nextInt(m+1);
      System.out.print(" " + a[i]);
    }
    selectionSort(a);
    System.out.print('\n');
    for (int i=0; i < n; i++) System.out.print(" " + a[i]);
  }

  public void selectionSort(int[] a) {
    // ...
  }


  public static void main(String[] args) {
     new SelSort(Integer.parseInt(args[0]), Integer.parseInt(args[1]));
   }

}
Jako argumenty wywołania podajemy rozmiar tablicy oraz maksymalną liczbę, jaka może znaleźć się w tablicy (powiedzmy max). Wartości elementów tablicy zostaną utworzone przez generator liczb pseudolosowych (0 <= a[i] <= max). Następnie wywołana zostanie metoda selectionSort.
Wydruk programu pokazuje tablicę nieposortowaną oraz posortowaną np.

 5 72 58 39 75 18 92 79 35 63
 5 18 35 39 58 63 72 75 79 92


Algorytm sortowania przez wybór należy do najwolniejszych algorytmów sortowania.
Jest wiele innych, lepszych algorytmow.
W praktycznym programowiu raczej rzadko pisze się algorytmy sortowania - korzysta się zwykle z gotowego oprogramownia. Również Java dostarcza  oprogramowanych algorytmów w postaci gotowych metod. Widzieliśmy już w dzialaniu metodę sortowania list z klasy Collections..
W pakiecie java.util istnieje klasa Arrays, w której zdefiniowano metody sort , umożliwiające sortowanie m.in. tablic liczb całkowitych i rzeczywistych. Dla ciekawości możemy sprawdzić jaka jest różnica czasowa zastosowania algorytmu selection sort oraz tego, który poslużył do sformułowania metod sort w klasie Arrays (jest to zmodyfikowany algorytm quicksort).

import java.util.*;

class QTimer {

   private final long start;

   public QTimer() {
     start = System.currentTimeMillis();
   }

   public long getElapsed() {
      return System.currentTimeMillis() - start;
   }
}


public class SelSort1 {

static public void selectionSort(int[] a) {
// ... jak na poprzednim wydruku
}


  public static void main(String[] args) {
    int n = Integer.parseInt(JOptionPane.showInputDialog("Liczba elementów w tablicy"));
    Random rand = new Random();
    int[] a = new int[n];
    int[] b = new int[n];
    for (int i = 0; i < n; i++) {
      a[i] = rand.nextInt(n * 10);
      b[i] = a[i];
    }
    System.out.println("Liczba elementów tablicy " + n);
    QTimer qt = new QTimer();
    selectionSort(a);
    System.out.println("Czas selection sort: " + qt.getElapsed());
    qt = new QTimer();
    Arrays.sort(b);
    System.out.println("Czas quicksort: " + qt.getElapsed());
  }
}
Przykładowo, dla losowo wygenerowanych tablic 100000 liczb całkowitych moglibyśmy uzyskać następujący wynik:
Liczba elementów tablicy 100000
Czas selection sort: 17359
Czas quicksort: 31

Widzimy więc, że dobry algorytm sortowania może być - przy dużej liczbie elementów tablicy - nawet  kilkaset razy szybszy niż bardzo proste, ale wolne sortowanie przez wybór.

A jak posortować napisy?
Oczywiście - powinniśmy skorzystać z metody compareTo z klasy String.
public class SortString {

  static public void selectionSort(String[] s) {
    for (int toInd=s.length-1; toInd>0; toInd--) {
      int indMax = 0;
      for (int k=1; k <= toInd; k++)
 	if (s[indMax].compareTo(s[k]) < 0) indMax = k;
      String temp = s[toInd];
      s[toInd] = s[indMax];
      s[indMax] = temp;
    }
  }


  public static void show(String[] s) {
    System.out.print('\n');
    for (int i=0; i < s.length; i++) System.out.print(" " + s[i]);
  }


  public static void main(String[] args) {
    String[] s =  {"A", "Z", "C", "B", "1", "3", "2", "A", "C" };
    show(s);
    selectionSort(s);
    show(s);
  }

}
Wydruk programu:

 A Z C B 1 3 2 A C
 1 2 3 A A B C C Z

W klasie Arrays znajdziemy również metodę sort(...), która za argument ma tablicę dowolnych obiektów. Jesli klasa tych obiektów odpowiednio definiuje metodę compareTo, to możemy z powodzeniem użyć tej metody (taka sytuacja dotyczy np. klasy String oraz klas "opakowujących" typy pierwotne: Integer, Double.itp. a także klasy Date, której obiekty reprezentują daty).
Jeśli klasa nie dostarcza odpowiedniej metody compareTo, to trzeba zastosować nieco bardziej zaawansowane podejście, o którym będzie mowa kiedy indziej.

Często też w programowaniu będziemy stykać się z problemem odnalezienia konkretnego elementu tablicy lub kolekcji.
W najprostszy sposób możemy to zrobić przeglądając po kolei wszystkie elementy tablicy, dopóki nie natrafiimy na poszukiwaną wartość.
Np. odnaleźć w tablicy liczb całkowitych  podaną liczbę, a następnie - jeśli występuje - zwrócić jej indeks, a jeśli takiej liczby w tablicy nie ma - zwrócić -1.

public class Search {

  public static int linearSearch(int[] tab, int v) {
    for (int i=0; i < tab.length; i++)
      if (tab[i] == v) return i;
    return -1;
  }

}

Taki sposób wyszukiwania nazywany jest wyszukiwaniem liniowym.

Wyszukiwanie liniowe polega na przeglądaniu kolejnych elementów tablicy i porównywaniu ich  z poszukiwaną wartością


Wyszukiwanie liniowe może okazać się  bardzo wolne. Jeśli np. szukany element znajduje się w tablicy na ostatniej pozycji, to liczba wykonanych iteracji i porównań będzie równa liczbie elementów tablicy, co przy bardzo dużych tablicach prowadzi do długiego dzialania programu.

Istnieje  nieco lepszy sposób przeszukiwania tablic nazywany wyszukiwaniem binarnym.
Rozważmy przykład.
Mamy posortowaną w porządku niemalejącym tablicę liczb całkowitych.
2 3 4 5 7 11 14 20

Będziemy szukać w tej tablicy liczby 14.
Podzielmy tablicę na dwie połowy. Ostatni element w lewej połowie = 5, jest mniejszy od szukanej liczby. Oznacza to, że (ponieważ tablica jest posortowana) szukana liczba znajduje się w prawej połowie tablicy. Podzielmy tę połowę znowu na dwie.  Ostatni element w "nowej" lewej połowie to 11 - znowu mniejszy od 14. Zatem szukana liczba znajduje się w "nowej" prawej połowie. Dzielimy ją znowu na dwie (w tym przypadku w każdej nowej połówce znajduje się jedna liczba) i odkrywamy że nowa "jednoliczbowa" lewa połówka zawiera poszukiwaną wartość
Ilsutruje to poniższy rysunek:

r

Gdybyśmy natomiast szukali liczby 4, to sekwencja kroków byłaby następująca:

r

Zwróćmy uwagę, że w każdym kroku binarnego wyszukiwania porównujemy poszukiwaną wartość z  wartością elementu znajdującego się na pozycji "dzielącej" tablicę na kolejne połówki (element ten na rysunkach zaznaczany jest na żółto, pokazano też jego indeks).
Można powiedzieć, że zawsze jest to "środkowy" element kolejnych podziałów.
Jeśli w tablicy znajduje się szukana wartość, to w ostatnim kroku binarnego wyszukiwania ten "środkowy" element będzie jej równy. Jeśli nie, jeśli jej nie ma - to okaże się, że nie można już więcej dzielić tablicy na kolejne połówki; stwierdzamy więc, że tablica nie zawiera poszukiwanej wartości.

Zatem możemy zdefiniowac następującą metodę binarnego wyszukiwania:

public class Search {

  // v - poszukiwana wartość w tablicy tab
  static public int binarySearch(int[] tab, int v)   {
    int low = 0;                // lewy skrajny indeks "aktualnej" połówki
    int high = tab.length - 1;  // prawy skrajny indeks "aktualnej" połowki

    // dopóki możemy dzielić tablicę
    while (low <= high) {
      int mid = (low + high) / 2;  // indeks środkowego elementu
                                   // zakresu low..high

      if (v  < tab[mid])         // jeżeli wartość jest w lewej połówce:
        high = mid - 1;          // zmodyfikować skrajny prawy indeks
      else if (v > tab[mid])     // w przeciwnym razie jeżeli w prawej połówce:
              low = mid + 1;     // zmodyfikować skrajny lewy indeks
           else return mid;      // w przeciwnym razie: zanleziony!
    }
    return -1;                    // w tablicy nie znaleziono wartości v
  }

Binarne wyszukiwanie jest szybkim algorytmem odnajdywania informacji w posortowanych tablicach. W każdym jego kroku zakres rozpatrywanych elementów tablicy zmniejsza się o połowę


Należy wyraźnie podkreślić, że binarne wyszukiwanie wymaga posortowanych danych. Często jednak opłaca się najpierw posortować dane, by później móc szybko odnajdywać wśród nich potrzebną informację.

W klasie Arrays znajdziemy statyczną metodę binarySearch(...), która pozwala na binarne przeszukiwanie tablic i zwalnia nas od pisania kodu. Podobnie jednak jak w przypadku metody sortującej jej zastosowanie dla tablic obiektów obarczone jest dodatkowymi warunkami (odpowiednie zdefiniowanie metody compareTo w klasie obiektów lub zastosowanie specjalnego obiektu, zwanego komparatorem - o tym wszystkim będziemy mówić w rozdziale o kolekcjach).


7. Rekurencja

Z ciała (kodu) metody  możemy wywołać ją samą. Takie wywołanie nazywa się wywołaniem rekurencyjnym.

Rekurencyjne wywołanie metody polega na wywołaniu tej metody z jej własnego ciała


Rozpatrzmy najprostsze przyklady.

public class Recurs {

  public static void show1(int i) {
    System.out.println("show1 " + i);
    if (i > 10) return;
    show1(i+1);
  }

  public static void main(String[] args) {
    show1(1);
 }

}
show1 1
show1 2
show1 3
show1 4
show1 5
show1 6
show1 7
show1 8
show1 9
show1 10
show1 11
 Tutaj sprawa jest dość prosta i do przewidzenia. Wywołanie show1(1) z metody main uruchamia łańcuch wywołań rekurencyjnych. Każde wywolanie wyprowadza przekazany argument, po czym sprawdzany jest warunek (i>10), i dopóki  jest on nieprawdziwy ponownie wywoływana jest metoda show1 z powiększoną o 1 wartością argumentu. Np. po wypisaniu 1 (pierwsze wywołanie) wywoływana jest metoda show1 z argumentem 2 = 1+1, ten argument jest wypisywany, wywoływana jest metoda show1 z argumenten 3 (= 2+1),  ten jest wypisywany itd. aż w "wewnętrznym wywołaniu" show1 argument nie osiągnie wartości 11. Wtedy - po jej wypisaniu - warunek okaże się prawdziwy i dopiero teraz sterowanie zwrócone zostanie do metody main.

Przy wywolaniach rekurencyjnych należy zapewnić warunek, którego spełnienie zakończy łańcuch rekurencji i spowoduje zwrócenie sterowania do miejsca, w którym po raz pierwszy wywołano metodę rekurencyjną


Zobaczmy drugi fragment kodu. Tym razem wywołamy z metody main - za pomocą odwołania show2(1) - następującą metodę.

  public static void show2(int i) {
    if (i > 10) return;
    show2(i+1);
    System.out.println("show2 " + i);
  }

Wynik dzialania tej metody może wydac się nieco zaskakujący.

show2 10
show2 9
show2 8
show2 7
show2 6
show2 5
show2 4
show2 3
show2 2
show2 1
Co się dzieje? Wywołanie show2(1) przekazuje jako argument 1, i z wnętrza show2 następuje wywołanie show2 z argumentem 2 (1+1). W tym momecie wykonanie dalszego kodu metody zostaje wstrzymane. Wykonanie zaczyna się od początku! Zaczyna działać jakby "nowy egzemplatrza" metody show2. Tym razem z nowym argumentem (2). I tak dalej. Gdy parametr i osiągnie wartość 11 powinna zadziałać instrukcja return. Ale najpierw muszą być "dokończone" wszystkie poprzednie, "wstrzymane",  wykonania metody show2. Ostatnie było z argumentem 10. Zatem wyprowadzona zostanie liczba 10 i wykonanie "tego egzemplarza" metody zostanie zakończone. Poprzedzało go wywołanie show2 z argumentem 9 - zostanie więc dokończone itd., aż dojdziemy do pierwszego wywołania show2 (z argumentem 1). Po zakończeniu wykonania metody z tym argumentem zostanie wykonana instrukcja return i sterowanie wróci do main.

Zauważmy, że w pierwszym przypadku mieliśmy tak naprawdę do czynienia z takim samym wstrzymywaniem wykonania kodu metody show1 inicjowanego przez kolejne wywołania rekurencyjne, tyle, że nie mogliśmy tego dostrzec, ponieważ wstrzymywanie następowało na ostatniej instrukcji metody, już po wyprowadzeniu liczby.

Oczywiście metody rekurencyjne mogą zwracać wartości.
Zobaczmy, jak np. można rekurencyjnie zapisać zadanie sumowania dodatnich liczb całkowitych.
W istocie rekurencja oznacza "zdefiniowanie czegoś przez to samo coś".
Weźmy sumę 1 + 2 + 3 + 4 + 5.
Możemy powiedzieć tak:
suma(1..5) = 5 + suma(1..4)
suma(1..4) = 4 + suma(1..3)

Definiujemy sumę przez sumę!

Ogólnie, suma liczb od 1 do n  równa jest n + suma(1..n-1)
Zatem jeśli nasza metoda sumowania otrzymuje argument n (oznaczający, że mamy zsumować liczby od 1 do n), to moglibyśmy spróbować zapisać:

int sum(int n) {
 return n + sum(n-1);
}

Łatwo jednak zauważyć, że wpadamy tu w "nieskończoną" rekurencję. Metoda sum  będzie wywoływana teoretycznie bez końca ze swojego wnętrza (praktycznym ograniczeniem będzie pamięć komputera - program skończy działanie z komunkatem "Stack overflow").

Musimy zatem zapewnić jakiś warunek zakończenia wywołań rekurencyjnych,
Uwzględnić jakiś szczególny przypadek wartości przekazanego argumentu, który przerwie nieskończone rekurencyjne wywolania.
W przypadku sumowania liczb od 1 do n, takim szczególnym przypadkiem jest wartość n = 1 (zwracamy wtedy 1).

public class Recurs {

  public static int sum(int n) {
    if (n == 1) return 1;
    else return n + sum(n-1);
  }

  public static void main(String[] args) {
    System.out.println(sum(100));
  }

}
Wyprowadzi: 5050.

Warto dokładnie przeanalizować działanie tej metody np. dla n = 5 , pamiętając, że kolejne zwroty wyników rekurencyjnego wywołania sum(...) są wstrzymywane dopóki n nie osiągnie wartości 1 i zauważając, że odtwarzanie tych wyników następuje w else, po kolei: 1, 2 + 1, 3 + (2 + 1), 4 + (3 + 2 + 1), 5 + (4 + 3 + 2 +1) i ta ostatnia wartość jest właśnie zwracana do punktu wywolania sum(..) w metodzie main.

Można się domyślić (choćby z przykładu sumowania), że rekurencyjne wywołania funkcji można zastąpić pętlami iteracyjnymi.

Bardzo często rekurencja będzie jednak prostsza do oprogramowania, bowiem odzwierciedla ona bezpośrednio pewien sposób rozumowania: nie wiemy jak rozwiązać cały problem, na którego rozwiązanie  składa się powiedzmy n kroków, ale wiemy jak wykonać jeden krok, gdy już n-1 poprzednich zostało wykonane. I to właśnie możemy (dość prosto) zapisać w postaci rekurencyjnej.


Trzeba jednak  też wiedzieć o tym, że nie zawsze podejście rekurencyjne prowadzi do efektywnych algorytmów; czasami iteracyjne wersje rozwiązania jakichś problemów są wielokrotnie szybsze od rekurencyjnych, a nawet - przy ograniczeniach na pamięć operacyjną i moc procesora - jedynie możliwe. Sztandarowym przykładem jest tu rekursywne oprogramowanie wyliczenia liczb ciągu Fibonacciego, które szybko prowadzi do wyczerpania pamięci.

Ciąg Fibonacciego dany jest za pomocą następującego równania, okreslającego wartości Fn kolejnych liczb ciagu (dla n = 0, 1, 2, ...):

F0   =  0,
F1   =  1,
Fn  =   Fn-2  + Fn-1, dla n > 1.

Czyli jest to ciąg liczb zaczynający się od liczb 0 i 1, przy czym każda następna liczba ciągu (poczynając od trzeciej) jest sumą dwóch poprzednich liczb ciągu:
0  1  1  2  3  5  8 13 21 34 55 89 144 233 377 610 987 ...
Liczby Fibonacciego mają niezwykle ciekawe właściwości. Nader często ciągi takich liczb obserwowane są w zjawiskach naturalnych, mają też intrygujące właściwości matematyczne. Więcej na ten temat zobacz np. na stronie Rona Knotta z Uniwersytetu w Surrey .

Jak widać. ciąg Fibonanciego jest ciagiem rekurencyjnym, zatem wyliczenie jego kolejnych wyrazów w naturalny sposob można zapisać w postaci rekurencyjnej.

  int fib(int n) {
    if (n < 2) return n;
    else return fib(n-1) + fib(n-2);
  }
Jednak wraz ze zwiększaniem wartości n czas obliczeń za pomocą tej metody rośnie katastrofalnie.
Dzieje się tak dlatego, że katastrofalnie rośnie liczba rekurencyjnych wywołań metody fib.
Większą część czasu zajmuje obliczanie już policzonych wartości!
Zobaczmy.
Używając zmodyfikowanej postaci metody fib:
  int fib(int n) {
    System.out.println("Wywołanie fib z argumentem " + n);
    int wynik = 0;
    if (n < 2) wynik = n;
    else wynik = fib(n-1) + fib(n-2);
    System.out.println("Zwrot wyniku: " + wynik);
    return wynik;
  }
i analizując wydruk po wywołaniu tej metody z jakimś argumentem (np. 8) - zobaczymy, że wielokrotnie powtarzają się rekurencyjne wywołania metody fib z tymi samymi argumentami i wielokrotnie powtarzają się zwroty tych samych wyników.


Możemy też automatycznie policzyć liczbę wywołań z różnymi argumentami za pomocą np. takiego programu:
public class ShowFibRec {

  int[] calls;

  ShowFibRec(int n) {
    calls = new int[n+1];
    fib(n);
    for(int i=0; i <= n; i++)
      System.out.println("Liczba wywołań fib z argumentem " + i
                          + " " + calls[i]);
  }

  int fib(int n) {
    calls[n]++;
    if (n < 2) return n;
    else return fib(n-1) + fib(n-2);
  }

  public static void main(String[] args) {
    int n = Integer.parseInt(args[0]);
    new ShowFibRec(n);
  }

}
Po kompilacji, możemy porównać liczbę "powtórnych" wywołań dla różnych n podawanych jako argument wywołania, np. 8 i 20.
Uzyskamy następujący wyniki:
Dla n = 8
Dla n = 20
Liczba wywołań fib z argumentem 0 13
Liczba wywołań fib z argumentem 1 21
Liczba wywołań fib z argumentem 2 13
Liczba wywołań fib z argumentem 3 8
Liczba wywołań fib z argumentem 4 5
Liczba wywołań fib z argumentem 5 3
Liczba wywołań fib z argumentem 6 2
Liczba wywołań fib z argumentem 7 1
Liczba wywołań fib z argumentem 8 1

Liczba wywołań fib z argumentem 0 4181
Liczba wywołań fib z argumentem 1 6765
Liczba wywołań fib z argumentem 2 4181
Liczba wywołań fib z argumentem 3 2584
Liczba wywołań fib z argumentem 4 1597
Liczba wywołań fib z argumentem 5 987
Liczba wywołań fib z argumentem 6 610
Liczba wywołań fib z argumentem 7 377
Liczba wywołań fib z argumentem 8 233
Liczba wywołań fib z argumentem 9 144
Liczba wywołań fib z argumentem 10 89
Liczba wywołań fib z argumentem 11 55
Liczba wywołań fib z argumentem 12 34
Liczba wywołań fib z argumentem 13 21
Liczba wywołań fib z argumentem 14 13
Liczba wywołań fib z argumentem 15 8
Liczba wywołań fib z argumentem 16 5
Liczba wywołań fib z argumentem 17 3
Liczba wywołań fib z argumentem 18 2
Liczba wywołań fib z argumentem 19 1
Liczba wywołań fib z argumentem 20 1

Ze względu na konstrukcję metody rekurencyjnej, liczba wielokrotnych wywołań metody z tym samym argumentem i , dla i =1,2...n, jest liczbą Fibonacciego: F(n-i+1) ! Zatem przy dużych n bardzo dużo czasu tracone jest na powtarzanie tych samych wywołań i zwracanie tych samych wyników.

Zadanie do samodzielnego wykonania.
Napisać iteracyjną wersję metody obliczającej wyrazy ciągu Fibonacciego. Za pomocą znanej nam już klasy QTimer porównać czas obliczeń wersji rekursywnej i iteracyjnej.

8. Niezmienniki pętli iteracyjnych

W tym podpunkcie zajmiemy się krótko zagadnieniem, które jest istotne m.in. w dowodzeniu poprawności algorytmów. Ta problematyka jest szczegółowo i znacznie bardziej formalnie przedstawiona w ramach kursów związanych bezpośrednio z algorytmami i strukturami danych. Tutaj w sposób możliwie najmniej formalny zwrócona zostanie uwaga na pojęcie niezmienników pętli iteracyjnych i to raczej pod kątem lepszego rozumienia działania (czy nawet sprawniejszego formułowania) pętli iteracyjnych niż dowodzenia ich poprawności.

Rozpatrzymy najpierw pętlę while (jak już wiemy - pętle for łatwo sprowadzić do pętli while), po czym zobaczymy w jaki sposób, mozna bezpośrednio formułowac niezmienniki dla pętli for.

Ogólną postać pętli while można zapisać jako:

inicjacja_zmiennych_używanych_w_pętli
while (warunek_kontynuacji_pętli) {
    ciało_pętli
}

gdzie przez ciało_pętli rozumiemy instrukcje wykonywane w pętli


Niezmiennikiem pętli while nazywamy relację pomiędzy wartościami zmiennych programu, która jest prawdziwa tuż przed rozpoczęciem pętli, przed każdym wykonaniem i po każdym wykonaniu ciala pętli w kolejnych iteracjach  oraz zaraz po zakończeniu pętli iteracyjnej; przy czym przynajmniej niektóre z wartości zmiennych, uczestniczących w tej relacji, zmieniają się w trakcie wykonywania iteracji pętli.


Jeśli przez K oznaczymy wyrażenia-warunek kontynuacji pętli, a przez N - wyrażenie-niezmiennik pętli, to prawidłowa konstrukcja pętli powinna wyglądać następująco:


    // N jest prawdziwe
    while (K) {
        // N i K jest prawdziwe
        ciało pętli
        // N jest prawdziwe
     }
    // N jest prawdziwe i jednocześnie K nie jest prawdziwe

przy czym instrukcje ciała pętli powinny zapewniać (w którymś momencie) zanegowanie wyrażenia K (po to, by pętla mogła zakończyć działanie).


Uwaga: w pętli takiej może zdarzyć się, że od początku K nie jest prawdziwe (wtedy pętla nie wykona się ani razu, ale powinny zachodzić pierwszy i ostatni warunki podane w komentarzach).

Spójrzmy na przykład znajdowania minimalnego elementu tablicy n liczb całkowitych.

 static int getMin(int[] a) {
   int n = a.length;
   int vmin = a[0];
   int i = 1;
   while (i < n) {
     if (a[i] < vmin) vmin = a[i];
     i++;
   }
   return vmin;
 }

Zauważmy, że po zakończeniu pętli chcemy, by spełniony był warunek:
wartość zmiennej vmin jest wartością minimalnego elementu tablicy a czyli minimalną wartością spośród wartości zmiennych a[0], a[1], ... a[n-1].

Inaczej mówiąc wartością wyrażenia (zapisanego w pseudokodzie):

vmin == minimum (a[0], ... ,a[n-1])

winno być true.

Na tej podstawie możemy łatwo wydedukować niezmiennik (który - w tym przypadku - bezpośrednio pomaga nam zbudować algorytm działania pętli) .


Niezmiennik: dla  każdego i = 1, ... n :  vmin == minimum ( a[0], .., a[i-1] )
.

Miejsca w którym warunek ten powinien być spełniony pokazano poniżej za pomocą komentarza  N z dodatkową liczbą 0, 1, 2, 3 - rozróżniającą umiejscowienie warunku:

 static int getMin(int[] a) {
   int n = a.length;
   int vmin = a[0];
   int i = 1;
   // N0
   while (i < n) {
     // N1
     if (a[i] < vmin) vmin = a[i];
     i++;
     // N2
   }
   // N3
   return vmin;
 }

Niezmiennik ten wyraża określoną relację pomiędzy zmiennymi vmin, a oraz i . Zwróćmy uwagę, że niektóre z tych zmiennych (vmin oraz i) zmieniają swoje wartości  w trakcie iteracji, a jednak wartość wyrażenia stanowiącego niezmiennik powinna być w każdej iteracji taka sama (stąd nazwa niezmiennik!).
Jeżeli możemy teraz wykazać, że wybrany jako niezmiennik warunek jest prawdziwy we wszystkich pokazanych miejscach, pętla kończy działanie, a po jej zakończeniu  z niezmiennika możemy wyprowadzić zamierzony wynik działania pętli, to - nasza pętla jest poprawna (gwoli ścisłości: trzeba jeszcze zapewnić spełnienie jakichś warunków początkowych, w naszym przykładzie będzie to n > 0).

Faktycznie, przed wejściem w pętlę, i=1, vmin = a[0], zatem vmin jest minimum z (a[0]...a[i-1]), bo zbiór ten zawiera tylko jedną liczbę a[0]. Wobec tego w miejscu N0 niezmiennik jest prawdziwy. Jeżeli w miejscu N0 niezmiennik jest prawdziwy, to w pierwszej iteracji - w miejscu N1 również będzie prawdziwy (bo wyrażenie stanowiące warunek kontynuacji nie zmienia wartości i, vmin, oraz elementów tablicy a).
Dalej możemy łatwo wykazać że, jeżeli w jakiejkolwiek iteracji niezmiennik jest prawdziwy w miejscu N1, to będzie również prawdziwy w miejscu N2. Weźmy i-tą iterację. W miejscu N1 zachodzi  vmin == minimum (a[0], ... a[i-1]). Następnie wartość a[i] porównywana jest z wartością vmin, jeśli jest mniejsza, to vmin = a[i], w przeciwnym razie minimum się nie zmienia i w obu przypadkach w miejscu N2 (po zwiększeniu i o 1) znowu zachodzi  vmin == minimum(a[0],  ... . a[i-1]).
Jeśli niezmiennik jest prawdziwy w miejscu N2, to - przy kontynuacji pętli - jest również prawdziwy w miejscu N1. Natomiast gdy warunek kontynuacji nie jest spełniony - prawdziwość w N2 implikuje prawdziwość niezmiennika w N3.
Pętla na pewno skończy działanie, bowiem w każdej iteracji i jest zwiększane, aż osiągnie wartość n.
W miejscu N3 wyrażenie stanowiącc niezmiennik bezpośrednio określa poprawność zamierzonego wyniku działania pętli: otrzymaliśmy minimum ze wszystkich elementów tablicy.

Zobaczmy więc: niezmiennik dobraliśmy tak by "prowadził nas" do końcowego wyniku. Byłoby raczej bez sensu użyć tu jakiegoś innego niezmiennika.
Nie zawsze uda się osiągnąć tak bezpośredni efekt, ale na podstawie dobrze dobranego niezmiennika pętli można znacznie sobie ułatwić  stworzenie poprawnego algorytmu rozwiązania jakiegoś problemu.

No, dobrze, ale czy zawsze musimy przekształcać pętle for do pętli while - by poprowadzić rozumowanie za pomocą niezmienników?
Niekoniecznie. Bardzo często w pętlach for, w których mamy do czynienia z prostą zmianą licznika iteracji  możemy tego nie robić.
Ze względu na konstrukcję takich pętli  (przypomnijmy: inicjacja licznika wykonywana jest raz, warunek kontynuacji sprawdzany jest przed każdym wykonaniem iteracji, a wyrażenie zmieniające licznik wykonywane jest po wykonaniu instrukcji pętli) niezmiennik można "umieścić" na początku każdej iteracji oraz po pętli:

for (.....) {
    // Niezmiennik
    instrukcje pętli
}
// Niezmiennik
     

W naszym przypadku poszukiwania minimum z elementów tablicy moglibyśmy zapisać:

  static int getMin(int[] a) {
    int vmin = a[0];
    for (int i=1; i < a.length; i++) {
      // NIEZMIENNIK: vmin == minimum (a[0],.. a[i-1])
      if (a[i] < vmin) vmin = a[i];
    }
    // NIEZMIENNIK: vmin == minimum (a[0],.. a[i-1])
   return vmin;
     
  }

Łatwo zobaczyć, że niezmiennik jest prawdziwy w każdej iteracji oraz po zakończeniu pętli.




9. Podsumowanie

W tekście omówiono:
Kończące wykład kilka słów o niezmiennikach pętli iteracyjnych powinno wspomóc prawidłowe ich programowani i  konstruowanie poprawnych algorytmów.