« poprzedni punkt  następny punkt »

3. Opakowanie typów pierwotnych

Widzieliśmy już w praktyce, że znakowa reprezentacja liczb  (napisy  reprezentujące liczby) - to zupelnie co innego niż ich wewnętrzna, binarna postać.
Właśnie dlatego uporczywie musieliśmy używać metody parseInt z klasy Integer do przekształcania znakowej (napisowej) postaci liczb całkowitych na ich binarną wewnętrzną postać.
Powstaje pytanie: co to za klasa Integer? I co robić z innymi typami liczb: np. rzeczywistymi ?
Otóż bardzo często występuje potrzeba traktowania liczb jako obiektów. Tymczasem liczby są reprezentowane przez typy pierwotne (i nie są obiektami). Dlatego w standardowym pakiecie java.lang umieszczono specjalne klasy, opakowujące liczby (w ogóle wszystkie typy pierwotne) i czyniące z nich obiekty.
Należą do nich następujące klasy:

  • Integer
  • Short
  • Byte
  • Long
  • Float
  • Double

Obiekty tych klas reprezentują (w sposób obiektowy) liczby odpowiednich typów.
Np. obiektowy odpowiednik liczby 5 typu int możemy uzyskać tworząc obiekt klasy Integer:

Integer a = new Integer(5);

Mówimy o opakowaniu liczby 5, bowiem liczba ta jest umieszczana "w środku" obiektu klasy Integer, jako jego element.

Klasy opakowujące nie dostarczają żadnych metod do wykonywania operacji na liczbach. Z obiektu takiej klasy musimy 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

Po co to wszystko? Po co takie opakowanie może być potrzebne? Pamiętamy z wykladów o klasach i tablicach, że:

  1. każda klasa w Javie dziedziczy pośrednio (lub bezpośrednio) klasę Object
  2. zatem referencji do obiektu klasy Object możemy przypisac referencję do obiektu dowolnej innej klasy
  3. tablice mogą zawierać referencje do dowolnych obiektów

Wyobraźmy sobie teraz, że mamy jakąś tablicę, w kolejnych elementach której chcemy zapisywać dowolne informacje. Czy to możliwe? Ależ tak - właśnie dzięki wymienionym wyżej trzem właściwościom - pod warunkiem jednak, że informacja ta będzie przedstawiana w sposób obiektowy (jako obiekty).
Możemy zatem stworzyć tablicę, za pomocą której jednocześnie będziemy rejestrować np. i napisy (łańcuchy znakowe) i liczby. Te ostatnie jednak musimy "opakować" odpowiednimi klasami: Integer, Double itd.

Na przykład:

Object[] tab = new Object[4];
tab[0] = "Kowalski";
tab[1]  = new Integer(28);
tab[2] = "Kowalska";
tab[3] = new Integer(30);

W łatwy sposób możemy pokazać wszystkie elementy tablicy.

Metoda toString() zastosowana wobec obiektu dowolnej klasy zwraca napis, będący znakową reprezentacją tego obiektu. Definicje tej metody w klasach okreslają jak obiekty tych klas są przedstawiane w postaci napisów

W przypadku powyższego przykładu wypisanie wszystkich elementów tablicy wygląda tak:

for (int i=0; i<tab.length; i++)
    System.out.println(tab[i].toString());

co da napisy:
Kowalski
28
Kowalska
30

Jednak, gdybyśmy wobec obiektów zarejestrowanych w tablicy chcieli użyć metod ich klas (albo - wobec obiektów klasy String - operatora konkatenacji łańcuchów znakowych), to natkniemy się na dwa problemy.

Po pierwsze, po to by zastosować metody jakiejś klasy musimy rozpoznać właściwe obiekty, stwierdzić do jakiej klasy należą.
Operujemy przecież elementami typu Object. W naszym przykładzie każdy z nich może wskazywać na obiekt klasy Integer albo obiekt klasy String.

    Stwierdzeniu do jakiej klasy należy obiekt służy operator instanceof.

Wyrażenie:

        nazwa_zmiennej  instanceof  nazwa_klasy

ma wartość true, jeśli zmienna nazwa_zmiennej   jest referencją do obiektu klasy nazwa_klasy

Np. jeśli:

Object[] tab = { "Kowalski", new Integer(28) };

to
        tab[0] instanceof String   - ma wartość true
        tab[0] instanceof Integer  - ma wartość false
        tab[1] instanceof String    -  ma wartość false
        tab[1] instanceof Integer  - ma wartość true

Drugi problem polega na tym, że dla kompilatora wszystkie elementy tablicy tab w omawianym przykładzie są typu Object. Jak pamiętamy, kompilator sprawdza zgodność typów. Zatem w kontekście:

Object[] tab = { "Kowalski", new Integer(28) };

takie zapisy:

String s = "";
if (tab[0] instanceof String) s += tab[0];

int i;
if (tab[1] instanceof Integer) i += tab[1].intValue();

spowodują błędy w kompilacji, bowiem:

  1. tab[1] jest typu Object, a wobec Object nie możemy stosować operatora +
  2. tab[2] jest typu Object, a w klasie Object nie ma  metody intValue

Ale przecież wewnątrz obiektów oznaczanych przez tab[1] i tab[2]  "siedzą" naprawdę (odpowiednio) - obiekt klasy String i obiekt klasy Integer. Mówi nam o tym wynik zastosowania operatora instanceof, który w obu przypadkach jest true. Możemy o tym powiedzieć kompilatorowi za pomocą zawężającej konwersji referencyjnej (lub - inaczej mówiąc - obiektowej).

Jeśli klasa B dziedziczy klasę A, to po:

    B b = new B();
    A a = b;

możemy poinformować kompilator, że a wskazuje na obiekt klasy B za pomocą jawnej konwersji zawężającej:

    B c = (B) a;

po czym wobec zmiennej c będziemy mogli używać metod klasy B

W naszym przypadku powinniśmy więc zapisać:

Object[] tab = { "Kowalski", new Integer(28) };

String s = "Jan";
if (tab[0] instanceof String) s +=  (String) tab[0];

int i = 200;
if (tab[1] instanceof Integer)  {
    Integer liczba = (Integer) tab[1];
    i += liczba.intValue();
}
 
i w tym momencie zarówno kompilacja jak i wykonaie programu będą pomyślne, dając:
s == "JanKowalski"
i == 228;

Zadanie: w dwóch tablicach zapisane są imiona graczy (w jakąś grę) oraz punkty przez nich uzyskane:
    String[] name = { "Ala", "Ela", "Jan"};
    int[]  points = { 10, 12, 11 };

Stworzyć jedną tablicę tab, w której będzie zapisana sekwencja wyników: imię, punkty, imię, punkty...
po czym, korzystając tylko z tablicy tab, w kolejnych wierszach wypisać zestawy imię - punkty, podać (w jednym wierszu) imiona wszystkich graczy, a następnie sumę punktów, którą uzyskali.
Przykładowy wydruk:

W grze punkty uzyskali:
Ala 10
Ela 12
Jan 11
Grali:  Ala Ela Jan
Zdobyli łącznie: 33 pkt.


Przed lekturą dalszego tekstu proszę to zadanie rozwiązać samodzielnie


Możliwe rozwiązanie:

public class Wrap {

  public static void main(String[] args) {


    String[] name = { "Ala", "Ela", "Jan"};
    int[]  points = { 10, 12, 11 };
    Object[] tab = new Object[name.length*2];

    int k = 0;
    for (int i=0; i<name.length; i++) {
      tab[k++] = name[i];
      tab[k++] = new Integer(points[i]);
    }

   System.out.println("W grze punkty uzyskali: ");
   for (int i=0; i <tab.length; i+=2)
     System.out.println(tab[i].toString() + " " + tab[i+1].toString());

   String players = "";
   int pointSum = 0;

   for (int i=0; i < tab.length; i++) {
     if (tab[i] instanceof String)
       players += " " + (String) tab[i];
     else if ( tab[i] instanceof Integer)
       pointSum += ((Integer) tab[i]).intValue();
   }

   System.out.println("Grali: " + players);
   System.out.println("Zdobyli łącznie: " + pointSum + " pkt.");

  }

}

Niejako przy okazji, w klasach opakowujących typy pierwotne zdefiniowano statyczne metody:

            public static ttt  parseTtt(String s)

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

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

A więc, po to by przekstałcić napis s reprezentujący liczbę rzeczywistą do typu double należy napisać:

double d = Double.parseDouble(s);

np.

String s = "12.4";
double d = Double.parseDouble(s);  // d == 12.4

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

isDigit()             // czy znak jest znakiem cyfry
isLetter()           // czy znak jest znakiem litery
isLetterOrDigit() // litera klub 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.

Proszę zapoznać się z dokumentacją klasy Character.

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ć zakresów 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);

  }

}


« poprzedni punkt  następny punkt »